├── src ├── Middleware │ ├── ConnectionNameAwareInterface.php │ ├── DebugMiddleware.php │ ├── IdleConnectionMiddleware.php │ └── BacktraceDebugDataHolder.php ├── Repository │ ├── ServiceEntityRepositoryInterface.php │ ├── ContainerRepositoryFactory.php │ └── ServiceEntityRepository.php ├── Attribute │ ├── AsMiddleware.php │ ├── AsDoctrineListener.php │ └── AsEntityListener.php ├── Mapping │ ├── EntityListenerServiceResolver.php │ ├── ClassMetadataFactory.php │ ├── MappingDriver.php │ └── ContainerEntityListenerResolver.php ├── Dbal │ ├── RegexSchemaAssetFilter.php │ ├── ManagerRegistryAwareConnectionProvider.php │ └── SchemaAssetsFilterManager.php ├── DependencyInjection │ └── Compiler │ │ ├── RemoveLoggingMiddlewarePass.php │ │ ├── RemoveProfilerControllerPass.php │ │ ├── ServiceRepositoryCompilerPass.php │ │ ├── CacheSchemaSubscriberPass.php │ │ ├── DbalSchemaFilterPass.php │ │ ├── IdGeneratorPass.php │ │ ├── MiddlewaresPass.php │ │ ├── EntityListenerPass.php │ │ └── DoctrineOrmMappingsPass.php ├── Command │ ├── DoctrineCommand.php │ ├── CreateDatabaseDoctrineCommand.php │ └── DropDatabaseDoctrineCommand.php ├── Orm │ └── ManagerRegistryAwareEntityManagerProvider.php ├── CacheWarmer │ └── DoctrineMetadataCacheWarmer.php ├── ManagerConfigurator.php ├── Registry.php ├── Controller │ └── ProfilerController.php ├── DoctrineBundle.php ├── Twig │ └── DoctrineExtension.php ├── ConnectionFactory.php └── DataCollector │ └── DoctrineDataCollector.php ├── templates └── Collector │ ├── database.svg │ ├── icon.svg │ ├── explain.html.twig │ └── db.html.twig ├── UPGRADE-3.1.md ├── UPGRADE-2.18.md ├── UPGRADE-2.12.md ├── UPGRADE-2.13.md ├── LICENSE ├── config ├── middlewares.php ├── messenger.php ├── dbal.php ├── orm.php └── schema │ └── doctrine-1.0.xsd ├── README.md ├── UPGRADE-2.17.md ├── UPGRADE-2.10.md ├── composer.json └── UPGRADE-3.0.md /src/Middleware/ConnectionNameAwareInterface.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/Mapping/EntityListenerServiceResolver.php: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | -------------------------------------------------------------------------------- /src/Dbal/RegexSchemaAssetFilter.php: -------------------------------------------------------------------------------- 1 | getName(); 22 | } 23 | 24 | return (bool) preg_match($this->filterExpression, $assetName); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Attribute/AsEntityListener.php: -------------------------------------------------------------------------------- 1 | has('logger')) { 16 | return; 17 | } 18 | 19 | $container->removeDefinition('doctrine.dbal.logging_middleware'); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/DependencyInjection/Compiler/RemoveProfilerControllerPass.php: -------------------------------------------------------------------------------- 1 | has('twig') && $container->has('profiler')) { 17 | return; 18 | } 19 | 20 | $container->removeDefinition(ProfilerController::class); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Dbal/ManagerRegistryAwareConnectionProvider.php: -------------------------------------------------------------------------------- 1 | managerRegistry->getConnection(); 21 | } 22 | 23 | public function getConnection(string $name): Connection 24 | { 25 | return $this->managerRegistry->getConnection($name); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Dbal/SchemaAssetsFilterManager.php: -------------------------------------------------------------------------------- 1 | schemaAssetFilters as $schemaAssetFilter) { 23 | if ($schemaAssetFilter($assetName) === false) { 24 | return false; 25 | } 26 | } 27 | 28 | return true; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Command/DoctrineCommand.php: -------------------------------------------------------------------------------- 1 | getDoctrine()->getConnection($name); 30 | } 31 | 32 | protected function getDoctrine(): ManagerRegistry 33 | { 34 | return $this->doctrine; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /templates/Collector/explain.html.twig: -------------------------------------------------------------------------------- 1 | {% if data[0]|length > 1 %} 2 | {# The platform returns a table for the explanation (e.g. MySQL), display all columns #} 3 | 4 | 5 | 6 | {% for label in data[0]|keys %} 7 | 8 | {% endfor %} 9 | 10 | 11 | 12 | {% for row in data %} 13 | 14 | {% for key, item in row %} 15 | 16 | {% endfor %} 17 | 18 | {% endfor %} 19 | 20 |
{{ label }}
{{ item|replace({',': ', '}) }}
21 | {% else %} 22 | {# The Platform returns a single column for a textual explanation (e.g. PostgreSQL), display all lines #} 23 |
24 |         {%- for row in data -%}
25 |             {{ row|first }}{{ "\n" }}
26 |         {%- endfor -%}
27 |     
28 | {% endif %} 29 | -------------------------------------------------------------------------------- /src/Middleware/DebugMiddleware.php: -------------------------------------------------------------------------------- 1 | connectionName = $name; 26 | } 27 | 28 | public function wrap(DriverInterface $driver): DriverInterface 29 | { 30 | return new Driver($driver, $this->debugDataHolder, $this->stopwatch, $this->connectionName); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2011 Fabien Potencier, 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, 6 | and 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 Software. 9 | 10 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 11 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 12 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 13 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 14 | -------------------------------------------------------------------------------- /src/Middleware/IdleConnectionMiddleware.php: -------------------------------------------------------------------------------- 1 | $connectionExpiries 18 | * @param array $ttlByConnection 19 | */ 20 | public function __construct( 21 | private readonly ArrayObject $connectionExpiries, 22 | private readonly array $ttlByConnection, 23 | ) { 24 | } 25 | 26 | public function setConnectionName(string $name): void 27 | { 28 | $this->connectionName = $name; 29 | } 30 | 31 | public function wrap(Driver $driver): IdleConnectionDriver 32 | { 33 | return new IdleConnectionDriver( 34 | $driver, 35 | $this->connectionExpiries, 36 | $this->ttlByConnection[$this->connectionName], 37 | $this->connectionName, 38 | ); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Orm/ManagerRegistryAwareEntityManagerProvider.php: -------------------------------------------------------------------------------- 1 | getManager($this->managerRegistry->getDefaultManagerName()); 25 | } 26 | 27 | public function getManager(string $name): EntityManagerInterface 28 | { 29 | $em = $this->managerRegistry->getManager($name); 30 | 31 | if ($em instanceof EntityManagerInterface) { 32 | return $em; 33 | } 34 | 35 | throw new RuntimeException( 36 | sprintf( 37 | 'Only managers of type "%s" are supported. Instance of "%s given.', 38 | EntityManagerInterface::class, 39 | get_debug_type($em), 40 | ), 41 | ); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/Mapping/ClassMetadataFactory.php: -------------------------------------------------------------------------------- 1 | customGeneratorDefinition; 23 | 24 | if (! isset($customGeneratorDefinition['instance'])) { 25 | return; 26 | } 27 | 28 | /** @phpstan-ignore function.impossibleType, instanceof.alwaysFalse */ 29 | assert($customGeneratorDefinition['instance'] instanceof AbstractIdGenerator); 30 | 31 | $class->setIdGeneratorType(ClassMetadata::GENERATOR_TYPE_CUSTOM); 32 | $class->setIdGenerator($customGeneratorDefinition['instance']); 33 | unset($customGeneratorDefinition['instance']); 34 | $class->setCustomGeneratorDefinition($customGeneratorDefinition); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/DependencyInjection/Compiler/ServiceRepositoryCompilerPass.php: -------------------------------------------------------------------------------- 1 | hasDefinition('doctrine.orm.container_repository_factory')) { 25 | return; 26 | } 27 | 28 | $locatorDef = $container->getDefinition('doctrine.orm.container_repository_factory'); 29 | 30 | $repoServiceIds = array_keys($container->findTaggedServiceIds(self::REPOSITORY_SERVICE_TAG)); 31 | $repoReferences = array_map(static fn (string $id): Reference => new Reference($id), $repoServiceIds); 32 | 33 | $ref = ServiceLocatorTagPass::register($container, array_combine($repoServiceIds, $repoReferences)); 34 | $locatorDef->replaceArgument(0, $ref); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/CacheWarmer/DoctrineMetadataCacheWarmer.php: -------------------------------------------------------------------------------- 1 | phpArrayFile)) { 33 | return false; 34 | } 35 | 36 | $metadataFactory = $this->entityManager->getMetadataFactory(); 37 | if ($metadataFactory->getLoadedMetadata()) { 38 | throw new LogicException('DoctrineMetadataCacheWarmer must load metadata first, check priority of your warmers.'); 39 | } 40 | 41 | $metadataFactory->setCache($arrayAdapter); 42 | $metadataFactory->getAllMetadata(); 43 | 44 | return true; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/DependencyInjection/Compiler/CacheSchemaSubscriberPass.php: -------------------------------------------------------------------------------- 1 | hasDefinition('doctrine.orm.listeners.doctrine_dbal_cache_adapter_schema_listener')) { 24 | return; 25 | } 26 | 27 | $subscriber = $container->getDefinition('doctrine.orm.listeners.doctrine_dbal_cache_adapter_schema_listener'); 28 | 29 | $cacheAdaptersReferences = []; 30 | foreach ($container->getDefinitions() as $id => $definition) { 31 | if ($definition->isAbstract() || $definition->isSynthetic()) { 32 | continue; 33 | } 34 | 35 | if ($definition->getClass() !== DoctrineDbalAdapter::class) { 36 | continue; 37 | } 38 | 39 | $cacheAdaptersReferences[] = new Reference($id); 40 | } 41 | 42 | $subscriber->replaceArgument(0, $cacheAdaptersReferences); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /config/middlewares.php: -------------------------------------------------------------------------------- 1 | services() 15 | 16 | ->set('doctrine.dbal.connection_expiries', ArrayObject::class) 17 | 18 | ->set('doctrine.dbal.logging_middleware', Middleware::class) 19 | ->abstract() 20 | ->args([ 21 | service('logger'), 22 | ]) 23 | ->tag('monolog.logger', ['channel' => 'doctrine']) 24 | 25 | ->set('doctrine.debug_data_holder', BacktraceDebugDataHolder::class) 26 | ->args([ 27 | [], 28 | ]) 29 | ->tag('kernel.reset', ['method' => 'reset']) 30 | 31 | ->set('doctrine.dbal.debug_middleware', DebugMiddleware::class) 32 | ->abstract() 33 | ->args([ 34 | service('doctrine.debug_data_holder'), 35 | service('debug.stopwatch')->nullOnInvalid(), 36 | ]) 37 | 38 | ->set('doctrine.dbal.idle_connection_middleware', IdleConnectionMiddleware::class) 39 | ->abstract() 40 | ->args([ 41 | service('doctrine.dbal.connection_expiries'), 42 | null, 43 | ]); 44 | }; 45 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Doctrine Bundle 2 | 3 | Doctrine DBAL & ORM Bundle for the Symfony Framework. 4 | 5 | [![Continuous Integration](https://github.com/doctrine/DoctrineBundle/actions/workflows/continuous-integration.yml/badge.svg)](https://github.com/doctrine/DoctrineBundle/actions/workflows/continuous-integration.yml) 6 | [![codecov](https://codecov.io/gh/doctrine/DoctrineBundle/graph/badge.svg?token=qtm3EQ3WgV)](https://codecov.io/gh/doctrine/DoctrineBundle) 7 | 8 | ## What is Doctrine? 9 | 10 | The Doctrine Project is the home of a selected set of PHP libraries primarily focused on providing persistence 11 | services and related functionality. Its prize projects are a Object Relational Mapper and the Database Abstraction 12 | Layer it is built on top of. You can read more about the projects below or view a list of all projects. 13 | 14 | Object relational mapper (ORM) for PHP that sits on top of a powerful database abstraction layer (DBAL). 15 | One of its key features is the option to write database queries in a proprietary object oriented SQL dialect 16 | called Doctrine Query Language (DQL), inspired by Hibernates HQL. This provides developers with a powerful 17 | alternative to SQL that maintains flexibility without requiring unnecessary code duplication. 18 | 19 | DBAL is a powerful database abstraction layer with many features for database schema introspection, 20 | schema management and PDO abstraction. 21 | 22 | ## Documentation 23 | 24 | The documentation is rendered on [the symfony.com website](https://symfony.com/doc/current/reference/configuration/doctrine.html). 25 | The source of the documentation is available in the docs folder. 26 | -------------------------------------------------------------------------------- /src/ManagerConfigurator.php: -------------------------------------------------------------------------------- 1 | > $filtersParameters 18 | */ 19 | public function __construct( 20 | private readonly array $enabledFilters = [], 21 | private readonly array $filtersParameters = [], 22 | ) { 23 | } 24 | 25 | /** 26 | * Create a connection by name. 27 | */ 28 | public function configure(EntityManagerInterface $entityManager): void 29 | { 30 | $this->enableFilters($entityManager); 31 | } 32 | 33 | /** 34 | * Enables filters for a given entity manager 35 | */ 36 | private function enableFilters(EntityManagerInterface $entityManager): void 37 | { 38 | if (empty($this->enabledFilters)) { 39 | return; 40 | } 41 | 42 | $filterCollection = $entityManager->getFilters(); 43 | foreach ($this->enabledFilters as $filter) { 44 | $this->setFilterParameters($filter, $filterCollection->enable($filter)); 45 | } 46 | } 47 | 48 | /** 49 | * Sets default parameters for a given filter 50 | */ 51 | private function setFilterParameters(string $name, SQLFilter $filter): void 52 | { 53 | if (empty($this->filtersParameters[$name])) { 54 | return; 55 | } 56 | 57 | $parameters = $this->filtersParameters[$name]; 58 | foreach ($parameters as $paramName => $paramValue) { 59 | $filter->setParameter($paramName, $paramValue); 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /UPGRADE-2.17.md: -------------------------------------------------------------------------------- 1 | UPGRADE FROM 2.16 to 2.17 2 | ========================= 3 | 4 | DoctrineExtension 5 | ================= 6 | 7 | Minor breaking change: 8 | `Doctrine\Bundle\DoctrineBundle\DependencyInjection\DoctrineExtension` no 9 | longer extends 10 | `Symfony\Bridge\Doctrine\DependencyInjection\AbstractDoctrineExtension`. 11 | 12 | Configuration 13 | ------------- 14 | 15 | ### The `doctrine.orm.entity_managers.some_em.report_fields_where_declared` configuration option is deprecated 16 | 17 | This option is a no-op when using `doctrine/orm` 3 and has been conditionally 18 | deprecated. You should stop using it as soon as you upgrade to Doctrine ORM 3. 19 | 20 | ### The `doctrine.dbal.connections.some_connection.disable_type_comments` configuration option is deprecated 21 | 22 | This option is a no-op when using `doctrine/dbal` 4 and has been conditionally 23 | deprecated. You should stop using it as soon as you upgrade to Doctrine DBAL 4. 24 | 25 | ### The `doctrine.dbal.connections.some_connection.use_savepoints` configuration option is deprecated 26 | 27 | This option is a no-op when using `doctrine/dbal` 4 and has been conditionally 28 | deprecated. You should stop using it as soon as you upgrade to Doctrine DBAL 4. 29 | 30 | ConnectionFactory::createConnection() signature change 31 | ------------------------------------------------------ 32 | 33 | The signature of `ConnectionFactory::createConnection()` will change with 34 | version 3.0 of the bundle. 35 | 36 | As soon as you upgrade to Doctrine DBAL 4, you should use stop passing an event 37 | manager argument. 38 | 39 | ```diff 40 | - $connectionFactory->createConnection($params, $config, $eventManager, $mappingTypes) 41 | + $connectionFactory->createConnection($params, $config, $mappingTypes) 42 | ``` 43 | 44 | As a small breaking change, it is no longer fully possible to use named 45 | arguments with that method until 3.0. 46 | -------------------------------------------------------------------------------- /src/DependencyInjection/Compiler/DbalSchemaFilterPass.php: -------------------------------------------------------------------------------- 1 | findTaggedServiceIds('doctrine.dbal.schema_filter'); 24 | 25 | $connectionFilters = []; 26 | foreach ($filters as $id => $tagAttributes) { 27 | foreach ($tagAttributes as $attributes) { 28 | $name = $attributes['connection'] ?? $container->getParameter('doctrine.default_connection'); 29 | 30 | if (! isset($connectionFilters[$name])) { 31 | $connectionFilters[$name] = []; 32 | } 33 | 34 | $connectionFilters[$name][] = new Reference($id); 35 | } 36 | } 37 | 38 | foreach ($connectionFilters as $name => $references) { 39 | $configurationId = sprintf('doctrine.dbal.%s_connection.configuration', $name); 40 | 41 | if (! $container->hasDefinition($configurationId)) { 42 | continue; 43 | } 44 | 45 | $definition = new ChildDefinition('doctrine.dbal.schema_asset_filter_manager'); 46 | $definition->setArgument(0, $references); 47 | 48 | $id = sprintf('doctrine.dbal.%s_schema_asset_filter_manager', $name); 49 | $container->setDefinition($id, $definition); 50 | $container->findDefinition($configurationId) 51 | ->addMethodCall('setSchemaAssetsFilter', [new Reference($id)]); 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/Mapping/MappingDriver.php: -------------------------------------------------------------------------------- 1 | driver->getAllClassNames(); 26 | } 27 | 28 | /** 29 | * {@inheritDoc} 30 | */ 31 | public function isTransient($className): bool 32 | { 33 | return $this->driver->isTransient($className); 34 | } 35 | 36 | /** 37 | * {@inheritDoc} 38 | */ 39 | public function loadMetadataForClass($className, ClassMetadata $metadata): void 40 | { 41 | $this->driver->loadMetadataForClass($className, $metadata); 42 | 43 | if ( 44 | ! $metadata instanceof OrmClassMetadata 45 | || $metadata->generatorType !== OrmClassMetadata::GENERATOR_TYPE_CUSTOM 46 | || ! isset($metadata->customGeneratorDefinition['class']) 47 | || ! $this->idGeneratorLocator->has($metadata->customGeneratorDefinition['class']) 48 | ) { 49 | return; 50 | } 51 | 52 | $idGenerator = $this->idGeneratorLocator->get($metadata->customGeneratorDefinition['class']); 53 | $metadata->setCustomGeneratorDefinition(['instance' => $idGenerator] + $metadata->customGeneratorDefinition); 54 | $metadata->setIdGeneratorType(OrmClassMetadata::GENERATOR_TYPE_NONE); 55 | } 56 | 57 | /** 58 | * Returns the inner driver 59 | */ 60 | public function getDriver(): MappingDriverInterface 61 | { 62 | return $this->driver; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /UPGRADE-2.10.md: -------------------------------------------------------------------------------- 1 | UPGRADE FROM 2.9 to 2.10 2 | ======================== 3 | 4 | Configuration 5 | ------------- 6 | 7 | ### Preparing for a new `report_fields_where_declared` mapping driver mode 8 | 9 | Doctrine ORM 2.16+ makes a change to how the annotations and attribute mapping drivers report fields inherited from parent classes. For details, see https://github.com/doctrine/orm/pull/10455. It will trigger a deprecation notice unless the new mode is activated. In ORM 3.0, the new mode will be the only one. 10 | 11 | The new mode ~does not~ should not make a difference for regular, valid use cases, but may lead to `MappingException`s for users with certain configurations that were not meant to be supported by the ORM in the first place. To avoid surprising users (even when their configuration is invalid) during a 2.16 _minor_ version upgrade, the transition to this new mode was implemented as an opt-in. This way, you can try and deal with the change any time you see fit. 12 | 13 | In version 2.10+ of this bundle, a new configuration setting `report_fields_where_declared` was added at the entity manager configuration level. Set it to `true` to switch the mapping driver for the corresponding entity manager to the new mode. It is only relevant for mapping configurations using attributes or annotations. 14 | 15 | Unless you set it to `true`, Doctrine ORM will emit deprecation messages mentioning this new setting. 16 | 17 | ### Preparing for the XSD validation for XML drivers 18 | 19 | Doctrine ORM 2.14+ adds support for validating the XSD of XML mapping files. In ORM 3.0, this validation will be mandatory. 20 | 21 | As the ecosystem is known to rely on custom elements in the XML mapping files that are forbidden when validating the XSD (for instance when using `gedmo/doctrine-extensions`), this validation is opt-in thanks to a `validate_xml_mapping` setting at the entity manager configuration level. 22 | 23 | Unless you set it to `true`, Doctrine ORM will emit deprecation messages mentioning the XSD validation. 24 | 25 | ### Deprecations 26 | 27 | - `Doctrine\Bundle\DoctrineBundle\EventSubscriber\EventSubscriberInterface` has been deprecated. Use the `#[AsDoctrineListener]` attribute instead. 28 | -------------------------------------------------------------------------------- /src/Registry.php: -------------------------------------------------------------------------------- 1 | container = $container; 30 | 31 | parent::__construct('ORM', $connections, $entityManagers, $defaultConnection, $defaultEntityManager, Proxy::class); 32 | } 33 | 34 | public function reset(): void 35 | { 36 | foreach ($this->getManagerNames() as $managerName => $serviceId) { 37 | $this->resetOrClearManager($managerName, $serviceId); 38 | } 39 | } 40 | 41 | private function resetOrClearManager(string $managerName, string $serviceId): void 42 | { 43 | if (! $this->container->initialized($serviceId)) { 44 | return; 45 | } 46 | 47 | $manager = $this->container->get($serviceId); 48 | 49 | assert($manager instanceof EntityManagerInterface); 50 | 51 | // Determine if the version of symfony/dependency-injection is >= 7.3 52 | /** @phpstan-ignore function.alreadyNarrowedType */ 53 | $sfNativeLazyObjects = method_exists('Symfony\Component\DependencyInjection\ContainerBuilder', 'findTaggedResourceIds'); 54 | 55 | if (! $sfNativeLazyObjects) { 56 | if (! $manager instanceof LazyObjectInterface || $manager->isOpen()) { 57 | $manager->clear(); 58 | 59 | return; 60 | } 61 | } else { 62 | $r = new ReflectionClass($manager); 63 | if ($r->isUninitializedLazyObject($manager)) { 64 | return; 65 | } 66 | 67 | if ($manager->isOpen()) { 68 | $manager->clear(); 69 | 70 | return; 71 | } 72 | } 73 | 74 | $this->resetManager($managerName); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /config/messenger.php: -------------------------------------------------------------------------------- 1 | services() 17 | 18 | ->set('messenger.middleware.doctrine_transaction', DoctrineTransactionMiddleware::class) 19 | ->abstract() 20 | ->args([ 21 | service('doctrine'), 22 | ]) 23 | 24 | ->set('messenger.middleware.doctrine_ping_connection', DoctrinePingConnectionMiddleware::class) 25 | ->abstract() 26 | ->args([ 27 | service('doctrine'), 28 | ]) 29 | 30 | ->set('messenger.middleware.doctrine_close_connection', DoctrineCloseConnectionMiddleware::class) 31 | ->abstract() 32 | ->args([ 33 | service('doctrine'), 34 | ]) 35 | 36 | ->set('messenger.middleware.doctrine_open_transaction_logger', DoctrineOpenTransactionLoggerMiddleware::class) 37 | ->abstract() 38 | ->args([ 39 | service('doctrine'), 40 | null, 41 | service('logger'), 42 | ]) 43 | 44 | ->set('doctrine.orm.messenger.event_subscriber.doctrine_clear_entity_manager', DoctrineClearEntityManagerWorkerSubscriber::class) 45 | ->tag('kernel.event_subscriber') 46 | ->args([ 47 | service('doctrine'), 48 | ]) 49 | 50 | ->set('messenger.transport.doctrine.factory', DoctrineTransportFactory::class) 51 | ->tag('messenger.transport_factory') 52 | ->args([ 53 | service('doctrine'), 54 | ]) 55 | 56 | ->set('doctrine.orm.messenger.doctrine_schema_listener', MessengerTransportDoctrineSchemaListener::class) 57 | ->args([ 58 | tagged_iterator('messenger.receiver'), 59 | ]) 60 | ->tag('doctrine.event_listener', ['event' => 'postGenerateSchema']) 61 | ->tag('doctrine.event_listener', ['event' => 'onSchemaCreateTable']); 62 | }; 63 | -------------------------------------------------------------------------------- /src/Middleware/BacktraceDebugDataHolder.php: -------------------------------------------------------------------------------- 1 | []> */ 19 | private array $backtraces = []; 20 | 21 | /** @param string[] $connWithBacktraces */ 22 | public function __construct( 23 | private readonly array $connWithBacktraces, 24 | ) { 25 | } 26 | 27 | public function reset(): void 28 | { 29 | parent::reset(); 30 | 31 | $this->backtraces = []; 32 | } 33 | 34 | public function addQuery(string $connectionName, Query $query): void 35 | { 36 | parent::addQuery($connectionName, $query); 37 | 38 | if (! in_array($connectionName, $this->connWithBacktraces, true)) { 39 | return; 40 | } 41 | 42 | // array_slice to skip middleware calls in the trace 43 | $this->backtraces[$connectionName][] = array_slice(debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS), 2); 44 | } 45 | 46 | /** @return array[]> */ 47 | public function getData(): array 48 | { 49 | $dataWithBacktraces = []; 50 | 51 | $data = parent::getData(); 52 | foreach ($data as $connectionName => $dataForConn) { 53 | $dataWithBacktraces[$connectionName] = $this->getDataForConnection($connectionName, $dataForConn); 54 | } 55 | 56 | return $dataWithBacktraces; 57 | } 58 | 59 | /** 60 | * @param mixed[][] $dataForConn 61 | * 62 | * @return mixed[][] 63 | */ 64 | private function getDataForConnection(string $connectionName, array $dataForConn): array 65 | { 66 | $data = []; 67 | 68 | foreach ($dataForConn as $idx => $record) { 69 | $data[] = $this->addBacktracesIfAvailable($connectionName, $record, $idx); 70 | } 71 | 72 | return $data; 73 | } 74 | 75 | /** 76 | * @param mixed[] $record 77 | * 78 | * @return mixed[] 79 | */ 80 | private function addBacktracesIfAvailable(string $connectionName, array $record, int $idx): array 81 | { 82 | if (! isset($this->backtraces[$connectionName])) { 83 | return $record; 84 | } 85 | 86 | $record['backtrace'] = $this->backtraces[$connectionName][$idx]; 87 | 88 | return $record; 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/Mapping/ContainerEntityListenerResolver.php: -------------------------------------------------------------------------------- 1 | instances = []; 38 | 39 | return; 40 | } 41 | 42 | $className = $this->normalizeClassName($className); 43 | 44 | unset($this->instances[$className]); 45 | } 46 | 47 | /** 48 | * {@inheritDoc} 49 | */ 50 | public function register($object): void 51 | { 52 | if (! is_object($object)) { 53 | throw new InvalidArgumentException(sprintf('An object was expected, but got "%s".', gettype($object))); 54 | } 55 | 56 | $className = $this->normalizeClassName($object::class); 57 | 58 | $this->instances[$className] = $object; 59 | } 60 | 61 | /** 62 | * {@inheritDoc} 63 | */ 64 | public function registerService($className, $serviceId) 65 | { 66 | $this->serviceIds[$this->normalizeClassName($className)] = $serviceId; 67 | } 68 | 69 | /** 70 | * {@inheritDoc} 71 | */ 72 | public function resolve($className): object 73 | { 74 | $className = $this->normalizeClassName($className); 75 | 76 | if (! isset($this->instances[$className])) { 77 | if (isset($this->serviceIds[$className])) { 78 | $this->instances[$className] = $this->resolveService($this->serviceIds[$className]); 79 | } else { 80 | $this->instances[$className] = new $className(); 81 | } 82 | } 83 | 84 | return $this->instances[$className]; 85 | } 86 | 87 | private function resolveService(string $serviceId): object 88 | { 89 | if (! $this->container->has($serviceId)) { 90 | throw new RuntimeException(sprintf('There is no service named "%s"', $serviceId)); 91 | } 92 | 93 | return $this->container->get($serviceId); 94 | } 95 | 96 | private function normalizeClassName(string $className): string 97 | { 98 | return trim($className, '\\'); 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/DependencyInjection/Compiler/IdGeneratorPass.php: -------------------------------------------------------------------------------- 1 | findTaggedServiceIds(self::ID_GENERATOR_TAG)); 30 | 31 | // when ORM is not enabled 32 | if (! $container->hasDefinition('doctrine.orm.configuration') || ! $generatorIds) { 33 | return; 34 | } 35 | 36 | $generatorRefs = array_map(static fn (string $id): Reference => new Reference($id), $generatorIds); 37 | 38 | $ref = ServiceLocatorTagPass::register($container, array_combine($generatorIds, $generatorRefs)); 39 | $container->setAlias('doctrine.id_generator_locator', new Alias((string) $ref, false)); 40 | 41 | foreach ($container->findTaggedServiceIds(self::CONFIGURATION_TAG) as $id => $tags) { 42 | $configurationDef = $container->getDefinition($id); 43 | $methodCalls = $configurationDef->getMethodCalls(); 44 | $metadataDriverImpl = null; 45 | 46 | foreach ($methodCalls as $i => [$method, $arguments]) { 47 | if ($method === 'setMetadataDriverImpl') { 48 | $metadataDriverImpl = (string) $arguments[0]; 49 | } 50 | 51 | if ($method !== 'setClassMetadataFactoryName') { 52 | continue; 53 | } 54 | 55 | if ($arguments[0] !== ORMClassMetadataFactory::class && $arguments[0] !== ClassMetadataFactory::class) { 56 | $class = $container->getReflectionClass($arguments[0]); 57 | 58 | if ($class && $class->isSubclassOf(ClassMetadataFactory::class)) { 59 | break; 60 | } 61 | 62 | continue 2; 63 | } 64 | 65 | $methodCalls[$i] = ['setClassMetadataFactoryName', [ClassMetadataFactory::class]]; 66 | } 67 | 68 | if ($metadataDriverImpl === null) { 69 | continue; 70 | } 71 | 72 | $configurationDef->setMethodCalls($methodCalls); 73 | $container->register('.' . $metadataDriverImpl, MappingDriver::class) 74 | ->setDecoratedService($metadataDriverImpl) 75 | ->setArguments([ 76 | new Reference(sprintf('.%s.inner', $metadataDriverImpl)), 77 | new Reference('doctrine.id_generator_locator'), 78 | ]); 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "doctrine/doctrine-bundle", 3 | "description": "Symfony DoctrineBundle", 4 | "license": "MIT", 5 | "type": "symfony-bundle", 6 | "keywords": [ 7 | "DBAL", 8 | "ORM", 9 | "Database", 10 | "Persistence" 11 | ], 12 | "authors": [ 13 | { 14 | "name": "Fabien Potencier", 15 | "email": "fabien@symfony.com" 16 | }, 17 | { 18 | "name": "Benjamin Eberlei", 19 | "email": "kontakt@beberlei.de" 20 | }, 21 | { 22 | "name": "Symfony Community", 23 | "homepage": "https://symfony.com/contributors" 24 | }, 25 | { 26 | "name": "Doctrine Project", 27 | "homepage": "https://www.doctrine-project.org/" 28 | } 29 | ], 30 | "homepage": "https://www.doctrine-project.org", 31 | "require": { 32 | "php": "^8.4", 33 | "doctrine/dbal": "^4.0", 34 | "doctrine/deprecations": "^1.0", 35 | "doctrine/persistence": "^4", 36 | "doctrine/sql-formatter": "^1.0.1", 37 | "symfony/cache": "^6.4 || ^7.0 || ^8.0", 38 | "symfony/config": "^6.4 || ^7.0 || ^8.0", 39 | "symfony/console": "^6.4 || ^7.0 || ^8.0", 40 | "symfony/dependency-injection": "^6.4 || ^7.0 || ^8.0", 41 | "symfony/doctrine-bridge": "^6.4.3 || ^7.0.3 || ^8.0", 42 | "symfony/framework-bundle": "^6.4 || ^7.0 || ^8.0", 43 | "symfony/service-contracts": "^3" 44 | }, 45 | "require-dev": { 46 | "doctrine/coding-standard": "^14", 47 | "doctrine/orm": "^3.4.4", 48 | "phpstan/phpstan": "2.1.1", 49 | "phpstan/phpstan-phpunit": "2.0.3", 50 | "phpstan/phpstan-strict-rules": "^2", 51 | "phpunit/phpunit": "^12.3.10", 52 | "psr/log": "^3.0", 53 | "symfony/doctrine-messenger": "^6.4 || ^7.0 || ^8.0", 54 | "symfony/expression-language": "^6.4 || ^7.0 || ^8.0", 55 | "symfony/messenger": "^6.4 || ^7.0 || ^8.0", 56 | "symfony/property-info": "^6.4 || ^7.0 || ^8.0", 57 | "symfony/security-bundle": "^6.4 || ^7.0 || ^8.0", 58 | "symfony/stopwatch": "^6.4 || ^7.0 || ^8.0", 59 | "symfony/string": "^6.4 || ^7.0 || ^8.0", 60 | "symfony/twig-bridge": "^6.4 || ^7.0 || ^8.0", 61 | "symfony/validator": "^6.4 || ^7.0 || ^8.0", 62 | "symfony/web-profiler-bundle": "^6.4 || ^7.0 || ^8.0", 63 | "symfony/yaml": "^6.4 || ^7.0 || ^8.0", 64 | "twig/twig": "^3.21.1" 65 | }, 66 | "conflict": { 67 | "doctrine/orm": "<3.0 || >=4.0", 68 | "twig/twig": "<3.0.4" 69 | }, 70 | "suggest": { 71 | "ext-pdo": "*", 72 | "doctrine/orm": "The Doctrine ORM integration is optional in the bundle.", 73 | "symfony/web-profiler-bundle": "To use the data collector." 74 | }, 75 | "minimum-stability": "dev", 76 | "autoload": { 77 | "psr-4": { 78 | "Doctrine\\Bundle\\DoctrineBundle\\": "src" 79 | } 80 | }, 81 | "autoload-dev": { 82 | "psr-4": { 83 | "Doctrine\\Bundle\\DoctrineBundle\\Tests\\": "tests", 84 | "Fixtures\\": "tests/DependencyInjection/Fixtures" 85 | } 86 | }, 87 | "config": { 88 | "allow-plugins": { 89 | "composer/package-versions-deprecated": true, 90 | "dealerdirect/phpcodesniffer-composer-installer": true, 91 | "symfony/flex": true 92 | }, 93 | "sort-packages": true 94 | }, 95 | "scripts": { 96 | "auto-scripts": { 97 | "cache:clear": "symfony-cmd", 98 | "assets:install %PUBLIC_DIR%": "symfony-cmd" 99 | } 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/DependencyInjection/Compiler/MiddlewaresPass.php: -------------------------------------------------------------------------------- 1 | hasParameter('doctrine.connections')) { 27 | return; 28 | } 29 | 30 | $middlewareAbstractDefs = []; 31 | $middlewareConnections = []; 32 | $middlewarePriorities = []; 33 | foreach ($container->findTaggedServiceIds('doctrine.middleware') as $id => $tags) { 34 | $middlewareAbstractDefs[$id] = $container->getDefinition($id); 35 | // When a def has doctrine.middleware tags with connection attributes equal to connection names 36 | // registration of this middleware is limited to the connections with these names 37 | foreach ($tags as $tag) { 38 | if (! isset($tag['connection'])) { 39 | if (isset($tag['priority']) && ! isset($middlewarePriorities[$id])) { 40 | $middlewarePriorities[$id] = $tag['priority']; 41 | } 42 | 43 | continue; 44 | } 45 | 46 | $middlewareConnections[$id][$tag['connection']] = $tag['priority'] ?? null; 47 | } 48 | } 49 | 50 | foreach (array_keys($container->getParameter('doctrine.connections')) as $name) { 51 | $middlewareRefs = []; 52 | $i = 0; 53 | foreach ($middlewareAbstractDefs as $id => $abstractDef) { 54 | if (isset($middlewareConnections[$id]) && ! array_key_exists($name, $middlewareConnections[$id])) { 55 | continue; 56 | } 57 | 58 | $childDef = $container->setDefinition( 59 | $childId = sprintf('%s.%s', $id, $name), 60 | (new ChildDefinition($id)) 61 | ->setTags($abstractDef->getTags())->clearTag('doctrine.middleware') 62 | ->setAutoconfigured($abstractDef->isAutoconfigured()) 63 | ->setAutowired($abstractDef->isAutowired()), 64 | ); 65 | $middlewareRefs[$id] = [new Reference($childId), ++$i]; 66 | 67 | if (! is_subclass_of($abstractDef->getClass(), ConnectionNameAwareInterface::class)) { 68 | continue; 69 | } 70 | 71 | $childDef->addMethodCall('setConnectionName', [$name]); 72 | } 73 | 74 | $middlewareRefs = array_map( 75 | static fn (string $id, array $ref) => [ 76 | $middlewareConnections[$id][$name] ?? $middlewarePriorities[$id] ?? 0, 77 | $ref[1], 78 | $ref[0], 79 | ], 80 | array_keys($middlewareRefs), 81 | array_values($middlewareRefs), 82 | ); 83 | usort($middlewareRefs, static fn (array $a, array $b): int => $b[0] <=> $a[0] ?: $a[1] <=> $b[1]); 84 | $middlewareRefs = array_map(static fn (array $value): Reference => $value[2], $middlewareRefs); 85 | 86 | $container 87 | ->getDefinition(sprintf('doctrine.dbal.%s_connection.configuration', $name)) 88 | ->addMethodCall('setMiddlewares', [$middlewareRefs]); 89 | } 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/Controller/ProfilerController.php: -------------------------------------------------------------------------------- 1 | profiler->disable(); 42 | 43 | $profile = $this->profiler->loadProfile($token); 44 | $collector = $profile->getCollector('db'); 45 | 46 | assert($collector instanceof DoctrineDataCollector); 47 | 48 | $queries = $collector->getQueries(); 49 | 50 | if (! isset($queries[$connectionName][$query])) { 51 | return new Response('This query does not exist.'); 52 | } 53 | 54 | $query = $queries[$connectionName][$query]; 55 | if (! $query['explainable']) { 56 | return new Response('This query cannot be explained.'); 57 | } 58 | 59 | $connection = $this->registry->getConnection($connectionName); 60 | assert($connection instanceof Connection); 61 | try { 62 | $platform = $connection->getDatabasePlatform(); 63 | if ($platform instanceof SQLitePlatform) { 64 | $results = $this->explainSQLitePlatform($connection, $query); 65 | } elseif ($platform instanceof SQLServerPlatform) { 66 | throw new Exception('Explain for SQLServerPlatform is currently not supported. Contributions are welcome.'); 67 | } elseif ($platform instanceof OraclePlatform) { 68 | $results = $this->explainOraclePlatform($connection, $query); 69 | } else { 70 | $results = $this->explainOtherPlatform($connection, $query); 71 | } 72 | } catch (Throwable) { 73 | return new Response('This query cannot be explained.'); 74 | } 75 | 76 | return new Response($this->twig->render('@Doctrine/Collector/explain.html.twig', [ 77 | 'data' => $results, 78 | 'query' => $query, 79 | ])); 80 | } 81 | 82 | /** 83 | * @param mixed[] $query 84 | * 85 | * @return mixed[] 86 | */ 87 | private function explainSQLitePlatform(Connection $connection, array $query): array 88 | { 89 | $params = $query['params']; 90 | 91 | if ($params instanceof Data) { 92 | $params = $params->getValue(true); 93 | } 94 | 95 | return $connection->executeQuery('EXPLAIN QUERY PLAN ' . $query['sql'], $params, $query['types']) 96 | ->fetchAllAssociative(); 97 | } 98 | 99 | /** 100 | * @param mixed[] $query 101 | * 102 | * @return mixed[] 103 | */ 104 | private function explainOtherPlatform(Connection $connection, array $query): array 105 | { 106 | $params = $query['params']; 107 | 108 | if ($params instanceof Data) { 109 | $params = $params->getValue(true); 110 | } 111 | 112 | return $connection->executeQuery('EXPLAIN ' . $query['sql'], $params, $query['types']) 113 | ->fetchAllAssociative(); 114 | } 115 | 116 | /** 117 | * @param mixed[] $query 118 | * 119 | * @return mixed[] 120 | */ 121 | private function explainOraclePlatform(Connection $connection, array $query): array 122 | { 123 | $connection->executeQuery('EXPLAIN PLAN FOR ' . $query['sql']); 124 | 125 | return $connection->executeQuery('SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY())') 126 | ->fetchAllAssociative(); 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /src/Command/CreateDatabaseDoctrineCommand.php: -------------------------------------------------------------------------------- 1 | setName('doctrine:database:create') 29 | ->setDescription('Creates the configured database') 30 | ->addOption('connection', 'c', InputOption::VALUE_REQUIRED, 'The connection to use for this command') 31 | ->addOption('if-not-exists', null, InputOption::VALUE_NONE, 'Don\'t trigger an error, when the database already exists') 32 | ->setHelp(<<<'EOT' 33 | The %command.name% command creates the default connections database: 34 | 35 | php %command.full_name% 36 | 37 | You can also optionally specify the name of a connection to create the database for: 38 | 39 | php %command.full_name% --connection=default 40 | EOT); 41 | } 42 | 43 | protected function execute(InputInterface $input, OutputInterface $output): int 44 | { 45 | $connectionName = $input->getOption('connection'); 46 | if (empty($connectionName)) { 47 | $connectionName = $this->getDoctrine()->getDefaultConnectionName(); 48 | } 49 | 50 | $connection = $this->getDoctrineConnection($connectionName); 51 | 52 | $ifNotExists = $input->getOption('if-not-exists'); 53 | 54 | $params = $connection->getParams(); 55 | 56 | if (isset($params['primary'])) { 57 | $params = $params['primary']; 58 | } 59 | 60 | $hasPath = isset($params['path']); 61 | $name = $hasPath ? $params['path'] : ($params['dbname'] ?? false); 62 | if (! $name) { 63 | throw new InvalidArgumentException("Connection does not contain a 'path' or 'dbname' parameter and cannot be created."); 64 | } 65 | 66 | // Need to get rid of _every_ occurrence of dbname from connection configuration as we have already extracted all relevant info from url 67 | /** @psalm-suppress InvalidArrayOffset Need to be compatible with DBAL < 4, which still has `$params['url']` */ 68 | /** @phpstan-ignore unset.offset */ 69 | unset($params['dbname'], $params['path'], $params['url']); 70 | 71 | if ($connection->getDatabasePlatform() instanceof PostgreSQLPlatform) { 72 | /** @phpstan-ignore nullCoalesce.offset (needed for DBAL < 4) */ 73 | $params['dbname'] = $params['default_dbname'] ?? 'postgres'; 74 | } 75 | 76 | $tmpConnection = DriverManager::getConnection($params, $connection->getConfiguration()); 77 | $schemaManager = $tmpConnection->createSchemaManager(); 78 | $shouldNotCreateDatabase = $ifNotExists && in_array($name, $schemaManager->listDatabases()); 79 | 80 | // Only quote if we don't have a path 81 | if (! $hasPath) { 82 | $name = $tmpConnection->getDatabasePlatform()->quoteSingleIdentifier($name); 83 | } 84 | 85 | $error = false; 86 | try { 87 | if ($shouldNotCreateDatabase) { 88 | $output->writeln(sprintf('Database %s for connection named %s already exists. Skipped.', $name, $connectionName)); 89 | } else { 90 | $schemaManager->createDatabase($name); 91 | $output->writeln(sprintf('Created database %s for connection named %s', $name, $connectionName)); 92 | } 93 | } catch (Throwable $e) { 94 | $output->writeln(sprintf('Could not create database %s for connection named %s', $name, $connectionName)); 95 | $output->writeln(sprintf('%s', $e->getMessage())); 96 | $error = true; 97 | } 98 | 99 | $tmpConnection->close(); 100 | 101 | return $error ? 1 : 0; 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/Repository/ContainerRepositoryFactory.php: -------------------------------------------------------------------------------- 1 | */ 30 | private array $managedRepositories = []; 31 | 32 | /** @param ContainerInterface $container A service locator containing the repositories */ 33 | public function __construct( 34 | private readonly ContainerInterface $container, 35 | ) { 36 | } 37 | 38 | /** 39 | * Gets the repository for an entity class. 40 | * 41 | * @param class-string $entityName 42 | * 43 | * @return EntityRepository 44 | * 45 | * @template T of object 46 | */ 47 | public function getRepository(EntityManagerInterface $entityManager, string $entityName): EntityRepository 48 | { 49 | $metadata = $entityManager->getClassMetadata($entityName); 50 | $repositoryServiceId = $metadata->customRepositoryClassName; 51 | 52 | $customRepositoryName = $metadata->customRepositoryClassName; 53 | if ($customRepositoryName !== null) { 54 | // fetch from the container 55 | if ($this->container->has($customRepositoryName)) { 56 | $repository = $this->container->get($customRepositoryName); 57 | 58 | if (! $repository instanceof EntityRepository) { 59 | throw new RuntimeException(sprintf( 60 | 'The service "%s" must extend EntityRepository (e.g. by extending ServiceEntityRepository), "%s" given.', 61 | $repositoryServiceId, 62 | get_debug_type($repository), 63 | )); 64 | } 65 | 66 | /** @phpstan-var EntityRepository */ 67 | return $repository; 68 | } 69 | 70 | // if not in the container but the class/id implements the interface, throw an error 71 | if (is_a($customRepositoryName, ServiceEntityRepositoryInterface::class, true)) { 72 | throw new RuntimeException(sprintf('The "%s" entity repository implements "%s", but its service could not be found. Make sure the service exists and is tagged with "%s".', $customRepositoryName, ServiceEntityRepositoryInterface::class, ServiceRepositoryCompilerPass::REPOSITORY_SERVICE_TAG)); 73 | } 74 | 75 | if (! class_exists($customRepositoryName)) { 76 | throw new RuntimeException(sprintf('The "%s" entity has a repositoryClass set to "%s", but this is not a valid class. Check your class naming. If this is meant to be a service id, make sure this service exists and is tagged with "%s".', $metadata->name, $customRepositoryName, ServiceRepositoryCompilerPass::REPOSITORY_SERVICE_TAG)); 77 | } 78 | 79 | // allow the repository to be created below 80 | } 81 | 82 | return $this->getOrCreateRepository($entityManager, $metadata); 83 | } 84 | 85 | /** 86 | * @param ClassMetadata $metadata 87 | * 88 | * @return ObjectRepository 89 | * 90 | * @template TEntity of object 91 | */ 92 | private function getOrCreateRepository( 93 | EntityManagerInterface $entityManager, 94 | ClassMetadata $metadata, 95 | ): ObjectRepository { 96 | $repositoryHash = $metadata->getName() . spl_object_hash($entityManager); 97 | if (isset($this->managedRepositories[$repositoryHash])) { 98 | /** @phpstan-var ObjectRepository */ 99 | return $this->managedRepositories[$repositoryHash]; 100 | } 101 | 102 | $repositoryClassName = $metadata->customRepositoryClassName ?: $entityManager->getConfiguration()->getDefaultRepositoryClassName(); 103 | 104 | /** @phpstan-var ObjectRepository */ 105 | return $this->managedRepositories[$repositoryHash] = new $repositoryClassName($entityManager, $metadata); 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /config/dbal.php: -------------------------------------------------------------------------------- 1 | parameters() 27 | ->set('doctrine.entity_managers', []) 28 | ->set('doctrine.default_entity_manager', ''); 29 | 30 | $container->services() 31 | 32 | ->alias(Connection::class, 'database_connection') 33 | ->alias(ManagerRegistry::class, 'doctrine') 34 | 35 | ->set('data_collector.doctrine', DoctrineDataCollector::class) 36 | ->args([ 37 | service('doctrine'), 38 | true, 39 | service('doctrine.debug_data_holder')->nullOnInvalid(), 40 | ]) 41 | ->tag('data_collector', ['template' => '@Doctrine/Collector/db.html.twig', 'id' => 'db', 'priority' => 250]) 42 | 43 | ->set('doctrine.dbal.connection_factory', ConnectionFactory::class) 44 | ->args([ 45 | (string) param('doctrine.dbal.connection_factory.types'), 46 | service('doctrine.dbal.connection_factory.dsn_parser'), 47 | ]) 48 | 49 | ->set('doctrine.dbal.connection_factory.dsn_parser', DsnParser::class) 50 | ->args([ 51 | [], 52 | ]) 53 | 54 | ->set('doctrine.dbal.connection', Connection::class) 55 | ->abstract() 56 | ->factory([service('doctrine.dbal.connection_factory'), 'createConnection']) 57 | 58 | ->set('doctrine.dbal.connection.event_manager', ContainerAwareEventManager::class) 59 | ->abstract() 60 | ->args([ 61 | service('service_container'), 62 | ]) 63 | 64 | ->set('doctrine.dbal.connection.configuration', Configuration::class) 65 | ->abstract() 66 | 67 | ->set('doctrine', Registry::class) 68 | ->public() 69 | ->args([ 70 | service('service_container'), 71 | (string) param('doctrine.connections'), 72 | (string) param('doctrine.entity_managers'), 73 | (string) param('doctrine.default_connection'), 74 | (string) param('doctrine.default_entity_manager'), 75 | ]) 76 | ->tag('kernel.reset', ['method' => 'reset']) 77 | 78 | ->set('doctrine.twig.doctrine_extension', DoctrineExtension::class) 79 | ->tag('twig.extension') 80 | 81 | ->set('doctrine.dbal.schema_asset_filter_manager', SchemaAssetsFilterManager::class) 82 | ->abstract() 83 | 84 | ->set('doctrine.database_create_command', CreateDatabaseDoctrineCommand::class) 85 | ->args([ 86 | service('doctrine'), 87 | ]) 88 | ->tag('console.command', ['command' => 'doctrine:database:create']) 89 | 90 | ->set('doctrine.database_drop_command', DropDatabaseDoctrineCommand::class) 91 | ->args([ 92 | service('doctrine'), 93 | ]) 94 | ->tag('console.command', ['command' => 'doctrine:database:drop']) 95 | 96 | ->set(RunSqlCommand::class) 97 | ->args([ 98 | service(ManagerRegistryAwareConnectionProvider::class)->nullOnInvalid(), 99 | ]) 100 | ->tag('console.command', ['command' => 'dbal:run-sql']) 101 | 102 | ->set(ProfilerController::class) 103 | ->args([ 104 | service('twig'), 105 | service('doctrine'), 106 | service('profiler'), 107 | ]) 108 | ->tag('controller.service_arguments') 109 | 110 | ->set('doctrine.dbal.idle_connection_listener', Listener::class) 111 | ->args([ 112 | service('doctrine.dbal.connection_expiries'), 113 | service('service_container'), 114 | ]) 115 | ->tag('kernel.event_subscriber') 116 | 117 | ->set('doctrine.dbal.default_schema_manager_factory', DefaultSchemaManagerFactory::class); 118 | }; 119 | -------------------------------------------------------------------------------- /src/DoctrineBundle.php: -------------------------------------------------------------------------------- 1 | addCompilerPass(new class () implements CompilerPassInterface { 38 | public function process(ContainerBuilder $container): void 39 | { 40 | if ($container->has('session.handler')) { 41 | return; 42 | } 43 | 44 | $container->removeDefinition('doctrine.orm.listeners.pdo_session_handler_schema_listener'); 45 | } 46 | }, PassConfig::TYPE_BEFORE_OPTIMIZATION); 47 | 48 | $container->addCompilerPass(new RegisterEventListenersAndSubscribersPass('doctrine.connections', 'doctrine.dbal.%s_connection.event_manager', 'doctrine'), PassConfig::TYPE_BEFORE_OPTIMIZATION); 49 | 50 | if ($container->hasExtension('security')) { 51 | $security = $container->getExtension('security'); 52 | 53 | if ($security instanceof SecurityExtension) { 54 | $security->addUserProviderFactory(new EntityFactory('entity', 'doctrine.orm.security.user.provider')); 55 | } 56 | } 57 | 58 | $container->addCompilerPass(new DoctrineValidationPass('orm')); 59 | $container->addCompilerPass(new EntityListenerPass()); 60 | $container->addCompilerPass(new ServiceRepositoryCompilerPass()); 61 | $container->addCompilerPass(new IdGeneratorPass()); 62 | $container->addCompilerPass(new DbalSchemaFilterPass()); 63 | $container->addCompilerPass(new CacheSchemaSubscriberPass(), PassConfig::TYPE_BEFORE_REMOVING, -10); 64 | $container->addCompilerPass(new RemoveProfilerControllerPass()); 65 | $container->addCompilerPass(new RemoveLoggingMiddlewarePass()); 66 | $container->addCompilerPass(new MiddlewaresPass()); 67 | $container->addCompilerPass(new RegisterUidTypePass()); 68 | 69 | if (! class_exists(RegisterDatePointTypePass::class)) { 70 | return; 71 | } 72 | 73 | $container->addCompilerPass(new RegisterDatePointTypePass()); 74 | } 75 | 76 | public function shutdown(): void 77 | { 78 | // Clear all entity managers to clear references to entities for GC 79 | if ($this->container->hasParameter('doctrine.entity_managers')) { 80 | foreach ($this->container->getParameter('doctrine.entity_managers') as $id) { 81 | if (! $this->container->initialized($id)) { 82 | continue; 83 | } 84 | 85 | $this->container->get($id)->clear(); 86 | } 87 | } 88 | 89 | // Close all connections to avoid reaching too many connections in the process when booting again later (tests) 90 | if (! $this->container->hasParameter('doctrine.connections')) { 91 | return; 92 | } 93 | 94 | foreach ($this->container->getParameter('doctrine.connections') as $id) { 95 | if (! $this->container->initialized($id)) { 96 | continue; 97 | } 98 | 99 | $this->container->get($id)->close(); 100 | } 101 | } 102 | 103 | public function registerCommands(Application $application): void 104 | { 105 | } 106 | 107 | public function getPath(): string 108 | { 109 | return dirname(__DIR__); 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /src/Twig/DoctrineExtension.php: -------------------------------------------------------------------------------- 1 | ['html']]), 49 | new TwigFilter('doctrine_format_sql', [$this, 'formatSql'], ['is_safe' => ['html']]), 50 | new TwigFilter('doctrine_replace_query_parameters', [$this, 'replaceQueryParameters']), 51 | ]; 52 | } 53 | 54 | /** 55 | * Escape parameters of a SQL query 56 | * DON'T USE THIS FUNCTION OUTSIDE ITS INTENDED SCOPE 57 | * 58 | * @internal 59 | */ 60 | public static function escapeFunction(mixed $parameter): string|int|float 61 | { 62 | $result = $parameter; 63 | 64 | switch (true) { 65 | // Check if result is non-unicode string using PCRE_UTF8 modifier 66 | case is_string($result) && ! preg_match('//u', $result): 67 | $result = '0x' . strtoupper(bin2hex($result)); 68 | break; 69 | 70 | case is_string($result): 71 | $result = "'" . addslashes($result) . "'"; 72 | break; 73 | 74 | case is_array($result): 75 | foreach ($result as &$value) { 76 | $value = static::escapeFunction($value); 77 | } 78 | 79 | $result = implode(', ', $result) ?: 'NULL'; 80 | break; 81 | 82 | case $result instanceof Stringable: 83 | $result = addslashes((string) $result); 84 | break; 85 | 86 | case $result === null: 87 | $result = 'NULL'; 88 | break; 89 | 90 | case is_bool($result): 91 | $result = $result ? '1' : '0'; 92 | break; 93 | } 94 | 95 | return $result; 96 | } 97 | 98 | /** 99 | * Return a query with the parameters replaced 100 | * 101 | * @param array|Data $parameters 102 | */ 103 | public function replaceQueryParameters(string $query, array|Data $parameters): string 104 | { 105 | if ($parameters instanceof Data) { 106 | $parameters = $parameters->getValue(true); 107 | } 108 | 109 | $keys = array_keys($parameters); 110 | if (count(array_filter($keys, 'is_int')) === count($keys)) { 111 | $parameters = array_values($parameters); 112 | } 113 | 114 | $i = 0; 115 | 116 | return preg_replace_callback( 117 | '/(?setUpSqlFormatter(); 137 | 138 | return $this->sqlFormatter->highlight($sql); 139 | } 140 | 141 | public function formatSql(string $sql, bool $highlight): string 142 | { 143 | $this->setUpSqlFormatter($highlight); 144 | 145 | return $this->sqlFormatter->format($sql); 146 | } 147 | 148 | private function setUpSqlFormatter(bool $highlight = true): void 149 | { 150 | $this->sqlFormatter = new SqlFormatter($highlight ? new HtmlHighlighter([ 151 | HtmlHighlighter::HIGHLIGHT_PRE => 'class="highlight highlight-sql"', 152 | HtmlHighlighter::HIGHLIGHT_QUOTE => 'class="string"', 153 | HtmlHighlighter::HIGHLIGHT_BACKTICK_QUOTE => 'class="string"', 154 | HtmlHighlighter::HIGHLIGHT_RESERVED => 'class="keyword"', 155 | HtmlHighlighter::HIGHLIGHT_BOUNDARY => 'class="symbol"', 156 | HtmlHighlighter::HIGHLIGHT_NUMBER => 'class="number"', 157 | HtmlHighlighter::HIGHLIGHT_WORD => 'class="word"', 158 | HtmlHighlighter::HIGHLIGHT_ERROR => 'class="error"', 159 | HtmlHighlighter::HIGHLIGHT_COMMENT => 'class="comment"', 160 | HtmlHighlighter::HIGHLIGHT_VARIABLE => 'class="variable"', 161 | ]) : new NullHighlighter()); 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /src/Repository/ServiceEntityRepository.php: -------------------------------------------------------------------------------- 1 | 37 | */ 38 | class ServiceEntityRepository extends EntityRepository implements ServiceEntityRepositoryInterface 39 | { 40 | /** @var EntityRepository */ 41 | private EntityRepository|null $repository = null; 42 | 43 | /** @param class-string $entityClass The class name of the entity this repository manages */ 44 | public function __construct( 45 | private readonly ManagerRegistry $registry, 46 | private readonly string $entityClass, 47 | ) { 48 | } 49 | 50 | public function createQueryBuilder(string $alias, string|null $indexBy = null): QueryBuilder 51 | { 52 | return ($this->repository ??= $this->resolveRepository()) 53 | ->createQueryBuilder($alias, $indexBy); 54 | } 55 | 56 | public function createResultSetMappingBuilder(string $alias): ResultSetMappingBuilder 57 | { 58 | return ($this->repository ??= $this->resolveRepository()) 59 | ->createResultSetMappingBuilder($alias); 60 | } 61 | 62 | public function find(mixed $id, LockMode|int|null $lockMode = null, int|null $lockVersion = null): object|null 63 | { 64 | /** @psalm-suppress InvalidReturnStatement This proxy is used only in combination with newer parent class */ 65 | return ($this->repository ??= $this->resolveRepository()) 66 | ->find($id, $lockMode, $lockVersion); 67 | } 68 | 69 | /** 70 | * {@inheritDoc} 71 | * 72 | * @psalm-suppress InvalidReturnStatement This proxy is used only in combination with newer parent class 73 | * @psalm-suppress InvalidReturnType This proxy is used only in combination with newer parent class 74 | */ 75 | public function findBy(array $criteria, array|null $orderBy = null, int|null $limit = null, int|null $offset = null): array 76 | { 77 | return ($this->repository ??= $this->resolveRepository()) 78 | ->findBy($criteria, $orderBy, $limit, $offset); 79 | } 80 | 81 | /** {@inheritDoc} */ 82 | public function findOneBy(array $criteria, array|null $orderBy = null): object|null 83 | { 84 | /** @psalm-suppress InvalidReturnStatement This proxy is used only in combination with newer parent class */ 85 | return ($this->repository ??= $this->resolveRepository()) 86 | ->findOneBy($criteria, $orderBy); 87 | } 88 | 89 | /** {@inheritDoc} */ 90 | public function count(array $criteria = []): int 91 | { 92 | return ($this->repository ??= $this->resolveRepository())->count($criteria); 93 | } 94 | 95 | /** 96 | * {@inheritDoc} 97 | */ 98 | public function __call(string $method, array $arguments): mixed 99 | { 100 | return ($this->repository ??= $this->resolveRepository())->$method(...$arguments); 101 | } 102 | 103 | protected function getEntityName(): string 104 | { 105 | return ($this->repository ??= $this->resolveRepository())->getEntityName(); 106 | } 107 | 108 | protected function getEntityManager(): EntityManagerInterface 109 | { 110 | return ($this->repository ??= $this->resolveRepository())->getEntityManager(); 111 | } 112 | 113 | /** @psalm-suppress InvalidReturnType This proxy is used only in combination with newer parent class */ 114 | protected function getClassMetadata(): ClassMetadata 115 | { 116 | /** @psalm-suppress InvalidReturnStatement This proxy is used only in combination with newer parent class */ 117 | return ($this->repository ??= $this->resolveRepository())->getClassMetadata(); 118 | } 119 | 120 | /** @phpstan-return AbstractLazyCollection&Selectable */ 121 | public function matching(Criteria $criteria): AbstractLazyCollection&Selectable 122 | { 123 | return ($this->repository ??= $this->resolveRepository())->matching($criteria); 124 | } 125 | 126 | /** @return EntityRepository */ 127 | private function resolveRepository(): EntityRepository 128 | { 129 | $manager = $this->registry->getManagerForClass($this->entityClass); 130 | 131 | if (! $manager instanceof EntityManagerInterface) { 132 | throw new LogicException(sprintf( 133 | 'Could not find the entity manager for class "%s". Check your Doctrine configuration to make sure it is configured to load this entity’s metadata.', 134 | $this->entityClass, 135 | )); 136 | } 137 | 138 | /** @var ClassMetadata $classMetadata */ 139 | $classMetadata = $manager->getClassMetadata($this->entityClass); 140 | 141 | return new EntityRepository($manager, $classMetadata); 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /src/Command/DropDatabaseDoctrineCommand.php: -------------------------------------------------------------------------------- 1 | setName('doctrine:database:drop') 36 | ->setDescription('Drops the configured database') 37 | ->addOption('connection', 'c', InputOption::VALUE_REQUIRED, 'The connection to use for this command') 38 | ->addOption('if-exists', null, InputOption::VALUE_NONE, 'Don\'t trigger an error, when the database doesn\'t exist') 39 | ->addOption('force', 'f', InputOption::VALUE_NONE, 'Set this parameter to execute this action') 40 | ->setHelp(<<<'EOT' 41 | The %command.name% command drops the default connections database: 42 | 43 | php %command.full_name% 44 | 45 | The --force parameter has to be used to actually drop the database. 46 | 47 | You can also optionally specify the name of a connection to drop the database for: 48 | 49 | php %command.full_name% --connection=default 50 | 51 | Be careful: All data in a given database will be lost when executing this command. 52 | EOT); 53 | } 54 | 55 | protected function execute(InputInterface $input, OutputInterface $output): int 56 | { 57 | $connectionName = $input->getOption('connection'); 58 | if (empty($connectionName)) { 59 | $connectionName = $this->getDoctrine()->getDefaultConnectionName(); 60 | } 61 | 62 | $connection = $this->getDoctrineConnection($connectionName); 63 | 64 | $ifExists = $input->getOption('if-exists'); 65 | 66 | $params = $connection->getParams(); 67 | 68 | if (isset($params['primary'])) { 69 | $params = $params['primary']; 70 | } 71 | 72 | $name = $params['path'] ?? ($params['dbname'] ?? false); 73 | if (! $name) { 74 | throw new InvalidArgumentException("Connection does not contain a 'path' or 'dbname' parameter and cannot be dropped."); 75 | } 76 | 77 | /* @phpstan-ignore unset.offset (Need to be compatible with DBAL < 4, which still has `$params['url']`) */ 78 | unset($params['dbname'], $params['url']); 79 | 80 | if ($connection->getDatabasePlatform() instanceof PostgreSQLPlatform) { 81 | /** @phpstan-ignore nullCoalesce.offset (for DBAL < 4) */ 82 | $params['dbname'] = $params['default_dbname'] ?? 'postgres'; 83 | } 84 | 85 | if (! $input->getOption('force')) { 86 | $output->writeln('ATTENTION: This operation should not be executed in a production environment.'); 87 | $output->writeln(''); 88 | $output->writeln(sprintf('Would drop the database %s for connection named %s.', $name, $connectionName)); 89 | $output->writeln('Please run the operation with --force to execute'); 90 | $output->writeln('All data will be lost!'); 91 | 92 | return self::RETURN_CODE_NO_FORCE; 93 | } 94 | 95 | // Reopen connection without database name set 96 | // as some vendors do not allow dropping the database connected to. 97 | $connection->close(); 98 | $connection = DriverManager::getConnection($params, $connection->getConfiguration()); 99 | $schemaManager = $connection->createSchemaManager(); 100 | $shouldDropDatabase = ! $ifExists || in_array($name, $schemaManager->listDatabases()); 101 | 102 | // Only quote if we don't have a path 103 | if (! isset($params['path'])) { 104 | $name = $connection->getDatabasePlatform()->quoteSingleIdentifier($name); 105 | } 106 | 107 | try { 108 | if ($shouldDropDatabase) { 109 | if ($schemaManager instanceof SQLiteSchemaManager) { 110 | // dropDatabase() is deprecated for Sqlite 111 | $connection->close(); 112 | if (file_exists($name)) { 113 | unlink($name); 114 | } 115 | } else { 116 | $schemaManager->dropDatabase($name); 117 | } 118 | 119 | $output->writeln(sprintf('Dropped database %s for connection named %s', $name, $connectionName)); 120 | } else { 121 | $output->writeln(sprintf('Database %s for connection named %s doesn\'t exist. Skipped.', $name, $connectionName)); 122 | } 123 | 124 | return 0; 125 | } catch (Throwable $e) { 126 | $output->writeln(sprintf('Could not drop database %s for connection named %s', $name, $connectionName)); 127 | $output->writeln(sprintf('%s', $e->getMessage())); 128 | 129 | return self::RETURN_CODE_NOT_DROP; 130 | } 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /src/DependencyInjection/Compiler/EntityListenerPass.php: -------------------------------------------------------------------------------- 1 | findTaggedServiceIds('doctrine.orm.entity_listener', true) as $id => $tags) { 36 | foreach ($tags as $attributes) { 37 | $serviceTags[] = [ 38 | 'serviceId' => $id, 39 | 'attributes' => $attributes, 40 | ]; 41 | } 42 | } 43 | 44 | usort($serviceTags, static fn (array $a, array $b) => ($b['attributes']['priority'] ?? 0) <=> ($a['attributes']['priority'] ?? 0)); 45 | 46 | foreach ($serviceTags as $tag) { 47 | $id = $tag['serviceId']; 48 | $attributes = $tag['attributes']; 49 | $name = $attributes['entity_manager'] ?? $container->getParameter('doctrine.default_entity_manager'); 50 | $entityManager = sprintf('doctrine.orm.%s_entity_manager', $name); 51 | 52 | if (! $container->hasDefinition($entityManager)) { 53 | continue; 54 | } 55 | 56 | $resolverId = sprintf('doctrine.orm.%s_entity_listener_resolver', $name); 57 | 58 | if (! $container->has($resolverId)) { 59 | continue; 60 | } 61 | 62 | $resolver = $container->findDefinition($resolverId); 63 | $resolver->setPublic(true); 64 | 65 | if (isset($attributes['entity'])) { 66 | $this->attachToListener($container, $name, $this->getConcreteDefinitionClass($container->findDefinition($id), $container, $id), $attributes); 67 | } 68 | 69 | $resolverClass = $this->getResolverClass($resolver, $container, $resolverId); 70 | $resolverSupportsLazyListeners = is_a($resolverClass, EntityListenerServiceResolver::class, true); 71 | 72 | $lazyByAttribute = isset($attributes['lazy']) && $attributes['lazy']; 73 | if ($lazyByAttribute && ! $resolverSupportsLazyListeners) { 74 | throw new InvalidArgumentException(sprintf( 75 | 'Lazy-loaded entity listeners can only be resolved by a resolver implementing %s.', 76 | EntityListenerServiceResolver::class, 77 | )); 78 | } 79 | 80 | if (! isset($attributes['lazy']) && $resolverSupportsLazyListeners || $lazyByAttribute) { 81 | $listener = $container->findDefinition($id); 82 | 83 | $resolver->addMethodCall('registerService', [$this->getConcreteDefinitionClass($listener, $container, $id), $id]); 84 | 85 | // if the resolver uses the default class we will use a service locator for all listeners 86 | if ($resolverClass === ContainerEntityListenerResolver::class) { 87 | if (! isset($lazyServiceReferencesByResolver[$resolverId])) { 88 | $lazyServiceReferencesByResolver[$resolverId] = []; 89 | } 90 | 91 | $lazyServiceReferencesByResolver[$resolverId][$id] = new Reference($id); 92 | } else { 93 | $listener->setPublic(true); 94 | } 95 | } else { 96 | $resolver->addMethodCall('register', [new Reference($id)]); 97 | } 98 | } 99 | 100 | foreach ($lazyServiceReferencesByResolver as $resolverId => $listenerReferences) { 101 | $container->findDefinition($resolverId)->setArgument(0, ServiceLocatorTagPass::register($container, $listenerReferences)); 102 | } 103 | } 104 | 105 | /** @param array{entity: class-string, event?: ?string, method?: string} $attributes */ 106 | private function attachToListener(ContainerBuilder $container, string $name, string $class, array $attributes): void 107 | { 108 | $listenerId = sprintf('doctrine.orm.%s_listeners.attach_entity_listeners', $name); 109 | 110 | if (! $container->has($listenerId)) { 111 | return; 112 | } 113 | 114 | $args = [ 115 | $attributes['entity'], 116 | $class, 117 | $attributes['event'] ?? null, 118 | ]; 119 | 120 | if (isset($attributes['method'])) { 121 | $args[] = $attributes['method']; 122 | } elseif (isset($attributes['event']) && ! method_exists($class, $attributes['event']) && method_exists($class, '__invoke')) { 123 | $args[] = '__invoke'; 124 | } 125 | 126 | $container->findDefinition($listenerId)->addMethodCall('addEntityListener', $args); 127 | } 128 | 129 | private function getResolverClass(Definition $resolver, ContainerBuilder $container, string $id): string 130 | { 131 | $resolverClass = $this->getConcreteDefinitionClass($resolver, $container, $id); 132 | 133 | if (substr($resolverClass, 0, 1) === '%') { 134 | // resolve container parameter first 135 | $resolverClass = $container->getParameterBag()->resolveValue($resolverClass); 136 | } 137 | 138 | return $resolverClass; 139 | } 140 | 141 | private function getConcreteDefinitionClass(Definition $definition, ContainerBuilder $container, string $id): string 142 | { 143 | $class = $definition->getClass(); 144 | if ($class) { 145 | return $class; 146 | } 147 | 148 | while ($definition instanceof ChildDefinition) { 149 | $definition = $container->findDefinition($definition->getParent()); 150 | 151 | $class = $definition->getClass(); 152 | if ($class) { 153 | return $class; 154 | } 155 | } 156 | 157 | throw new InvalidArgumentException(sprintf('The service "%s" must define its class.', $id)); 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /src/DependencyInjection/Compiler/DoctrineOrmMappingsPass.php: -------------------------------------------------------------------------------- 1 | createConnection($params, $config, $eventManager, $mappingTypes) 163 | + $connectionFactory->createConnection($params, $config, $mappingTypes) 164 | ``` 165 | 166 | DependencyInjection namespace becomes internal 167 | ---------------------------------------------- 168 | 169 | The `Doctrine\Bundle\DoctrineBundle\DependencyInjection` namespace is now 170 | considered internal, and all classes inside it are marked as final, except for 171 | `Doctrine\Bundle\DoctrineBundle\DependencyInjection\Compiler\DoctrineORMMappingsPass`. 172 | Don't reference any classes from this namespace directly, except the one 173 | mentioned earlier. 174 | 175 | `Doctrine\Bundle\DoctrineBundle\EventSubscriber\EventListenerInterface` has been removed 176 | ---------------------------------------------------------------------------------------- 177 | 178 | Use the `#[Doctrine\Bundle\DoctrineBundle\Attribute\AsDoctrineListener]` 179 | attribute instead. 180 | 181 | `Doctrine\Bundle\DoctrineBundle\Dbal\BlacklistSchemaAssetsFilter` has been removed 182 | ---------------------------------------------------------------------------------- 183 | 184 | Implement your own include/exclude mechanism instead. 185 | 186 | Type declarations 187 | ----------------- 188 | 189 | Native type declarations have been added to all constants, properties, and 190 | methods. 191 | 192 | Twig filters 193 | ------------ 194 | 195 | The Twig filter `doctrine_pretty_query` has been removed. 196 | -------------------------------------------------------------------------------- /src/ConnectionFactory.php: -------------------------------------------------------------------------------- 1 | 'ibm_db2', 37 | 'mssql' => 'pdo_sqlsrv', 38 | 'mysql' => 'pdo_mysql', 39 | 'mysql2' => 'pdo_mysql', // Amazon RDS, for some weird reason 40 | 'postgres' => 'pdo_pgsql', 41 | 'postgresql' => 'pdo_pgsql', 42 | 'pgsql' => 'pdo_pgsql', 43 | 'sqlite' => 'pdo_sqlite', 44 | 'sqlite3' => 'pdo_sqlite', 45 | ]; 46 | 47 | /** @phpstan-ignore property.onlyWritten */ 48 | private readonly DsnParser $dsnParser; 49 | 50 | private bool $initialized = false; 51 | 52 | /** @param mixed[][] $typesConfig */ 53 | public function __construct( 54 | private readonly array $typesConfig = [], 55 | DsnParser|null $dsnParser = null, 56 | ) { 57 | $this->dsnParser = $dsnParser ?? new DsnParser(self::DEFAULT_SCHEME_MAP); 58 | } 59 | 60 | /** 61 | * Create a connection by name. 62 | * 63 | * @param mixed[] $params 64 | * @param array $mappingTypes 65 | * @phpstan-param Params $params 66 | */ 67 | public function createConnection( 68 | array $params, 69 | Configuration|null $config = null, 70 | array $mappingTypes = [], 71 | ): Connection { 72 | if (! $this->initialized) { 73 | $this->initializeTypes(); 74 | } 75 | 76 | $params = $this->parseDatabaseUrl($params); 77 | 78 | // URL support for PrimaryReplicaConnection 79 | if (isset($params['primary'])) { 80 | $params['primary'] = $this->parseDatabaseUrl($params['primary']); 81 | } 82 | 83 | if (isset($params['replica'])) { 84 | foreach ($params['replica'] as $key => $replicaParams) { 85 | $params['replica'][$key] = $this->parseDatabaseUrl($replicaParams); 86 | } 87 | } 88 | 89 | /** @phpstan-ignore-next-line We should adjust when https://github.com/phpstan/phpstan/issues/12414 is fixed */ 90 | if (! isset($params['pdo']) && (! isset($params['charset']) || isset($params['dbname_suffix']))) { 91 | $wrapperClass = null; 92 | 93 | if (isset($params['wrapperClass'])) { 94 | if (! is_subclass_of($params['wrapperClass'], Connection::class)) { 95 | throw InvalidWrapperClass::new($params['wrapperClass']); 96 | } 97 | 98 | $wrapperClass = $params['wrapperClass']; 99 | $params['wrapperClass'] = null; 100 | } 101 | 102 | $connection = DriverManager::getConnection($params, $config); 103 | $params = $this->addDatabaseSuffix($connection->getParams()); 104 | $driver = $connection->getDriver(); 105 | $platform = $driver->getDatabasePlatform(new StaticServerVersionProvider( 106 | $params['serverVersion'] ?? $params['primary']['serverVersion'] ?? '', 107 | )); 108 | 109 | if (! isset($params['charset'])) { 110 | if ($platform instanceof AbstractMySQLPlatform) { 111 | $params['charset'] = 'utf8mb4'; 112 | if (! isset($params['defaultTableOptions']['collation'])) { 113 | $params['defaultTableOptions']['collation'] = 'utf8mb4_unicode_ci'; 114 | } 115 | } else { 116 | $params['charset'] = 'utf8'; 117 | } 118 | } 119 | 120 | if ($wrapperClass !== null) { 121 | $params['wrapperClass'] = $wrapperClass; 122 | } else { 123 | $wrapperClass = Connection::class; 124 | } 125 | 126 | $connection = new $wrapperClass($params, $driver, $config); 127 | } else { 128 | $connection = DriverManager::getConnection($params, $config); 129 | } 130 | 131 | if (! empty($mappingTypes)) { 132 | $platform = $this->getDatabasePlatform($connection); 133 | foreach ($mappingTypes as $dbType => $doctrineType) { 134 | $platform->registerDoctrineTypeMapping($dbType, $doctrineType); 135 | } 136 | } 137 | 138 | return $connection; 139 | } 140 | 141 | /** 142 | * Try to get the database platform. 143 | * 144 | * This could fail if types should be registered to an predefined/unused connection 145 | * and the platform version is unknown. 146 | * 147 | * @link https://github.com/doctrine/DoctrineBundle/issues/673 148 | * 149 | * @throws DBALException 150 | */ 151 | private function getDatabasePlatform(Connection $connection): AbstractPlatform 152 | { 153 | try { 154 | return $connection->getDatabasePlatform(); 155 | } catch (DriverException $driverException) { 156 | throw new ConnectionException( 157 | 'An exception occurred while establishing a connection to figure out your platform version.' . PHP_EOL . 158 | "You can circumvent this by setting a 'server_version' configuration value" . PHP_EOL . PHP_EOL . 159 | 'For further information have a look at:' . PHP_EOL . 160 | 'https://github.com/doctrine/DoctrineBundle/issues/673', 161 | 0, 162 | $driverException, 163 | ); 164 | } 165 | } 166 | 167 | /** 168 | * initialize the types 169 | */ 170 | private function initializeTypes(): void 171 | { 172 | foreach ($this->typesConfig as $typeName => $typeConfig) { 173 | if (Type::hasType($typeName)) { 174 | Type::overrideType($typeName, $typeConfig['class']); 175 | } else { 176 | Type::addType($typeName, $typeConfig['class']); 177 | } 178 | } 179 | 180 | $this->initialized = true; 181 | } 182 | 183 | /** 184 | * @param array $params 185 | * 186 | * @return array 187 | */ 188 | private function addDatabaseSuffix(array $params): array 189 | { 190 | if (isset($params['dbname']) && isset($params['dbname_suffix'])) { 191 | $params['dbname'] .= $params['dbname_suffix']; 192 | } 193 | 194 | foreach ($params['replica'] ?? [] as $key => $replicaParams) { 195 | if (! isset($replicaParams['dbname'], $replicaParams['dbname_suffix'])) { 196 | continue; 197 | } 198 | 199 | $params['replica'][$key]['dbname'] .= $replicaParams['dbname_suffix']; 200 | } 201 | 202 | if (isset($params['primary']['dbname'], $params['primary']['dbname_suffix'])) { 203 | $params['primary']['dbname'] .= $params['primary']['dbname_suffix']; 204 | } 205 | 206 | return $params; 207 | } 208 | 209 | /** 210 | * Extracts parts from a database URL, if present, and returns an 211 | * updated list of parameters. 212 | * 213 | * @param mixed[] $params The list of parameters. 214 | * @phpstan-param Params $params 215 | * 216 | * @return mixed[] A modified list of parameters with info from a database 217 | * URL extracted into individual parameter parts. 218 | * @phpstan-return Params 219 | * 220 | * @throws DBALException 221 | * 222 | * @phpstan-ignore throws.unusedType 223 | */ 224 | private function parseDatabaseUrl(array $params): array 225 | { 226 | /** @phpstan-ignore isset.offset (for DBAL < 4) */ 227 | if (! isset($params['url'])) { 228 | return $params; 229 | } 230 | 231 | /** @phpstan-ignore deadCode.unreachable */ 232 | try { 233 | $parsedParams = $this->dsnParser->parse($params['url']); 234 | } catch (MalformedDsnException $e) { 235 | throw new MalformedDsnException('Malformed parameter "url".', 0, $e); 236 | } 237 | 238 | if (isset($parsedParams['driver'])) { 239 | // The requested driver from the URL scheme takes precedence 240 | // over the default custom driver from the connection parameters (if any). 241 | unset($params['driverClass']); 242 | } 243 | 244 | $params = array_merge($params, $parsedParams); 245 | 246 | // If a schemeless connection URL is given, we require a default driver or default custom driver 247 | // as connection parameter. 248 | if (! isset($params['driverClass']) && ! isset($params['driver'])) { 249 | throw DriverRequired::new($params['url']); 250 | } 251 | 252 | unset($params['url']); 253 | 254 | return $params; 255 | } 256 | } 257 | -------------------------------------------------------------------------------- /config/orm.php: -------------------------------------------------------------------------------- 1 | services() 51 | 52 | ->alias(EntityManagerInterface::class, 'doctrine.orm.entity_manager') 53 | 54 | ->set('form.type_guesser.doctrine', DoctrineOrmTypeGuesser::class) 55 | ->tag('form.type_guesser') 56 | ->args([ 57 | service('doctrine'), 58 | ]) 59 | 60 | ->set('form.type.entity', EntityType::class) 61 | ->tag('form.type', ['alias' => 'entity']) 62 | ->args([ 63 | service('doctrine'), 64 | ]) 65 | 66 | ->set('doctrine.orm.configuration', Configuration::class)->abstract() 67 | 68 | ->set('doctrine.orm.entity_manager.abstract', EntityManager::class) 69 | ->abstract() 70 | ->lazy() 71 | 72 | ->set('doctrine.orm.container_repository_factory', ContainerRepositoryFactory::class) 73 | ->args([ 74 | inline_service(ServiceLocator::class)->args([ 75 | [], 76 | ]), 77 | ]) 78 | 79 | ->set('doctrine.orm.manager_configurator.abstract', ManagerConfigurator::class) 80 | ->abstract() 81 | ->args([ 82 | [], 83 | [], 84 | ]) 85 | 86 | ->set('doctrine.orm.validator.unique', UniqueEntityValidator::class) 87 | ->tag('validator.constraint_validator', ['alias' => 'doctrine.orm.validator.unique']) 88 | ->args([ 89 | service('doctrine'), 90 | ]) 91 | 92 | ->set('doctrine.orm.validator_initializer', DoctrineInitializer::class) 93 | ->tag('validator.initializer') 94 | ->args([ 95 | service('doctrine'), 96 | ]) 97 | 98 | ->set('doctrine.orm.security.user.provider', EntityUserProvider::class) 99 | ->abstract() 100 | ->args([ 101 | service('doctrine'), 102 | ]) 103 | 104 | ->set('doctrine.orm.listeners.resolve_target_entity', ResolveTargetEntityListener::class) 105 | 106 | ->set('doctrine.orm.listeners.doctrine_dbal_cache_adapter_schema_listener', DoctrineDbalCacheAdapterSchemaListener::class) 107 | ->args([ 108 | [], 109 | ]) 110 | ->tag('doctrine.event_listener', ['event' => 'postGenerateSchema']) 111 | 112 | ->set('doctrine.orm.listeners.doctrine_token_provider_schema_listener', RememberMeTokenProviderDoctrineSchemaListener::class) 113 | ->args([ 114 | tagged_iterator('security.remember_me_handler'), 115 | ]) 116 | ->tag('doctrine.event_listener', ['event' => 'postGenerateSchema']) 117 | 118 | ->set('doctrine.orm.listeners.pdo_session_handler_schema_listener', PdoSessionHandlerSchemaListener::class) 119 | ->args([ 120 | service('session.handler'), 121 | ]) 122 | ->tag('doctrine.event_listener', ['event' => 'postGenerateSchema']) 123 | 124 | ->set('doctrine.orm.listeners.lock_store_schema_listener', LockStoreSchemaListener::class) 125 | ->args([ 126 | tagged_iterator('lock.store'), 127 | ]) 128 | ->tag('doctrine.event_listener', ['event' => 'postGenerateSchema']) 129 | 130 | ->set('doctrine.orm.naming_strategy.default', DefaultNamingStrategy::class) 131 | 132 | ->set('doctrine.orm.naming_strategy.underscore', UnderscoreNamingStrategy::class) 133 | 134 | ->set('doctrine.orm.naming_strategy.underscore_number_aware', UnderscoreNamingStrategy::class) 135 | ->args([ 136 | CASE_LOWER, 137 | true, 138 | ]) 139 | 140 | ->set('doctrine.orm.quote_strategy.default', DefaultQuoteStrategy::class) 141 | 142 | ->set('doctrine.orm.quote_strategy.ansi', AnsiQuoteStrategy::class) 143 | 144 | ->set('doctrine.orm.typed_field_mapper.default', DefaultTypedFieldMapper::class) 145 | 146 | ->set('doctrine.ulid_generator', UlidGenerator::class) 147 | ->args([ 148 | service('ulid.factory')->ignoreOnInvalid(), 149 | ]) 150 | ->tag('doctrine.id_generator') 151 | 152 | ->set('doctrine.uuid_generator', UuidGenerator::class) 153 | ->args([ 154 | service('uuid.factory')->ignoreOnInvalid(), 155 | ]) 156 | ->tag('doctrine.id_generator') 157 | 158 | ->set('doctrine.orm.command.entity_manager_provider', ManagerRegistryAwareEntityManagerProvider::class) 159 | ->args([ 160 | service('doctrine'), 161 | ]) 162 | 163 | ->set('doctrine.orm.entity_value_resolver', EntityValueResolver::class) 164 | ->args([ 165 | service('doctrine'), 166 | service('doctrine.orm.entity_value_resolver.expression_language')->ignoreOnInvalid(), 167 | ]) 168 | ->tag('controller.argument_value_resolver', ['priority' => 110, 'name' => EntityValueResolver::class]) 169 | 170 | ->set('doctrine.orm.entity_value_resolver.expression_language', ExpressionLanguage::class) 171 | 172 | ->set('doctrine.cache_clear_metadata_command', MetadataCommand::class) 173 | ->args([ 174 | service('doctrine.orm.command.entity_manager_provider'), 175 | ]) 176 | ->tag('console.command', ['command' => 'doctrine:cache:clear-metadata']) 177 | 178 | ->set('doctrine.cache_clear_query_cache_command', QueryCommand::class) 179 | ->args([ 180 | service('doctrine.orm.command.entity_manager_provider'), 181 | ]) 182 | ->tag('console.command', ['command' => 'doctrine:cache:clear-query']) 183 | 184 | ->set('doctrine.cache_clear_result_command', ResultCommand::class) 185 | ->args([ 186 | service('doctrine.orm.command.entity_manager_provider'), 187 | ]) 188 | ->tag('console.command', ['command' => 'doctrine:cache:clear-result']) 189 | 190 | ->set('doctrine.cache_collection_region_command', CollectionRegionCommand::class) 191 | ->args([ 192 | service('doctrine.orm.command.entity_manager_provider'), 193 | ]) 194 | ->tag('console.command', ['command' => 'doctrine:cache:clear-collection-region']) 195 | 196 | ->set('doctrine.schema_create_command', CreateCommand::class) 197 | ->args([ 198 | service('doctrine.orm.command.entity_manager_provider'), 199 | ]) 200 | ->tag('console.command', ['command' => 'doctrine:schema:create']) 201 | 202 | ->set('doctrine.schema_drop_command', DropCommand::class) 203 | ->args([ 204 | service('doctrine.orm.command.entity_manager_provider'), 205 | ]) 206 | ->tag('console.command', ['command' => 'doctrine:schema:drop']) 207 | 208 | ->set('doctrine.clear_entity_region_command', EntityRegionCommand::class) 209 | ->args([ 210 | service('doctrine.orm.command.entity_manager_provider'), 211 | ]) 212 | ->tag('console.command', ['command' => 'doctrine:cache:clear-entity-region']) 213 | 214 | ->set('doctrine.mapping_info_command', InfoCommand::class) 215 | ->args([ 216 | service('doctrine.orm.command.entity_manager_provider'), 217 | ]) 218 | ->tag('console.command', ['command' => 'doctrine:mapping:info']) 219 | 220 | ->set('doctrine.mapping_describe_command', MappingDescribeCommand::class) 221 | ->args([ 222 | service('doctrine.orm.command.entity_manager_provider'), 223 | ]) 224 | ->tag('console.command', ['command' => 'doctrine:mapping:describe']) 225 | 226 | ->set('doctrine.clear_query_region_command', QueryRegionCommand::class) 227 | ->args([ 228 | service('doctrine.orm.command.entity_manager_provider'), 229 | ]) 230 | ->tag('console.command', ['command' => 'doctrine:cache:clear-query-region']) 231 | 232 | ->set('doctrine.query_dql_command', RunDqlCommand::class) 233 | ->args([ 234 | service('doctrine.orm.command.entity_manager_provider'), 235 | ]) 236 | ->tag('console.command', ['command' => 'doctrine:query:dql']) 237 | 238 | ->set('doctrine.schema_update_command', UpdateCommand::class) 239 | ->args([ 240 | service('doctrine.orm.command.entity_manager_provider'), 241 | ]) 242 | ->tag('console.command', ['command' => 'doctrine:schema:update']) 243 | 244 | ->set('doctrine.schema_validate_command', ValidateSchemaCommand::class) 245 | ->args([ 246 | service('doctrine.orm.command.entity_manager_provider'), 247 | ]) 248 | ->tag('console.command', ['command' => 'doctrine:schema:validate']); 249 | }; 250 | -------------------------------------------------------------------------------- /src/DataCollector/DoctrineDataCollector.php: -------------------------------------------------------------------------------- 1 | , 33 | * runnable: bool, 34 | * types: ?array, 35 | * } 36 | * @phpstan-type DataType = array{ 37 | * caches: array{ 38 | * enabled: bool, 39 | * counts: array<"puts"|"hits"|"misses", int>, 40 | * log_enabled: bool, 41 | * regions: array<"puts"|"hits"|"misses", array>, 42 | * }, 43 | * connections: list, 44 | * entities: array>, 45 | * errors: array>>, 46 | * managers: list, 47 | * queries: array>, 48 | * entityCounts: array> 49 | * } 50 | * @psalm-property DataType $data 51 | */ 52 | class DoctrineDataCollector extends BaseCollector 53 | { 54 | private int|null $invalidEntityCount = null; 55 | 56 | private int|null $managedEntityCount = null; 57 | 58 | /** 59 | * @var mixed[][]|null 60 | * @phpstan-var ?array> 61 | * @phpstan-ignore property.unusedType 62 | */ 63 | private array|null $groupedQueries = null; 64 | 65 | public function __construct( 66 | private readonly ManagerRegistry $registry, 67 | private readonly bool $shouldValidateSchema = true, 68 | DebugDataHolder|null $debugDataHolder = null, 69 | ) { 70 | parent::__construct($registry, $debugDataHolder); 71 | } 72 | 73 | public function collect(Request $request, Response $response, Throwable|null $exception = null): void 74 | { 75 | parent::collect($request, $response, $exception); 76 | 77 | $errors = []; 78 | $entities = []; 79 | $entityCounts = []; 80 | $caches = [ 81 | 'enabled' => false, 82 | 'log_enabled' => false, 83 | 'counts' => [ 84 | 'puts' => 0, 85 | 'hits' => 0, 86 | 'misses' => 0, 87 | ], 88 | 'regions' => [ 89 | 'puts' => [], 90 | 'hits' => [], 91 | 'misses' => [], 92 | ], 93 | ]; 94 | 95 | foreach ($this->registry->getManagers() as $name => $em) { 96 | assert($em instanceof EntityManagerInterface); 97 | if ($this->shouldValidateSchema) { 98 | $entities[$name] = []; 99 | 100 | $factory = $em->getMetadataFactory(); 101 | $validator = new SchemaValidator($em); 102 | 103 | foreach ($factory->getLoadedMetadata() as $class) { 104 | if (isset($entities[$name][$class->getName()])) { 105 | continue; 106 | } 107 | 108 | $classErrors = $validator->validateClass($class); 109 | $r = $class->getReflectionClass(); 110 | $entities[$name][$class->getName()] = [ 111 | 'class' => $class->getName(), 112 | 'file' => $r->getFileName(), 113 | 'line' => $r->getStartLine(), 114 | ]; 115 | 116 | if (empty($classErrors)) { 117 | continue; 118 | } 119 | 120 | $errors[$name][$class->getName()] = $classErrors; 121 | } 122 | } 123 | 124 | $entityCounts[$name] = []; 125 | foreach ($em->getUnitOfWork()->getIdentityMap() as $className => $entityList) { 126 | $entityCounts[$name][$className] = count($entityList); 127 | } 128 | 129 | // Sort entities by count (in descending order) 130 | arsort($entityCounts[$name]); 131 | 132 | $emConfig = $em->getConfiguration(); 133 | $slcEnabled = $emConfig->isSecondLevelCacheEnabled(); 134 | 135 | if (! $slcEnabled) { 136 | continue; 137 | } 138 | 139 | $caches['enabled'] = true; 140 | 141 | $cacheConfiguration = $emConfig->getSecondLevelCacheConfiguration(); 142 | assert($cacheConfiguration instanceof CacheConfiguration); 143 | $cacheLoggerChain = $cacheConfiguration->getCacheLogger(); 144 | assert($cacheLoggerChain instanceof CacheLoggerChain || $cacheLoggerChain === null); 145 | 146 | if (! $cacheLoggerChain || ! $cacheLoggerChain->getLogger('statistics')) { 147 | continue; 148 | } 149 | 150 | $cacheLoggerStats = $cacheLoggerChain->getLogger('statistics'); 151 | assert($cacheLoggerStats instanceof StatisticsCacheLogger); 152 | $caches['log_enabled'] = true; 153 | 154 | $caches['counts']['puts'] += $cacheLoggerStats->getPutCount(); 155 | $caches['counts']['hits'] += $cacheLoggerStats->getHitCount(); 156 | $caches['counts']['misses'] += $cacheLoggerStats->getMissCount(); 157 | 158 | foreach ($cacheLoggerStats->getRegionsPut() as $key => $value) { 159 | if (! isset($caches['regions']['puts'][$key])) { 160 | $caches['regions']['puts'][$key] = 0; 161 | } 162 | 163 | $caches['regions']['puts'][$key] += $value; 164 | } 165 | 166 | foreach ($cacheLoggerStats->getRegionsHit() as $key => $value) { 167 | if (! isset($caches['regions']['hits'][$key])) { 168 | $caches['regions']['hits'][$key] = 0; 169 | } 170 | 171 | $caches['regions']['hits'][$key] += $value; 172 | } 173 | 174 | foreach ($cacheLoggerStats->getRegionsMiss() as $key => $value) { 175 | if (! isset($caches['regions']['misses'][$key])) { 176 | $caches['regions']['misses'][$key] = 0; 177 | } 178 | 179 | $caches['regions']['misses'][$key] += $value; 180 | } 181 | } 182 | 183 | $this->data['entities'] = $entities; 184 | $this->data['errors'] = $errors; 185 | $this->data['caches'] = $caches; 186 | $this->data['entityCounts'] = $entityCounts; 187 | $this->groupedQueries = null; 188 | } 189 | 190 | /** @return array> */ 191 | public function getEntities(): array 192 | { 193 | return $this->data['entities']; 194 | } 195 | 196 | /** @return array>> */ 197 | public function getMappingErrors(): array 198 | { 199 | return $this->data['errors']; 200 | } 201 | 202 | public function getCacheHitsCount(): int 203 | { 204 | return $this->data['caches']['counts']['hits']; 205 | } 206 | 207 | public function getCachePutsCount(): int 208 | { 209 | return $this->data['caches']['counts']['puts']; 210 | } 211 | 212 | public function getCacheMissesCount(): int 213 | { 214 | return $this->data['caches']['counts']['misses']; 215 | } 216 | 217 | public function getCacheEnabled(): bool 218 | { 219 | return $this->data['caches']['enabled']; 220 | } 221 | 222 | /** 223 | * @return array> 224 | * @phpstan-return array<"puts"|"hits"|"misses", array> 225 | */ 226 | public function getCacheRegions(): array 227 | { 228 | return $this->data['caches']['regions']; 229 | } 230 | 231 | /** @return array */ 232 | public function getCacheCounts(): array 233 | { 234 | return $this->data['caches']['counts']; 235 | } 236 | 237 | public function getInvalidEntityCount(): int 238 | { 239 | return $this->invalidEntityCount ??= array_sum(array_map('count', $this->data['errors'])); 240 | } 241 | 242 | public function getManagedEntityCount(): int 243 | { 244 | if ($this->managedEntityCount === null) { 245 | $total = 0; 246 | foreach ($this->data['entityCounts'] as $entities) { 247 | $total += array_sum($entities); 248 | } 249 | 250 | $this->managedEntityCount = $total; 251 | } 252 | 253 | return $this->managedEntityCount; 254 | } 255 | 256 | /** @return array> */ 257 | public function getManagedEntityCountByClass(): array 258 | { 259 | return $this->data['entityCounts']; 260 | } 261 | 262 | /** 263 | * @return string[][] 264 | * @phpstan-return array> 265 | */ 266 | public function getGroupedQueries(): array 267 | { 268 | if ($this->groupedQueries !== null) { 269 | return $this->groupedQueries; 270 | } 271 | 272 | $this->groupedQueries = []; 273 | $totalExecutionMS = 0; 274 | foreach ($this->data['queries'] as $connection => $queries) { 275 | $connectionGroupedQueries = []; 276 | foreach ($queries as $i => $query) { 277 | $key = $query['sql']; 278 | if (! isset($connectionGroupedQueries[$key])) { 279 | $connectionGroupedQueries[$key] = $query; 280 | $connectionGroupedQueries[$key]['executionMS'] = 0; 281 | $connectionGroupedQueries[$key]['count'] = 0; 282 | $connectionGroupedQueries[$key]['index'] = $i; // "Explain query" relies on query index in 'queries'. 283 | } 284 | 285 | $connectionGroupedQueries[$key]['executionMS'] += $query['executionMS']; 286 | $connectionGroupedQueries[$key]['count']++; 287 | $totalExecutionMS += $query['executionMS']; 288 | } 289 | 290 | usort($connectionGroupedQueries, static function ($a, $b) { 291 | if ($a['executionMS'] === $b['executionMS']) { 292 | return 0; 293 | } 294 | 295 | return $a['executionMS'] < $b['executionMS'] ? 1 : -1; 296 | }); 297 | $this->groupedQueries[$connection] = $connectionGroupedQueries; 298 | } 299 | 300 | foreach ($this->groupedQueries as $connection => $queries) { 301 | foreach ($queries as $i => $query) { 302 | $this->groupedQueries[$connection][$i]['executionPercent'] = 303 | $this->executionTimePercentage($query['executionMS'], $totalExecutionMS); 304 | } 305 | } 306 | 307 | return $this->groupedQueries; 308 | } 309 | 310 | private function executionTimePercentage(float $executionTimeMS, float $totalExecutionTimeMS): float 311 | { 312 | if (! $totalExecutionTimeMS) { 313 | return 0; 314 | } 315 | 316 | return $executionTimeMS / $totalExecutionTimeMS * 100; 317 | } 318 | 319 | public function getGroupedQueryCount(): int 320 | { 321 | $count = 0; 322 | foreach ($this->getGroupedQueries() as $connectionGroupedQueries) { 323 | $count += count($connectionGroupedQueries); 324 | } 325 | 326 | return $count; 327 | } 328 | } 329 | -------------------------------------------------------------------------------- /config/schema/doctrine-1.0.xsd: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | -------------------------------------------------------------------------------- /templates/Collector/db.html.twig: -------------------------------------------------------------------------------- 1 | {% extends request.isXmlHttpRequest ? '@WebProfiler/Profiler/ajax_layout.html.twig' : '@WebProfiler/Profiler/layout.html.twig' %} 2 | 3 | {% import _self as helper %} 4 | 5 | {% block toolbar %} 6 | {% if collector.querycount > 0 or collector.invalidEntityCount > 0 %} 7 | 8 | {% set icon %} 9 | {% set status = collector.invalidEntityCount > 0 ? 'red' : collector.querycount > 50 ? 'yellow' %} 10 | 11 | {% if profiler_markup_version >= 3 %} 12 | {{ include('@Doctrine/Collector/database.svg') }} 13 | {% else %} 14 | {{ include('@Doctrine/Collector/icon.svg') }} 15 | {% endif %} 16 | 17 | {% if collector.querycount == 0 and collector.invalidEntityCount > 0 %} 18 | {{ collector.invalidEntityCount }} 19 | errors 20 | {% else %} 21 | {{ collector.querycount }} 22 | 23 | in 24 | {{ '%0.2f'|format(collector.time * 1000) }} 25 | ms 26 | 27 | {% endif %} 28 | {% endset %} 29 | 30 | {% set text %} 31 |
32 | Database Queries 33 | {{ collector.querycount }} 34 |
35 |
36 | Different statements 37 | {{ collector.groupedQueryCount }} 38 |
39 |
40 | Query time 41 | {{ '%0.2f'|format(collector.time * 1000) }} ms 42 |
43 |
44 | Invalid entities 45 | {{ collector.invalidEntityCount }} 46 |
47 |
48 | Managed entities 49 | {{ collector.managedEntityCount }} 50 |
51 | {% if collector.cacheEnabled %} 52 |
53 | Cache hits 54 | {{ collector.cacheHitsCount }} 55 |
56 |
57 | Cache misses 58 | {{ collector.cacheMissesCount }} 59 |
60 |
61 | Cache puts 62 | {{ collector.cachePutsCount }} 63 |
64 | {% else %} 65 |
66 | Second Level Cache 67 | disabled 68 |
69 | {% endif %} 70 | {% endset %} 71 | 72 | {{ include('@WebProfiler/Profiler/toolbar_item.html.twig', { link: profiler_url, status: status|default('') }) }} 73 | 74 | {% endif %} 75 | {% endblock %} 76 | 77 | {% block menu %} 78 | 79 | {{ include('@Doctrine/Collector/' ~ (profiler_markup_version < 3 ? 'icon' : 'database') ~ '.svg') }} 80 | Doctrine 81 | {% if collector.invalidEntityCount %} 82 | 83 | {{ collector.invalidEntityCount }} 84 | 85 | {% endif %} 86 | 87 | {% endblock %} 88 | 89 | {% block panel %} 90 | {% if 'explain' == page %} 91 | {{ render(controller('Doctrine\\Bundle\\DoctrineBundle\\Controller\\ProfilerController::explainAction', { 92 | token: token, 93 | panel: 'db', 94 | connectionName: request.query.get('connection'), 95 | query: request.query.get('query') 96 | })) }} 97 | {% else %} 98 | {{ block('queries') }} 99 | {% endif %} 100 | {% endblock %} 101 | 102 | {% block queries %} 103 | 120 | 121 |

Query Metrics

122 | 123 |
124 |
125 |
126 | {{ collector.querycount }} 127 | Database Queries 128 |
129 | 130 |
131 | {{ collector.groupedQueryCount }} 132 | Different statements 133 |
134 | 135 |
136 | {{ '%0.2f'|format(collector.time * 1000) }} ms 137 | Query time 138 |
139 | 140 |
141 | {{ collector.invalidEntityCount }} 142 | Invalid entities 143 |
144 | 145 |
146 | {{ collector.managedEntityCount }} 147 | Managed entities 148 |
149 |
150 | 151 | {% if collector.cacheEnabled %} 152 |
153 |
154 | {{ collector.cacheHitsCount }} 155 | Cache hits 156 |
157 |
158 | {{ collector.cacheMissesCount }} 159 | Cache misses 160 |
161 |
162 | {{ collector.cachePutsCount }} 163 | Cache puts 164 |
165 |
166 | {% endif %} 167 |
168 | 169 |
170 |
171 | {% set group_queries = request.query.getBoolean('group') %} 172 |

173 | {% if group_queries %} 174 | Grouped Statements 175 | {% else %} 176 | Queries 177 | {% endif %} 178 |

179 | 180 |
181 | {% if not collector.queries %} 182 |
183 |

No executed queries.

184 |
185 | {% else %} 186 | {% if group_queries %} 187 |

Show all queries

188 | {% else %} 189 |

Group similar statements

190 | {% endif %} 191 | 192 | {% for connection, queries in collector.queries %} 193 | {% if collector.connections|length > 1 %} 194 |

{{ connection }} connection

195 | {% endif %} 196 | 197 | {% if queries is empty %} 198 |
199 |

No database queries were performed.

200 |
201 | {% else %} 202 | {% if group_queries %} 203 | {% set queries = collector.groupedQueries[connection] %} 204 | {% endif %} 205 | 206 | 207 | 208 | {% if group_queries %} 209 | 210 | 211 | {% else %} 212 | 213 | 214 | {% endif %} 215 | 216 | 217 | 218 | 219 | {% for i, query in queries %} 220 | {% set i = group_queries ? query.index : i %} 221 | 222 | {% if group_queries %} 223 | 227 | 228 | {% else %} 229 | 230 | 231 | {% endif %} 232 | 309 | 310 | {% endfor %} 311 | 312 |
TimeCount#TimeInfo
224 | 225 | {{ '%0.2f'|format(query.executionMS * 1000) }} ms
({{ '%0.2f'|format(query.executionPercent) }}%)
226 |
{{ query.count }}{{ loop.index }}{{ '%0.2f'|format(query.executionMS * 1000) }} ms 233 | {{ query.sql|doctrine_prettify_sql }} 234 | 235 |
236 | Parameters: {{ profiler_dump(query.params, 2) }} 237 |
238 | 239 |
240 | View formatted query 241 | 242 | {% if query.runnable %} 243 |    244 | View runnable query 245 | {% endif %} 246 | 247 | {% if query.explainable %} 248 |    249 | Explain query 250 | {% endif %} 251 | 252 | {% if query.backtrace is defined %} 253 |    254 | View query backtrace 255 | {% endif %} 256 |
257 | 258 | 262 | 263 | {% if query.runnable %} 264 | 269 | {% endif %} 270 | 271 | {% if query.explainable %} 272 |
273 | {% endif %} 274 | 275 | {% if query.backtrace is defined %} 276 | 307 | {% endif %} 308 |
313 | {% endif %} 314 | {% endfor %} 315 | {% endif %} 316 |
317 |
318 | 319 |
320 |

Database Connections

321 |
322 | {% if not collector.connections %} 323 |
324 |

There are no configured database connections.

325 |
326 | {% else %} 327 | {{ helper.render_simple_table('Name', 'Service', collector.connections) }} 328 | {% endif %} 329 |
330 |
331 | 332 |
333 |

Entity Managers

334 |
335 | 336 | {% if not collector.managers %} 337 |
338 |

There are no configured entity managers.

339 |
340 | {% else %} 341 | {{ helper.render_simple_table('Name', 'Service', collector.managers) }} 342 | {% endif %} 343 |
344 |
345 | 346 |
347 |

Second Level Cache

348 |
349 | 350 | {% if not collector.cacheEnabled %} 351 |
352 |

Second Level Cache is not enabled.

353 |
354 | {% else %} 355 | {% if not collector.cacheCounts %} 356 |
357 |

Second level cache information is not available.

358 |
359 | {% else %} 360 |
361 |
362 | {{ collector.cacheCounts.hits }} 363 | Hits 364 |
365 | 366 |
367 | {{ collector.cacheCounts.misses }} 368 | Misses 369 |
370 | 371 |
372 | {{ collector.cacheCounts.puts }} 373 | Puts 374 |
375 |
376 | 377 | {% if collector.cacheRegions.hits %} 378 |

Number of cache hits

379 | {{ helper.render_simple_table('Region', 'Hits', collector.cacheRegions.hits) }} 380 | {% endif %} 381 | 382 | {% if collector.cacheRegions.misses %} 383 |

Number of cache misses

384 | {{ helper.render_simple_table('Region', 'Misses', collector.cacheRegions.misses) }} 385 | {% endif %} 386 | 387 | {% if collector.cacheRegions.puts %} 388 |

Number of cache puts

389 | {{ helper.render_simple_table('Region', 'Puts', collector.cacheRegions.puts) }} 390 | {% endif %} 391 | {% endif %} 392 | {% endif %} 393 |
394 |
395 | 396 |
397 |

Managed Entities

398 |
399 | {% if not collector.managedEntityCountByClass %} 400 |
401 |

No managed entities.

402 |
403 | {% else %} 404 | {% for manager, entityCounts in collector.managedEntityCountByClass %} 405 |

{{ manager }} entity manager

406 | {{ helper.render_simple_table('Class', 'Amount of managed objects', entityCounts) }} 407 | {% endfor %} 408 | {% endif %} 409 |
410 |
411 | 412 |
413 |

Entities Mapping

414 |
415 | 416 | {% if not collector.entities %} 417 |
418 |

No mapped entities.

419 |
420 | {% else %} 421 | {% for manager, classes in collector.entities %} 422 | {% if collector.managers|length > 1 %} 423 |

{{ manager }} entity manager

424 | {% endif %} 425 | 426 | {% if classes is empty %} 427 |
428 |

No loaded entities.

429 |
430 | {% else %} 431 | 432 | 433 | 434 | 435 | 436 | 437 | 438 | 439 | {% for class in classes %} 440 | {% set contains_errors = collector.mappingErrors[manager] is defined and collector.mappingErrors[manager][class.class] is defined %} 441 | 442 | 445 | 456 | 457 | {% endfor %} 458 | 459 |
ClassMapping errors
443 | {{ class. class}} 444 | 446 | {% if contains_errors %} 447 |
    448 | {% for error in collector.mappingErrors[manager][class.class] %} 449 |
  • {{ error }}
  • 450 | {% endfor %} 451 |
452 | {% else %} 453 | No errors. 454 | {% endif %} 455 |
460 | {% endif %} 461 | {% endfor %} 462 | {% endif %} 463 |
464 |
465 |
466 | 467 | 539 | {% endblock %} 540 | 541 | {% macro render_simple_table(label1, label2, data) %} 542 | 543 | 544 | 545 | 546 | 547 | 548 | 549 | 550 | {% for key, value in data %} 551 | 552 | 553 | 554 | 555 | {% endfor %} 556 | 557 |
{{ label1 }}{{ label2 }}
{{ key }}{{ value }}
558 | {% endmacro %} 559 | --------------------------------------------------------------------------------