This commit is contained in:
Marek
2026-03-24 00:04:55 +01:00
commit c5229e48ed
4225 changed files with 511461 additions and 0 deletions

View File

@@ -0,0 +1,13 @@
Copyright (c) 2011 Fabien Potencier, Doctrine Project
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
documentation files (the "Software"), to deal in the Software without restriction, including without limitation
the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software,
and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@@ -0,0 +1,25 @@
# Doctrine Bundle
Doctrine DBAL & ORM Bundle for the Symfony Framework.
[![Continuous Integration](https://github.com/doctrine/DoctrineBundle/actions/workflows/continuous-integration.yml/badge.svg)](https://github.com/doctrine/DoctrineBundle/actions/workflows/continuous-integration.yml)
[![codecov](https://codecov.io/gh/doctrine/DoctrineBundle/graph/badge.svg?token=qtm3EQ3WgV)](https://codecov.io/gh/doctrine/DoctrineBundle)
## What is Doctrine?
The Doctrine Project is the home of a selected set of PHP libraries primarily focused on providing persistence
services and related functionality. Its prize projects are a Object Relational Mapper and the Database Abstraction
Layer it is built on top of. You can read more about the projects below or view a list of all projects.
Object relational mapper (ORM) for PHP that sits on top of a powerful database abstraction layer (DBAL).
One of its key features is the option to write database queries in a proprietary object oriented SQL dialect
called Doctrine Query Language (DQL), inspired by Hibernates HQL. This provides developers with a powerful
alternative to SQL that maintains flexibility without requiring unnecessary code duplication.
DBAL is a powerful database abstraction layer with many features for database schema introspection,
schema management and PDO abstraction.
## Documentation
The documentation is rendered on [the symfony.com website](https://symfony.com/doc/current/reference/configuration/doctrine.html).
The source of the documentation is available in the docs folder.

View File

@@ -0,0 +1,27 @@
UPGRADE FROM 2.9 to 2.10
========================
Configuration
-------------
### Preparing for a new `report_fields_where_declared` mapping driver mode
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.
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.
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.
Unless you set it to `true`, Doctrine ORM will emit deprecation messages mentioning this new setting.
### Preparing for the XSD validation for XML drivers
Doctrine ORM 2.14+ adds support for validating the XSD of XML mapping files. In ORM 3.0, this validation will be mandatory.
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.
Unless you set it to `true`, Doctrine ORM will emit deprecation messages mentioning the XSD validation.
### Deprecations
- `Doctrine\Bundle\DoctrineBundle\EventSubscriber\EventSubscriberInterface` has been deprecated. Use the `#[AsDoctrineListener]` attribute instead.

View File

@@ -0,0 +1,13 @@
UPGRADE FROM 2.11 to 2.12
========================
Configuration
-------------
### Controller resolver auto mapping default configuration will be changed
The default value of `doctrine.orm.controller_resolver.auto_mapping` will be changed from `true` to `false` in 3.0.
Auto mapping uses any route parameter that matches with a field name of the Entity to resolve as criteria in a find by query.
If you are relying on this functionality, you will need to configure it explicitly to silence the deprecation notice.

View File

@@ -0,0 +1,13 @@
UPGRADE FROM 2.12 to 2.13
========================
Configuration
-------------
### Controller resolver auto mapping deprecated
The controller resolver auto mapping functionality has been deprecated with Symfony 7.1, and is replaced with explicit mapped route parameters. Enabling the auto mapper by default using this bundle is now deprecated as well.
Auto mapping uses any route parameter that matches with a field name of the Entity to resolve as criteria in a find by query.
If you are relying on this functionality, you can update your code to use explicit mapped route parameters instead.

View File

@@ -0,0 +1,28 @@
UPGRADE FROM 2.16 to 2.17
=========================
DoctrineExtension
=================
Minor breaking change:
`Doctrine\Bundle\DoctrineBundle\DependencyInjection\DoctrineExtension` no
longer extends
`Symfony\Bridge\Doctrine\DependencyInjection\AbstractDoctrineExtension`.
Configuration
-------------
### The `doctrine.orm.entity_managers.some_em.report_fields_where_declared` configuration option is deprecated
This option is a no-op when using `doctrine/orm` 3 and has been conditionally
deprecated. You should stop using it as soon as you upgrade to Doctrine ORM 3.
### The `doctrine.dbal.connections.some_connection.disable_type_comments` configuration option is deprecated
This option is a no-op when using `doctrine/dbal` 4 and has been conditionally
deprecated. You should stop using it as soon as you upgrade to Doctrine DBAL 4.
### The `doctrine.dbal.connections.some_connection.use_savepoints` configuration option is deprecated
This option is a no-op when using `doctrine/dbal` 4 and has been conditionally
deprecated. You should stop using it as soon as you upgrade to Doctrine DBAL 4.

View File

@@ -0,0 +1,15 @@
UPGRADE FROM 2.17 to 2.18
=========================
DoctrineOrmMappingsPass
-----------------------
### The `DoctrineOrmMappingsPass::createYamlMappingDriver()` method is deprecated
This method is deprecated with no replacement planned and will be removed in
DoctrineBundle 3.0.
### The `DoctrineOrmMappingsPass::createAnnotationMappingDriver()` method is deprecated
This method is deprecated with no replacement planned and will be removed in
DoctrineBundle 3.0.

View File

@@ -0,0 +1,19 @@
UPGRADE FROM 2.x to 3.0
=======================
Configuration
-------------
### Controller resolver auto mapping can no longer be configured
The `doctrine.orm.controller_resolver.auto_mapping` option now only accepts `false` as value, to disallow the usage of the controller resolver auto mapping feature by default. The configuration option will be fully removed in 4.0.
Auto mapping used any route parameter that matches with a field name of the Entity to resolve as criteria in a find by query. This method has been deprecated in Symfony 7.1 and is replaced with mapped route parameters.
If you were relying on this functionality, you will need to update your code to use explicit mapped route parameters instead.
Types
-----
* The `commented` configuration option for types is no longer supported and
deprecated.

View File

@@ -0,0 +1,108 @@
{
"name": "doctrine/doctrine-bundle",
"description": "Symfony DoctrineBundle",
"license": "MIT",
"type": "symfony-bundle",
"keywords": [
"DBAL",
"ORM",
"Database",
"Persistence"
],
"authors": [
{
"name": "Fabien Potencier",
"email": "fabien@symfony.com"
},
{
"name": "Benjamin Eberlei",
"email": "kontakt@beberlei.de"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
},
{
"name": "Doctrine Project",
"homepage": "https://www.doctrine-project.org/"
}
],
"homepage": "https://www.doctrine-project.org",
"require": {
"php": "^8.1",
"doctrine/dbal": "^3.7.0 || ^4.0",
"doctrine/deprecations": "^1.0",
"doctrine/persistence": "^3.1 || ^4",
"doctrine/sql-formatter": "^1.0.1",
"symfony/cache": "^6.4 || ^7.0",
"symfony/config": "^6.4 || ^7.0",
"symfony/console": "^6.4 || ^7.0",
"symfony/dependency-injection": "^6.4 || ^7.0",
"symfony/doctrine-bridge": "^6.4.3 || ^7.0.3",
"symfony/framework-bundle": "^6.4 || ^7.0",
"symfony/service-contracts": "^2.5 || ^3"
},
"require-dev": {
"doctrine/annotations": "^1 || ^2",
"doctrine/cache": "^1.11 || ^2.0",
"doctrine/coding-standard": "^14",
"doctrine/orm": "^2.17 || ^3.1",
"friendsofphp/proxy-manager-lts": "^1.0",
"phpstan/phpstan": "2.1.1",
"phpstan/phpstan-phpunit": "2.0.3",
"phpstan/phpstan-strict-rules": "^2",
"phpunit/phpunit": "^10.5.53 || ^12.3.10",
"psr/log": "^1.1.4 || ^2.0 || ^3.0",
"symfony/doctrine-messenger": "^6.4 || ^7.0",
"symfony/expression-language": "^6.4 || ^7.0",
"symfony/messenger": "^6.4 || ^7.0",
"symfony/property-info": "^6.4 || ^7.0",
"symfony/security-bundle": "^6.4 || ^7.0",
"symfony/stopwatch": "^6.4 || ^7.0",
"symfony/string": "^6.4 || ^7.0",
"symfony/twig-bridge": "^6.4 || ^7.0",
"symfony/validator": "^6.4 || ^7.0",
"symfony/var-exporter": "^6.4.1 || ^7.0.1",
"symfony/web-profiler-bundle": "^6.4 || ^7.0",
"symfony/yaml": "^6.4 || ^7.0",
"twig/twig": "^2.14.7 || ^3.0.4"
},
"conflict": {
"doctrine/annotations": ">=3.0",
"doctrine/cache": "< 1.11",
"doctrine/orm": "<2.17 || >=4.0",
"symfony/var-exporter": "< 6.4.1 || 7.0.0",
"twig/twig": "<2.13 || >=3.0 <3.0.4"
},
"suggest": {
"ext-pdo": "*",
"doctrine/orm": "The Doctrine ORM integration is optional in the bundle.",
"symfony/web-profiler-bundle": "To use the data collector."
},
"minimum-stability": "dev",
"autoload": {
"psr-4": {
"Doctrine\\Bundle\\DoctrineBundle\\": "src"
}
},
"autoload-dev": {
"psr-4": {
"Doctrine\\Bundle\\DoctrineBundle\\Tests\\": "tests",
"Fixtures\\": "tests/DependencyInjection/Fixtures"
}
},
"config": {
"allow-plugins": {
"composer/package-versions-deprecated": true,
"dealerdirect/phpcodesniffer-composer-installer": true,
"symfony/flex": true
},
"sort-packages": true
},
"scripts": {
"auto-scripts": {
"cache:clear": "symfony-cmd",
"assets:install %PUBLIC_DIR%": "symfony-cmd"
}
}
}

View File

@@ -0,0 +1,145 @@
<?php
declare(strict_types=1);
namespace Symfony\Component\DependencyInjection\Loader\Configurator;
use Doctrine\Bundle\DoctrineBundle\Command\CreateDatabaseDoctrineCommand;
use Doctrine\Bundle\DoctrineBundle\Command\DropDatabaseDoctrineCommand;
use Doctrine\Bundle\DoctrineBundle\Command\Proxy\RunSqlDoctrineCommand;
use Doctrine\Bundle\DoctrineBundle\ConnectionFactory;
use Doctrine\Bundle\DoctrineBundle\Controller\ProfilerController;
use Doctrine\Bundle\DoctrineBundle\DataCollector\DoctrineDataCollector;
use Doctrine\Bundle\DoctrineBundle\Dbal\BlacklistSchemaAssetFilter;
use Doctrine\Bundle\DoctrineBundle\Dbal\ManagerRegistryAwareConnectionProvider;
use Doctrine\Bundle\DoctrineBundle\Dbal\SchemaAssetsFilterManager;
use Doctrine\Bundle\DoctrineBundle\Registry;
use Doctrine\Bundle\DoctrineBundle\Twig\DoctrineExtension;
use Doctrine\Common\Persistence\ManagerRegistry as LegacyManagerRegistry;
use Doctrine\DBAL\Configuration;
use Doctrine\DBAL\Connection;
use Doctrine\DBAL\Event\Listeners\MysqlSessionInit;
use Doctrine\DBAL\Event\Listeners\OracleSessionInit;
use Doctrine\DBAL\Schema\DefaultSchemaManagerFactory;
use Doctrine\DBAL\Schema\LegacySchemaManagerFactory;
use Doctrine\DBAL\Tools\Console\Command\RunSqlCommand;
use Doctrine\DBAL\Tools\DsnParser;
use Doctrine\Persistence\ManagerRegistry;
use Symfony\Bridge\Doctrine\ContainerAwareEventManager;
use Symfony\Bridge\Doctrine\Middleware\IdleConnection\Listener;
return static function (ContainerConfigurator $container): void {
$container->parameters()
->set('doctrine.dbal.configuration.class', Configuration::class)
->set('doctrine.data_collector.class', DoctrineDataCollector::class)
->set('doctrine.dbal.connection.event_manager.class', ContainerAwareEventManager::class)
->set('doctrine.dbal.connection_factory.class', ConnectionFactory::class)
->set('doctrine.dbal.events.mysql_session_init.class', MysqlSessionInit::class)
->set('doctrine.dbal.events.oracle_session_init.class', OracleSessionInit::class)
->set('doctrine.class', Registry::class)
->set('doctrine.entity_managers', [])
->set('doctrine.default_entity_manager', '');
$container->services()
->alias(Connection::class, 'database_connection')
->alias(ManagerRegistry::class, 'doctrine')
->alias(LegacyManagerRegistry::class, 'doctrine')
->set('data_collector.doctrine', (string) param('doctrine.data_collector.class'))
->args([
service('doctrine'),
true,
service('doctrine.debug_data_holder')->nullOnInvalid(),
])
->tag('data_collector', ['template' => '@Doctrine/Collector/db.html.twig', 'id' => 'db', 'priority' => 250])
->set('doctrine.dbal.connection_factory', (string) param('doctrine.dbal.connection_factory.class'))
->args([
(string) param('doctrine.dbal.connection_factory.types'),
service('doctrine.dbal.connection_factory.dsn_parser'),
])
->set('doctrine.dbal.connection_factory.dsn_parser', DsnParser::class)
->args([
[],
])
->set('doctrine.dbal.connection', Connection::class)
->abstract()
->factory([service('doctrine.dbal.connection_factory'), 'createConnection'])
->set('doctrine.dbal.connection.event_manager', (string) param('doctrine.dbal.connection.event_manager.class'))
->abstract()
->args([
service('service_container'),
])
->set('doctrine.dbal.connection.configuration', (string) param('doctrine.dbal.configuration.class'))
->abstract()
->set('doctrine', (string) param('doctrine.class'))
->public()
->args([
service('service_container'),
(string) param('doctrine.connections'),
(string) param('doctrine.entity_managers'),
(string) param('doctrine.default_connection'),
(string) param('doctrine.default_entity_manager'),
])
->tag('kernel.reset', ['method' => 'reset'])
->set('doctrine.twig.doctrine_extension', DoctrineExtension::class)
->tag('twig.extension')
->set('doctrine.dbal.schema_asset_filter_manager', SchemaAssetsFilterManager::class)
->abstract()
->set('doctrine.dbal.well_known_schema_asset_filter', BlacklistSchemaAssetFilter::class)
->args([
[],
])
->set('doctrine.database_create_command', CreateDatabaseDoctrineCommand::class)
->args([
service('doctrine'),
])
->tag('console.command', ['command' => 'doctrine:database:create'])
->set('doctrine.database_drop_command', DropDatabaseDoctrineCommand::class)
->args([
service('doctrine'),
])
->tag('console.command', ['command' => 'doctrine:database:drop'])
->set('doctrine.query_sql_command', RunSqlDoctrineCommand::class)
->args([
service(ManagerRegistryAwareConnectionProvider::class)->nullOnInvalid(),
])
->tag('console.command', ['command' => 'doctrine:query:sql'])
->set(RunSqlCommand::class)
->args([
service(ManagerRegistryAwareConnectionProvider::class)->nullOnInvalid(),
])
->tag('console.command', ['command' => 'dbal:run-sql'])
->set(ProfilerController::class)
->args([
service('twig'),
service('doctrine'),
service('profiler'),
])
->tag('controller.service_arguments')
->set('doctrine.dbal.idle_connection_listener', Listener::class)
->args([
service('doctrine.dbal.connection_expiries'),
service('service_container'),
])
->tag('kernel.event_subscriber')
->set('doctrine.dbal.default_schema_manager_factory', DefaultSchemaManagerFactory::class)
->set('doctrine.dbal.legacy_schema_manager_factory', LegacySchemaManagerFactory::class);
};

View File

@@ -0,0 +1,62 @@
<?php
declare(strict_types=1);
namespace Symfony\Component\DependencyInjection\Loader\Configurator;
use Symfony\Bridge\Doctrine\Messenger\DoctrineClearEntityManagerWorkerSubscriber;
use Symfony\Bridge\Doctrine\Messenger\DoctrineCloseConnectionMiddleware;
use Symfony\Bridge\Doctrine\Messenger\DoctrineOpenTransactionLoggerMiddleware;
use Symfony\Bridge\Doctrine\Messenger\DoctrinePingConnectionMiddleware;
use Symfony\Bridge\Doctrine\Messenger\DoctrineTransactionMiddleware;
use Symfony\Bridge\Doctrine\SchemaListener\MessengerTransportDoctrineSchemaListener;
use Symfony\Component\Messenger\Bridge\Doctrine\Transport\DoctrineTransportFactory;
return static function (ContainerConfigurator $container): void {
$container->services()
->set('messenger.middleware.doctrine_transaction', DoctrineTransactionMiddleware::class)
->abstract()
->args([
service('doctrine'),
])
->set('messenger.middleware.doctrine_ping_connection', DoctrinePingConnectionMiddleware::class)
->abstract()
->args([
service('doctrine'),
])
->set('messenger.middleware.doctrine_close_connection', DoctrineCloseConnectionMiddleware::class)
->abstract()
->args([
service('doctrine'),
])
->set('messenger.middleware.doctrine_open_transaction_logger', DoctrineOpenTransactionLoggerMiddleware::class)
->abstract()
->args([
service('doctrine'),
null,
service('logger'),
])
->set('doctrine.orm.messenger.event_subscriber.doctrine_clear_entity_manager', DoctrineClearEntityManagerWorkerSubscriber::class)
->tag('kernel.event_subscriber')
->args([
service('doctrine'),
])
->set('messenger.transport.doctrine.factory', DoctrineTransportFactory::class)
->tag('messenger.transport_factory')
->args([
service('doctrine'),
])
->set('doctrine.orm.messenger.doctrine_schema_listener', MessengerTransportDoctrineSchemaListener::class)
->args([
tagged_iterator('messenger.receiver'),
])
->tag('doctrine.event_listener', ['event' => 'postGenerateSchema'])
->tag('doctrine.event_listener', ['event' => 'onSchemaCreateTable']);
};

View File

@@ -0,0 +1,44 @@
<?php
declare(strict_types=1);
namespace Symfony\Component\DependencyInjection\Loader\Configurator;
use ArrayObject;
use Doctrine\Bundle\DoctrineBundle\Middleware\BacktraceDebugDataHolder;
use Doctrine\Bundle\DoctrineBundle\Middleware\DebugMiddleware;
use Doctrine\Bundle\DoctrineBundle\Middleware\IdleConnectionMiddleware;
use Doctrine\DBAL\Logging\Middleware;
return static function (ContainerConfigurator $container): void {
$container->services()
->set('doctrine.dbal.connection_expiries', ArrayObject::class)
->set('doctrine.dbal.logging_middleware', Middleware::class)
->abstract()
->args([
service('logger'),
])
->tag('monolog.logger', ['channel' => 'doctrine'])
->set('doctrine.debug_data_holder', BacktraceDebugDataHolder::class)
->args([
[],
])
->tag('kernel.reset', ['method' => 'reset'])
->set('doctrine.dbal.debug_middleware', DebugMiddleware::class)
->abstract()
->args([
service('doctrine.debug_data_holder'),
service('debug.stopwatch')->nullOnInvalid(),
])
->set('doctrine.dbal.idle_connection_middleware', IdleConnectionMiddleware::class)
->abstract()
->args([
service('doctrine.dbal.connection_expiries'),
null,
]);
};

View File

@@ -0,0 +1,368 @@
<?php
declare(strict_types=1);
namespace Symfony\Component\DependencyInjection\Loader\Configurator;
use Doctrine\Bundle\DoctrineBundle\Command\ImportMappingDoctrineCommand;
use Doctrine\Bundle\DoctrineBundle\ManagerConfigurator;
use Doctrine\Bundle\DoctrineBundle\Mapping\ContainerEntityListenerResolver;
use Doctrine\Bundle\DoctrineBundle\Orm\ManagerRegistryAwareEntityManagerProvider;
use Doctrine\Bundle\DoctrineBundle\Repository\ContainerRepositoryFactory;
use Doctrine\ORM\Cache\CacheConfiguration;
use Doctrine\ORM\Cache\DefaultCacheFactory;
use Doctrine\ORM\Cache\Logging\CacheLoggerChain;
use Doctrine\ORM\Cache\Logging\StatisticsCacheLogger;
use Doctrine\ORM\Cache\Region\DefaultRegion;
use Doctrine\ORM\Cache\Region\FileLockRegion;
use Doctrine\ORM\Cache\RegionsConfiguration;
use Doctrine\ORM\Configuration;
use Doctrine\ORM\EntityManager;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\Mapping\AnsiQuoteStrategy;
use Doctrine\ORM\Mapping\DefaultNamingStrategy;
use Doctrine\ORM\Mapping\DefaultQuoteStrategy;
use Doctrine\ORM\Mapping\DefaultTypedFieldMapper;
use Doctrine\ORM\Mapping\Driver\AnnotationDriver;
use Doctrine\ORM\Mapping\Driver\AttributeDriver;
use Doctrine\ORM\Mapping\Driver\SimplifiedXmlDriver;
use Doctrine\ORM\Mapping\Driver\SimplifiedYamlDriver;
use Doctrine\ORM\Mapping\UnderscoreNamingStrategy;
use Doctrine\ORM\Tools\AttachEntityListenersListener;
use Doctrine\ORM\Tools\Console\Command\ClearCache\CollectionRegionCommand;
use Doctrine\ORM\Tools\Console\Command\ClearCache\EntityRegionCommand;
use Doctrine\ORM\Tools\Console\Command\ClearCache\MetadataCommand;
use Doctrine\ORM\Tools\Console\Command\ClearCache\QueryCommand;
use Doctrine\ORM\Tools\Console\Command\ClearCache\QueryRegionCommand;
use Doctrine\ORM\Tools\Console\Command\ClearCache\ResultCommand;
use Doctrine\ORM\Tools\Console\Command\ConvertMappingCommand;
use Doctrine\ORM\Tools\Console\Command\EnsureProductionSettingsCommand;
use Doctrine\ORM\Tools\Console\Command\InfoCommand;
use Doctrine\ORM\Tools\Console\Command\MappingDescribeCommand;
use Doctrine\ORM\Tools\Console\Command\RunDqlCommand;
use Doctrine\ORM\Tools\Console\Command\SchemaTool\CreateCommand;
use Doctrine\ORM\Tools\Console\Command\SchemaTool\DropCommand;
use Doctrine\ORM\Tools\Console\Command\SchemaTool\UpdateCommand;
use Doctrine\ORM\Tools\Console\Command\ValidateSchemaCommand;
use Doctrine\ORM\Tools\ResolveTargetEntityListener;
use Doctrine\Persistence\Mapping\Driver\MappingDriverChain;
use Doctrine\Persistence\Mapping\Driver\PHPDriver;
use Doctrine\Persistence\Mapping\Driver\StaticPHPDriver;
use Symfony\Bridge\Doctrine\ArgumentResolver\EntityValueResolver;
use Symfony\Bridge\Doctrine\CacheWarmer\ProxyCacheWarmer;
use Symfony\Bridge\Doctrine\Form\DoctrineOrmTypeGuesser;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Symfony\Bridge\Doctrine\SchemaListener\DoctrineDbalCacheAdapterSchemaListener;
use Symfony\Bridge\Doctrine\SchemaListener\LockStoreSchemaListener;
use Symfony\Bridge\Doctrine\SchemaListener\PdoSessionHandlerSchemaListener;
use Symfony\Bridge\Doctrine\SchemaListener\RememberMeTokenProviderDoctrineSchemaListener;
use Symfony\Bridge\Doctrine\Security\User\EntityUserProvider;
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntityValidator;
use Symfony\Bridge\Doctrine\Validator\DoctrineInitializer;
use Symfony\Component\DependencyInjection\ServiceLocator;
use Symfony\Component\ExpressionLanguage\ExpressionLanguage;
use const CASE_LOWER;
return static function (ContainerConfigurator $container): void {
$container->parameters()
->set('doctrine.orm.configuration.class', Configuration::class)
->set('doctrine.orm.entity_manager.class', EntityManager::class)
->set('doctrine.orm.manager_configurator.class', ManagerConfigurator::class)
// cache (keep classes as strings to avoid legacy class resolution issues)
->set('doctrine.orm.cache.array.class', 'Doctrine\\Common\\Cache\\ArrayCache')
->set('doctrine.orm.cache.apc.class', 'Doctrine\\Common\\Cache\\ApcCache')
->set('doctrine.orm.cache.memcache.class', 'Doctrine\\Common\\Cache\\MemcacheCache')
->set('doctrine.orm.cache.memcache_host', 'localhost')
->set('doctrine.orm.cache.memcache_port', 11211)
->set('doctrine.orm.cache.memcache_instance.class', 'Memcache')
->set('doctrine.orm.cache.memcached.class', 'Doctrine\\Common\\Cache\\MemcachedCache')
->set('doctrine.orm.cache.memcached_host', 'localhost')
->set('doctrine.orm.cache.memcached_port', 11211)
->set('doctrine.orm.cache.memcached_instance.class', 'Memcached')
->set('doctrine.orm.cache.redis.class', 'Doctrine\\Common\\Cache\\RedisCache')
->set('doctrine.orm.cache.redis_host', 'localhost')
->set('doctrine.orm.cache.redis_port', 6379)
->set('doctrine.orm.cache.redis_instance.class', 'Redis')
->set('doctrine.orm.cache.xcache.class', 'Doctrine\\Common\\Cache\\XcacheCache')
->set('doctrine.orm.cache.wincache.class', 'Doctrine\\Common\\Cache\\WinCacheCache')
->set('doctrine.orm.cache.zenddata.class', 'Doctrine\\Common\\Cache\\ZendDataCache')
// metadata drivers
->set('doctrine.orm.metadata.driver_chain.class', MappingDriverChain::class)
->set('doctrine.orm.metadata.annotation.class', AnnotationDriver::class)
->set('doctrine.orm.metadata.xml.class', SimplifiedXmlDriver::class)
->set('doctrine.orm.metadata.yml.class', SimplifiedYamlDriver::class)
->set('doctrine.orm.metadata.php.class', PHPDriver::class)
->set('doctrine.orm.metadata.staticphp.class', StaticPHPDriver::class)
->set('doctrine.orm.metadata.attribute.class', AttributeDriver::class)
// cache warmer
->set('doctrine.orm.proxy_cache_warmer.class', ProxyCacheWarmer::class)
// form field factory guesser
->set('form.type_guesser.doctrine.class', DoctrineOrmTypeGuesser::class)
// validator
->set('doctrine.orm.validator.unique.class', UniqueEntityValidator::class)
->set('doctrine.orm.validator_initializer.class', DoctrineInitializer::class)
// security
->set('doctrine.orm.security.user.provider.class', EntityUserProvider::class)
// listeners
->set('doctrine.orm.listeners.resolve_target_entity.class', ResolveTargetEntityListener::class)
->set('doctrine.orm.listeners.attach_entity_listeners.class', AttachEntityListenersListener::class)
// naming strategy
->set('doctrine.orm.naming_strategy.default.class', DefaultNamingStrategy::class)
->set('doctrine.orm.naming_strategy.underscore.class', UnderscoreNamingStrategy::class)
// quote strategy
->set('doctrine.orm.quote_strategy.default.class', DefaultQuoteStrategy::class)
->set('doctrine.orm.quote_strategy.ansi.class', AnsiQuoteStrategy::class)
// typed field mapper
->set('doctrine.orm.typed_field_mapper.default.class', DefaultTypedFieldMapper::class)
// entity listener resolver
->set('doctrine.orm.entity_listener_resolver.class', ContainerEntityListenerResolver::class)
// second level cache
->set('doctrine.orm.second_level_cache.default_cache_factory.class', DefaultCacheFactory::class)
->set('doctrine.orm.second_level_cache.default_region.class', DefaultRegion::class)
->set('doctrine.orm.second_level_cache.filelock_region.class', FileLockRegion::class)
->set('doctrine.orm.second_level_cache.logger_chain.class', CacheLoggerChain::class)
->set('doctrine.orm.second_level_cache.logger_statistics.class', StatisticsCacheLogger::class)
->set('doctrine.orm.second_level_cache.cache_configuration.class', CacheConfiguration::class)
->set('doctrine.orm.second_level_cache.regions_configuration.class', RegionsConfiguration::class);
$container->services()
->alias(EntityManagerInterface::class, 'doctrine.orm.entity_manager')
->alias('doctrine.orm.metadata.annotation_reader', 'annotation_reader')
->set('doctrine.orm.proxy_cache_warmer', (string) param('doctrine.orm.proxy_cache_warmer.class'))
->tag('kernel.cache_warmer')
->args([
service('doctrine'),
])
->set('form.type_guesser.doctrine', (string) param('form.type_guesser.doctrine.class'))
->tag('form.type_guesser')
->args([
service('doctrine'),
])
->set('form.type.entity', EntityType::class)
->tag('form.type', ['alias' => 'entity'])
->args([
service('doctrine'),
])
->set('doctrine.orm.configuration', (string) param('doctrine.orm.configuration.class'))
->abstract()
->set('doctrine.orm.entity_manager.abstract', (string) param('doctrine.orm.entity_manager.class'))
->abstract()
->lazy()
->set('doctrine.orm.container_repository_factory', ContainerRepositoryFactory::class)
->args([
inline_service(ServiceLocator::class)->args([
[],
]),
])
->set('doctrine.orm.manager_configurator.abstract', (string) param('doctrine.orm.manager_configurator.class'))
->abstract()
->args([
[],
[],
])
->set('doctrine.orm.validator.unique', (string) param('doctrine.orm.validator.unique.class'))
->tag('validator.constraint_validator', ['alias' => 'doctrine.orm.validator.unique'])
->args([
service('doctrine'),
])
->set('doctrine.orm.validator_initializer', (string) param('doctrine.orm.validator_initializer.class'))
->tag('validator.initializer')
->args([
service('doctrine'),
])
->set('doctrine.orm.security.user.provider', (string) param('doctrine.orm.security.user.provider.class'))
->abstract()
->args([
service('doctrine'),
])
->set('doctrine.orm.listeners.resolve_target_entity', (string) param('doctrine.orm.listeners.resolve_target_entity.class'))
->set('doctrine.orm.listeners.doctrine_dbal_cache_adapter_schema_listener', DoctrineDbalCacheAdapterSchemaListener::class)
->args([
[],
])
->tag('doctrine.event_listener', ['event' => 'postGenerateSchema'])
->set('doctrine.orm.listeners.doctrine_token_provider_schema_listener', RememberMeTokenProviderDoctrineSchemaListener::class)
->args([
tagged_iterator('security.remember_me_handler'),
])
->tag('doctrine.event_listener', ['event' => 'postGenerateSchema'])
->set('doctrine.orm.listeners.pdo_session_handler_schema_listener', PdoSessionHandlerSchemaListener::class)
->args([
service('session.handler'),
])
->tag('doctrine.event_listener', ['event' => 'postGenerateSchema'])
->set('doctrine.orm.listeners.lock_store_schema_listener', LockStoreSchemaListener::class)
->args([
tagged_iterator('lock.store'),
])
->tag('doctrine.event_listener', ['event' => 'postGenerateSchema'])
->set('doctrine.orm.naming_strategy.default', (string) param('doctrine.orm.naming_strategy.default.class'))
->set('doctrine.orm.naming_strategy.underscore', (string) param('doctrine.orm.naming_strategy.underscore.class'))
->set('doctrine.orm.naming_strategy.underscore_number_aware', (string) param('doctrine.orm.naming_strategy.underscore.class'))
->args([
CASE_LOWER,
true,
])
->set('doctrine.orm.quote_strategy.default', (string) param('doctrine.orm.quote_strategy.default.class'))
->set('doctrine.orm.quote_strategy.ansi', (string) param('doctrine.orm.quote_strategy.ansi.class'))
->set('doctrine.orm.typed_field_mapper.default', (string) param('doctrine.orm.typed_field_mapper.default.class'))
->set('doctrine.ulid_generator', 'Symfony\\Bridge\\Doctrine\\IdGenerator\\UlidGenerator')
->args([
service('ulid.factory')->ignoreOnInvalid(),
])
->tag('doctrine.id_generator')
->set('doctrine.uuid_generator', 'Symfony\\Bridge\\Doctrine\\IdGenerator\\UuidGenerator')
->args([
service('uuid.factory')->ignoreOnInvalid(),
])
->tag('doctrine.id_generator')
->set('doctrine.orm.command.entity_manager_provider', ManagerRegistryAwareEntityManagerProvider::class)
->args([
service('doctrine'),
])
->set('doctrine.orm.entity_value_resolver', EntityValueResolver::class)
->args([
service('doctrine'),
service('doctrine.orm.entity_value_resolver.expression_language')->ignoreOnInvalid(),
])
->tag('controller.argument_value_resolver', ['priority' => 110, 'name' => EntityValueResolver::class])
->set('doctrine.orm.entity_value_resolver.expression_language', ExpressionLanguage::class)
->set('doctrine.cache_clear_metadata_command', MetadataCommand::class)
->args([
service('doctrine.orm.command.entity_manager_provider'),
])
->tag('console.command', ['command' => 'doctrine:cache:clear-metadata'])
->set('doctrine.cache_clear_query_cache_command', QueryCommand::class)
->args([
service('doctrine.orm.command.entity_manager_provider'),
])
->tag('console.command', ['command' => 'doctrine:cache:clear-query'])
->set('doctrine.cache_clear_result_command', ResultCommand::class)
->args([
service('doctrine.orm.command.entity_manager_provider'),
])
->tag('console.command', ['command' => 'doctrine:cache:clear-result'])
->set('doctrine.cache_collection_region_command', CollectionRegionCommand::class)
->args([
service('doctrine.orm.command.entity_manager_provider'),
])
->tag('console.command', ['command' => 'doctrine:cache:clear-collection-region'])
->set('doctrine.mapping_convert_command', ConvertMappingCommand::class)
->args([
service('doctrine.orm.command.entity_manager_provider'),
])
->tag('console.command', ['command' => 'doctrine:mapping:convert'])
->set('doctrine.schema_create_command', CreateCommand::class)
->args([
service('doctrine.orm.command.entity_manager_provider'),
])
->tag('console.command', ['command' => 'doctrine:schema:create'])
->set('doctrine.schema_drop_command', DropCommand::class)
->args([
service('doctrine.orm.command.entity_manager_provider'),
])
->tag('console.command', ['command' => 'doctrine:schema:drop'])
->set('doctrine.ensure_production_settings_command', EnsureProductionSettingsCommand::class)
->args([
service('doctrine.orm.command.entity_manager_provider'),
])
->tag('console.command', ['command' => 'doctrine:ensure-production-settings'])
->set('doctrine.clear_entity_region_command', EntityRegionCommand::class)
->args([
service('doctrine.orm.command.entity_manager_provider'),
])
->tag('console.command', ['command' => 'doctrine:cache:clear-entity-region'])
->set('doctrine.mapping_info_command', InfoCommand::class)
->args([
service('doctrine.orm.command.entity_manager_provider'),
])
->tag('console.command', ['command' => 'doctrine:mapping:info'])
->set('doctrine.mapping_describe_command', MappingDescribeCommand::class)
->args([
service('doctrine.orm.command.entity_manager_provider'),
])
->tag('console.command', ['command' => 'doctrine:mapping:describe'])
->set('doctrine.clear_query_region_command', QueryRegionCommand::class)
->args([
service('doctrine.orm.command.entity_manager_provider'),
])
->tag('console.command', ['command' => 'doctrine:cache:clear-query-region'])
->set('doctrine.query_dql_command', RunDqlCommand::class)
->args([
service('doctrine.orm.command.entity_manager_provider'),
])
->tag('console.command', ['command' => 'doctrine:query:dql'])
->set('doctrine.schema_update_command', UpdateCommand::class)
->args([
service('doctrine.orm.command.entity_manager_provider'),
])
->tag('console.command', ['command' => 'doctrine:schema:update'])
->set('doctrine.schema_validate_command', ValidateSchemaCommand::class)
->args([
service('doctrine.orm.command.entity_manager_provider'),
])
->tag('console.command', ['command' => 'doctrine:schema:validate'])
->set('doctrine.mapping_import_command', ImportMappingDoctrineCommand::class)
->args([
service('doctrine'),
(string) param('kernel.bundles'),
])
->tag('console.command', ['command' => 'doctrine:mapping:import']);
};

View File

@@ -0,0 +1,296 @@
<?xml version="1.0" encoding="UTF-8" ?>
<xsd:schema xmlns="http://symfony.com/schema/dic/doctrine"
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
targetNamespace="http://symfony.com/schema/dic/doctrine"
elementFormDefault="qualified">
<xsd:element name="config">
<xsd:complexType>
<xsd:all>
<xsd:element name="dbal" type="dbal" minOccurs="0" maxOccurs="1" />
<xsd:element name="orm" type="orm" minOccurs="0" maxOccurs="1" />
</xsd:all>
</xsd:complexType>
</xsd:element>
<xsd:complexType name="named_scalar">
<xsd:simpleContent>
<xsd:extension base="xsd:string">
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:extension>
</xsd:simpleContent>
</xsd:complexType>
<!-- DBAL configuration -->
<xsd:attributeGroup name="connection-config">
<xsd:attribute name="driver" type="xsd:string" />
<xsd:attribute name="driver-class" type="xsd:string" />
<xsd:attribute name="wrapper-class" type="xsd:string" />
<xsd:attribute name="keep-slave" type="xsd:string" />
<xsd:attribute name="keep-replica" type="xsd:string" />
<xsd:attribute name="platform-service" type="xsd:string" />
<xsd:attribute name="auto-commit" type="xsd:string" />
<xsd:attribute name="schema-filter" type="xsd:string" />
<xsd:attribute name="logging" type="xsd:string" default="false" />
<xsd:attribute name="profiling" type="xsd:string" default="false" />
<xsd:attribute name="profiling-collect-backtrace" type="xsd:string" default="false" />
<xsd:attribute name="profiling-collect-schema-errors" type="xsd:string" default="true" />
<xsd:attribute name="server-version" type="xsd:string" />
<xsd:attribute name="schema-manager-factory" type="xsd:string" />
<xsd:attribute name="result-cache" type="xsd:string" />
<xsd:attribute name="use-savepoints" type="xsd:boolean" />
<xsd:attribute name="disable-type-comments" type="xsd:boolean" />
<xsd:attributeGroup ref="driver-config" />
</xsd:attributeGroup>
<xsd:attributeGroup name="driver-config">
<xsd:attribute name="url" type="xsd:string" />
<xsd:attribute name="dbname" type="xsd:string" />
<xsd:attribute name="host" type="xsd:string" />
<xsd:attribute name="port" type="xsd:string" />
<xsd:attribute name="user" type="xsd:string" />
<xsd:attribute name="password" type="xsd:string" />
<xsd:attribute name="override-url" type="xsd:boolean" />
<xsd:attribute name="dbname-suffix" type="xsd:string" />
<xsd:attribute name="application-name" type="xsd:string" />
<xsd:attribute name="path" type="xsd:string" />
<xsd:attribute name="unix-socket" type="xsd:string" />
<xsd:attribute name="memory" type="xsd:string" />
<xsd:attribute name="charset" type="xsd:string" />
<xsd:attribute name="persistent" type="xsd:string" />
<xsd:attribute name="protocol" type="xsd:string" />
<xsd:attribute name="server" type="xsd:string" />
<xsd:attribute name="service" type="xsd:string" />
<xsd:attribute name="servicename" type="xsd:string" />
<xsd:attribute name="session-mode" type="xsd:string" />
<xsd:attribute name="default_dbname" type="xsd:string" />
<xsd:attribute name="sslmode" type="xsd:string" />
<xsd:attribute name="sslrootcert" type="xsd:string" />
<xsd:attribute name="sslcert" type="xsd:string" />
<xsd:attribute name="sslkey" type="xsd:string" />
<xsd:attribute name="sslcrl" type="xsd:string" />
<xsd:attribute name="pooled" type="xsd:string" />
<xsd:attribute name="multiple-active-result-sets" type="xsd:string" />
<xsd:attribute name="connectstring" type="xsd:string" />
<xsd:attribute name="instancename" type="xsd:string" />
</xsd:attributeGroup>
<xsd:group name="connection-child-config">
<xsd:choice>
<xsd:element name="option" type="option" />
<xsd:element name="mapping-type" type="named_scalar" />
<xsd:element name="slave" type="replica" />
<xsd:element name="replica" type="replica" />
<xsd:element name="default-table-option" type="named_scalar" />
</xsd:choice>
</xsd:group>
<xsd:complexType name="dbal">
<xsd:choice minOccurs="0" maxOccurs="unbounded">
<xsd:element name="connection" type="connection" />
<xsd:element name="type" type="named_scalar" />
<xsd:element name="driver-scheme" type="driver_scheme" />
<xsd:group ref="connection-child-config" />
</xsd:choice>
<xsd:attribute name="default-connection" type="xsd:string" />
<xsd:attributeGroup ref="connection-config" />
</xsd:complexType>
<xsd:complexType name="driver_scheme">
<xsd:simpleContent>
<xsd:extension base="xsd:string">
<xsd:attribute name="scheme" type="xsd:string" use="required" />
</xsd:extension>
</xsd:simpleContent>
</xsd:complexType>
<xsd:complexType name="option">
<xsd:complexContent>
<xsd:extension base="xsd:anyType">
<xsd:attribute name="key" type="xsd:string" use="required" />
</xsd:extension>
</xsd:complexContent>
</xsd:complexType>
<xsd:complexType name="connection">
<xsd:group ref="connection-child-config" minOccurs="0" maxOccurs="unbounded" />
<xsd:attribute name="name" type="xsd:string" use="required" />
<xsd:attributeGroup ref="connection-config" />
</xsd:complexType>
<xsd:complexType name="replica">
<xsd:attribute name="name" type="xsd:string" use="required" />
<xsd:attributeGroup ref="driver-config" />
</xsd:complexType>
<!-- ORM configuration -->
<xsd:complexType name="mapping">
<xsd:attribute name="name" type="xsd:string" use="required" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="dir" type="xsd:string" />
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="prefix" type="xsd:string" />
<xsd:attribute name="is-bundle" type="xsd:string" />
</xsd:complexType>
<xsd:complexType name="orm">
<xsd:choice minOccurs="0" maxOccurs="unbounded">
<xsd:element name="entity-manager" type="entity_manager" />
<xsd:element name="resolve-target-entity" type="resolve_target_entity" minOccurs="0" maxOccurs="unbounded" />
<xsd:group ref="entity-manager-child-config" />
</xsd:choice>
<xsd:attribute name="default-entity-manager" type="xsd:string" />
<xsd:attribute name="proxy-dir" type="xsd:string" />
<xsd:attribute name="proxy-namespace" type="xsd:string" />
<xsd:attribute name="auto-generate-proxy-classes" type="xsd:string" default="false" />
<xsd:attribute name="enable-lazy-ghost-objects" type="xsd:boolean" />
<xsd:attribute name="enable-native-lazy-objects" type="xsd:boolean" />
<xsd:attributeGroup ref="entity-manager-config" />
</xsd:complexType>
<xsd:complexType name="resolve_target_entity">
<xsd:simpleContent>
<xsd:extension base="xsd:string">
<xsd:attribute name="interface" type="xsd:string" use="required" />
</xsd:extension>
</xsd:simpleContent>
</xsd:complexType>
<xsd:simpleType name="cache_driver_type">
<xsd:restriction base="xsd:token">
<xsd:enumeration value="pool"/>
<xsd:enumeration value="service"/>
</xsd:restriction>
</xsd:simpleType>
<xsd:complexType name="cache_driver">
<xsd:attribute name="type" type="cache_driver_type" default="pool" />
<xsd:attribute name="id" type="xsd:string" />
<xsd:attribute name="pool" type="xsd:string" />
</xsd:complexType>
<xsd:complexType name="entity_listeners">
<xsd:choice minOccurs="1">
<xsd:element name="entity" type="entity_listeners_entity" minOccurs="1" maxOccurs="unbounded" />
</xsd:choice>
</xsd:complexType>
<xsd:complexType name="entity_listeners_entity">
<xsd:choice minOccurs="1">
<xsd:element name="listener" type="entity_listeners_listener" minOccurs="1" maxOccurs="unbounded" />
</xsd:choice>
<xsd:attribute name="class" type="xsd:string" />
</xsd:complexType>
<xsd:complexType name="entity_listeners_listener">
<xsd:choice minOccurs="1">
<xsd:element name="event" type="entity_listeners_event" minOccurs="1" maxOccurs="unbounded" />
</xsd:choice>
<xsd:attribute name="class" type="xsd:string" />
</xsd:complexType>
<xsd:complexType name="entity_listeners_event">
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="method" type="xsd:string" />
</xsd:complexType>
<xsd:complexType name="entity_manager">
<xsd:group ref="entity-manager-child-config" minOccurs="0" maxOccurs="unbounded" />
<xsd:attribute name="name" type="xsd:string" use="required" />
<xsd:attributeGroup ref="entity-manager-config" />
</xsd:complexType>
<xsd:group name="entity-manager-child-config">
<xsd:choice>
<xsd:element name="mapping" type="mapping" />
<xsd:element name="metadata-cache-driver" type="cache_driver" minOccurs="0" maxOccurs="1" />
<xsd:element name="result-cache-driver" type="cache_driver" minOccurs="0" maxOccurs="1" />
<xsd:element name="query-cache-driver" type="cache_driver" minOccurs="0" maxOccurs="1" />
<xsd:element name="dql" type="dql" minOccurs="0" maxOccurs="1" />
<xsd:element name="hydrator" type="named_scalar" minOccurs="0" maxOccurs="unbounded" />
<xsd:element name="filter" type="filter" minOccurs="0" maxOccurs="unbounded" />
<xsd:element name="entity-listeners" type="entity_listeners" minOccurs="0" maxOccurs="1" />
<xsd:element name="second-level-cache" type="second-level-cache" minOccurs="0" maxOccurs="1" />
<xsd:element name="schema-ignore-class" type="xsd:string" minOccurs="0" maxOccurs="unbounded" />
<xsd:element name="identity-generation-preference" type="identity_generation_preference" minOccurs="0" maxOccurs="unbounded" />
</xsd:choice>
</xsd:group>
<xsd:attributeGroup name="entity-manager-config">
<xsd:attribute name="auto-mapping" type="xsd:string" />
<xsd:attribute name="connection" type="xsd:string" />
<xsd:attribute name="default-repository-class" type="xsd:string" />
<xsd:attribute name="class-metadata-factory-name" type="xsd:string" />
<xsd:attribute name="fetch-mode-subselect-batch-size" type="xsd:int" />
<xsd:attribute name="naming-strategy" type="xsd:string" />
<xsd:attribute name="quote-strategy" type="xsd:string" />
<xsd:attribute name="typed-field-mapper" type="xsd:string" />
<xsd:attribute name="entity-listener-resolver" type="xsd:string" />
<xsd:attribute name="repository-factory" type="xsd:string" />
<xsd:attribute name="report-fields-where-declared" type="xsd:boolean" />
<xsd:attribute name="validate-xml-mapping" type="xsd:boolean" />
</xsd:attributeGroup>
<xsd:complexType name="filter" mixed="true">
<xsd:choice minOccurs="0">
<xsd:element name="parameter" type="named_scalar" minOccurs="0" maxOccurs="unbounded" />
</xsd:choice>
<xsd:attribute name="name" type="xsd:string" use="required" />
<xsd:attribute name="class" type="xsd:string" />
<xsd:attribute name="enabled" type="xsd:boolean" />
</xsd:complexType>
<xsd:complexType name="second-level-cache-region" mixed="true">
<xsd:choice minOccurs="0" maxOccurs="unbounded">
<xsd:element name="cache-driver" type="cache_driver" minOccurs="0" maxOccurs="1" />
</xsd:choice>
<xsd:attribute name="name" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="service" type="xsd:string" />
<xsd:attribute name="lifetime" type="xsd:integer" />
<xsd:attribute name="lock-lifetime" type="xsd:integer" />
<xsd:attribute name="cache-driver" type="xsd:string" />
</xsd:complexType>
<xsd:complexType name="second-level-cache-logger" mixed="true">
<xsd:attribute name="name" type="xsd:string" />
<xsd:attribute name="service" type="xsd:string" />
</xsd:complexType>
<xsd:complexType name="second-level-cache" mixed="true">
<xsd:choice minOccurs="0" maxOccurs="unbounded">
<xsd:element name="logger" type="second-level-cache-logger" minOccurs="0" maxOccurs="unbounded" />
<xsd:element name="region" type="second-level-cache-region" minOccurs="0" maxOccurs="unbounded" />
<xsd:element name="region-cache-driver" type="cache_driver" minOccurs="0" maxOccurs="1" />
</xsd:choice>
<xsd:attribute name="enabled" type="xsd:boolean" default="true"/>
<xsd:attribute name="log-enabled" type="xsd:boolean" default="true"/>
<xsd:attribute name="factory" type="xsd:string" />
<xsd:attribute name="query-validator" type="xsd:string" />
<xsd:attribute name="region-lifetime" type="xsd:integer" />
<xsd:attribute name="region-lock-lifetime" type="xsd:integer" />
</xsd:complexType>
<xsd:complexType name="dql">
<xsd:choice minOccurs="0" maxOccurs="unbounded">
<xsd:element name="string-function" type="named_scalar" />
<xsd:element name="numeric-function" type="named_scalar" />
<xsd:element name="datetime-function" type="named_scalar" />
</xsd:choice>
</xsd:complexType>
<xsd:complexType name="identity_generation_preference">
<xsd:simpleContent>
<xsd:extension base="xsd:string">
<xsd:attribute name="platform" type="xsd:string" use="required" />
</xsd:extension>
</xsd:simpleContent>
</xsd:complexType>
</xsd:schema>

View File

@@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
namespace Doctrine\Bundle\DoctrineBundle\Attribute;
use Attribute;
/**
* Service tag to autoconfigure event listeners.
*/
#[Attribute(Attribute::TARGET_CLASS | Attribute::IS_REPEATABLE)]
class AsDoctrineListener
{
public function __construct(
public string $event,
public int|null $priority = null,
public string|null $connection = null,
) {
}
}

View File

@@ -0,0 +1,24 @@
<?php
declare(strict_types=1);
namespace Doctrine\Bundle\DoctrineBundle\Attribute;
use Attribute;
/**
* Service tag to autoconfigure entity listeners.
*/
#[Attribute(Attribute::TARGET_CLASS | Attribute::IS_REPEATABLE)]
class AsEntityListener
{
public function __construct(
public string|null $event = null,
public string|null $method = null,
public bool|null $lazy = null,
public string|null $entityManager = null,
public string|null $entity = null,
public int|null $priority = null,
) {
}
}

View File

@@ -0,0 +1,18 @@
<?php
declare(strict_types=1);
namespace Doctrine\Bundle\DoctrineBundle\Attribute;
use Attribute;
#[Attribute(Attribute::TARGET_CLASS)]
class AsMiddleware
{
/** @param string[] $connections */
public function __construct(
public array $connections = [],
public int|null $priority = null,
) {
}
}

View File

@@ -0,0 +1,49 @@
<?php
declare(strict_types=1);
namespace Doctrine\Bundle\DoctrineBundle\CacheWarmer;
use Doctrine\ORM\EntityManagerInterface;
use LogicException;
use Symfony\Bundle\FrameworkBundle\CacheWarmer\AbstractPhpFileCacheWarmer;
use Symfony\Component\Cache\Adapter\ArrayAdapter;
use function is_file;
/** @final since 2.11 */
class DoctrineMetadataCacheWarmer extends AbstractPhpFileCacheWarmer
{
public function __construct(
private readonly EntityManagerInterface $entityManager,
private readonly string $phpArrayFile,
) {
parent::__construct($phpArrayFile);
}
/**
* It must not be optional because it should be called before ProxyCacheWarmer which is not optional.
*/
public function isOptional(): bool
{
return false;
}
protected function doWarmUp(string $cacheDir, ArrayAdapter $arrayAdapter, string|null $buildDir = null): bool
{
// cache already warmed up, no needs to do it again
if (is_file($this->phpArrayFile)) {
return false;
}
$metadataFactory = $this->entityManager->getMetadataFactory();
if ($metadataFactory->getLoadedMetadata()) {
throw new LogicException('DoctrineMetadataCacheWarmer must load metadata first, check priority of your warmers.');
}
$metadataFactory->setCache($arrayAdapter);
$metadataFactory->getAllMetadata();
return true;
}
}

View File

@@ -0,0 +1,103 @@
<?php
declare(strict_types=1);
namespace Doctrine\Bundle\DoctrineBundle\Command;
use Doctrine\DBAL\DriverManager;
use Doctrine\DBAL\Platforms\PostgreSQLPlatform;
use InvalidArgumentException;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Throwable;
use function in_array;
use function sprintf;
/**
* Database tool allows you to easily create your configured databases.
*
* @final
*/
class CreateDatabaseDoctrineCommand extends DoctrineCommand
{
protected function configure(): void
{
$this
->setName('doctrine:database:create')
->setDescription('Creates the configured database')
->addOption('connection', 'c', InputOption::VALUE_REQUIRED, 'The connection to use for this command')
->addOption('if-not-exists', null, InputOption::VALUE_NONE, 'Don\'t trigger an error, when the database already exists')
->setHelp(<<<'EOT'
The <info>%command.name%</info> command creates the default connections database:
<info>php %command.full_name%</info>
You can also optionally specify the name of a connection to create the database for:
<info>php %command.full_name% --connection=default</info>
EOT);
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$connectionName = $input->getOption('connection');
if (empty($connectionName)) {
$connectionName = $this->getDoctrine()->getDefaultConnectionName();
}
$connection = $this->getDoctrineConnection($connectionName);
$ifNotExists = $input->getOption('if-not-exists');
$params = $connection->getParams();
if (isset($params['primary'])) {
$params = $params['primary'];
}
$hasPath = isset($params['path']);
$name = $hasPath ? $params['path'] : ($params['dbname'] ?? false);
if (! $name) {
throw new InvalidArgumentException("Connection does not contain a 'path' or 'dbname' parameter and cannot be created.");
}
// Need to get rid of _every_ occurrence of dbname from connection configuration as we have already extracted all relevant info from url
/** @psalm-suppress InvalidArrayOffset Need to be compatible with DBAL < 4, which still has `$params['url']` */
/** @phpstan-ignore unset.offset */
unset($params['dbname'], $params['path'], $params['url']);
if ($connection->getDatabasePlatform() instanceof PostgreSQLPlatform) {
/** @phpstan-ignore nullCoalesce.offset (needed for DBAL < 4) */
$params['dbname'] = $params['default_dbname'] ?? 'postgres';
}
$tmpConnection = DriverManager::getConnection($params, $connection->getConfiguration());
$schemaManager = $tmpConnection->createSchemaManager();
$shouldNotCreateDatabase = $ifNotExists && in_array($name, $schemaManager->listDatabases());
// Only quote if we don't have a path
if (! $hasPath) {
$name = $tmpConnection->getDatabasePlatform()->quoteSingleIdentifier($name);
}
$error = false;
try {
if ($shouldNotCreateDatabase) {
$output->writeln(sprintf('<info>Database <comment>%s</comment> for connection named <comment>%s</comment> already exists. Skipped.</info>', $name, $connectionName));
} else {
$schemaManager->createDatabase($name);
$output->writeln(sprintf('<info>Created database <comment>%s</comment> for connection named <comment>%s</comment></info>', $name, $connectionName));
}
} catch (Throwable $e) {
$output->writeln(sprintf('<error>Could not create database <comment>%s</comment> for connection named <comment>%s</comment></error>', $name, $connectionName));
$output->writeln(sprintf('<error>%s</error>', $e->getMessage()));
$error = true;
}
$tmpConnection->close();
return $error ? 1 : 0;
}
}

View File

@@ -0,0 +1,85 @@
<?php
declare(strict_types=1);
namespace Doctrine\Bundle\DoctrineBundle\Command;
use Doctrine\DBAL\Connection;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\Tools\EntityGenerator;
use Doctrine\Persistence\ManagerRegistry;
use InvalidArgumentException;
use Symfony\Component\Console\Command\Command;
use function assert;
/**
* Base class for Doctrine console commands to extend from.
*
* @internal
*/
abstract class DoctrineCommand extends Command
{
public function __construct(
private readonly ManagerRegistry $doctrine,
) {
parent::__construct();
}
/**
* get a doctrine entity generator
*
* @return EntityGenerator
*/
protected function getEntityGenerator()
{
$entityGenerator = new EntityGenerator();
$entityGenerator->setGenerateAnnotations(false);
$entityGenerator->setGenerateStubMethods(true);
$entityGenerator->setRegenerateEntityIfExists(false);
$entityGenerator->setUpdateEntityIfExists(true);
$entityGenerator->setNumSpaces(4);
$entityGenerator->setAnnotationPrefix('ORM\\');
return $entityGenerator;
}
/**
* Get a doctrine entity manager by symfony name.
*
* @param string $name
* @param int|null $shardId
*
* @return EntityManagerInterface
*/
protected function getEntityManager($name, $shardId = null)
{
$manager = $this->getDoctrine()->getManager($name);
if ($shardId !== null) {
throw new InvalidArgumentException('Shards are not supported anymore using doctrine/dbal >= 3');
}
assert($manager instanceof EntityManagerInterface);
return $manager;
}
/**
* Get a doctrine dbal connection by symfony name.
*
* @param string $name
*
* @return Connection
*/
protected function getDoctrineConnection($name)
{
return $this->getDoctrine()->getConnection($name);
}
/** @return ManagerRegistry */
protected function getDoctrine()
{
return $this->doctrine;
}
}

View File

@@ -0,0 +1,133 @@
<?php
declare(strict_types=1);
namespace Doctrine\Bundle\DoctrineBundle\Command;
use Doctrine\DBAL\DriverManager;
use Doctrine\DBAL\Platforms\PostgreSQLPlatform;
use Doctrine\DBAL\Schema\SQLiteSchemaManager;
use InvalidArgumentException;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Throwable;
use function file_exists;
use function in_array;
use function sprintf;
use function unlink;
/**
* Database tool allows you to easily drop your configured databases.
*
* @final
*/
class DropDatabaseDoctrineCommand extends DoctrineCommand
{
public const RETURN_CODE_NOT_DROP = 1;
public const RETURN_CODE_NO_FORCE = 2;
/** @return void */
protected function configure()
{
$this
->setName('doctrine:database:drop')
->setDescription('Drops the configured database')
->addOption('connection', 'c', InputOption::VALUE_REQUIRED, 'The connection to use for this command')
->addOption('if-exists', null, InputOption::VALUE_NONE, 'Don\'t trigger an error, when the database doesn\'t exist')
->addOption('force', 'f', InputOption::VALUE_NONE, 'Set this parameter to execute this action')
->setHelp(<<<'EOT'
The <info>%command.name%</info> command drops the default connections database:
<info>php %command.full_name%</info>
The <info>--force</info> parameter has to be used to actually drop the database.
You can also optionally specify the name of a connection to drop the database for:
<info>php %command.full_name% --connection=default</info>
<error>Be careful: All data in a given database will be lost when executing this command.</error>
EOT);
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$connectionName = $input->getOption('connection');
if (empty($connectionName)) {
$connectionName = $this->getDoctrine()->getDefaultConnectionName();
}
$connection = $this->getDoctrineConnection($connectionName);
$ifExists = $input->getOption('if-exists');
$params = $connection->getParams();
if (isset($params['primary'])) {
$params = $params['primary'];
}
$name = $params['path'] ?? ($params['dbname'] ?? false);
if (! $name) {
throw new InvalidArgumentException("Connection does not contain a 'path' or 'dbname' parameter and cannot be dropped.");
}
/* @phpstan-ignore unset.offset (Need to be compatible with DBAL < 4, which still has `$params['url']`) */
unset($params['dbname'], $params['url']);
if ($connection->getDatabasePlatform() instanceof PostgreSQLPlatform) {
/** @phpstan-ignore nullCoalesce.offset (for DBAL < 4) */
$params['dbname'] = $params['default_dbname'] ?? 'postgres';
}
if (! $input->getOption('force')) {
$output->writeln('<error>ATTENTION:</error> This operation should not be executed in a production environment.');
$output->writeln('');
$output->writeln(sprintf('<info>Would drop the database <comment>%s</comment> for connection named <comment>%s</comment>.</info>', $name, $connectionName));
$output->writeln('Please run the operation with --force to execute');
$output->writeln('<error>All data will be lost!</error>');
return self::RETURN_CODE_NO_FORCE;
}
// Reopen connection without database name set
// as some vendors do not allow dropping the database connected to.
$connection->close();
$connection = DriverManager::getConnection($params, $connection->getConfiguration());
$schemaManager = $connection->createSchemaManager();
$shouldDropDatabase = ! $ifExists || in_array($name, $schemaManager->listDatabases());
// Only quote if we don't have a path
if (! isset($params['path'])) {
$name = $connection->getDatabasePlatform()->quoteSingleIdentifier($name);
}
try {
if ($shouldDropDatabase) {
if ($schemaManager instanceof SQLiteSchemaManager) {
// dropDatabase() is deprecated for Sqlite
$connection->close();
if (file_exists($name)) {
unlink($name);
}
} else {
$schemaManager->dropDatabase($name);
}
$output->writeln(sprintf('<info>Dropped database <comment>%s</comment> for connection named <comment>%s</comment></info>', $name, $connectionName));
} else {
$output->writeln(sprintf('<info>Database <comment>%s</comment> for connection named <comment>%s</comment> doesn\'t exist. Skipped.</info>', $name, $connectionName));
}
return 0;
} catch (Throwable $e) {
$output->writeln(sprintf('<error>Could not drop database <comment>%s</comment> for connection named <comment>%s</comment></error>', $name, $connectionName));
$output->writeln(sprintf('<error>%s</error>', $e->getMessage()));
return self::RETURN_CODE_NOT_DROP;
}
}
}

View File

@@ -0,0 +1,168 @@
<?php
declare(strict_types=1);
namespace Doctrine\Bundle\DoctrineBundle\Command;
use Doctrine\ORM\Mapping\ClassMetadata;
use Doctrine\ORM\Mapping\Driver\DatabaseDriver;
use Doctrine\ORM\Tools\Console\MetadataFilter;
use Doctrine\ORM\Tools\DisconnectedClassMetadataFactory;
use Doctrine\ORM\Tools\Export\ClassMetadataExporter;
use Doctrine\Persistence\ManagerRegistry;
use InvalidArgumentException;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use function assert;
use function chmod;
use function dirname;
use function file_put_contents;
use function is_dir;
use function mkdir;
use function sprintf;
use function str_replace;
/**
* Import Doctrine ORM metadata mapping information from an existing database.
*
* @deprecated
*
* @final
*/
class ImportMappingDoctrineCommand extends DoctrineCommand
{
/** @param string[] $bundles */
public function __construct(
ManagerRegistry $doctrine,
private readonly array $bundles,
) {
parent::__construct($doctrine);
}
protected function configure(): void
{
$this
->setName('doctrine:mapping:import')
->addArgument('name', InputArgument::REQUIRED, 'The bundle or namespace to import the mapping information to')
->addArgument('mapping-type', InputArgument::OPTIONAL, 'The mapping type to export the imported mapping information to')
->addOption('em', null, InputOption::VALUE_REQUIRED, 'The entity manager to use for this command')
->addOption('filter', null, InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 'A string pattern used to match entities that should be mapped.')
->addOption('force', null, InputOption::VALUE_NONE, 'Force to overwrite existing mapping files.')
->addOption('path', null, InputOption::VALUE_REQUIRED, 'The path where the files would be generated (not used when a bundle is passed).')
->setDescription('Imports mapping information from an existing database')
->setHelp(<<<'EOT'
The <info>%command.name%</info> command imports mapping information
from an existing database:
Generate annotation mappings into the src/ directory using App as the namespace:
<info>php %command.full_name% App\\Entity annotation --path=src/Entity</info>
Generate xml mappings into the config/doctrine/ directory using App as the namespace:
<info>php %command.full_name% App\\Entity xml --path=config/doctrine</info>
Generate XML mappings into a bundle:
<info>php %command.full_name% "MyCustomBundle" xml</info>
You can also optionally specify which entity manager to import from with the
<info>--em</info> option:
<info>php %command.full_name% "MyCustomBundle" xml --em=default</info>
If you don't want to map every entity that can be found in the database, use the
<info>--filter</info> option. It will try to match the targeted mapped entity with the
provided pattern string.
<info>php %command.full_name% "MyCustomBundle" xml --filter=MyMatchedEntity</info>
Use the <info>--force</info> option, if you want to override existing mapping files:
<info>php %command.full_name% "MyCustomBundle" xml --force</info>
EOT);
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$type = $input->getArgument('mapping-type') ?: 'xml';
if ($type === 'yaml') {
$type = 'yml';
}
$namespaceOrBundle = $input->getArgument('name');
if (isset($this->bundles[$namespaceOrBundle])) {
/** @phpstan-ignore method.notFound */
$bundle = $this->getApplication()->getKernel()->getBundle($namespaceOrBundle);
$namespace = $bundle->getNamespace() . '\Entity';
$destPath = $bundle->getPath();
if ($type === 'annotation') {
$destPath .= '/Entity';
} else {
$destPath .= '/Resources/config/doctrine';
}
} else {
// assume a namespace has been passed
$namespace = $namespaceOrBundle;
$destPath = $input->getOption('path');
if ($destPath === null) {
throw new InvalidArgumentException('The --path option is required when passing a namespace (e.g. --path=src). If you intended to pass a bundle name, check your spelling.');
}
}
/* @phpstan-ignore class.notFound */
$cme = new ClassMetadataExporter();
$exporter = $cme->getExporter($type);
$exporter->setOverwriteExistingFiles($input->getOption('force'));
if ($type === 'annotation') {
$entityGenerator = $this->getEntityGenerator();
$exporter->setEntityGenerator($entityGenerator);
}
$em = $this->getEntityManager($input->getOption('em'));
/* @phpstan-ignore method.notFound (Available in DBAL < 4) */
$databaseDriver = new DatabaseDriver($em->getConnection()->getSchemaManager());
$em->getConfiguration()->setMetadataDriverImpl($databaseDriver);
$emName = $input->getOption('em');
$emName = $emName ? $emName : 'default';
$cmf = new DisconnectedClassMetadataFactory();
$cmf->setEntityManager($em);
$metadata = $cmf->getAllMetadata();
$metadata = MetadataFilter::filter($metadata, $input->getOption('filter'));
if ($metadata) {
$output->writeln(sprintf('Importing mapping information from "<info>%s</info>" entity manager', $emName));
foreach ($metadata as $class) {
assert($class instanceof ClassMetadata);
$className = $class->name;
$class->name = $namespace . '\\' . $className;
if ($type === 'annotation') {
$path = $destPath . '/' . str_replace('\\', '.', $className) . '.php';
} else {
$path = $destPath . '/' . str_replace('\\', '.', $className) . '.orm.' . $type;
}
$output->writeln(sprintf(' > writing <comment>%s</comment>', $path));
$code = $exporter->exportClassMetadata($class);
$dir = dirname($path);
if (! is_dir($dir)) {
mkdir($dir, 0775, true);
}
file_put_contents($path, $code);
chmod($path, 0664);
}
return 0;
}
$output->writeln('Database does not have any mapping information.');
$output->writeln('');
return 1;
}
}

View File

@@ -0,0 +1,33 @@
<?php
declare(strict_types=1);
namespace Doctrine\Bundle\DoctrineBundle\Command\Proxy;
use Doctrine\ORM\Tools\Console\Command\ClearCache\MetadataCommand;
use Symfony\Component\Console\Input\InputOption;
/**
* Command to clear the metadata cache of the various cache drivers.
*
* @deprecated use Doctrine\ORM\Tools\Console\Command\ClearCache\MetadataCommand instead
*/
class ClearMetadataCacheDoctrineCommand extends MetadataCommand
{
use OrmProxyCommand;
protected function configure(): void
{
parent::configure();
$this
->setName('doctrine:cache:clear-metadata')
->setDescription('Clears all metadata cache for an entity manager');
if ($this->getDefinition()->hasOption('em')) {
return;
}
$this->addOption('em', null, InputOption::VALUE_REQUIRED, 'The entity manager to use for this command');
}
}

View File

@@ -0,0 +1,33 @@
<?php
declare(strict_types=1);
namespace Doctrine\Bundle\DoctrineBundle\Command\Proxy;
use Doctrine\ORM\Tools\Console\Command\ClearCache\QueryCommand;
use Symfony\Component\Console\Input\InputOption;
/**
* Command to clear the query cache of the various cache drivers.
*
* @deprecated use Doctrine\ORM\Tools\Console\Command\ClearCache\QueryCommand instead
*/
class ClearQueryCacheDoctrineCommand extends QueryCommand
{
use OrmProxyCommand;
protected function configure(): void
{
parent::configure();
$this
->setName('doctrine:cache:clear-query')
->setDescription('Clears all query cache for an entity manager');
if ($this->getDefinition()->hasOption('em')) {
return;
}
$this->addOption('em', null, InputOption::VALUE_REQUIRED, 'The entity manager to use for this command');
}
}

View File

@@ -0,0 +1,33 @@
<?php
declare(strict_types=1);
namespace Doctrine\Bundle\DoctrineBundle\Command\Proxy;
use Doctrine\ORM\Tools\Console\Command\ClearCache\ResultCommand;
use Symfony\Component\Console\Input\InputOption;
/**
* Command to clear the result cache of the various cache drivers.
*
* @deprecated use Doctrine\ORM\Tools\Console\Command\ClearCache\ResultCommand instead
*/
class ClearResultCacheDoctrineCommand extends ResultCommand
{
use OrmProxyCommand;
protected function configure(): void
{
parent::configure();
$this
->setName('doctrine:cache:clear-result')
->setDescription('Clears result cache for an entity manager');
if ($this->getDefinition()->hasOption('em')) {
return;
}
$this->addOption('em', null, InputOption::VALUE_REQUIRED, 'The entity manager to use for this command');
}
}

View File

@@ -0,0 +1,32 @@
<?php
declare(strict_types=1);
namespace Doctrine\Bundle\DoctrineBundle\Command\Proxy;
use Doctrine\ORM\Tools\Console\Command\ClearCache\CollectionRegionCommand;
use Symfony\Component\Console\Input\InputOption;
/**
* Command to clear a collection cache region.
*
* @deprecated use Doctrine\ORM\Tools\Console\Command\ClearCache\CollectionRegionCommand instead
*/
class CollectionRegionDoctrineCommand extends CollectionRegionCommand
{
use OrmProxyCommand;
protected function configure(): void
{
parent::configure();
$this
->setName('doctrine:cache:clear-collection-region');
if ($this->getDefinition()->hasOption('em')) {
return;
}
$this->addOption('em', null, InputOption::VALUE_REQUIRED, 'The entity manager to use for this command');
}
}

View File

@@ -0,0 +1,60 @@
<?php
declare(strict_types=1);
namespace Doctrine\Bundle\DoctrineBundle\Command\Proxy;
use Doctrine\ORM\Tools\Console\Command\ConvertMappingCommand;
use Doctrine\ORM\Tools\Export\Driver\AbstractExporter;
use Doctrine\ORM\Tools\Export\Driver\XmlExporter;
use Doctrine\ORM\Tools\Export\Driver\YamlExporter;
use Symfony\Component\Console\Input\InputOption;
use function assert;
/**
* Convert Doctrine ORM metadata mapping information between the various supported
* formats.
*
* @deprecated use Doctrine\ORM\Tools\Console\Command\ConvertMappingCommand instead
*
* @psalm-suppress UndefinedClass ORM < 3
*/
class ConvertMappingDoctrineCommand extends ConvertMappingCommand
{
use OrmProxyCommand;
/** @return void */
protected function configure()
{
parent::configure();
$this
->setName('doctrine:mapping:convert');
if ($this->getDefinition()->hasOption('em')) {
return;
}
$this->addOption('em', null, InputOption::VALUE_REQUIRED, 'The entity manager to use for this command');
}
/**
* @param string $toType
* @param string $destPath
*
* @return AbstractExporter
*/
protected function getExporter($toType, $destPath)
{
$exporter = parent::getExporter($toType, $destPath);
assert($exporter instanceof AbstractExporter);
if ($exporter instanceof XmlExporter) {
$exporter->setExtension('.orm.xml');
} elseif ($exporter instanceof YamlExporter) {
$exporter->setExtension('.orm.yml');
}
return $exporter;
}
}

View File

@@ -0,0 +1,34 @@
<?php
declare(strict_types=1);
namespace Doctrine\Bundle\DoctrineBundle\Command\Proxy;
use Doctrine\ORM\Tools\Console\Command\SchemaTool\CreateCommand;
use Symfony\Component\Console\Input\InputOption;
/**
* Command to execute the SQL needed to generate the database schema for
* a given entity manager.
*
* @deprecated use Doctrine\ORM\Tools\Console\Command\SchemaTool\CreateCommand instead
*/
class CreateSchemaDoctrineCommand extends CreateCommand
{
use OrmProxyCommand;
protected function configure(): void
{
parent::configure();
$this
->setName('doctrine:schema:create')
->setDescription('Executes (or dumps) the SQL needed to generate the database schema');
if ($this->getDefinition()->hasOption('em')) {
return;
}
$this->addOption('em', null, InputOption::VALUE_REQUIRED, 'The entity manager to use for this command');
}
}

View File

@@ -0,0 +1,45 @@
<?php
declare(strict_types=1);
namespace Doctrine\Bundle\DoctrineBundle\Command\Proxy;
use Doctrine\Deprecations\Deprecation;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\Tools\Console\EntityManagerProvider;
use Doctrine\ORM\Tools\Console\Helper\EntityManagerHelper;
use Symfony\Bundle\FrameworkBundle\Console\Application;
use function assert;
/**
* Provides some helper and convenience methods to configure doctrine commands in the context of bundles
* and multiple connections/entity managers.
*
* @deprecated since DoctrineBundle 2.7 and will be removed in 3.0
*/
abstract class DoctrineCommandHelper
{
/**
* Convenience method to push the helper sets of a given entity manager into the application.
*
* @param string $emName
*/
public static function setApplicationEntityManager(Application $application, $emName)
{
$em = $application->getKernel()->getContainer()->get('doctrine')->getManager($emName);
assert($em instanceof EntityManagerInterface);
$helperSet = $application->getHelperSet();
/* @phpstan-ignore class.notFound, argument.type (ORM < 3 specific) */
$helperSet->set(new EntityManagerHelper($em), 'em');
Deprecation::trigger(
'doctrine/doctrine-bundle',
'https://github.com/doctrine/DoctrineBundle/pull/1513',
'Providing an EntityManager using "%s" is deprecated. Use an instance of "%s" instead.',
/* @phpstan-ignore class.notFound */
EntityManagerHelper::class,
EntityManagerProvider::class,
);
}
}

View File

@@ -0,0 +1,33 @@
<?php
declare(strict_types=1);
namespace Doctrine\Bundle\DoctrineBundle\Command\Proxy;
use Doctrine\ORM\Tools\Console\Command\SchemaTool\DropCommand;
use Symfony\Component\Console\Input\InputOption;
/**
* Command to drop the database schema for a set of classes based on their mappings.
*
* @deprecated use Doctrine\ORM\Tools\Console\Command\SchemaTool\DropCommand instead
*/
class DropSchemaDoctrineCommand extends DropCommand
{
use OrmProxyCommand;
protected function configure(): void
{
parent::configure();
$this
->setName('doctrine:schema:drop')
->setDescription('Executes (or dumps) the SQL needed to drop the current database schema');
if ($this->getDefinition()->hasOption('em')) {
return;
}
$this->addOption('em', null, InputOption::VALUE_REQUIRED, 'The entity manager to use for this command');
}
}

View File

@@ -0,0 +1,34 @@
<?php
declare(strict_types=1);
namespace Doctrine\Bundle\DoctrineBundle\Command\Proxy;
use Doctrine\ORM\Tools\Console\Command\EnsureProductionSettingsCommand;
use Symfony\Component\Console\Input\InputOption;
/**
* Ensure the Doctrine ORM is configured properly for a production environment.
*
* @deprecated use Doctrine\ORM\Tools\Console\Command\EnsureProductionSettingsCommand instead
*
* @psalm-suppress UndefinedClass ORM < 3 specific
*/
class EnsureProductionSettingsDoctrineCommand extends EnsureProductionSettingsCommand
{
use OrmProxyCommand;
protected function configure(): void
{
parent::configure();
$this
->setName('doctrine:ensure-production-settings');
if ($this->getDefinition()->hasOption('em')) {
return;
}
$this->addOption('em', null, InputOption::VALUE_REQUIRED, 'The entity manager to use for this command');
}
}

View File

@@ -0,0 +1,32 @@
<?php
declare(strict_types=1);
namespace Doctrine\Bundle\DoctrineBundle\Command\Proxy;
use Doctrine\ORM\Tools\Console\Command\ClearCache\EntityRegionCommand;
use Symfony\Component\Console\Input\InputOption;
/**
* Command to clear a entity cache region.
*
* @deprecated use Doctrine\ORM\Tools\Console\Command\ClearCache\EntityRegionCommand instead
*/
class EntityRegionCacheDoctrineCommand extends EntityRegionCommand
{
use OrmProxyCommand;
protected function configure(): void
{
parent::configure();
$this
->setName('doctrine:cache:clear-entity-region');
if ($this->getDefinition()->hasOption('em')) {
return;
}
$this->addOption('em', null, InputOption::VALUE_REQUIRED, 'The entity manager to use for this command');
}
}

View File

@@ -0,0 +1,30 @@
<?php
declare(strict_types=1);
namespace Doctrine\Bundle\DoctrineBundle\Command\Proxy;
use Doctrine\ORM\Tools\Console\Command\InfoCommand;
use Symfony\Component\Console\Input\InputOption;
/**
* Show information about mapped entities
*
* @deprecated use Doctrine\ORM\Tools\Console\Command\InfoCommand instead
*/
class InfoDoctrineCommand extends InfoCommand
{
use OrmProxyCommand;
protected function configure(): void
{
$this
->setName('doctrine:mapping:info');
if ($this->getDefinition()->hasOption('em')) {
return;
}
$this->addOption('em', null, InputOption::VALUE_REQUIRED, 'The entity manager to use for this command');
}
}

View File

@@ -0,0 +1,41 @@
<?php
declare(strict_types=1);
namespace Doctrine\Bundle\DoctrineBundle\Command\Proxy;
use Doctrine\Deprecations\Deprecation;
use Doctrine\ORM\Tools\Console\EntityManagerProvider;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
/**
* @internal
* @deprecated
*/
trait OrmProxyCommand
{
public function __construct(
private readonly EntityManagerProvider|null $entityManagerProvider = null,
) {
parent::__construct($entityManagerProvider);
Deprecation::trigger(
'doctrine/doctrine-bundle',
'https://github.com/doctrine/DoctrineBundle/pull/1581',
'Class "%s" is deprecated. Use "%s" instead.',
self::class,
parent::class,
);
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
if (! $this->entityManagerProvider) {
/* @phpstan-ignore argument.type (ORM < 3 specific) */
DoctrineCommandHelper::setApplicationEntityManager($this->getApplication(), $input->getOption('em'));
}
return parent::execute($input, $output);
}
}

View File

@@ -0,0 +1,32 @@
<?php
declare(strict_types=1);
namespace Doctrine\Bundle\DoctrineBundle\Command\Proxy;
use Doctrine\ORM\Tools\Console\Command\ClearCache\QueryRegionCommand;
use Symfony\Component\Console\Input\InputOption;
/**
* Command to clear a query cache region.
*
* @deprecated use Doctrine\ORM\Tools\Console\Command\ClearCache\QueryRegionCommand instead
*/
class QueryRegionCacheDoctrineCommand extends QueryRegionCommand
{
use OrmProxyCommand;
protected function configure(): void
{
parent::configure();
$this
->setName('doctrine:cache:clear-query-region');
if ($this->getDefinition()->hasOption('em')) {
return;
}
$this->addOption('em', null, InputOption::VALUE_REQUIRED, 'The entity manager to use for this command');
}
}

View File

@@ -0,0 +1,48 @@
<?php
declare(strict_types=1);
namespace Doctrine\Bundle\DoctrineBundle\Command\Proxy;
use Doctrine\ORM\Tools\Console\Command\RunDqlCommand;
use Symfony\Component\Console\Input\InputOption;
/**
* Execute a Doctrine DQL query and output the results.
*
* @deprecated use Doctrine\ORM\Tools\Console\Command\RunDqlCommand instead
*/
class RunDqlDoctrineCommand extends RunDqlCommand
{
use OrmProxyCommand;
protected function configure(): void
{
parent::configure();
$this
->setName('doctrine:query:dql')
->setHelp(<<<'EOT'
The <info>%command.name%</info> command executes the given DQL query and
outputs the results:
<info>php %command.full_name% "SELECT u FROM UserBundle:User u"</info>
You can also optional specify some additional options like what type of
hydration to use when executing the query:
<info>php %command.full_name% "SELECT u FROM UserBundle:User u" --hydrate=array</info>
Additionally you can specify the first result and maximum amount of results to
show:
<info>php %command.full_name% "SELECT u FROM UserBundle:User u" --first-result=0 --max-result=30</info>
EOT);
if ($this->getDefinition()->hasOption('em')) {
return;
}
$this->addOption('em', null, InputOption::VALUE_REQUIRED, 'The entity manager to use for this command');
}
}

View File

@@ -0,0 +1,44 @@
<?php
declare(strict_types=1);
namespace Doctrine\Bundle\DoctrineBundle\Command\Proxy;
use Doctrine\DBAL\Tools\Console\Command\RunSqlCommand;
use Doctrine\Deprecations\Deprecation;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
/**
* Execute a SQL query and output the results.
*
* @deprecated use Doctrine\DBAL\Tools\Console\Command\RunSqlCommand instead
*/
class RunSqlDoctrineCommand extends RunSqlCommand
{
protected function configure(): void
{
parent::configure();
$this
->setName('doctrine:query:sql')
->setHelp(<<<'EOT'
The <info>%command.name%</info> command executes the given SQL query and
outputs the results:
<info>php %command.full_name% "SELECT * FROM users"</info>
EOT);
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
Deprecation::trigger(
'doctrine/doctrine-bundle',
'https://github.com/doctrine/DoctrineBundle/pull/1231',
'The "%s" (doctrine:query:sql) is deprecated, use dbal:run-sql command instead.',
self::class,
);
return parent::execute($input, $output);
}
}

View File

@@ -0,0 +1,33 @@
<?php
declare(strict_types=1);
namespace Doctrine\Bundle\DoctrineBundle\Command\Proxy;
use Doctrine\ORM\Tools\Console\Command\SchemaTool\UpdateCommand;
use Symfony\Component\Console\Input\InputOption;
/**
* Command to generate the SQL needed to update the database schema to match
* the current mapping information.
*
* @deprecated use Doctrine\ORM\Tools\Console\Command\SchemaTool\UpdateCommand instead
*/
class UpdateSchemaDoctrineCommand extends UpdateCommand
{
use OrmProxyCommand;
protected function configure(): void
{
parent::configure();
$this
->setName('doctrine:schema:update');
if ($this->getDefinition()->hasOption('em')) {
return;
}
$this->addOption('em', null, InputOption::VALUE_REQUIRED, 'The entity manager to use for this command');
}
}

View File

@@ -0,0 +1,32 @@
<?php
declare(strict_types=1);
namespace Doctrine\Bundle\DoctrineBundle\Command\Proxy;
use Doctrine\ORM\Tools\Console\Command\ValidateSchemaCommand as DoctrineValidateSchemaCommand;
use Symfony\Component\Console\Input\InputOption;
/**
* Command to run Doctrine ValidateSchema() on the current mappings.
*
* @deprecated use Doctrine\ORM\Tools\Console\Command\ValidateSchemaCommand instead
*/
class ValidateSchemaCommand extends DoctrineValidateSchemaCommand
{
use OrmProxyCommand;
protected function configure(): void
{
parent::configure();
$this
->setName('doctrine:schema:validate');
if ($this->getDefinition()->hasOption('em')) {
return;
}
$this->addOption('em', null, InputOption::VALUE_REQUIRED, 'The entity manager to use for this command');
}
}

View File

@@ -0,0 +1,299 @@
<?php
declare(strict_types=1);
namespace Doctrine\Bundle\DoctrineBundle;
use Doctrine\Common\EventManager;
use Doctrine\DBAL\Configuration;
use Doctrine\DBAL\Connection;
use Doctrine\DBAL\Connection\StaticServerVersionProvider;
use Doctrine\DBAL\ConnectionException;
use Doctrine\DBAL\DriverManager;
use Doctrine\DBAL\Exception as DBALException;
use Doctrine\DBAL\Exception\DriverException;
use Doctrine\DBAL\Exception\DriverRequired;
use Doctrine\DBAL\Exception\InvalidWrapperClass;
use Doctrine\DBAL\Exception\MalformedDsnException;
use Doctrine\DBAL\Platforms\AbstractMySQLPlatform;
use Doctrine\DBAL\Platforms\AbstractPlatform;
use Doctrine\DBAL\Tools\DsnParser;
use Doctrine\DBAL\Types\Type;
use Doctrine\Deprecations\Deprecation;
use InvalidArgumentException;
use function array_merge;
use function class_exists;
use function is_subclass_of;
use function method_exists;
use const PHP_EOL;
/** @phpstan-import-type Params from DriverManager */
class ConnectionFactory
{
/** @internal */
public const DEFAULT_SCHEME_MAP = [
'db2' => 'ibm_db2',
'mssql' => 'pdo_sqlsrv',
'mysql' => 'pdo_mysql',
'mysql2' => 'pdo_mysql', // Amazon RDS, for some weird reason
'postgres' => 'pdo_pgsql',
'postgresql' => 'pdo_pgsql',
'pgsql' => 'pdo_pgsql',
'sqlite' => 'pdo_sqlite',
'sqlite3' => 'pdo_sqlite',
];
/** @phpstan-ignore property.onlyWritten */
private readonly DsnParser $dsnParser;
private bool $initialized = false;
/** @param mixed[][] $typesConfig */
public function __construct(
private readonly array $typesConfig = [],
DsnParser|null $dsnParser = null,
) {
$this->dsnParser = $dsnParser ?? new DsnParser(self::DEFAULT_SCHEME_MAP);
}
/**
* Create a connection by name.
*
* @param mixed[] $params
* @param array<string, string> $mappingTypes
* @phpstan-param Params $params
*
* @return Connection
*/
public function createConnection(array $params, Configuration|null $config = null, EventManager|null $eventManager = null, array $mappingTypes = [])
{
if (! method_exists(Connection::class, 'getEventManager') && $eventManager !== null) {
throw new InvalidArgumentException('Passing an EventManager instance is not supported with DBAL > 3');
}
if (! $this->initialized) {
$this->initializeTypes();
}
$overriddenOptions = [];
/** @phpstan-ignore isset.offset (We should adjust when https://github.com/phpstan/phpstan/issues/12414 is fixed) */
if (isset($params['connection_override_options'])) {
Deprecation::trigger(
'doctrine/doctrine-bundle',
'https://github.com/doctrine/DoctrineBundle/pull/1342',
'The "connection_override_options" connection parameter is deprecated',
);
$overriddenOptions = $params['connection_override_options'];
unset($params['connection_override_options']);
}
$params = $this->parseDatabaseUrl($params);
// URL support for PrimaryReplicaConnection
if (isset($params['primary'])) {
$params['primary'] = $this->parseDatabaseUrl($params['primary']);
}
if (isset($params['replica'])) {
foreach ($params['replica'] as $key => $replicaParams) {
$params['replica'][$key] = $this->parseDatabaseUrl($replicaParams);
}
}
/** @phpstan-ignore-next-line We should adjust when https://github.com/phpstan/phpstan/issues/12414 is fixed */
if (! isset($params['pdo']) && (! isset($params['charset']) || $overriddenOptions || isset($params['dbname_suffix']))) {
$wrapperClass = null;
if (isset($params['wrapperClass'])) {
if (! is_subclass_of($params['wrapperClass'], Connection::class)) {
if (class_exists(InvalidWrapperClass::class)) {
throw InvalidWrapperClass::new($params['wrapperClass']);
}
/* @phpstan-ignore staticMethod.notFound */
throw DBALException::invalidWrapperClass($params['wrapperClass']);
}
$wrapperClass = $params['wrapperClass'];
$params['wrapperClass'] = null;
}
$connection = DriverManager::getConnection(...array_merge([$params, $config], $eventManager ? [$eventManager] : []));
$params = $this->addDatabaseSuffix(array_merge($connection->getParams(), $overriddenOptions));
$driver = $connection->getDriver();
/** @phpstan-ignore arguments.count (DBAL < 4.x doesn't accept an argument) */
$platform = $driver->getDatabasePlatform(
...(class_exists(StaticServerVersionProvider::class)
? [new StaticServerVersionProvider($params['serverVersion'] ?? $params['primary']['serverVersion'] ?? '')]
: []
),
);
if (! isset($params['charset'])) {
if ($platform instanceof AbstractMySQLPlatform) {
$params['charset'] = 'utf8mb4';
if (isset($params['defaultTableOptions']['collate'])) {
Deprecation::trigger(
'doctrine/doctrine-bundle',
'https://github.com/doctrine/dbal/issues/5214',
'The "collate" default table option is deprecated in favor of "collation" and will be removed in doctrine/doctrine-bundle 3.0. ',
);
$params['defaultTableOptions']['collation'] = $params['defaultTableOptions']['collate'];
unset($params['defaultTableOptions']['collate']);
}
if (! isset($params['defaultTableOptions']['collation'])) {
$params['defaultTableOptions']['collation'] = 'utf8mb4_unicode_ci';
}
} else {
$params['charset'] = 'utf8';
}
}
if ($wrapperClass !== null) {
$params['wrapperClass'] = $wrapperClass;
} else {
$wrapperClass = Connection::class;
}
$connection = new $wrapperClass($params, $driver, $config, $eventManager);
} else {
$connection = DriverManager::getConnection(...array_merge([$params, $config], $eventManager ? [$eventManager] : []));
}
if (! empty($mappingTypes)) {
$platform = $this->getDatabasePlatform($connection);
foreach ($mappingTypes as $dbType => $doctrineType) {
$platform->registerDoctrineTypeMapping($dbType, $doctrineType);
}
}
return $connection;
}
/**
* Try to get the database platform.
*
* This could fail if types should be registered to an predefined/unused connection
* and the platform version is unknown.
*
* @link https://github.com/doctrine/DoctrineBundle/issues/673
*
* @throws DBALException
*/
private function getDatabasePlatform(Connection $connection): AbstractPlatform
{
try {
return $connection->getDatabasePlatform();
} catch (DriverException $driverException) {
$class = class_exists(DBALException::class) ? DBALException::class : ConnectionException::class;
/* @phpstan-ignore new.interface */
throw new $class(
'An exception occurred while establishing a connection to figure out your platform version.' . PHP_EOL .
"You can circumvent this by setting a 'server_version' configuration value" . PHP_EOL . PHP_EOL .
'For further information have a look at:' . PHP_EOL .
'https://github.com/doctrine/DoctrineBundle/issues/673',
0,
$driverException,
);
}
}
/**
* initialize the types
*/
private function initializeTypes(): void
{
foreach ($this->typesConfig as $typeName => $typeConfig) {
if (Type::hasType($typeName)) {
Type::overrideType($typeName, $typeConfig['class']);
} else {
Type::addType($typeName, $typeConfig['class']);
}
}
$this->initialized = true;
}
/**
* @param array<string, mixed> $params
*
* @return array<string, mixed>
*/
private function addDatabaseSuffix(array $params): array
{
if (isset($params['dbname']) && isset($params['dbname_suffix'])) {
$params['dbname'] .= $params['dbname_suffix'];
}
foreach ($params['replica'] ?? [] as $key => $replicaParams) {
if (! isset($replicaParams['dbname'], $replicaParams['dbname_suffix'])) {
continue;
}
$params['replica'][$key]['dbname'] .= $replicaParams['dbname_suffix'];
}
if (isset($params['primary']['dbname'], $params['primary']['dbname_suffix'])) {
$params['primary']['dbname'] .= $params['primary']['dbname_suffix'];
}
return $params;
}
/**
* Extracts parts from a database URL, if present, and returns an
* updated list of parameters.
*
* @param mixed[] $params The list of parameters.
* @phpstan-param Params $params
*
* @return mixed[] A modified list of parameters with info from a database
* URL extracted into individual parameter parts.
* @phpstan-return Params
*
* @throws DBALException
*
* @phpstan-ignore throws.unusedType
*/
private function parseDatabaseUrl(array $params): array
{
/** @phpstan-ignore isset.offset (for DBAL < 4) */
if (! isset($params['url'])) {
return $params;
}
/** @phpstan-ignore deadCode.unreachable */
try {
$parsedParams = $this->dsnParser->parse($params['url']);
} catch (MalformedDsnException $e) {
throw new MalformedDsnException('Malformed parameter "url".', 0, $e);
}
if (isset($parsedParams['driver'])) {
// The requested driver from the URL scheme takes precedence
// over the default custom driver from the connection parameters (if any).
unset($params['driverClass']);
}
$params = array_merge($params, $parsedParams);
// If a schemeless connection URL is given, we require a default driver or default custom driver
// as connection parameter.
if (! isset($params['driverClass']) && ! isset($params['driver'])) {
if (class_exists(DriverRequired::class)) {
throw DriverRequired::new($params['url']);
}
throw DBALException::driverRequired($params['url']);
}
unset($params['url']);
return $params;
}
}

View File

@@ -0,0 +1,130 @@
<?php
declare(strict_types=1);
namespace Doctrine\Bundle\DoctrineBundle\Controller;
use Doctrine\DBAL\Connection;
use Doctrine\DBAL\Platforms\OraclePlatform;
use Doctrine\DBAL\Platforms\SQLitePlatform;
use Doctrine\DBAL\Platforms\SQLServerPlatform;
use Doctrine\Persistence\ConnectionRegistry;
use Exception;
use Symfony\Bridge\Doctrine\DataCollector\DoctrineDataCollector;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Profiler\Profiler;
use Symfony\Component\VarDumper\Cloner\Data;
use Throwable;
use Twig\Environment;
use function assert;
/** @internal */
class ProfilerController
{
public function __construct(
private readonly Environment $twig,
private readonly ConnectionRegistry $registry,
private readonly Profiler $profiler,
) {
}
/**
* Renders the profiler panel for the given token.
*
* @param string $token The profiler token
* @param string $connectionName
* @param int $query
*
* @return Response A Response instance
*/
public function explainAction($token, $connectionName, $query)
{
$this->profiler->disable();
$profile = $this->profiler->loadProfile($token);
$collector = $profile->getCollector('db');
assert($collector instanceof DoctrineDataCollector);
$queries = $collector->getQueries();
if (! isset($queries[$connectionName][$query])) {
return new Response('This query does not exist.');
}
$query = $queries[$connectionName][$query];
if (! $query['explainable']) {
return new Response('This query cannot be explained.');
}
$connection = $this->registry->getConnection($connectionName);
assert($connection instanceof Connection);
try {
$platform = $connection->getDatabasePlatform();
if ($platform instanceof SQLitePlatform) {
$results = $this->explainSQLitePlatform($connection, $query);
} elseif ($platform instanceof SQLServerPlatform) {
throw new Exception('Explain for SQLServerPlatform is currently not supported. Contributions are welcome.');
} elseif ($platform instanceof OraclePlatform) {
$results = $this->explainOraclePlatform($connection, $query);
} else {
$results = $this->explainOtherPlatform($connection, $query);
}
} catch (Throwable) {
return new Response('This query cannot be explained.');
}
return new Response($this->twig->render('@Doctrine/Collector/explain.html.twig', [
'data' => $results,
'query' => $query,
]));
}
/**
* @param mixed[] $query
*
* @return mixed[]
*/
private function explainSQLitePlatform(Connection $connection, array $query): array
{
$params = $query['params'];
if ($params instanceof Data) {
$params = $params->getValue(true);
}
return $connection->executeQuery('EXPLAIN QUERY PLAN ' . $query['sql'], $params, $query['types'])
->fetchAllAssociative();
}
/**
* @param mixed[] $query
*
* @return mixed[]
*/
private function explainOtherPlatform(Connection $connection, array $query): array
{
$params = $query['params'];
if ($params instanceof Data) {
$params = $params->getValue(true);
}
return $connection->executeQuery('EXPLAIN ' . $query['sql'], $params, $query['types'])
->fetchAllAssociative();
}
/**
* @param mixed[] $query
*
* @return mixed[]
*/
private function explainOraclePlatform(Connection $connection, array $query): array
{
$connection->executeQuery('EXPLAIN PLAN FOR ' . $query['sql']);
return $connection->executeQuery('SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY())')
->fetchAllAssociative();
}
}

View File

@@ -0,0 +1,334 @@
<?php
declare(strict_types=1);
namespace Doctrine\Bundle\DoctrineBundle\DataCollector;
use Doctrine\DBAL\Types\Type;
use Doctrine\ORM\Cache\CacheConfiguration;
use Doctrine\ORM\Cache\Logging\CacheLoggerChain;
use Doctrine\ORM\Cache\Logging\StatisticsCacheLogger;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\Tools\SchemaValidator;
use Doctrine\Persistence\ManagerRegistry;
use Symfony\Bridge\Doctrine\DataCollector\DoctrineDataCollector as BaseCollector;
use Symfony\Bridge\Doctrine\Middleware\Debug\DebugDataHolder;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Throwable;
use function array_map;
use function array_sum;
use function arsort;
use function assert;
use function count;
use function usort;
/**
* @phpstan-type QueryType = array{
* executionMS: float,
* explainable: bool,
* sql: string,
* params: ?array<array-key, mixed>,
* runnable: bool,
* types: ?array<array-key, Type|int|string|null>,
* }
* @phpstan-type DataType = array{
* caches: array{
* enabled: bool,
* counts: array<"puts"|"hits"|"misses", int>,
* log_enabled: bool,
* regions: array<"puts"|"hits"|"misses", array<string, int>>,
* },
* connections: list<string>,
* entities: array<string, array<class-string, array{class: class-string, file: false|string, line: false|int}>>,
* errors: array<string, array<class-string, list<string>>>,
* managers: list<string>,
* queries: array<string, list<QueryType>>,
* entityCounts: array<string, array<class-string, int>>
* }
* @psalm-property DataType $data
*/
class DoctrineDataCollector extends BaseCollector
{
private int|null $invalidEntityCount = null;
private int|null $managedEntityCount = null;
/**
* @var mixed[][]|null
* @phpstan-var ?array<string, list<QueryType&array{count: int, index: int, executionPercent?: float}>>
* @phpstan-ignore property.unusedType
*/
private array|null $groupedQueries = null;
public function __construct(
private readonly ManagerRegistry $registry,
private readonly bool $shouldValidateSchema = true,
DebugDataHolder|null $debugDataHolder = null,
) {
parent::__construct($registry, $debugDataHolder);
}
public function collect(Request $request, Response $response, Throwable|null $exception = null): void
{
parent::collect($request, $response, $exception);
$errors = [];
$entities = [];
$entityCounts = [];
$caches = [
'enabled' => false,
'log_enabled' => false,
'counts' => [
'puts' => 0,
'hits' => 0,
'misses' => 0,
],
'regions' => [
'puts' => [],
'hits' => [],
'misses' => [],
],
];
foreach ($this->registry->getManagers() as $name => $em) {
assert($em instanceof EntityManagerInterface);
if ($this->shouldValidateSchema) {
$entities[$name] = [];
$factory = $em->getMetadataFactory();
$validator = new SchemaValidator($em);
foreach ($factory->getLoadedMetadata() as $class) {
if (isset($entities[$name][$class->getName()])) {
continue;
}
$classErrors = $validator->validateClass($class);
$r = $class->getReflectionClass();
$entities[$name][$class->getName()] = [
'class' => $class->getName(),
'file' => $r->getFileName(),
'line' => $r->getStartLine(),
];
if (empty($classErrors)) {
continue;
}
$errors[$name][$class->getName()] = $classErrors;
}
}
$entityCounts[$name] = [];
foreach ($em->getUnitOfWork()->getIdentityMap() as $className => $entityList) {
$entityCounts[$name][$className] = count($entityList);
}
// Sort entities by count (in descending order)
arsort($entityCounts[$name]);
$emConfig = $em->getConfiguration();
$slcEnabled = $emConfig->isSecondLevelCacheEnabled();
if (! $slcEnabled) {
continue;
}
$caches['enabled'] = true;
$cacheConfiguration = $emConfig->getSecondLevelCacheConfiguration();
assert($cacheConfiguration instanceof CacheConfiguration);
$cacheLoggerChain = $cacheConfiguration->getCacheLogger();
assert($cacheLoggerChain instanceof CacheLoggerChain || $cacheLoggerChain === null);
if (! $cacheLoggerChain || ! $cacheLoggerChain->getLogger('statistics')) {
continue;
}
$cacheLoggerStats = $cacheLoggerChain->getLogger('statistics');
assert($cacheLoggerStats instanceof StatisticsCacheLogger);
$caches['log_enabled'] = true;
$caches['counts']['puts'] += $cacheLoggerStats->getPutCount();
$caches['counts']['hits'] += $cacheLoggerStats->getHitCount();
$caches['counts']['misses'] += $cacheLoggerStats->getMissCount();
foreach ($cacheLoggerStats->getRegionsPut() as $key => $value) {
if (! isset($caches['regions']['puts'][$key])) {
$caches['regions']['puts'][$key] = 0;
}
$caches['regions']['puts'][$key] += $value;
}
foreach ($cacheLoggerStats->getRegionsHit() as $key => $value) {
if (! isset($caches['regions']['hits'][$key])) {
$caches['regions']['hits'][$key] = 0;
}
$caches['regions']['hits'][$key] += $value;
}
foreach ($cacheLoggerStats->getRegionsMiss() as $key => $value) {
if (! isset($caches['regions']['misses'][$key])) {
$caches['regions']['misses'][$key] = 0;
}
$caches['regions']['misses'][$key] += $value;
}
}
$this->data['entities'] = $entities;
$this->data['errors'] = $errors;
$this->data['caches'] = $caches;
$this->data['entityCounts'] = $entityCounts;
$this->groupedQueries = null;
}
/** @return array<string, array<class-string, array{class: class-string, file: false|string, line: false|int}>> */
public function getEntities()
{
return $this->data['entities'];
}
/** @return array<string, array<string, list<string>>> */
public function getMappingErrors()
{
return $this->data['errors'];
}
/** @return int */
public function getCacheHitsCount()
{
return $this->data['caches']['counts']['hits'];
}
/** @return int */
public function getCachePutsCount()
{
return $this->data['caches']['counts']['puts'];
}
/** @return int */
public function getCacheMissesCount()
{
return $this->data['caches']['counts']['misses'];
}
/** @return bool */
public function getCacheEnabled()
{
return $this->data['caches']['enabled'];
}
/**
* @return array<string, array<string, int>>
* @phpstan-return array<"puts"|"hits"|"misses", array<string, int>>
*/
public function getCacheRegions()
{
return $this->data['caches']['regions'];
}
/** @return array<string, int> */
public function getCacheCounts()
{
return $this->data['caches']['counts'];
}
/** @return int */
public function getInvalidEntityCount()
{
return $this->invalidEntityCount ??= array_sum(array_map('count', $this->data['errors']));
}
public function getManagedEntityCount(): int
{
if ($this->managedEntityCount === null) {
$total = 0;
foreach ($this->data['entityCounts'] as $entities) {
$total += array_sum($entities);
}
$this->managedEntityCount = $total;
}
return $this->managedEntityCount;
}
/** @return array<string, array<class-string, int>> */
public function getManagedEntityCountByClass(): array
{
return $this->data['entityCounts'];
}
/**
* @return string[][]
* @phpstan-return array<string, list<QueryType&array{count: int, index: int, executionPercent?: float}>>
*/
public function getGroupedQueries()
{
if ($this->groupedQueries !== null) {
return $this->groupedQueries;
}
$this->groupedQueries = [];
$totalExecutionMS = 0;
foreach ($this->data['queries'] as $connection => $queries) {
$connectionGroupedQueries = [];
foreach ($queries as $i => $query) {
$key = $query['sql'];
if (! isset($connectionGroupedQueries[$key])) {
$connectionGroupedQueries[$key] = $query;
$connectionGroupedQueries[$key]['executionMS'] = 0;
$connectionGroupedQueries[$key]['count'] = 0;
$connectionGroupedQueries[$key]['index'] = $i; // "Explain query" relies on query index in 'queries'.
}
$connectionGroupedQueries[$key]['executionMS'] += $query['executionMS'];
$connectionGroupedQueries[$key]['count']++;
$totalExecutionMS += $query['executionMS'];
}
usort($connectionGroupedQueries, static function ($a, $b) {
if ($a['executionMS'] === $b['executionMS']) {
return 0;
}
return $a['executionMS'] < $b['executionMS'] ? 1 : -1;
});
$this->groupedQueries[$connection] = $connectionGroupedQueries;
}
foreach ($this->groupedQueries as $connection => $queries) {
foreach ($queries as $i => $query) {
$this->groupedQueries[$connection][$i]['executionPercent'] =
$this->executionTimePercentage($query['executionMS'], $totalExecutionMS);
}
}
return $this->groupedQueries;
}
private function executionTimePercentage(float $executionTimeMS, float $totalExecutionTimeMS): float
{
if (! $totalExecutionTimeMS) {
return 0;
}
return $executionTimeMS / $totalExecutionTimeMS * 100;
}
/** @return int */
public function getGroupedQueryCount()
{
$count = 0;
foreach ($this->getGroupedQueries() as $connectionGroupedQueries) {
$count += count($connectionGroupedQueries);
}
return $count;
}
}

View File

@@ -0,0 +1,29 @@
<?php
declare(strict_types=1);
namespace Doctrine\Bundle\DoctrineBundle\Dbal;
use Doctrine\DBAL\Schema\AbstractAsset;
use function in_array;
/** @deprecated Implement your own include/exclude mechanism */
class BlacklistSchemaAssetFilter
{
/** @param string[] $blacklist */
public function __construct(
private readonly array $blacklist,
) {
}
/** @param string|AbstractAsset $assetName */
public function __invoke($assetName): bool
{
if ($assetName instanceof AbstractAsset) {
$assetName = $assetName->getName();
}
return ! in_array($assetName, $this->blacklist, true);
}
}

View File

@@ -0,0 +1,27 @@
<?php
declare(strict_types=1);
namespace Doctrine\Bundle\DoctrineBundle\Dbal;
use Doctrine\DBAL\Connection;
use Doctrine\DBAL\Tools\Console\ConnectionProvider;
use Doctrine\Persistence\AbstractManagerRegistry;
class ManagerRegistryAwareConnectionProvider implements ConnectionProvider
{
public function __construct(
private readonly AbstractManagerRegistry $managerRegistry,
) {
}
public function getDefaultConnection(): Connection
{
return $this->managerRegistry->getConnection();
}
public function getConnection(string $name): Connection
{
return $this->managerRegistry->getConnection($name);
}
}

View File

@@ -0,0 +1,26 @@
<?php
declare(strict_types=1);
namespace Doctrine\Bundle\DoctrineBundle\Dbal;
use Doctrine\DBAL\Schema\AbstractAsset;
use function preg_match;
class RegexSchemaAssetFilter
{
public function __construct(
private readonly string $filterExpression,
) {
}
public function __invoke(string|AbstractAsset $assetName): bool
{
if ($assetName instanceof AbstractAsset) {
$assetName = $assetName->getName();
}
return (bool) preg_match($this->filterExpression, $assetName);
}
}

View File

@@ -0,0 +1,31 @@
<?php
declare(strict_types=1);
namespace Doctrine\Bundle\DoctrineBundle\Dbal;
use Doctrine\DBAL\Schema\AbstractAsset;
/**
* Manages schema filters passed to Connection::setSchemaAssetsFilter()
*/
class SchemaAssetsFilterManager
{
/** @param callable[] $schemaAssetFilters */
public function __construct(
private readonly array $schemaAssetFilters,
) {
}
/** @param string|AbstractAsset $assetName */
public function __invoke($assetName): bool
{
foreach ($this->schemaAssetFilters as $schemaAssetFilter) {
if ($schemaAssetFilter($assetName) === false) {
return false;
}
}
return true;
}
}

View File

@@ -0,0 +1,127 @@
<?php
declare(strict_types=1);
namespace Doctrine\Bundle\DoctrineBundle\DependencyInjection\Compiler;
use Doctrine\Common\Cache\Psr6\CacheAdapter;
use Doctrine\Deprecations\Deprecation;
use Psr\Cache\CacheItemPoolInterface;
use Symfony\Component\DependencyInjection\ChildDefinition;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Reference;
use function array_keys;
use function assert;
use function in_array;
use function is_a;
/** @internal */
final class CacheCompatibilityPass implements CompilerPassInterface
{
private const CONFIGURATION_TAG = 'doctrine.orm.configuration';
private const CACHE_METHODS_PSR6_SUPPORT = [
'setMetadataCache',
'setQueryCache',
'setResultCache',
];
public function process(ContainerBuilder $container): void
{
foreach (array_keys($container->findTaggedServiceIds(self::CONFIGURATION_TAG)) as $id) {
foreach ($container->getDefinition($id)->getMethodCalls() as $methodCall) {
if ($methodCall[0] === 'setSecondLevelCacheConfiguration') {
$this->updateSecondLevelCache($container, $methodCall[1][0]);
continue;
}
if (! in_array($methodCall[0], self::CACHE_METHODS_PSR6_SUPPORT, true)) {
continue;
}
$aliasId = (string) $methodCall[1][0];
$definitionId = (string) $container->getAlias($aliasId);
$this->wrapIfNecessary($container, $aliasId, $definitionId);
}
}
}
private function updateSecondLevelCache(ContainerBuilder $container, Definition $slcConfigDefinition): void
{
foreach ($slcConfigDefinition->getMethodCalls() as $methodCall) {
if ($methodCall[0] !== 'setCacheFactory') {
continue;
}
$factoryDefinition = $methodCall[1][0];
assert($factoryDefinition instanceof Definition);
$aliasId = (string) $factoryDefinition->getArgument(1);
$this->wrapIfNecessary($container, $aliasId, (string) $container->getAlias($aliasId));
foreach ($factoryDefinition->getMethodCalls() as $factoryMethodCall) {
if ($factoryMethodCall[0] !== 'setRegion') {
continue;
}
$regionDefinition = $container->getDefinition((string) $factoryMethodCall[1][0]);
// Get inner service for FileLock
if ($regionDefinition->getClass() === '%doctrine.orm.second_level_cache.filelock_region.class%') {
$regionDefinition = $container->getDefinition((string) $regionDefinition->getArgument(0));
}
// We don't know how to adjust custom region classes
if ($regionDefinition->getClass() !== '%doctrine.orm.second_level_cache.default_region.class%') {
continue;
}
$driverId = (string) $regionDefinition->getArgument(1);
if (! $container->hasAlias($driverId)) {
continue;
}
$this->wrapIfNecessary($container, $driverId, (string) $container->getAlias($driverId));
}
break;
}
}
private function createCompatibilityLayerDefinition(ContainerBuilder $container, string $definitionId): Definition|null
{
$definition = $container->getDefinition($definitionId);
while (! $definition->getClass() && $definition instanceof ChildDefinition) {
$definition = $container->findDefinition($definition->getParent());
}
if (is_a($definition->getClass(), CacheItemPoolInterface::class, true)) {
return null;
}
Deprecation::trigger(
'doctrine/doctrine-bundle',
'https://github.com/doctrine/DoctrineBundle/pull/1365',
'Configuring doctrine/cache is deprecated. Please update the cache service "%s" to use a PSR-6 cache.',
$definitionId,
);
return (new Definition(CacheItemPoolInterface::class))
->setFactory([CacheAdapter::class, 'wrap'])
->addArgument(new Reference($definitionId));
}
private function wrapIfNecessary(ContainerBuilder $container, string $aliasId, string $definitionId): void
{
$compatibilityLayer = $this->createCompatibilityLayerDefinition($container, $definitionId);
if ($compatibilityLayer === null) {
return;
}
$compatibilityLayerId = $definitionId . '.compatibility_layer';
$container->setAlias($aliasId, $compatibilityLayerId);
$container->setDefinition($compatibilityLayerId, $compatibilityLayer);
}
}

View File

@@ -0,0 +1,45 @@
<?php
declare(strict_types=1);
namespace Doctrine\Bundle\DoctrineBundle\DependencyInjection\Compiler;
use Symfony\Component\Cache\Adapter\DoctrineDbalAdapter;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Reference;
/**
* Injects Doctrine DBAL adapters into their schema subscriber.
*
* Must be run later after ResolveChildDefinitionsPass.
*
* @final since 2.9
*/
class CacheSchemaSubscriberPass implements CompilerPassInterface
{
/** @return void */
public function process(ContainerBuilder $container)
{
if (! $container->hasDefinition('doctrine.orm.listeners.doctrine_dbal_cache_adapter_schema_listener')) {
return;
}
$subscriber = $container->getDefinition('doctrine.orm.listeners.doctrine_dbal_cache_adapter_schema_listener');
$cacheAdaptersReferences = [];
foreach ($container->getDefinitions() as $id => $definition) {
if ($definition->isAbstract() || $definition->isSynthetic()) {
continue;
}
if ($definition->getClass() !== DoctrineDbalAdapter::class) {
continue;
}
$cacheAdaptersReferences[] = new Reference($id);
}
$subscriber->replaceArgument(0, $cacheAdaptersReferences);
}
}

View File

@@ -0,0 +1,55 @@
<?php
declare(strict_types=1);
namespace Doctrine\Bundle\DoctrineBundle\DependencyInjection\Compiler;
use Symfony\Component\DependencyInjection\ChildDefinition;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Reference;
use function sprintf;
/**
* Processes the doctrine.dbal.schema_filter
*
* @final since 2.9
*/
class DbalSchemaFilterPass implements CompilerPassInterface
{
/** @return void */
public function process(ContainerBuilder $container)
{
$filters = $container->findTaggedServiceIds('doctrine.dbal.schema_filter');
$connectionFilters = [];
foreach ($filters as $id => $tagAttributes) {
foreach ($tagAttributes as $attributes) {
$name = $attributes['connection'] ?? $container->getParameter('doctrine.default_connection');
if (! isset($connectionFilters[$name])) {
$connectionFilters[$name] = [];
}
$connectionFilters[$name][] = new Reference($id);
}
}
foreach ($connectionFilters as $name => $references) {
$configurationId = sprintf('doctrine.dbal.%s_connection.configuration', $name);
if (! $container->hasDefinition($configurationId)) {
continue;
}
$definition = new ChildDefinition('doctrine.dbal.schema_asset_filter_manager');
$definition->setArgument(0, $references);
$id = sprintf('doctrine.dbal.%s_schema_asset_filter_manager', $name);
$container->setDefinition($id, $definition);
$container->findDefinition($configurationId)
->addMethodCall('setSchemaAssetsFilter', [new Reference($id)]);
}
}
}

View File

@@ -0,0 +1,205 @@
<?php
declare(strict_types=1);
namespace Doctrine\Bundle\DoctrineBundle\DependencyInjection\Compiler;
use Doctrine\Deprecations\Deprecation;
use Doctrine\ORM\Mapping\Driver\AnnotationDriver;
use Doctrine\ORM\Mapping\Driver\AttributeDriver;
use Doctrine\ORM\Mapping\Driver\XmlDriver;
use Doctrine\ORM\Mapping\Driver\YamlDriver;
use Doctrine\Persistence\Mapping\Driver\PHPDriver;
use Doctrine\Persistence\Mapping\Driver\StaticPHPDriver;
use Doctrine\Persistence\Mapping\Driver\SymfonyFileLocator;
use Symfony\Bridge\Doctrine\DependencyInjection\CompilerPass\RegisterMappingsPass;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Reference;
/**
* Class for Symfony bundles to configure mappings for model classes not in the
* auto-mapped folder.
*
* @final since 2.9
*/
class DoctrineOrmMappingsPass extends RegisterMappingsPass
{
/**
* You should not directly instantiate this class but use one of the
* factory methods.
*
* @param Definition|Reference $driver Driver DI definition or reference.
* @param string[] $namespaces List of namespaces handled by $driver.
* @param string[] $managerParameters Ordered list of container parameters that
* could hold the manager name.
* doctrine.default_entity_manager is appended
* automatically.
* @param string|false $enabledParameter If specified, the compiler pass only executes
* if this parameter is defined in the service
* container.
* @param string[] $aliasMap Map of alias to namespace.
*/
public function __construct($driver, array $namespaces, array $managerParameters, $enabledParameter = false, array $aliasMap = [])
{
$managerParameters[] = 'doctrine.default_entity_manager';
parent::__construct(
$driver,
$namespaces,
$managerParameters,
'doctrine.orm.%s_metadata_driver',
$enabledParameter,
'doctrine.orm.%s_configuration',
'addEntityNamespace',
$aliasMap,
);
}
/**
* @param string[] $namespaces Hashmap of directory path to namespace.
* @param string[] $managerParameters List of parameters that could which object manager name
* your bundle uses. This compiler pass will automatically
* append the parameter name for the default entity manager
* to this list.
* @param string|false $enabledParameter Service container parameter that must be present to
* enable the mapping. Set to false to not do any check,
* optional.
* @param string[] $aliasMap Map of alias to namespace.
*
* @return self
*/
public static function createXmlMappingDriver(array $namespaces, array $managerParameters = [], $enabledParameter = false, array $aliasMap = [], bool $enableXsdValidation = false)
{
$locator = new Definition(SymfonyFileLocator::class, [$namespaces, '.orm.xml']);
$driver = new Definition(XmlDriver::class, [$locator, XmlDriver::DEFAULT_FILE_EXTENSION, $enableXsdValidation]);
return new DoctrineOrmMappingsPass($driver, $namespaces, $managerParameters, $enabledParameter, $aliasMap);
}
/**
* @deprecated no replacement planned
*
* @param string[] $namespaces Hashmap of directory path to namespace
* @param string[] $managerParameters List of parameters that could which object manager name
* your bundle uses. This compiler pass will automatically
* append the parameter name for the default entity manager
* to this list.
* @param string|false $enabledParameter Service container parameter that must be present to
* enable the mapping. Set to false to not do any check,
* optional.
* @param string[] $aliasMap Map of alias to namespace.
*
* @return self
*/
public static function createYamlMappingDriver(array $namespaces, array $managerParameters = [], $enabledParameter = false, array $aliasMap = [])
{
Deprecation::trigger(
'doctrine/doctrine-bundle',
'https://github.com/doctrine/DoctrineBundle/pull/2088',
'The "%s()" method is deprecated and will be removed in DoctrineBundle 3.0.',
__METHOD__,
);
$locator = new Definition(SymfonyFileLocator::class, [$namespaces, '.orm.yml']);
/* @phpstan-ignore class.notFound */
$driver = new Definition(YamlDriver::class, [$locator]);
return new DoctrineOrmMappingsPass($driver, $namespaces, $managerParameters, $enabledParameter, $aliasMap);
}
/**
* @param string[] $namespaces Hashmap of directory path to namespace
* @param string[] $managerParameters List of parameters that could which object manager name
* your bundle uses. This compiler pass will automatically
* append the parameter name for the default entity manager
* to this list.
* @param string|false $enabledParameter Service container parameter that must be present to
* enable the mapping. Set to false to not do any check,
* optional.
* @param string[] $aliasMap Map of alias to namespace.
*
* @return self
*/
public static function createPhpMappingDriver(array $namespaces, array $managerParameters = [], $enabledParameter = false, array $aliasMap = [])
{
$locator = new Definition(SymfonyFileLocator::class, [$namespaces, '.php']);
$driver = new Definition(PHPDriver::class, [$locator]);
return new DoctrineOrmMappingsPass($driver, $namespaces, $managerParameters, $enabledParameter, $aliasMap);
}
/**
* @deprecated no replacement planned
*
* @param string[] $namespaces List of namespaces that are handled with annotation mapping
* @param string[] $directories List of directories to look for annotated classes
* @param string[] $managerParameters List of parameters that could which object manager name
* your bundle uses. This compiler pass will automatically
* append the parameter name for the default entity manager
* to this list.
* @param string|false $enabledParameter Service container parameter that must be present to
* enable the mapping. Set to false to not do any check,
* optional.
* @param string[] $aliasMap Map of alias to namespace.
* @param bool $reportFieldsWhereDeclared Will report fields for the classes where they are declared
*
* @return self
*/
public static function createAnnotationMappingDriver(array $namespaces, array $directories, array $managerParameters = [], $enabledParameter = false, array $aliasMap = [], bool $reportFieldsWhereDeclared = false)
{
Deprecation::trigger(
'doctrine/doctrine-bundle',
'https://github.com/doctrine/DoctrineBundle/pull/2088',
'The "%s()" method is deprecated and will be removed in DoctrineBundle 3.0.',
__METHOD__,
);
$reader = new Reference('annotation_reader');
/* @phpstan-ignore class.notFound */
$driver = new Definition(AnnotationDriver::class, [$reader, $directories, $reportFieldsWhereDeclared]);
return new DoctrineOrmMappingsPass($driver, $namespaces, $managerParameters, $enabledParameter, $aliasMap);
}
/**
* @param string[] $namespaces List of namespaces that are handled with attribute mapping
* @param string[] $directories List of directories to look for classes with attributes
* @param string[] $managerParameters List of parameters that could which object manager name
* your bundle uses. This compiler pass will automatically
* append the parameter name for the default entity manager
* to this list.
* @param string|false $enabledParameter Service container parameter that must be present to
* enable the mapping. Set to false to not do any check,
* optional.
* @param string[] $aliasMap Map of alias to namespace.
* @param bool $reportFieldsWhereDeclared Will report fields for the classes where they are declared
*
* @return self
*/
public static function createAttributeMappingDriver(array $namespaces, array $directories, array $managerParameters = [], $enabledParameter = false, array $aliasMap = [], bool $reportFieldsWhereDeclared = false)
{
$driver = new Definition(AttributeDriver::class, [$directories, $reportFieldsWhereDeclared]);
return new DoctrineOrmMappingsPass($driver, $namespaces, $managerParameters, $enabledParameter, $aliasMap);
}
/**
* @param string[] $namespaces List of namespaces that are handled with static php mapping
* @param string[] $directories List of directories to look for static php mapping files
* @param string[] $managerParameters List of parameters that could which object manager name
* your bundle uses. This compiler pass will automatically
* append the parameter name for the default entity manager
* to this list.
* @param string|false $enabledParameter Service container parameter that must be present to
* enable the mapping. Set to false to not do any check,
* optional.
* @param string[] $aliasMap Map of alias to namespace.
*
* @return self
*/
public static function createStaticPhpMappingDriver(array $namespaces, array $directories, array $managerParameters = [], $enabledParameter = false, array $aliasMap = [])
{
$driver = new Definition(StaticPHPDriver::class, [$directories]);
return new DoctrineOrmMappingsPass($driver, $namespaces, $managerParameters, $enabledParameter, $aliasMap);
}
}

View File

@@ -0,0 +1,160 @@
<?php
declare(strict_types=1);
namespace Doctrine\Bundle\DoctrineBundle\DependencyInjection\Compiler;
use Doctrine\Bundle\DoctrineBundle\Mapping\ContainerEntityListenerResolver;
use Doctrine\Bundle\DoctrineBundle\Mapping\EntityListenerServiceResolver;
use Symfony\Component\DependencyInjection\ChildDefinition;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\Compiler\ServiceLocatorTagPass;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
use Symfony\Component\DependencyInjection\Reference;
use function is_a;
use function method_exists;
use function sprintf;
use function substr;
use function usort;
/**
* Class for Symfony bundles to register entity listeners
*
* @final since 2.9
*/
class EntityListenerPass implements CompilerPassInterface
{
/** @return void */
public function process(ContainerBuilder $container)
{
$lazyServiceReferencesByResolver = [];
$serviceTags = [];
foreach ($container->findTaggedServiceIds('doctrine.orm.entity_listener', true) as $id => $tags) {
foreach ($tags as $attributes) {
$serviceTags[] = [
'serviceId' => $id,
'attributes' => $attributes,
];
}
}
usort($serviceTags, static fn (array $a, array $b) => ($b['attributes']['priority'] ?? 0) <=> ($a['attributes']['priority'] ?? 0));
foreach ($serviceTags as $tag) {
$id = $tag['serviceId'];
$attributes = $tag['attributes'];
$name = $attributes['entity_manager'] ?? $container->getParameter('doctrine.default_entity_manager');
$entityManager = sprintf('doctrine.orm.%s_entity_manager', $name);
if (! $container->hasDefinition($entityManager)) {
continue;
}
$resolverId = sprintf('doctrine.orm.%s_entity_listener_resolver', $name);
if (! $container->has($resolverId)) {
continue;
}
$resolver = $container->findDefinition($resolverId);
$resolver->setPublic(true);
if (isset($attributes['entity'])) {
$this->attachToListener($container, $name, $this->getConcreteDefinitionClass($container->findDefinition($id), $container, $id), $attributes);
}
$resolverClass = $this->getResolverClass($resolver, $container, $resolverId);
$resolverSupportsLazyListeners = is_a($resolverClass, EntityListenerServiceResolver::class, true);
$lazyByAttribute = isset($attributes['lazy']) && $attributes['lazy'];
if ($lazyByAttribute && ! $resolverSupportsLazyListeners) {
throw new InvalidArgumentException(sprintf(
'Lazy-loaded entity listeners can only be resolved by a resolver implementing %s.',
EntityListenerServiceResolver::class,
));
}
if (! isset($attributes['lazy']) && $resolverSupportsLazyListeners || $lazyByAttribute) {
$listener = $container->findDefinition($id);
$resolver->addMethodCall('registerService', [$this->getConcreteDefinitionClass($listener, $container, $id), $id]);
// if the resolver uses the default class we will use a service locator for all listeners
if ($resolverClass === ContainerEntityListenerResolver::class) {
if (! isset($lazyServiceReferencesByResolver[$resolverId])) {
$lazyServiceReferencesByResolver[$resolverId] = [];
}
$lazyServiceReferencesByResolver[$resolverId][$id] = new Reference($id);
} else {
$listener->setPublic(true);
}
} else {
$resolver->addMethodCall('register', [new Reference($id)]);
}
}
foreach ($lazyServiceReferencesByResolver as $resolverId => $listenerReferences) {
$container->findDefinition($resolverId)->setArgument(0, ServiceLocatorTagPass::register($container, $listenerReferences));
}
}
/** @param array{entity: class-string, event?: ?string, method?: string} $attributes */
private function attachToListener(ContainerBuilder $container, string $name, string $class, array $attributes): void
{
$listenerId = sprintf('doctrine.orm.%s_listeners.attach_entity_listeners', $name);
if (! $container->has($listenerId)) {
return;
}
$args = [
$attributes['entity'],
$class,
$attributes['event'] ?? null,
];
if (isset($attributes['method'])) {
$args[] = $attributes['method'];
} elseif (isset($attributes['event']) && ! method_exists($class, $attributes['event']) && method_exists($class, '__invoke')) {
$args[] = '__invoke';
}
$container->findDefinition($listenerId)->addMethodCall('addEntityListener', $args);
}
private function getResolverClass(Definition $resolver, ContainerBuilder $container, string $id): string
{
$resolverClass = $this->getConcreteDefinitionClass($resolver, $container, $id);
if (substr($resolverClass, 0, 1) === '%') {
// resolve container parameter first
$resolverClass = $container->getParameterBag()->resolveValue($resolverClass);
}
return $resolverClass;
}
private function getConcreteDefinitionClass(Definition $definition, ContainerBuilder $container, string $id): string
{
$class = $definition->getClass();
if ($class) {
return $class;
}
while ($definition instanceof ChildDefinition) {
$definition = $container->findDefinition($definition->getParent());
$class = $definition->getClass();
if ($class) {
return $class;
}
}
throw new InvalidArgumentException(sprintf('The service "%s" must define its class.', $id));
}
}

View File

@@ -0,0 +1,80 @@
<?php
declare(strict_types=1);
namespace Doctrine\Bundle\DoctrineBundle\DependencyInjection\Compiler;
use Doctrine\Bundle\DoctrineBundle\Mapping\ClassMetadataFactory;
use Doctrine\Bundle\DoctrineBundle\Mapping\MappingDriver;
use Doctrine\ORM\Mapping\ClassMetadataFactory as ORMClassMetadataFactory;
use Symfony\Component\DependencyInjection\Alias;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\Compiler\ServiceLocatorTagPass;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Reference;
use function array_combine;
use function array_keys;
use function array_map;
use function sprintf;
final class IdGeneratorPass implements CompilerPassInterface
{
public const ID_GENERATOR_TAG = 'doctrine.id_generator';
public const CONFIGURATION_TAG = 'doctrine.orm.configuration';
public function process(ContainerBuilder $container): void
{
$generatorIds = array_keys($container->findTaggedServiceIds(self::ID_GENERATOR_TAG));
// when ORM is not enabled
if (! $container->hasDefinition('doctrine.orm.configuration') || ! $generatorIds) {
return;
}
$generatorRefs = array_map(static fn (string $id): Reference => new Reference($id), $generatorIds);
$ref = ServiceLocatorTagPass::register($container, array_combine($generatorIds, $generatorRefs));
$container->setAlias('doctrine.id_generator_locator', new Alias((string) $ref, false));
foreach ($container->findTaggedServiceIds(self::CONFIGURATION_TAG) as $id => $tags) {
$configurationDef = $container->getDefinition($id);
$methodCalls = $configurationDef->getMethodCalls();
$metadataDriverImpl = null;
foreach ($methodCalls as $i => [$method, $arguments]) {
if ($method === 'setMetadataDriverImpl') {
$metadataDriverImpl = (string) $arguments[0];
}
if ($method !== 'setClassMetadataFactoryName') {
continue;
}
if ($arguments[0] !== ORMClassMetadataFactory::class && $arguments[0] !== ClassMetadataFactory::class) {
$class = $container->getReflectionClass($arguments[0]);
if ($class && $class->isSubclassOf(ClassMetadataFactory::class)) {
break;
}
continue 2;
}
$methodCalls[$i] = ['setClassMetadataFactoryName', [ClassMetadataFactory::class]];
}
if ($metadataDriverImpl === null) {
continue;
}
$configurationDef->setMethodCalls($methodCalls);
$container->register('.' . $metadataDriverImpl, MappingDriver::class)
->setDecoratedService($metadataDriverImpl)
->setArguments([
new Reference(sprintf('.%s.inner', $metadataDriverImpl)),
new Reference('doctrine.id_generator_locator'),
]);
}
}
}

View File

@@ -0,0 +1,90 @@
<?php
declare(strict_types=1);
namespace Doctrine\Bundle\DoctrineBundle\DependencyInjection\Compiler;
use Doctrine\Bundle\DoctrineBundle\Middleware\ConnectionNameAwareInterface;
use Symfony\Component\DependencyInjection\ChildDefinition;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Reference;
use function array_key_exists;
use function array_keys;
use function array_map;
use function array_values;
use function is_subclass_of;
use function sprintf;
use function usort;
final class MiddlewaresPass implements CompilerPassInterface
{
public function process(ContainerBuilder $container): void
{
if (! $container->hasParameter('doctrine.connections')) {
return;
}
$middlewareAbstractDefs = [];
$middlewareConnections = [];
$middlewarePriorities = [];
foreach ($container->findTaggedServiceIds('doctrine.middleware') as $id => $tags) {
$middlewareAbstractDefs[$id] = $container->getDefinition($id);
// When a def has doctrine.middleware tags with connection attributes equal to connection names
// registration of this middleware is limited to the connections with these names
foreach ($tags as $tag) {
if (! isset($tag['connection'])) {
if (isset($tag['priority']) && ! isset($middlewarePriorities[$id])) {
$middlewarePriorities[$id] = $tag['priority'];
}
continue;
}
$middlewareConnections[$id][$tag['connection']] = $tag['priority'] ?? null;
}
}
foreach (array_keys($container->getParameter('doctrine.connections')) as $name) {
$middlewareRefs = [];
$i = 0;
foreach ($middlewareAbstractDefs as $id => $abstractDef) {
if (isset($middlewareConnections[$id]) && ! array_key_exists($name, $middlewareConnections[$id])) {
continue;
}
$childDef = $container->setDefinition(
$childId = sprintf('%s.%s', $id, $name),
(new ChildDefinition($id))
->setTags($abstractDef->getTags())->clearTag('doctrine.middleware')
->setAutoconfigured($abstractDef->isAutoconfigured())
->setAutowired($abstractDef->isAutowired()),
);
$middlewareRefs[$id] = [new Reference($childId), ++$i];
if (! is_subclass_of($abstractDef->getClass(), ConnectionNameAwareInterface::class)) {
continue;
}
$childDef->addMethodCall('setConnectionName', [$name]);
}
$middlewareRefs = array_map(
static fn (string $id, array $ref) => [
$middlewareConnections[$id][$name] ?? $middlewarePriorities[$id] ?? 0,
$ref[1],
$ref[0],
],
array_keys($middlewareRefs),
array_values($middlewareRefs),
);
usort($middlewareRefs, static fn (array $a, array $b): int => $b[0] <=> $a[0] ?: $a[1] <=> $b[1]);
$middlewareRefs = array_map(static fn (array $value): Reference => $value[2], $middlewareRefs);
$container
->getDefinition(sprintf('doctrine.dbal.%s_connection.configuration', $name))
->addMethodCall('setMiddlewares', [$middlewareRefs]);
}
}
}

View File

@@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
namespace Doctrine\Bundle\DoctrineBundle\DependencyInjection\Compiler;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
/** @internal */
final class RemoveLoggingMiddlewarePass implements CompilerPassInterface
{
public function process(ContainerBuilder $container): void
{
if ($container->has('logger')) {
return;
}
$container->removeDefinition('doctrine.dbal.logging_middleware');
}
}

View File

@@ -0,0 +1,22 @@
<?php
declare(strict_types=1);
namespace Doctrine\Bundle\DoctrineBundle\DependencyInjection\Compiler;
use Doctrine\Bundle\DoctrineBundle\Controller\ProfilerController;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
/** @internal */
final class RemoveProfilerControllerPass implements CompilerPassInterface
{
public function process(ContainerBuilder $container): void
{
if ($container->has('twig') && $container->has('profiler')) {
return;
}
$container->removeDefinition(ProfilerController::class);
}
}

View File

@@ -0,0 +1,35 @@
<?php
declare(strict_types=1);
namespace Doctrine\Bundle\DoctrineBundle\DependencyInjection\Compiler;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\Compiler\ServiceLocatorTagPass;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Reference;
use function array_combine;
use function array_keys;
use function array_map;
final class ServiceRepositoryCompilerPass implements CompilerPassInterface
{
public const REPOSITORY_SERVICE_TAG = 'doctrine.repository_service';
public function process(ContainerBuilder $container): void
{
// when ORM is not enabled
if (! $container->hasDefinition('doctrine.orm.container_repository_factory')) {
return;
}
$locatorDef = $container->getDefinition('doctrine.orm.container_repository_factory');
$repoServiceIds = array_keys($container->findTaggedServiceIds(self::REPOSITORY_SERVICE_TAG));
$repoReferences = array_map(static fn (string $id): Reference => new Reference($id), $repoServiceIds);
$ref = ServiceLocatorTagPass::register($container, array_combine($repoServiceIds, $repoReferences));
$locatorDef->replaceArgument(0, $ref);
}
}

View File

@@ -0,0 +1,922 @@
<?php
declare(strict_types=1);
namespace Doctrine\Bundle\DoctrineBundle\DependencyInjection;
use Doctrine\DBAL\Connection;
use Doctrine\DBAL\Schema\LegacySchemaManagerFactory;
use Doctrine\Deprecations\Deprecation;
use Doctrine\ORM\EntityManager;
use Doctrine\ORM\EntityRepository;
use Doctrine\ORM\Mapping\ClassMetadata;
use Doctrine\ORM\Mapping\ClassMetadataFactory;
use Doctrine\ORM\Mapping\Driver\AnnotationDriver;
use Doctrine\ORM\Proxy\ProxyFactory;
use InvalidArgumentException;
use ReflectionClass;
use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition;
use Symfony\Component\Config\Definition\Builder\NodeDefinition;
use Symfony\Component\Config\Definition\Builder\TreeBuilder;
use Symfony\Component\Config\Definition\ConfigurationInterface;
use Symfony\Component\DependencyInjection\Exception\LogicException;
use function array_diff_key;
use function array_intersect_key;
use function array_key_exists;
use function array_keys;
use function array_pop;
use function class_exists;
use function constant;
use function count;
use function defined;
use function implode;
use function in_array;
use function is_array;
use function is_bool;
use function is_int;
use function is_string;
use function key;
use function method_exists;
use function reset;
use function sprintf;
use function strlen;
use function strpos;
use function strtoupper;
use function substr;
/**
* This class contains the configuration information for the bundle
*
* This information is solely responsible for how the different configuration
* sections are normalized, and merged.
*
* @final since 2.9
*/
class Configuration implements ConfigurationInterface
{
/** @param bool $debug Whether to use the debug mode */
public function __construct(private bool $debug)
{
}
public function getConfigTreeBuilder(): TreeBuilder
{
$treeBuilder = new TreeBuilder('doctrine');
$rootNode = $treeBuilder->getRootNode();
$this->addDbalSection($rootNode);
$this->addOrmSection($rootNode);
return $treeBuilder;
}
/**
* Add DBAL section to configuration tree
*/
private function addDbalSection(ArrayNodeDefinition $node): void
{
// Key that should not be rewritten to the connection config
$excludedKeys = ['default_connection' => true, 'driver_schemes' => true, 'driver_scheme' => true, 'types' => true, 'type' => true];
$node
->children()
->arrayNode('dbal')
->beforeNormalization()
->ifTrue(static function ($v) use ($excludedKeys) {
if (! is_array($v)) {
return false;
}
if (array_key_exists('connections', $v) || array_key_exists('connection', $v)) {
return false;
}
// Is there actually anything to use once excluded keys are considered?
return (bool) array_diff_key($v, $excludedKeys);
})
->then(static function ($v) use ($excludedKeys) {
$connection = [];
foreach ($v as $key => $value) {
if (isset($excludedKeys[$key])) {
continue;
}
$connection[$key] = $v[$key];
unset($v[$key]);
}
$v['connections'] = [($v['default_connection'] ?? 'default') => $connection];
return $v;
})
->end()
->children()
->scalarNode('default_connection')->end()
->end()
->fixXmlConfig('type')
->children()
->arrayNode('types')
->useAttributeAsKey('name')
->prototype('array')
->beforeNormalization()
->ifString()
->then(static fn ($v) => ['class' => $v])
->end()
->children()
->scalarNode('class')->isRequired()->end()
->booleanNode('commented')
->setDeprecated(
'doctrine/doctrine-bundle',
'2.0',
'The doctrine-bundle type commenting features were removed; the corresponding config parameter was deprecated in 2.0 and will be dropped in 3.0.',
)
->end()
->end()
->end()
->end()
->end()
->fixXmlConfig('driver_scheme')
->children()
->arrayNode('driver_schemes')
->useAttributeAsKey('scheme')
->normalizeKeys(false)
->scalarPrototype()->end()
->info('Defines a driver for given URL schemes. Schemes being driver names cannot be redefined. However, other default schemes can be overwritten.')
->validate()
->always()
->then(static function (array $value) {
$unsupportedSchemes = [];
foreach ($value as $scheme => $driver) {
if (! in_array($scheme, ['pdo-mysql', 'pdo-sqlite', 'pdo-pgsql', 'pdo-oci', 'oci8', 'ibm-db2', 'pdo-sqlsrv', 'mysqli', 'pgsql', 'sqlsrv', 'sqlite3'], true)) {
continue;
}
$unsupportedSchemes[] = $scheme;
}
if ($unsupportedSchemes) {
throw new InvalidArgumentException(sprintf('Registering a scheme with the name of one of the official drivers is forbidden, as those are defined in DBAL itself. The following schemes are forbidden: %s', implode(', ', $unsupportedSchemes)));
}
return $value;
})
->end()
->end()
->end()
->fixXmlConfig('connection')
->append($this->getDbalConnectionsNode())
->end();
}
/**
* Return the dbal connections node
*/
private function getDbalConnectionsNode(): ArrayNodeDefinition
{
$treeBuilder = new TreeBuilder('connections');
$node = $treeBuilder->getRootNode();
$connectionNode = $node
->requiresAtLeastOneElement()
->useAttributeAsKey('name')
->prototype('array');
$this->configureDbalDriverNode($connectionNode);
$collationKey = defined('Doctrine\DBAL\Connection::PARAM_ASCII_STR_ARRAY')
? 'collate'
: 'collation';
$connectionNode
->fixXmlConfig('option')
->fixXmlConfig('mapping_type')
->fixXmlConfig('slave')
->fixXmlConfig('replica')
->fixXmlConfig('default_table_option')
->children()
->scalarNode('driver')->defaultValue('pdo_mysql')->end()
->scalarNode('platform_service')
->setDeprecated(
'doctrine/doctrine-bundle',
'2.9',
'The "platform_service" configuration key is deprecated since doctrine-bundle 2.9. DBAL 4 will not support setting a custom platform via connection params anymore.',
)
->end()
->booleanNode('auto_commit')->end()
->scalarNode('schema_filter')->end()
->booleanNode('logging')->defaultValue($this->debug)->end()
->booleanNode('profiling')->defaultValue($this->debug)->end()
->booleanNode('profiling_collect_backtrace')
->defaultValue(false)
->info('Enables collecting backtraces when profiling is enabled')
->end()
->booleanNode('profiling_collect_schema_errors')
->defaultValue(true)
->info('Enables collecting schema errors when profiling is enabled')
->end()
->booleanNode('disable_type_comments')
->beforeNormalization()
->ifTrue(static fn ($v): bool => isset($v) && ! method_exists(Connection::class, 'getEventManager'))
->then(static function ($v) {
Deprecation::trigger(
'doctrine/doctrine-bundle',
'https://github.com/doctrine/DoctrineBundle/pull/2048',
'The "disable_type_comments" configuration key is deprecated when using DBAL 4 and will be removed in DoctrineBundle 3.0.',
);
return $v;
})
->end()
->end()
->scalarNode('server_version')->end()
->integerNode('idle_connection_ttl')->defaultValue(600)->end()
->scalarNode('driver_class')->end()
->scalarNode('wrapper_class')->end()
->booleanNode('keep_slave')
->setDeprecated(
'doctrine/doctrine-bundle',
'2.2',
'The "keep_slave" configuration key is deprecated since doctrine-bundle 2.2. Use the "keep_replica" configuration key instead.',
)
->end()
->booleanNode('keep_replica')->end()
->arrayNode('options')
->useAttributeAsKey('key')
->prototype('variable')->end()
->end()
->arrayNode('mapping_types')
->useAttributeAsKey('name')
->prototype('scalar')->end()
->end()
->arrayNode('default_table_options')
->info(sprintf(
"This option is used by the schema-tool and affects generated SQL. Possible keys include 'charset','%s', and 'engine'.",
$collationKey,
))
->useAttributeAsKey('name')
->prototype('scalar')->end()
->end()
->scalarNode('schema_manager_factory')
->cannotBeEmpty()
->defaultValue($this->getDefaultSchemaManagerFactory())
->end()
->scalarNode('result_cache')->end()
->end();
// dbal < 2.11
$slaveNode = $connectionNode
->children()
->arrayNode('slaves')
->setDeprecated(
'doctrine/doctrine-bundle',
'2.2',
'The "slaves" configuration key will be renamed to "replicas" in doctrine-bundle 3.0. "slaves" is deprecated since doctrine-bundle 2.2.',
)
->useAttributeAsKey('name')
->prototype('array');
$this->configureDbalDriverNode($slaveNode);
// dbal >= 2.11
$replicaNode = $connectionNode
->children()
->arrayNode('replicas')
->useAttributeAsKey('name')
->prototype('array');
$this->configureDbalDriverNode($replicaNode);
return $node;
}
/**
* Adds config keys related to params processed by the DBAL drivers
*
* These keys are available for replica configurations too.
*/
private function configureDbalDriverNode(ArrayNodeDefinition $node): void
{
$node
->validate()
->always(static function (array $values) {
if (! isset($values['url'])) {
return $values;
}
$urlConflictingOptions = ['host' => true, 'port' => true, 'user' => true, 'password' => true, 'path' => true, 'dbname' => true, 'unix_socket' => true, 'memory' => true];
$urlConflictingValues = array_keys(array_intersect_key($values, $urlConflictingOptions));
if ($urlConflictingValues) {
$tail = count($urlConflictingValues) > 1 ? sprintf('or "%s" options', array_pop($urlConflictingValues)) : 'option';
Deprecation::trigger(
'doctrine/doctrine-bundle',
'https://github.com/doctrine/DoctrineBundle/pull/1342',
'Setting the "doctrine.dbal.%s" %s while the "url" one is defined is deprecated',
implode('", "', $urlConflictingValues),
$tail,
);
}
return $values;
})
->end()
->children()
->scalarNode('url')->info('A URL with connection information; any parameter value parsed from this string will override explicitly set parameters')->end()
->scalarNode('dbname')->end()
->scalarNode('host')->info('Defaults to "localhost" at runtime.')->end()
->scalarNode('port')->info('Defaults to null at runtime.')->end()
->scalarNode('user')->info('Defaults to "root" at runtime.')->end()
->scalarNode('password')->info('Defaults to null at runtime.')->end()
->booleanNode('override_url')->setDeprecated(
'doctrine/doctrine-bundle',
'2.4',
'The "doctrine.dbal.override_url" configuration key is deprecated.',
)->end()
->scalarNode('dbname_suffix')->info('Adds the given suffix to the configured database name, this option has no effects for the SQLite platform')->end()
->scalarNode('application_name')->end()
->scalarNode('charset')->end()
->scalarNode('path')->end()
->booleanNode('memory')->end()
->scalarNode('unix_socket')->info('The unix socket to use for MySQL')->end()
->booleanNode('persistent')->info('True to use as persistent connection for the ibm_db2 driver')->end()
->scalarNode('protocol')->info('The protocol to use for the ibm_db2 driver (default to TCPIP if omitted)')->end()
->booleanNode('service')
->info('True to use SERVICE_NAME as connection parameter instead of SID for Oracle')
->end()
->scalarNode('servicename')
->info(
'Overrules dbname parameter if given and used as SERVICE_NAME or SID connection parameter ' .
'for Oracle depending on the service parameter.',
)
->end()
->scalarNode('sessionMode')
->info('The session mode to use for the oci8 driver')
->end()
->scalarNode('server')
->info('The name of a running database server to connect to for SQL Anywhere.')
->end()
->scalarNode('default_dbname')
->info(
'Override the default database (postgres) to connect to for PostgreSQL connexion.',
)
->end()
->scalarNode('sslmode')
->info(
'Determines whether or with what priority a SSL TCP/IP connection will be negotiated with ' .
'the server for PostgreSQL.',
)
->end()
->scalarNode('sslrootcert')
->info(
'The name of a file containing SSL certificate authority (CA) certificate(s). ' .
'If the file exists, the server\'s certificate will be verified to be signed by one of these authorities.',
)
->end()
->scalarNode('sslcert')
->info(
'The path to the SSL client certificate file for PostgreSQL.',
)
->end()
->scalarNode('sslkey')
->info(
'The path to the SSL client key file for PostgreSQL.',
)
->end()
->scalarNode('sslcrl')
->info(
'The file name of the SSL certificate revocation list for PostgreSQL.',
)
->end()
->booleanNode('pooled')->info('True to use a pooled server with the oci8/pdo_oracle driver')->end()
->booleanNode('MultipleActiveResultSets')->info('Configuring MultipleActiveResultSets for the pdo_sqlsrv driver')->end()
->booleanNode('use_savepoints')
->info('Use savepoints for nested transactions')
->beforeNormalization()
->ifTrue(static fn ($v): bool => isset($v) && ! method_exists(Connection::class, 'getEventManager'))
->then(static function ($v) {
Deprecation::trigger(
'doctrine/doctrine-bundle',
'https://github.com/doctrine/DoctrineBundle/pull/2055',
'The "use_savepoints" configuration key is deprecated when using DBAL 4 and will be removed in DoctrineBundle 3.0.',
);
return $v;
})
->end()
->end()
->scalarNode('instancename')
->info(
'Optional parameter, complete whether to add the INSTANCE_NAME parameter in the connection.' .
' It is generally used to connect to an Oracle RAC server to select the name' .
' of a particular instance.',
)
->end()
->scalarNode('connectstring')
->info(
'Complete Easy Connect connection descriptor, see https://docs.oracle.com/database/121/NETAG/naming.htm.' .
'When using this option, you will still need to provide the user and password parameters, but the other ' .
'parameters will no longer be used. Note that when using this parameter, the getHost and getPort methods' .
' from Doctrine\DBAL\Connection will no longer function as expected.',
)
->end()
->end()
->beforeNormalization()
->ifTrue(static fn ($v) => ! isset($v['sessionMode']) && isset($v['session_mode']))
->then(static function ($v) {
$v['sessionMode'] = $v['session_mode'];
unset($v['session_mode']);
return $v;
})
->end()
->beforeNormalization()
->ifTrue(static fn ($v) => ! isset($v['MultipleActiveResultSets']) && isset($v['multiple_active_result_sets']))
->then(static function ($v) {
$v['MultipleActiveResultSets'] = $v['multiple_active_result_sets'];
unset($v['multiple_active_result_sets']);
return $v;
})
->end();
}
/**
* Add the ORM section to configuration tree
*/
private function addOrmSection(ArrayNodeDefinition $node): void
{
// Key that should not be rewritten to the entity-manager config
$excludedKeys = [
'default_entity_manager' => true,
'auto_generate_proxy_classes' => true,
'enable_lazy_ghost_objects' => true,
'enable_native_lazy_objects' => true,
'proxy_dir' => true,
'proxy_namespace' => true,
'resolve_target_entities' => true,
'resolve_target_entity' => true,
'controller_resolver' => true,
];
$node
->children()
->arrayNode('orm')
->beforeNormalization()
->ifTrue(static function ($v) use ($excludedKeys) {
if (! empty($v) && ! class_exists(EntityManager::class)) {
throw new LogicException('The doctrine/orm package is required when the doctrine.orm config is set.');
}
if (! is_array($v)) {
return false;
}
if (array_key_exists('entity_managers', $v) || array_key_exists('entity_manager', $v)) {
return false;
}
// Is there actually anything to use once excluded keys are considered?
return (bool) array_diff_key($v, $excludedKeys);
})
->then(static function ($v) use ($excludedKeys) {
$entityManager = [];
foreach ($v as $key => $value) {
if (isset($excludedKeys[$key])) {
continue;
}
$entityManager[$key] = $v[$key];
unset($v[$key]);
}
$v['entity_managers'] = [($v['default_entity_manager'] ?? 'default') => $entityManager];
return $v;
})
->end()
->children()
->scalarNode('default_entity_manager')->end()
->scalarNode('auto_generate_proxy_classes')->defaultValue(false)
->info('Auto generate mode possible values are: "NEVER", "ALWAYS", "FILE_NOT_EXISTS", "EVAL", "FILE_NOT_EXISTS_OR_CHANGED", this option is ignored when the "enable_native_lazy_objects" option is true')
->validate()
->ifTrue(function ($v) {
$generationModes = $this->getAutoGenerateModes();
if (is_int($v) && in_array($v, $generationModes['values']/*array(0, 1, 2, 3)*/)) {
return false;
}
if (is_bool($v)) {
return false;
}
if (is_string($v)) {
if (in_array(strtoupper($v), $generationModes['names']/*array('NEVER', 'ALWAYS', 'FILE_NOT_EXISTS', 'EVAL', 'FILE_NOT_EXISTS_OR_CHANGED')*/)) {
return false;
}
}
return true;
})
->thenInvalid('Invalid auto generate mode value %s')
->end()
->validate()
->ifString()
->then(static fn (string $v) => constant('Doctrine\ORM\Proxy\ProxyFactory::AUTOGENERATE_' . strtoupper($v)))
->end()
->end()
->booleanNode('enable_lazy_ghost_objects')
->defaultValue(! method_exists(ProxyFactory::class, 'resetUninitializedProxy'))
->info('Enables the new implementation of proxies based on lazy ghosts instead of using the legacy implementation')
->end()
->booleanNode('enable_native_lazy_objects')
->defaultFalse()
->info('Enables the new native implementation of PHP lazy objects instead of generated proxies')
->end()
->scalarNode('proxy_dir')
->defaultValue('%kernel.build_dir%/doctrine/orm/Proxies')
->info('Configures the path where generated proxy classes are saved when using non-native lazy objects, this option is ignored when the "enable_native_lazy_objects" option is true')
->end()
->scalarNode('proxy_namespace')
->defaultValue('Proxies')
->info('Defines the root namespace for generated proxy classes when using non-native lazy objects, this option is ignored when the "enable_native_lazy_objects" option is true')
->end()
->arrayNode('controller_resolver')
->canBeDisabled()
->children()
->booleanNode('auto_mapping')
->defaultNull()
->info('Set to false to disable using route placeholders as lookup criteria when the primary key doesn\'t match the argument name')
->end()
->booleanNode('evict_cache')
->info('Set to true to fetch the entity from the database instead of using the cache, if any')
->defaultFalse()
->end()
->end()
->end()
->end()
->fixXmlConfig('entity_manager')
->append($this->getOrmEntityManagersNode())
->fixXmlConfig('resolve_target_entity', 'resolve_target_entities')
->append($this->getOrmTargetEntityResolverNode())
->end()
->end();
}
/**
* Return ORM target entity resolver node
*/
private function getOrmTargetEntityResolverNode(): NodeDefinition
{
$treeBuilder = new TreeBuilder('resolve_target_entities');
$node = $treeBuilder->getRootNode();
$node
->useAttributeAsKey('interface')
->prototype('scalar')
->cannotBeEmpty()
->end();
return $node;
}
/**
* Return ORM entity listener node
*/
private function getOrmEntityListenersNode(): NodeDefinition
{
$treeBuilder = new TreeBuilder('entity_listeners');
$node = $treeBuilder->getRootNode();
$normalizer = static function ($mappings) {
$entities = [];
foreach ($mappings as $entityClass => $mapping) {
$listeners = [];
foreach ($mapping as $listenerClass => $listenerEvent) {
$events = [];
foreach ($listenerEvent as $eventType => $eventMapping) {
if ($eventMapping === null) {
$eventMapping = [null];
}
foreach ($eventMapping as $method) {
$events[] = [
'type' => $eventType,
'method' => $method,
];
}
}
$listeners[] = [
'class' => $listenerClass,
'event' => $events,
];
}
$entities[] = [
'class' => $entityClass,
'listener' => $listeners,
];
}
return ['entities' => $entities];
};
$node
->beforeNormalization()
// Yaml normalization
->ifTrue(static fn ($v) => is_array(reset($v)) && is_string(key(reset($v))))
->then($normalizer)
->end()
->fixXmlConfig('entity', 'entities')
->children()
->arrayNode('entities')
->useAttributeAsKey('class')
->prototype('array')
->fixXmlConfig('listener')
->children()
->arrayNode('listeners')
->useAttributeAsKey('class')
->prototype('array')
->fixXmlConfig('event')
->children()
->arrayNode('events')
->prototype('array')
->children()
->scalarNode('type')->end()
->scalarNode('method')->defaultNull()->end()
->end()
->end()
->end()
->end()
->end()
->end()
->end()
->end()
->end()
->end();
return $node;
}
/**
* Return ORM entity manager node
*/
private function getOrmEntityManagersNode(): ArrayNodeDefinition
{
$treeBuilder = new TreeBuilder('entity_managers');
$node = $treeBuilder->getRootNode();
$node
->requiresAtLeastOneElement()
->useAttributeAsKey('name')
->prototype('array')
->addDefaultsIfNotSet()
->append($this->getOrmCacheDriverNode('query_cache_driver'))
->append($this->getOrmCacheDriverNode('metadata_cache_driver'))
->append($this->getOrmCacheDriverNode('result_cache_driver'))
->append($this->getOrmEntityListenersNode())
->fixXmlConfig('schema_ignore_class', 'schema_ignore_classes')
->children()
->scalarNode('connection')->end()
->scalarNode('class_metadata_factory_name')->defaultValue(ClassMetadataFactory::class)->end()
->scalarNode('default_repository_class')->defaultValue(EntityRepository::class)->end()
->scalarNode('auto_mapping')->defaultFalse()->end()
->scalarNode('naming_strategy')->defaultValue('doctrine.orm.naming_strategy.default')->end()
->scalarNode('quote_strategy')->defaultValue('doctrine.orm.quote_strategy.default')->end()
->scalarNode('typed_field_mapper')->defaultValue('doctrine.orm.typed_field_mapper.default')->end()
->scalarNode('entity_listener_resolver')->defaultNull()->end()
->scalarNode('fetch_mode_subselect_batch_size')->end()
->scalarNode('repository_factory')->defaultValue('doctrine.orm.container_repository_factory')->end()
->arrayNode('schema_ignore_classes')
->prototype('scalar')->end()
->end()
->booleanNode('report_fields_where_declared')
->beforeNormalization()
->ifTrue(static fn ($v): bool => isset($v) && ! class_exists(AnnotationDriver::class))
->then(static function ($v) {
Deprecation::trigger(
'doctrine/doctrine-bundle',
'https://github.com/doctrine/DoctrineBundle/pull/1962',
'The "report_fields_where_declared" configuration option is deprecated and will be removed in DoctrineBundle 3.0. When using ORM 3, report_fields_where_declared will always be true.',
);
return $v;
})
->end()
->defaultValue(! class_exists(AnnotationDriver::class))
->info('Set to "true" to opt-in to the new mapping driver mode that was added in Doctrine ORM 2.16 and will be mandatory in ORM 3.0. See https://github.com/doctrine/orm/pull/10455.')
->validate()
->ifTrue(static fn (bool $v): bool => ! class_exists(AnnotationDriver::class) && ! $v)
->thenInvalid('The setting "report_fields_where_declared" cannot be disabled for ORM 3.')
->end()
->end()
->booleanNode('validate_xml_mapping')->defaultFalse()->info('Set to "true" to opt-in to the new mapping driver mode that was added in Doctrine ORM 2.14. See https://github.com/doctrine/orm/pull/6728.')->end()
->end()
->children()
->arrayNode('second_level_cache')
->children()
->append($this->getOrmCacheDriverNode('region_cache_driver'))
->scalarNode('region_lock_lifetime')->defaultValue(60)->end()
->booleanNode('log_enabled')->defaultValue($this->debug)->end()
->scalarNode('region_lifetime')->defaultValue(3600)->end()
->booleanNode('enabled')->defaultValue(true)->end()
->scalarNode('factory')->end()
->end()
->fixXmlConfig('region')
->children()
->arrayNode('regions')
->useAttributeAsKey('name')
->prototype('array')
->children()
->append($this->getOrmCacheDriverNode('cache_driver'))
->scalarNode('lock_path')->defaultValue('%kernel.cache_dir%/doctrine/orm/slc/filelock')->end()
->scalarNode('lock_lifetime')->defaultValue(60)->end()
->scalarNode('type')->defaultValue('default')->end()
->scalarNode('lifetime')->defaultValue(0)->end()
->scalarNode('service')->end()
->scalarNode('name')->end()
->end()
->end()
->end()
->end()
->fixXmlConfig('logger')
->children()
->arrayNode('loggers')
->useAttributeAsKey('name')
->prototype('array')
->children()
->scalarNode('name')->end()
->scalarNode('service')->end()
->end()
->end()
->end()
->end()
->end()
->end()
->fixXmlConfig('hydrator')
->children()
->arrayNode('hydrators')
->useAttributeAsKey('name')
->prototype('scalar')->end()
->end()
->end()
->fixXmlConfig('mapping')
->children()
->arrayNode('mappings')
->useAttributeAsKey('name')
->prototype('array')
->beforeNormalization()
->ifString()
->then(static fn ($v) => ['type' => $v])
->end()
->treatNullLike([])
->treatFalseLike(['mapping' => false])
->performNoDeepMerging()
->children()
->scalarNode('mapping')->defaultValue(true)->end()
->scalarNode('type')->end()
->scalarNode('dir')->end()
->scalarNode('alias')->end()
->scalarNode('prefix')->end()
->booleanNode('is_bundle')->end()
->end()
->end()
->end()
->arrayNode('dql')
->fixXmlConfig('string_function')
->fixXmlConfig('numeric_function')
->fixXmlConfig('datetime_function')
->children()
->arrayNode('string_functions')
->useAttributeAsKey('name')
->prototype('scalar')->end()
->end()
->arrayNode('numeric_functions')
->useAttributeAsKey('name')
->prototype('scalar')->end()
->end()
->arrayNode('datetime_functions')
->useAttributeAsKey('name')
->prototype('scalar')->end()
->end()
->end()
->end()
->end()
->fixXmlConfig('filter')
->children()
->arrayNode('filters')
->info('Register SQL Filters in the entity manager')
->useAttributeAsKey('name')
->prototype('array')
->beforeNormalization()
->ifString()
->then(static fn ($v) => ['class' => $v])
->end()
->beforeNormalization()
// The content of the XML node is returned as the "value" key so we need to rename it
->ifTrue(static fn ($v) => is_array($v) && isset($v['value']))
->then(static function ($v) {
$v['class'] = $v['value'];
unset($v['value']);
return $v;
})
->end()
->fixXmlConfig('parameter')
->children()
->scalarNode('class')->isRequired()->end()
->booleanNode('enabled')->defaultFalse()->end()
->arrayNode('parameters')
->useAttributeAsKey('name')
->prototype('variable')->end()
->end()
->end()
->end()
->end()
->end()
->fixXmlConfig('identity_generation_preference')
->children()
->arrayNode('identity_generation_preferences')
->info('Configures the preferences for identity generation when using the AUTO strategy. Valid values are "SEQUENCE" or "IDENTITY".')
->useAttributeAsKey('platform')
->prototype('scalar')
->beforeNormalization()
->ifString()
->then(static fn (string $v) => constant(ClassMetadata::class . '::GENERATOR_TYPE_' . strtoupper($v)))
->end()
->end()
->end()
->end()
->end();
return $node;
}
/**
* Return an ORM cache driver node for a given entity manager
*/
private function getOrmCacheDriverNode(string $name): ArrayNodeDefinition
{
$treeBuilder = new TreeBuilder($name);
$node = $treeBuilder->getRootNode();
$node
->beforeNormalization()
->ifString()
->then(static fn ($v): array => ['type' => $v])
->end()
->children()
->scalarNode('type')->defaultNull()->end()
->scalarNode('id')->end()
->scalarNode('pool')->end()
->end();
if ($name !== 'metadata_cache_driver') {
$node->addDefaultsIfNotSet();
}
return $node;
}
/**
* Find proxy auto generate modes for their names and int values
*
* @return array{names: list<string>, values: list<int>}
*/
private function getAutoGenerateModes(): array
{
$constPrefix = 'AUTOGENERATE_';
$prefixLen = strlen($constPrefix);
$refClass = new ReflectionClass(ProxyFactory::class);
$constsArray = $refClass->getConstants();
$namesArray = [];
$valuesArray = [];
foreach ($constsArray as $key => $value) {
if (strpos($key, $constPrefix) !== 0) {
continue;
}
$namesArray[] = substr($key, $prefixLen);
$valuesArray[] = (int) $value;
}
return [
'names' => $namesArray,
'values' => $valuesArray,
];
}
private function getDefaultSchemaManagerFactory(): string
{
if (class_exists(LegacySchemaManagerFactory::class)) {
return 'doctrine.dbal.legacy_schema_manager_factory';
}
return 'doctrine.dbal.default_schema_manager_factory';
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,179 @@
<?php
declare(strict_types=1);
namespace Doctrine\Bundle\DoctrineBundle;
use Closure;
use Doctrine\Bundle\DoctrineBundle\DependencyInjection\Compiler\CacheCompatibilityPass;
use Doctrine\Bundle\DoctrineBundle\DependencyInjection\Compiler\CacheSchemaSubscriberPass;
use Doctrine\Bundle\DoctrineBundle\DependencyInjection\Compiler\DbalSchemaFilterPass;
use Doctrine\Bundle\DoctrineBundle\DependencyInjection\Compiler\EntityListenerPass;
use Doctrine\Bundle\DoctrineBundle\DependencyInjection\Compiler\IdGeneratorPass;
use Doctrine\Bundle\DoctrineBundle\DependencyInjection\Compiler\MiddlewaresPass;
use Doctrine\Bundle\DoctrineBundle\DependencyInjection\Compiler\RemoveLoggingMiddlewarePass;
use Doctrine\Bundle\DoctrineBundle\DependencyInjection\Compiler\RemoveProfilerControllerPass;
use Doctrine\Bundle\DoctrineBundle\DependencyInjection\Compiler\ServiceRepositoryCompilerPass;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\Proxy\Autoloader;
use Doctrine\ORM\Proxy\DefaultProxyClassNameResolver;
use Symfony\Bridge\Doctrine\DependencyInjection\CompilerPass\DoctrineValidationPass;
use Symfony\Bridge\Doctrine\DependencyInjection\CompilerPass\RegisterDatePointTypePass;
use Symfony\Bridge\Doctrine\DependencyInjection\CompilerPass\RegisterEventListenersAndSubscribersPass;
use Symfony\Bridge\Doctrine\DependencyInjection\CompilerPass\RegisterUidTypePass;
use Symfony\Bridge\Doctrine\DependencyInjection\Security\UserProvider\EntityFactory;
use Symfony\Bundle\SecurityBundle\DependencyInjection\SecurityExtension;
use Symfony\Component\Console\Application;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\Compiler\PassConfig;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\HttpKernel\Bundle\Bundle;
use function assert;
use function class_exists;
use function clearstatcache;
use function dirname;
use function spl_autoload_unregister;
/** @final since 2.9 */
class DoctrineBundle extends Bundle
{
private Closure|null $autoloader = null;
public function build(ContainerBuilder $container): void
{
parent::build($container);
$container->addCompilerPass(new class () implements CompilerPassInterface {
public function process(ContainerBuilder $container): void
{
if ($container->has('session.handler')) {
return;
}
$container->removeDefinition('doctrine.orm.listeners.pdo_session_handler_schema_listener');
}
}, PassConfig::TYPE_BEFORE_OPTIMIZATION);
$container->addCompilerPass(new RegisterEventListenersAndSubscribersPass('doctrine.connections', 'doctrine.dbal.%s_connection.event_manager', 'doctrine'), PassConfig::TYPE_BEFORE_OPTIMIZATION);
if ($container->hasExtension('security')) {
$security = $container->getExtension('security');
if ($security instanceof SecurityExtension) {
$security->addUserProviderFactory(new EntityFactory('entity', 'doctrine.orm.security.user.provider'));
}
}
$container->addCompilerPass(new CacheCompatibilityPass());
$container->addCompilerPass(new DoctrineValidationPass('orm'));
$container->addCompilerPass(new EntityListenerPass());
$container->addCompilerPass(new ServiceRepositoryCompilerPass());
$container->addCompilerPass(new IdGeneratorPass());
$container->addCompilerPass(new DbalSchemaFilterPass());
$container->addCompilerPass(new CacheSchemaSubscriberPass(), PassConfig::TYPE_BEFORE_REMOVING, -10);
$container->addCompilerPass(new RemoveProfilerControllerPass());
$container->addCompilerPass(new RemoveLoggingMiddlewarePass());
$container->addCompilerPass(new MiddlewaresPass());
$container->addCompilerPass(new RegisterUidTypePass());
if (! class_exists(RegisterDatePointTypePass::class)) {
return;
}
$container->addCompilerPass(new RegisterDatePointTypePass());
}
public function boot(): void
{
// Register an autoloader for proxies when native lazy objects are not in use
// to avoid issues when unserializing them when the ORM is used.
if ($this->container->hasParameter('doctrine.orm.enable_native_lazy_objects') && $this->container->getParameter('doctrine.orm.enable_native_lazy_objects')) {
return;
}
if (! $this->container->hasParameter('doctrine.orm.proxy_namespace')) {
return;
}
$namespace = (string) $this->container->getParameter('doctrine.orm.proxy_namespace');
$dir = (string) $this->container->getParameter('doctrine.orm.proxy_dir');
$proxyGenerator = null;
if ($this->container->getParameter('doctrine.orm.auto_generate_proxy_classes')) {
// See https://github.com/symfony/symfony/pull/3419 for usage of references
/** @psalm-suppress UnsupportedPropertyReferenceUsage */
$container = &$this->container;
$proxyGenerator = static function ($proxyDir, $proxyNamespace, $class) use (&$container): void {
$originalClassName = (new DefaultProxyClassNameResolver())->resolveClassName($class);
$registry = $container->get('doctrine');
assert($registry instanceof Registry);
foreach ($registry->getManagers() as $em) {
assert($em instanceof EntityManagerInterface);
if (! $em->getConfiguration()->getAutoGenerateProxyClasses()) {
continue;
}
$metadataFactory = $em->getMetadataFactory();
if ($metadataFactory->isTransient($originalClassName)) {
continue;
}
$classMetadata = $metadataFactory->getMetadataFor($originalClassName);
$em->getProxyFactory()->generateProxyClasses([$classMetadata]);
clearstatcache(true, Autoloader::resolveFile($proxyDir, $proxyNamespace, $class));
break;
}
};
}
$this->autoloader = Autoloader::register($dir, $namespace, $proxyGenerator);
}
public function shutdown(): void
{
if ($this->autoloader !== null) {
spl_autoload_unregister($this->autoloader);
$this->autoloader = null;
}
// Clear all entity managers to clear references to entities for GC
if ($this->container->hasParameter('doctrine.entity_managers')) {
foreach ($this->container->getParameter('doctrine.entity_managers') as $id) {
if (! $this->container->initialized($id)) {
continue;
}
$this->container->get($id)->clear();
}
}
// Close all connections to avoid reaching too many connections in the process when booting again later (tests)
if (! $this->container->hasParameter('doctrine.connections')) {
return;
}
foreach ($this->container->getParameter('doctrine.connections') as $id) {
if (! $this->container->initialized($id)) {
continue;
}
$this->container->get($id)->close();
}
}
public function registerCommands(Application $application): void
{
}
public function getPath(): string
{
return dirname(__DIR__);
}
}

View File

@@ -0,0 +1,12 @@
<?php
declare(strict_types=1);
namespace Doctrine\Bundle\DoctrineBundle\EventSubscriber;
use Doctrine\Common\EventSubscriber;
/** @deprecated use the {@see AsDoctrineListener} attribute instead */
interface EventSubscriberInterface extends EventSubscriber
{
}

View File

@@ -0,0 +1,64 @@
<?php
declare(strict_types=1);
namespace Doctrine\Bundle\DoctrineBundle;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\Query\Filter\SQLFilter;
/**
* Configurator for an EntityManager
*/
class ManagerConfigurator
{
/**
* @param string[] $enabledFilters
* @param array<string,array<string,string>> $filtersParameters
*/
public function __construct(
private readonly array $enabledFilters = [],
private readonly array $filtersParameters = [],
) {
}
/**
* Create a connection by name.
*
* @return void
*/
public function configure(EntityManagerInterface $entityManager)
{
$this->enableFilters($entityManager);
}
/**
* Enables filters for a given entity manager
*/
private function enableFilters(EntityManagerInterface $entityManager): void
{
if (empty($this->enabledFilters)) {
return;
}
$filterCollection = $entityManager->getFilters();
foreach ($this->enabledFilters as $filter) {
$this->setFilterParameters($filter, $filterCollection->enable($filter));
}
}
/**
* Sets default parameters for a given filter
*/
private function setFilterParameters(string $name, SQLFilter $filter): void
{
if (empty($this->filtersParameters[$name])) {
return;
}
$parameters = $this->filtersParameters[$name];
foreach ($parameters as $paramName => $paramValue) {
$filter->setParameter($paramName, $paramValue);
}
}
}

View File

@@ -0,0 +1,49 @@
<?php
declare(strict_types=1);
namespace Doctrine\Bundle\DoctrineBundle\Mapping;
use Doctrine\ORM\Mapping\ClassMetadata;
class ClassMetadataCollection
{
private string|null $path = null;
private string|null $namespace = null;
/** @param ClassMetadata[] $metadata */
public function __construct(
private readonly array $metadata,
) {
}
/** @return ClassMetadata[] */
public function getMetadata()
{
return $this->metadata;
}
/** @param string $path */
public function setPath($path)
{
$this->path = $path;
}
/** @return string|null */
public function getPath()
{
return $this->path;
}
/** @param string $namespace */
public function setNamespace($namespace)
{
$this->namespace = $namespace;
}
/** @return string|null */
public function getNamespace()
{
return $this->namespace;
}
}

View File

@@ -0,0 +1,36 @@
<?php
declare(strict_types=1);
namespace Doctrine\Bundle\DoctrineBundle\Mapping;
use Doctrine\ORM\Id\AbstractIdGenerator;
use Doctrine\ORM\Mapping\ClassMetadata;
use Doctrine\ORM\Mapping\ClassMetadataFactory as BaseClassMetadataFactory;
use function assert;
class ClassMetadataFactory extends BaseClassMetadataFactory
{
/**
* {@inheritDoc}
*/
protected function doLoadMetadata($class, $parent, $rootEntityFound, array $nonSuperclassParents): void
{
parent::doLoadMetadata($class, $parent, $rootEntityFound, $nonSuperclassParents);
$customGeneratorDefinition = $class->customGeneratorDefinition;
if (! isset($customGeneratorDefinition['instance'])) {
return;
}
/** @phpstan-ignore function.impossibleType, instanceof.alwaysFalse */
assert($customGeneratorDefinition['instance'] instanceof AbstractIdGenerator);
$class->setIdGeneratorType(ClassMetadata::GENERATOR_TYPE_CUSTOM);
$class->setIdGenerator($customGeneratorDefinition['instance']);
unset($customGeneratorDefinition['instance']);
$class->setCustomGeneratorDefinition($customGeneratorDefinition);
}
}

View File

@@ -0,0 +1,100 @@
<?php
declare(strict_types=1);
namespace Doctrine\Bundle\DoctrineBundle\Mapping;
use InvalidArgumentException;
use Psr\Container\ContainerInterface;
use RuntimeException;
use function gettype;
use function is_object;
use function sprintf;
use function trim;
/** @final */
class ContainerEntityListenerResolver implements EntityListenerServiceResolver
{
/** @var object[] Map to store entity listener instances. */
private array $instances = [];
/** @var string[] Map to store registered service ids */
private array $serviceIds = [];
/** @param ContainerInterface $container a service locator for listeners */
public function __construct(
private readonly ContainerInterface $container,
) {
}
/**
* {@inheritDoc}
*/
public function clear($className = null): void
{
if ($className === null) {
$this->instances = [];
return;
}
$className = $this->normalizeClassName($className);
unset($this->instances[$className]);
}
/**
* {@inheritDoc}
*/
public function register($object): void
{
if (! is_object($object)) {
throw new InvalidArgumentException(sprintf('An object was expected, but got "%s".', gettype($object)));
}
$className = $this->normalizeClassName($object::class);
$this->instances[$className] = $object;
}
/**
* {@inheritDoc}
*/
public function registerService($className, $serviceId)
{
$this->serviceIds[$this->normalizeClassName($className)] = $serviceId;
}
/**
* {@inheritDoc}
*/
public function resolve($className): object
{
$className = $this->normalizeClassName($className);
if (! isset($this->instances[$className])) {
if (isset($this->serviceIds[$className])) {
$this->instances[$className] = $this->resolveService($this->serviceIds[$className]);
} else {
$this->instances[$className] = new $className();
}
}
return $this->instances[$className];
}
private function resolveService(string $serviceId): object
{
if (! $this->container->has($serviceId)) {
throw new RuntimeException(sprintf('There is no service named "%s"', $serviceId));
}
return $this->container->get($serviceId);
}
private function normalizeClassName(string $className): string
{
return trim($className, '\\');
}
}

View File

@@ -0,0 +1,176 @@
<?php
declare(strict_types=1);
namespace Doctrine\Bundle\DoctrineBundle\Mapping;
use Doctrine\ORM\Mapping\ClassMetadata;
use Doctrine\ORM\Mapping\MappingException;
use Doctrine\ORM\Tools\DisconnectedClassMetadataFactory;
use Doctrine\Persistence\ManagerRegistry;
use ReflectionClass;
use RuntimeException;
use Symfony\Component\HttpKernel\Bundle\BundleInterface;
use function dirname;
use function sprintf;
use function str_replace;
use function strpos;
/**
* This class provides methods to access Doctrine entity class metadata for a
* given bundle, namespace or entity class, for generation purposes
*/
class DisconnectedMetadataFactory
{
public function __construct(
private readonly ManagerRegistry $registry,
) {
}
/**
* Gets the metadata of all classes of a bundle.
*
* @param BundleInterface $bundle A BundleInterface instance
*
* @return ClassMetadataCollection A ClassMetadataCollection instance
*
* @throws RuntimeException When bundle does not contain mapped entities.
*/
public function getBundleMetadata(BundleInterface $bundle)
{
$namespace = $bundle->getNamespace();
$metadata = $this->getMetadataForNamespace($namespace);
if (! $metadata->getMetadata()) {
throw new RuntimeException(sprintf('Bundle "%s" does not contain any mapped entities.', $bundle->getName()));
}
$path = $this->getBasePathForClass($bundle->getName(), $bundle->getNamespace(), $bundle->getPath());
$metadata->setPath($path);
$metadata->setNamespace($bundle->getNamespace());
return $metadata;
}
/**
* Gets the metadata of a class.
*
* @param string $class A class name
* @param string $path The path where the class is stored (if known)
*
* @return ClassMetadataCollection A ClassMetadataCollection instance
*
* @throws MappingException When class is not valid entity or mapped superclass.
*/
public function getClassMetadata($class, $path = null)
{
$metadata = $this->getMetadataForClass($class);
if (! $metadata->getMetadata()) {
throw MappingException::classIsNotAValidEntityOrMappedSuperClass($class);
}
$this->findNamespaceAndPathForMetadata($metadata, $path);
return $metadata;
}
/**
* Gets the metadata of all classes of a namespace.
*
* @param string $namespace A namespace name
* @param string $path The path where the class is stored (if known)
*
* @return ClassMetadataCollection A ClassMetadataCollection instance
*
* @throws RuntimeException When namespace not contain mapped entities.
*/
public function getNamespaceMetadata($namespace, $path = null)
{
$metadata = $this->getMetadataForNamespace($namespace);
if (! $metadata->getMetadata()) {
throw new RuntimeException(sprintf('Namespace "%s" does not contain any mapped entities.', $namespace));
}
$this->findNamespaceAndPathForMetadata($metadata, $path);
return $metadata;
}
/**
* Find and configure path and namespace for the metadata collection.
*
* @param string|null $path
*
* @throws RuntimeException When unable to determine the path.
*/
public function findNamespaceAndPathForMetadata(ClassMetadataCollection $metadata, $path = null)
{
$r = new ReflectionClass($metadata->getMetadata()[0]->name);
$metadata->setPath($this->getBasePathForClass($r->getName(), $r->getNamespaceName(), dirname($r->getFilename())));
$metadata->setNamespace($r->getNamespaceName());
}
/**
* Get a base path for a class
*
* @throws RuntimeException When base path not found.
*/
private function getBasePathForClass(string $name, string $namespace, string $path): string
{
$namespace = str_replace('\\', '/', $namespace);
$search = str_replace('\\', '/', $path);
$destination = str_replace('/' . $namespace, '', $search, $c);
if ($c !== 1) {
throw new RuntimeException(sprintf('Can\'t find base path for "%s" (path: "%s", destination: "%s").', $name, $path, $destination));
}
return $destination;
}
private function getMetadataForNamespace(string $namespace): ClassMetadataCollection
{
$metadata = [];
foreach ($this->getAllMetadata() as $m) {
if (strpos($m->name, $namespace) !== 0) {
continue;
}
$metadata[] = $m;
}
return new ClassMetadataCollection($metadata);
}
private function getMetadataForClass(string $entity): ClassMetadataCollection
{
foreach ($this->registry->getManagers() as $em) {
/* @phpstan-ignore class.notFound */
$cmf = new DisconnectedClassMetadataFactory();
$cmf->setEntityManager($em);
if (! $cmf->isTransient($entity)) {
return new ClassMetadataCollection([$cmf->getMetadataFor($entity)]);
}
}
return new ClassMetadataCollection([]);
}
/** @return ClassMetadata[] */
private function getAllMetadata(): array
{
$metadata = [];
foreach ($this->registry->getManagers() as $em) {
/* @phpstan-ignore class.notFound */
$cmf = new DisconnectedClassMetadataFactory();
$cmf->setEntityManager($em);
foreach ($cmf->getAllMetadata() as $m) {
$metadata[] = $m;
}
}
return $metadata;
}
}

View File

@@ -0,0 +1,17 @@
<?php
declare(strict_types=1);
namespace Doctrine\Bundle\DoctrineBundle\Mapping;
use Doctrine\ORM\Mapping\EntityListenerResolver;
interface EntityListenerServiceResolver extends EntityListenerResolver
{
/**
* @param string $className
* @param string $serviceId
*/
// phpcs:ignore
public function registerService($className, $serviceId);
}

View File

@@ -0,0 +1,64 @@
<?php
declare(strict_types=1);
namespace Doctrine\Bundle\DoctrineBundle\Mapping;
use Doctrine\ORM\Mapping\ClassMetadata as OrmClassMetadata;
use Doctrine\Persistence\Mapping\ClassMetadata;
use Doctrine\Persistence\Mapping\Driver\MappingDriver as MappingDriverInterface;
use Psr\Container\ContainerInterface;
class MappingDriver implements MappingDriverInterface
{
public function __construct(
private readonly MappingDriverInterface $driver,
private readonly ContainerInterface $idGeneratorLocator,
) {
}
/**
* {@inheritDoc}
*/
public function getAllClassNames(): array
{
return $this->driver->getAllClassNames();
}
/**
* {@inheritDoc}
*/
public function isTransient($className): bool
{
return $this->driver->isTransient($className);
}
/**
* {@inheritDoc}
*/
public function loadMetadataForClass($className, ClassMetadata $metadata): void
{
$this->driver->loadMetadataForClass($className, $metadata);
if (
! $metadata instanceof OrmClassMetadata
|| $metadata->generatorType !== OrmClassMetadata::GENERATOR_TYPE_CUSTOM
|| ! isset($metadata->customGeneratorDefinition['class'])
|| ! $this->idGeneratorLocator->has($metadata->customGeneratorDefinition['class'])
) {
return;
}
$idGenerator = $this->idGeneratorLocator->get($metadata->customGeneratorDefinition['class']);
$metadata->setCustomGeneratorDefinition(['instance' => $idGenerator] + $metadata->customGeneratorDefinition);
$metadata->setIdGeneratorType(OrmClassMetadata::GENERATOR_TYPE_NONE);
}
/**
* Returns the inner driver
*/
public function getDriver(): MappingDriverInterface
{
return $this->driver;
}
}

View File

@@ -0,0 +1,90 @@
<?php
declare(strict_types=1);
namespace Doctrine\Bundle\DoctrineBundle\Middleware;
use Symfony\Bridge\Doctrine\Middleware\Debug\DebugDataHolder;
use Symfony\Bridge\Doctrine\Middleware\Debug\Query;
use function array_slice;
use function debug_backtrace;
use function in_array;
use const DEBUG_BACKTRACE_IGNORE_ARGS;
class BacktraceDebugDataHolder extends DebugDataHolder
{
/** @var array<string, array<int|string, mixed>[]> */
private array $backtraces = [];
/** @param string[] $connWithBacktraces */
public function __construct(
private readonly array $connWithBacktraces,
) {
}
public function reset(): void
{
parent::reset();
$this->backtraces = [];
}
public function addQuery(string $connectionName, Query $query): void
{
parent::addQuery($connectionName, $query);
if (! in_array($connectionName, $this->connWithBacktraces, true)) {
return;
}
// array_slice to skip middleware calls in the trace
$this->backtraces[$connectionName][] = array_slice(debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS), 2);
}
/** @return array<string, array<string, mixed>[]> */
public function getData(): array
{
$dataWithBacktraces = [];
$data = parent::getData();
foreach ($data as $connectionName => $dataForConn) {
$dataWithBacktraces[$connectionName] = $this->getDataForConnection($connectionName, $dataForConn);
}
return $dataWithBacktraces;
}
/**
* @param mixed[][] $dataForConn
*
* @return mixed[][]
*/
private function getDataForConnection(string $connectionName, array $dataForConn): array
{
$data = [];
foreach ($dataForConn as $idx => $record) {
$data[] = $this->addBacktracesIfAvailable($connectionName, $record, $idx);
}
return $data;
}
/**
* @param mixed[] $record
*
* @return mixed[]
*/
private function addBacktracesIfAvailable(string $connectionName, array $record, int $idx): array
{
if (! isset($this->backtraces[$connectionName])) {
return $record;
}
$record['backtrace'] = $this->backtraces[$connectionName][$idx];
return $record;
}
}

View File

@@ -0,0 +1,10 @@
<?php
declare(strict_types=1);
namespace Doctrine\Bundle\DoctrineBundle\Middleware;
interface ConnectionNameAwareInterface
{
public function setConnectionName(string $name): void;
}

View File

@@ -0,0 +1,32 @@
<?php
declare(strict_types=1);
namespace Doctrine\Bundle\DoctrineBundle\Middleware;
use Doctrine\DBAL\Driver as DriverInterface;
use Doctrine\DBAL\Driver\Middleware;
use Symfony\Bridge\Doctrine\Middleware\Debug\DebugDataHolder;
use Symfony\Bridge\Doctrine\Middleware\Debug\Driver;
use Symfony\Component\Stopwatch\Stopwatch;
class DebugMiddleware implements Middleware, ConnectionNameAwareInterface
{
private string $connectionName = 'default';
public function __construct(
private readonly DebugDataHolder $debugDataHolder,
private readonly Stopwatch|null $stopwatch,
) {
}
public function setConnectionName(string $name): void
{
$this->connectionName = $name;
}
public function wrap(DriverInterface $driver): DriverInterface
{
return new Driver($driver, $this->debugDataHolder, $this->stopwatch, $this->connectionName);
}
}

View File

@@ -0,0 +1,40 @@
<?php
declare(strict_types=1);
namespace Doctrine\Bundle\DoctrineBundle\Middleware;
use ArrayObject;
use Doctrine\DBAL\Driver;
use Doctrine\DBAL\Driver\Middleware;
use Symfony\Bridge\Doctrine\Middleware\IdleConnection\Driver as IdleConnectionDriver;
class IdleConnectionMiddleware implements Middleware, ConnectionNameAwareInterface
{
private string $connectionName;
/**
* @param ArrayObject<string, int> $connectionExpiries
* @param array<string, int> $ttlByConnection
*/
public function __construct(
private readonly ArrayObject $connectionExpiries,
private readonly array $ttlByConnection,
) {
}
public function setConnectionName(string $name): void
{
$this->connectionName = $name;
}
public function wrap(Driver $driver): IdleConnectionDriver
{
return new IdleConnectionDriver(
$driver,
$this->connectionExpiries,
$this->ttlByConnection[$this->connectionName],
$this->connectionName,
);
}
}

View File

@@ -0,0 +1,43 @@
<?php
declare(strict_types=1);
namespace Doctrine\Bundle\DoctrineBundle\Orm;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\Tools\Console\EntityManagerProvider;
use Doctrine\Persistence\ManagerRegistry;
use RuntimeException;
use function get_debug_type;
use function sprintf;
final class ManagerRegistryAwareEntityManagerProvider implements EntityManagerProvider
{
public function __construct(
private readonly ManagerRegistry $managerRegistry,
) {
}
public function getDefaultManager(): EntityManagerInterface
{
return $this->getManager($this->managerRegistry->getDefaultManagerName());
}
public function getManager(string $name): EntityManagerInterface
{
$em = $this->managerRegistry->getManager($name);
if ($em instanceof EntityManagerInterface) {
return $em;
}
throw new RuntimeException(
sprintf(
'Only managers of type "%s" are supported. Instance of "%s given.',
EntityManagerInterface::class,
get_debug_type($em),
),
);
}
}

View File

@@ -0,0 +1,113 @@
<?php
declare(strict_types=1);
namespace Doctrine\Bundle\DoctrineBundle;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\ORMException;
use Doctrine\Persistence\Proxy;
use ProxyManager\Proxy\LazyLoadingInterface;
use ReflectionClass;
use Symfony\Bridge\Doctrine\ManagerRegistry;
use Symfony\Component\DependencyInjection\Container;
use Symfony\Component\VarExporter\LazyObjectInterface;
use Symfony\Contracts\Service\ResetInterface;
use function array_keys;
use function assert;
use function method_exists;
use const PHP_VERSION_ID;
/**
* References all Doctrine connections and entity managers in a given Container.
*/
class Registry extends ManagerRegistry implements ResetInterface
{
/**
* @param string[] $connections
* @param string[] $entityManagers
*/
public function __construct(Container $container, array $connections, array $entityManagers, string $defaultConnection, string $defaultEntityManager)
{
$this->container = $container;
parent::__construct('ORM', $connections, $entityManagers, $defaultConnection, $defaultEntityManager, Proxy::class);
}
/**
* Resolves a registered namespace alias to the full namespace.
*
* This method looks for the alias in all registered entity managers.
*
* @see Configuration::getEntityNamespace
*
* @param string $alias The alias
*
* @return string The full namespace
*/
public function getAliasNamespace($alias)
{
foreach (array_keys($this->getManagers()) as $name) {
$objectManager = $this->getManager($name);
if (! $objectManager instanceof EntityManagerInterface) {
continue;
}
try {
/** @phpstan-ignore method.notFound (ORM < 3 specific) */
return $objectManager->getConfiguration()->getEntityNamespace($alias);
/* @phpstan-ignore class.notFound */
} catch (ORMException) {
}
}
/* @phpstan-ignore class.notFound */
throw ORMException::unknownEntityNamespace($alias);
}
public function reset(): void
{
foreach ($this->getManagerNames() as $managerName => $serviceId) {
$this->resetOrClearManager($managerName, $serviceId);
}
}
private function resetOrClearManager(string $managerName, string $serviceId): void
{
if (! $this->container->initialized($serviceId)) {
return;
}
$manager = $this->container->get($serviceId);
assert($manager instanceof EntityManagerInterface);
// Determine if the version of symfony/dependency-injection is >= 7.3
/** @phpstan-ignore function.alreadyNarrowedType */
$sfNativeLazyObjects = method_exists('Symfony\Component\DependencyInjection\ContainerBuilder', 'findTaggedResourceIds');
if (PHP_VERSION_ID < 80400 || ! $sfNativeLazyObjects) {
if ((! $manager instanceof LazyLoadingInterface && ! $manager instanceof LazyObjectInterface) || $manager->isOpen()) {
$manager->clear();
return;
}
} else {
$r = new ReflectionClass($manager);
if ($r->isUninitializedLazyObject($manager)) {
return;
}
if ($manager->isOpen()) {
$manager->clear();
return;
}
}
$this->resetManager($managerName);
}
}

View File

@@ -0,0 +1,118 @@
<?php
declare(strict_types=1);
namespace Doctrine\Bundle\DoctrineBundle\Repository;
use Doctrine\Bundle\DoctrineBundle\DependencyInjection\Compiler\ServiceRepositoryCompilerPass;
use Doctrine\Deprecations\Deprecation;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\EntityRepository;
use Doctrine\ORM\Mapping\ClassMetadata;
use Doctrine\ORM\Repository\RepositoryFactory;
use Doctrine\Persistence\ObjectRepository;
use Psr\Container\ContainerInterface;
use RuntimeException;
use function class_exists;
use function get_debug_type;
use function is_a;
use function spl_object_hash;
use function sprintf;
/**
* Fetches repositories from the container or falls back to normal creation.
*/
final class ContainerRepositoryFactory implements RepositoryFactory
{
use RepositoryFactoryCompatibility;
/** @var array<string, ObjectRepository> */
private array $managedRepositories = [];
/** @param ContainerInterface $container A service locator containing the repositories */
public function __construct(
private readonly ContainerInterface $container,
) {
}
/**
* @param class-string<T> $entityName
*
* @return ObjectRepository<T>
* @phpstan-return ($strictTypeCheck is true ? EntityRepository<T> : ObjectRepository<T>)
*
* @template T of object
*/
private function doGetRepository(EntityManagerInterface $entityManager, string $entityName, bool $strictTypeCheck): ObjectRepository
{
$metadata = $entityManager->getClassMetadata($entityName);
$repositoryServiceId = $metadata->customRepositoryClassName;
$customRepositoryName = $metadata->customRepositoryClassName;
if ($customRepositoryName !== null) {
// fetch from the container
if ($this->container->has($customRepositoryName)) {
$repository = $this->container->get($customRepositoryName);
if (! $repository instanceof EntityRepository && $strictTypeCheck) {
throw new RuntimeException(sprintf('The service "%s" must extend EntityRepository (e.g. by extending ServiceEntityRepository), "%s" given.', $repositoryServiceId, get_debug_type($repository)));
}
if (! $repository instanceof ObjectRepository) {
throw new RuntimeException(sprintf('The service "%s" must implement ObjectRepository (or extend a base class, like ServiceEntityRepository), "%s" given.', $repositoryServiceId, get_debug_type($repository)));
}
if (! $repository instanceof EntityRepository) {
Deprecation::trigger(
'doctrine/doctrine-bundle',
'https://github.com/doctrine/DoctrineBundle/pull/1722',
'The service "%s" of type "%s" should extend "%s", not doing so is deprecated.',
$repositoryServiceId,
get_debug_type($repository),
EntityRepository::class,
);
}
/** @phpstan-var ObjectRepository<T> */
return $repository;
}
// if not in the container but the class/id implements the interface, throw an error
if (is_a($customRepositoryName, ServiceEntityRepositoryInterface::class, true)) {
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));
}
if (! class_exists($customRepositoryName)) {
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));
}
// allow the repository to be created below
}
return $this->getOrCreateRepository($entityManager, $metadata);
}
/**
* @param ClassMetadata<TEntity> $metadata
*
* @return ObjectRepository<TEntity>
*
* @template TEntity of object
*/
private function getOrCreateRepository(
EntityManagerInterface $entityManager,
ClassMetadata $metadata,
): ObjectRepository {
$repositoryHash = $metadata->getName() . spl_object_hash($entityManager);
if (isset($this->managedRepositories[$repositoryHash])) {
/** @phpstan-var ObjectRepository<TEntity> */
return $this->managedRepositories[$repositoryHash];
}
$repositoryClassName = $metadata->customRepositoryClassName ?: $entityManager->getConfiguration()->getDefaultRepositoryClassName();
/** @phpstan-var ObjectRepository<TEntity> */
return $this->managedRepositories[$repositoryHash] = new $repositoryClassName($entityManager, $metadata);
}
}

View File

@@ -0,0 +1,77 @@
<?php
declare(strict_types=1);
namespace Doctrine\Bundle\DoctrineBundle\Repository;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\EntityRepository;
use Doctrine\Persistence\ManagerRegistry;
use LogicException;
use Symfony\Component\VarExporter\LazyObjectInterface;
use function debug_backtrace;
use function sprintf;
use const DEBUG_BACKTRACE_IGNORE_ARGS;
/**
* @internal Extend {@see ServiceEntityRepository} instead.
*
* @template T of object
* @template-extends EntityRepository<T>
*/
class LazyServiceEntityRepository extends EntityRepository implements ServiceEntityRepositoryInterface
{
/**
* @param string $entityClass The class name of the entity this repository manages
* @phpstan-param class-string<T> $entityClass
*/
public function __construct(
private readonly ManagerRegistry $registry,
private readonly string $entityClass,
) {
if ($this instanceof LazyObjectInterface) {
$this->initialize();
return;
}
unset($this->_em);
unset($this->_class);
unset($this->_entityName);
}
/** @return mixed */
public function __get(string $name)
{
$this->initialize();
$scope = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2)[1]['class'] ?? null;
return (fn (): mixed => $this->$name)->bindTo($this, $scope)();
}
public function __isset(string $name): bool
{
$this->initialize();
$scope = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2)[1]['class'] ?? null;
return (fn (): bool => isset($this->$name))->bindTo($this, $scope)();
}
private function initialize(): void
{
$manager = $this->registry->getManagerForClass($this->entityClass);
if (! $manager instanceof EntityManagerInterface) {
throw new LogicException(sprintf(
'Could not find the entity manager for class "%s". Check your Doctrine configuration to make sure it is configured to load this entitys metadata.',
$this->entityClass,
));
}
parent::__construct($manager, $manager->getClassMetadata($this->entityClass));
}
}

View File

@@ -0,0 +1,43 @@
<?php
declare(strict_types=1);
namespace Doctrine\Bundle\DoctrineBundle\Repository;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\EntityRepository;
use Doctrine\ORM\Repository\RepositoryFactory;
use Doctrine\Persistence\ObjectRepository;
use ReflectionMethod;
if ((new ReflectionMethod(RepositoryFactory::class, 'getRepository'))->hasReturnType()) {
// ORM >= 3
/** @internal */
trait RepositoryFactoryCompatibility
{
/**
* Gets the repository for an entity class.
*
* @param class-string<T> $entityName
*
* @return EntityRepository<T>
*
* @template T of object
*/
public function getRepository(EntityManagerInterface $entityManager, string $entityName): EntityRepository
{
return $this->doGetRepository($entityManager, $entityName, true);
}
}
} else {
// ORM 2
/** @internal */
trait RepositoryFactoryCompatibility
{
/** {@inheritDoc} */
public function getRepository(EntityManagerInterface $entityManager, $entityName): ObjectRepository
{
return $this->doGetRepository($entityManager, $entityName, false);
}
}
}

View File

@@ -0,0 +1,55 @@
<?php
declare(strict_types=1);
namespace Doctrine\Bundle\DoctrineBundle\Repository;
use Doctrine\ORM\EntityRepository;
use function property_exists;
if (property_exists(EntityRepository::class, '_entityName')) {
// ORM 2
/**
* Optional EntityRepository base class with a simplified constructor (for autowiring).
*
* To use in your class, inject the "registry" service and call
* the parent constructor. For example:
*
* class YourEntityRepository extends ServiceEntityRepository
* {
* public function __construct(ManagerRegistry $registry)
* {
* parent::__construct($registry, YourEntity::class);
* }
* }
*
* @template T of object
* @template-extends LazyServiceEntityRepository<T>
*/
class ServiceEntityRepository extends LazyServiceEntityRepository
{
}
} else {
// ORM 3
/**
* Optional EntityRepository base class with a simplified constructor (for autowiring).
*
* To use in your class, inject the "registry" service and call
* the parent constructor. For example:
*
* class YourEntityRepository extends ServiceEntityRepository
* {
* public function __construct(ManagerRegistry $registry)
* {
* parent::__construct($registry, YourEntity::class);
* }
* }
*
* @template T of object
* @template-extends ServiceEntityRepositoryProxy<T>
*/
class ServiceEntityRepository extends ServiceEntityRepositoryProxy
{
}
}

View File

@@ -0,0 +1,12 @@
<?php
declare(strict_types=1);
namespace Doctrine\Bundle\DoctrineBundle\Repository;
/**
* This interface signals that your repository should be loaded from the container.
*/
interface ServiceEntityRepositoryInterface
{
}

View File

@@ -0,0 +1,138 @@
<?php
declare(strict_types=1);
namespace Doctrine\Bundle\DoctrineBundle\Repository;
use Doctrine\Common\Collections\AbstractLazyCollection;
use Doctrine\Common\Collections\Criteria;
use Doctrine\Common\Collections\Selectable;
use Doctrine\DBAL\LockMode;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\EntityRepository;
use Doctrine\ORM\Mapping\ClassMetadata;
use Doctrine\ORM\Query\ResultSetMappingBuilder;
use Doctrine\ORM\QueryBuilder;
use Doctrine\Persistence\ManagerRegistry;
use LogicException;
use Symfony\Component\VarExporter\LazyObjectInterface;
use function sprintf;
/**
* @internal Extend {@see ServiceEntityRepository} instead.
*
* @template T of object
* @template-extends EntityRepository<T>
*/
class ServiceEntityRepositoryProxy extends EntityRepository implements ServiceEntityRepositoryInterface
{
/** @var EntityRepository<T> */
private EntityRepository|null $repository = null;
/** @param class-string<T> $entityClass The class name of the entity this repository manages */
public function __construct(
private readonly ManagerRegistry $registry,
private readonly string $entityClass,
) {
if (! $this instanceof LazyObjectInterface) {
return;
}
$this->repository = $this->resolveRepository();
}
public function createQueryBuilder(string $alias, string|null $indexBy = null): QueryBuilder
{
return ($this->repository ??= $this->resolveRepository())
->createQueryBuilder($alias, $indexBy);
}
public function createResultSetMappingBuilder(string $alias): ResultSetMappingBuilder
{
return ($this->repository ??= $this->resolveRepository())
->createResultSetMappingBuilder($alias);
}
public function find(mixed $id, LockMode|int|null $lockMode = null, int|null $lockVersion = null): object|null
{
/** @psalm-suppress InvalidReturnStatement This proxy is used only in combination with newer parent class */
return ($this->repository ??= $this->resolveRepository())
->find($id, $lockMode, $lockVersion);
}
/**
* {@inheritDoc}
*
* @psalm-suppress InvalidReturnStatement This proxy is used only in combination with newer parent class
* @psalm-suppress InvalidReturnType This proxy is used only in combination with newer parent class
*/
public function findBy(array $criteria, array|null $orderBy = null, int|null $limit = null, int|null $offset = null): array
{
return ($this->repository ??= $this->resolveRepository())
->findBy($criteria, $orderBy, $limit, $offset);
}
/** {@inheritDoc} */
public function findOneBy(array $criteria, array|null $orderBy = null): object|null
{
/** @psalm-suppress InvalidReturnStatement This proxy is used only in combination with newer parent class */
return ($this->repository ??= $this->resolveRepository())
->findOneBy($criteria, $orderBy);
}
/** {@inheritDoc} */
public function count(array $criteria = []): int
{
return ($this->repository ??= $this->resolveRepository())->count($criteria);
}
/**
* {@inheritDoc}
*/
public function __call(string $method, array $arguments): mixed
{
return ($this->repository ??= $this->resolveRepository())->$method(...$arguments);
}
protected function getEntityName(): string
{
return ($this->repository ??= $this->resolveRepository())->getEntityName();
}
protected function getEntityManager(): EntityManagerInterface
{
return ($this->repository ??= $this->resolveRepository())->getEntityManager();
}
/** @psalm-suppress InvalidReturnType This proxy is used only in combination with newer parent class */
protected function getClassMetadata(): ClassMetadata
{
/** @psalm-suppress InvalidReturnStatement This proxy is used only in combination with newer parent class */
return ($this->repository ??= $this->resolveRepository())->getClassMetadata();
}
/** @phpstan-return AbstractLazyCollection<int, T>&Selectable<int, T> */
public function matching(Criteria $criteria): AbstractLazyCollection&Selectable
{
return ($this->repository ??= $this->resolveRepository())->matching($criteria);
}
/** @return EntityRepository<T> */
private function resolveRepository(): EntityRepository
{
$manager = $this->registry->getManagerForClass($this->entityClass);
if (! $manager instanceof EntityManagerInterface) {
throw new LogicException(sprintf(
'Could not find the entity manager for class "%s". Check your Doctrine configuration to make sure it is configured to load this entitys metadata.',
$this->entityClass,
));
}
/** @var ClassMetadata<T> $classMetadata */
$classMetadata = $manager->getClassMetadata($this->entityClass);
return new EntityRepository($manager, $classMetadata);
}
}

View File

@@ -0,0 +1,211 @@
<?php
declare(strict_types=1);
namespace Doctrine\Bundle\DoctrineBundle\Twig;
use Doctrine\Deprecations\Deprecation;
use Doctrine\SqlFormatter\HtmlHighlighter;
use Doctrine\SqlFormatter\NullHighlighter;
use Doctrine\SqlFormatter\SqlFormatter;
use Stringable;
use Symfony\Component\VarDumper\Cloner\Data;
use Twig\DeprecatedCallableInfo;
use Twig\Extension\AbstractExtension;
use Twig\TwigFilter;
use function addslashes;
use function array_filter;
use function array_key_exists;
use function array_keys;
use function array_merge;
use function array_values;
use function bin2hex;
use function class_exists;
use function count;
use function implode;
use function is_array;
use function is_bool;
use function is_string;
use function preg_match;
use function preg_replace_callback;
use function sprintf;
use function strtoupper;
use function substr;
/**
* This class contains the needed functions in order to do the query highlighting
*
* @internal since 2.11
*/
class DoctrineExtension extends AbstractExtension
{
private SqlFormatter $sqlFormatter;
/**
* Define our functions
*
* @return TwigFilter[]
*/
public function getFilters()
{
$out = [
new TwigFilter('doctrine_prettify_sql', [$this, 'prettifySql'], ['is_safe' => ['html']]),
new TwigFilter('doctrine_format_sql', [$this, 'formatSql'], ['is_safe' => ['html']]),
new TwigFilter('doctrine_replace_query_parameters', [$this, 'replaceQueryParameters']),
];
$options = ['deprecated' => true];
// exists since twig/twig 3.15
if (class_exists(DeprecatedCallableInfo::class)) {
$options = ['deprecation_info' => new DeprecatedCallableInfo('doctrine/doctrine-bundle', '2.1')];
}
return array_merge($out, [
new TwigFilter('doctrine_pretty_query', [$this, 'formatQuery'], ['is_safe' => ['html']] + $options),
]);
}
/**
* Escape parameters of a SQL query
* DON'T USE THIS FUNCTION OUTSIDE ITS INTENDED SCOPE
*
* @internal
*
* @return string
*/
public static function escapeFunction(mixed $parameter)
{
$result = $parameter;
switch (true) {
// Check if result is non-unicode string using PCRE_UTF8 modifier
case is_string($result) && ! preg_match('//u', $result):
$result = '0x' . strtoupper(bin2hex($result));
break;
case is_string($result):
$result = "'" . addslashes($result) . "'";
break;
case is_array($result):
foreach ($result as &$value) {
$value = static::escapeFunction($value);
}
$result = implode(', ', $result) ?: 'NULL';
break;
case $result instanceof Stringable:
$result = addslashes((string) $result);
break;
case $result === null:
$result = 'NULL';
break;
case is_bool($result):
$result = $result ? '1' : '0';
break;
}
return $result;
}
/**
* Return a query with the parameters replaced
*
* @param string $query
* @param array<array-key, mixed>|Data $parameters
*
* @return string
*/
public function replaceQueryParameters($query, $parameters)
{
if ($parameters instanceof Data) {
$parameters = $parameters->getValue(true);
}
$keys = array_keys($parameters);
if (count(array_filter($keys, 'is_int')) === count($keys)) {
$parameters = array_values($parameters);
}
$i = 0;
return preg_replace_callback(
'/(?<!\?)\?(?!\?)|(?<!:)(:[a-z0-9_]+)/i',
static function ($matches) use ($parameters, &$i) {
$key = substr($matches[0], 1);
if (! array_key_exists($i, $parameters) && ! array_key_exists($key, $parameters)) {
return $matches[0];
}
$value = array_key_exists($i, $parameters) ? $parameters[$i] : $parameters[$key];
$i++;
return DoctrineExtension::escapeFunction($value);
},
$query,
);
}
/**
* Formats and/or highlights the given SQL statement.
*
* @param string $sql
* @param bool $highlightOnly If true the query is not formatted, just highlighted
*
* @return string
*/
public function formatQuery($sql, $highlightOnly = false)
{
Deprecation::trigger(
'doctrine/doctrine-bundle',
'https://github.com/doctrine/DoctrineBundle/pull/1056',
'The "%s()" method is deprecated and will be removed in doctrine-bundle 3.0.',
__METHOD__,
);
$this->setUpSqlFormatter(true, true);
if ($highlightOnly) {
return $this->sqlFormatter->highlight($sql);
}
return sprintf(
'<div class="highlight highlight-sql"><pre>%s</pre></div>',
$this->sqlFormatter->format($sql),
);
}
public function prettifySql(string $sql): string
{
$this->setUpSqlFormatter();
return $this->sqlFormatter->highlight($sql);
}
public function formatSql(string $sql, bool $highlight): string
{
$this->setUpSqlFormatter($highlight);
return $this->sqlFormatter->format($sql);
}
private function setUpSqlFormatter(bool $highlight = true, bool $legacy = false): void
{
$this->sqlFormatter = new SqlFormatter($highlight ? new HtmlHighlighter([
HtmlHighlighter::HIGHLIGHT_PRE => 'class="highlight highlight-sql"',
HtmlHighlighter::HIGHLIGHT_QUOTE => 'class="string"',
HtmlHighlighter::HIGHLIGHT_BACKTICK_QUOTE => 'class="string"',
HtmlHighlighter::HIGHLIGHT_RESERVED => 'class="keyword"',
HtmlHighlighter::HIGHLIGHT_BOUNDARY => 'class="symbol"',
HtmlHighlighter::HIGHLIGHT_NUMBER => 'class="number"',
HtmlHighlighter::HIGHLIGHT_WORD => 'class="word"',
HtmlHighlighter::HIGHLIGHT_ERROR => 'class="error"',
HtmlHighlighter::HIGHLIGHT_COMMENT => 'class="comment"',
HtmlHighlighter::HIGHLIGHT_VARIABLE => 'class="variable"',
], ! $legacy) : new NullHighlighter());
}
}

View File

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" data-icon-name="icon-tabler-database" width="24" height="24" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<ellipse cx="12" cy="6" rx="8" ry="3"></ellipse>
<path d="M4 6v6a8 3 0 0 0 16 0v-6"></path>
<path d="M4 12v6a8 3 0 0 0 16 0v-6"></path>
</svg>

After

Width:  |  Height:  |  Size: 439 B

View File

@@ -0,0 +1,558 @@
{% extends request.isXmlHttpRequest ? '@WebProfiler/Profiler/ajax_layout.html.twig' : '@WebProfiler/Profiler/layout.html.twig' %}
{% import _self as helper %}
{% block toolbar %}
{% if collector.querycount > 0 or collector.invalidEntityCount > 0 %}
{% set icon %}
{% set status = collector.invalidEntityCount > 0 ? 'red' : collector.querycount > 50 ? 'yellow' %}
{% if profiler_markup_version >= 3 %}
{{ include('@Doctrine/Collector/database.svg') }}
{% else %}
<span class="icon">{{ include('@Doctrine/Collector/icon.svg') }}</span>
{% endif %}
{% if collector.querycount == 0 and collector.invalidEntityCount > 0 %}
<span class="sf-toolbar-value">{{ collector.invalidEntityCount }}</span>
<span class="sf-toolbar-label">errors</span>
{% else %}
<span class="sf-toolbar-value">{{ collector.querycount }}</span>
<span class="sf-toolbar-info-piece-additional-detail">
<span class="sf-toolbar-label">in</span>
<span class="sf-toolbar-value">{{ '%0.2f'|format(collector.time * 1000) }}</span>
<span class="sf-toolbar-label">ms</span>
</span>
{% endif %}
{% endset %}
{% set text %}
<div class="sf-toolbar-info-piece">
<b>Database Queries</b>
<span class="sf-toolbar-status {{ collector.querycount > 50 ? 'sf-toolbar-status-yellow' : '' }}">{{ collector.querycount }}</span>
</div>
<div class="sf-toolbar-info-piece">
<b>Different statements</b>
<span class="sf-toolbar-status">{{ collector.groupedQueryCount }}</span>
</div>
<div class="sf-toolbar-info-piece">
<b>Query time</b>
<span>{{ '%0.2f'|format(collector.time * 1000) }} ms</span>
</div>
<div class="sf-toolbar-info-piece">
<b>Invalid entities</b>
<span class="sf-toolbar-status {{ collector.invalidEntityCount > 0 ? 'sf-toolbar-status-red' : '' }}">{{ collector.invalidEntityCount }}</span>
</div>
<div class="sf-toolbar-info-piece">
<b>Managed entities</b>
<span class="sf-toolbar-status">{{ collector.managedEntityCount }}</span>
</div>
{% if collector.cacheEnabled %}
<div class="sf-toolbar-info-piece">
<b>Cache hits</b>
<span class="sf-toolbar-status sf-toolbar-status-green">{{ collector.cacheHitsCount }}</span>
</div>
<div class="sf-toolbar-info-piece">
<b>Cache misses</b>
<span class="sf-toolbar-status {{ collector.cacheMissesCount > 0 ? 'sf-toolbar-status-yellow' : '' }}">{{ collector.cacheMissesCount }}</span>
</div>
<div class="sf-toolbar-info-piece">
<b>Cache puts</b>
<span class="sf-toolbar-status {{ collector.cachePutsCount > 0 ? 'sf-toolbar-status-yellow' : '' }}">{{ collector.cachePutsCount }}</span>
</div>
{% else %}
<div class="sf-toolbar-info-piece">
<b>Second Level Cache</b>
<span class="sf-toolbar-status">disabled</span>
</div>
{% endif %}
{% endset %}
{{ include('@WebProfiler/Profiler/toolbar_item.html.twig', { link: profiler_url, status: status|default('') }) }}
{% endif %}
{% endblock %}
{% block menu %}
<span class="label {{ collector.invalidEntityCount > 0 ? 'label-status-error' }} {{ collector.querycount == 0 ? 'disabled' }}">
<span class="icon">{{ include('@Doctrine/Collector/' ~ (profiler_markup_version < 3 ? 'icon' : 'database') ~ '.svg') }}</span>
<strong>Doctrine</strong>
{% if collector.invalidEntityCount %}
<span class="count">
<span>{{ collector.invalidEntityCount }}</span>
</span>
{% endif %}
</span>
{% endblock %}
{% block panel %}
{% if 'explain' == page %}
{{ render(controller('Doctrine\\Bundle\\DoctrineBundle\\Controller\\ProfilerController::explainAction', {
token: token,
panel: 'db',
connectionName: request.query.get('connection'),
query: request.query.get('query')
})) }}
{% else %}
{{ block('queries') }}
{% endif %}
{% endblock %}
{% block queries %}
<style>
.time-container { position: relative; }
.time-container .nowrap { position: relative; z-index: 1; text-shadow: 0 0 2px #fff; }
.time-bar { display: block; position: absolute; top: 0; left: 0; bottom: 0; background: #e0e0e0; }
.sql-runnable.sf-toggle-content.sf-toggle-visible { display: flex; flex-direction: column; }
.sql-runnable button { align-self: end; }
{% if profiler_markup_version >= 3 %}
.highlight .keyword { color: var(--highlight-keyword); font-weight: bold; }
.highlight .word { color: var(--color-text); }
.highlight .variable { color: var(--highlight-variable); }
.highlight .symbol { color: var(--color-text); }
.highlight .comment { color: var(--highlight-comment); }
.highlight .string { color: var(--highlight-string); }
.highlight .number { color: var(--highlight-constant); font-weight: bold; }
.highlight .error { color: var(--highlight-error); }
{% endif %}
</style>
<h2>Query Metrics</h2>
<div class="metrics">
<div class="metric-group">
<div class="metric">
<span class="value">{{ collector.querycount }}</span>
<span class="label">Database Queries</span>
</div>
<div class="metric">
<span class="value">{{ collector.groupedQueryCount }}</span>
<span class="label">Different statements</span>
</div>
<div class="metric">
<span class="value">{{ '%0.2f'|format(collector.time * 1000) }} ms</span>
<span class="label">Query time</span>
</div>
<div class="metric">
<span class="value">{{ collector.invalidEntityCount }}</span>
<span class="label">Invalid entities</span>
</div>
<div class="metric">
<span class="value">{{ collector.managedEntityCount }}</span>
<span class="label">Managed entities</span>
</div>
</div>
{% if collector.cacheEnabled %}
<div class="metric-group">
<div class="metric">
<span class="value">{{ collector.cacheHitsCount }}</span>
<span class="label">Cache hits</span>
</div>
<div class="metric">
<span class="value">{{ collector.cacheMissesCount }}</span>
<span class="label">Cache misses</span>
</div>
<div class="metric">
<span class="value">{{ collector.cachePutsCount }}</span>
<span class="label">Cache puts</span>
</div>
</div>
{% endif %}
</div>
<div class="sf-tabs" style="margin-top: 20px;">
<div class="tab {{ collector.queries is empty ? 'disabled' }}">
{% set group_queries = request.query.getBoolean('group') %}
<h3 class="tab-title">
{% if group_queries %}
Grouped Statements
{% else %}
Queries
{% endif %}
</h3>
<div class="tab-content">
{% if not collector.queries %}
<div class="empty">
<p>No executed queries.</p>
</div>
{% else %}
{% if group_queries %}
<p><a href="{{ path('_profiler', { panel: 'db', token: token }) }}">Show all queries</a></p>
{% else %}
<p><a href="{{ path('_profiler', { panel: 'db', token: token, group: true }) }}">Group similar statements</a></p>
{% endif %}
{% for connection, queries in collector.queries %}
{% if collector.connections|length > 1 %}
<h3>{{ connection }} <small>connection</small></h3>
{% endif %}
{% if queries is empty %}
<div class="empty">
<p>No database queries were performed.</p>
</div>
{% else %}
{% if group_queries %}
{% set queries = collector.groupedQueries[connection] %}
{% endif %}
<table class="alt queries-table">
<thead>
<tr>
{% if group_queries %}
<th class="nowrap" onclick="javascript:sortTable(this, 0, 'queries-{{ loop.index }}')" data-sort-direction="1" style="cursor: pointer;">Time<span class="text-muted">&#9660;</span></th>
<th class="nowrap" onclick="javascript:sortTable(this, 1, 'queries-{{ loop.index }}')" style="cursor: pointer;">Count<span></span></th>
{% else %}
<th class="nowrap" onclick="javascript:sortTable(this, 0, 'queries-{{ loop.index }}')" data-sort-direction="-1" style="cursor: pointer;">#<span class="text-muted">&#9650;</span></th>
<th class="nowrap" onclick="javascript:sortTable(this, 1, 'queries-{{ loop.index }}')" style="cursor: pointer;">Time<span></span></th>
{% endif %}
<th style="width: 100%;">Info</th>
</tr>
</thead>
<tbody id="queries-{{ loop.index }}">
{% for i, query in queries %}
{% set i = group_queries ? query.index : i %}
<tr id="queryNo-{{ i }}-{{ loop.parent.loop.index }}">
{% if group_queries %}
<td class="time-container">
<span class="time-bar" style="width:{{ '%0.2f'|format(query.executionPercent) }}%"></span>
<span class="nowrap">{{ '%0.2f'|format(query.executionMS * 1000) }}&nbsp;ms<br />({{ '%0.2f'|format(query.executionPercent) }}%)</span>
</td>
<td class="nowrap">{{ query.count }}</td>
{% else %}
<td class="nowrap">{{ loop.index }}</td>
<td class="nowrap">{{ '%0.2f'|format(query.executionMS * 1000) }}&nbsp;ms</td>
{% endif %}
<td>
{{ query.sql|doctrine_prettify_sql }}
<div>
<strong class="font-normal text-small">Parameters</strong>: {{ profiler_dump(query.params, 2) }}
</div>
<div class="text-small font-normal">
<a href="#" class="sf-toggle link-inverse" data-toggle-selector="#formatted-query-{{ i }}-{{ loop.parent.loop.index }}" data-toggle-alt-content="Hide formatted query">View formatted query</a>
{% if query.runnable %}
&nbsp;&nbsp;
<a href="#" class="sf-toggle link-inverse" data-toggle-selector="#original-query-{{ i }}-{{ loop.parent.loop.index }}" data-toggle-alt-content="Hide runnable query">View runnable query</a>
{% endif %}
{% if query.explainable %}
&nbsp;&nbsp;
<a class="link-inverse" href="{{ path('_profiler', { panel: 'db', token: token, page: 'explain', connection: connection, query: i }) }}" onclick="return explain(this);" data-target-id="explain-{{ i }}-{{ loop.parent.loop.index }}">Explain query</a>
{% endif %}
{% if query.backtrace is defined %}
&nbsp;&nbsp;
<a href="#" class="sf-toggle link-inverse" data-toggle-selector="#backtrace-{{ i }}-{{ loop.parent.loop.index }}" data-toggle-alt-content="Hide query backtrace">View query backtrace</a>
{% endif %}
</div>
<div id="formatted-query-{{ i }}-{{ loop.parent.loop.index }}" class="sql-runnable hidden">
{{ query.sql|doctrine_format_sql(highlight = true) }}
<button class="btn btn-sm hidden" data-clipboard-text="{{ query.sql|doctrine_format_sql(highlight = false)|e('html_attr') }}">Copy</button>
</div>
{% if query.runnable %}
<div id="original-query-{{ i }}-{{ loop.parent.loop.index }}" class="sql-runnable hidden">
{% set runnable_sql = (query.sql ~ ';')|doctrine_replace_query_parameters(query.params) %}
{{ runnable_sql|doctrine_prettify_sql }}
<button class="btn btn-sm hidden" data-clipboard-text="{{ runnable_sql|e('html_attr') }}">Copy</button>
</div>
{% endif %}
{% if query.explainable %}
<div id="explain-{{ i }}-{{ loop.parent.loop.index }}" class="sql-explain"></div>
{% endif %}
{% if query.backtrace is defined %}
<div id="backtrace-{{ i }}-{{ loop.parent.loop.index }}" class="hidden">
<table>
<thead>
<tr>
<th scope="col">#</th>
<th scope="col">File/Call</th>
</tr>
</thead>
<tbody>
{% for trace in query.backtrace %}
<tr>
<td>{{ loop.index }}</td>
<td>
<span class="text-small">
{% set line_number = trace.line|default(query.backtrace[loop.index].line|default(1)) %}
{% if trace.file is defined %}
<a href="{{ trace.file|file_link(line_number) }}">
{% endif %}
{{- query.backtrace[loop.index].class|default ~ (query.backtrace[loop.index].class is defined ? trace.type|default('::')) -}}
<span class="status-warning">{{ query.backtrace[loop.index].function|default(trace.function) }}</span>
{% if trace.file is defined %}
</a>
{% endif %}
(line {{ line_number }})
</span>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endif %}
{% endfor %}
{% endif %}
</div>
</div>
<div class="tab {{ collector.connections is empty ? 'disabled' }}">
<h3 class="tab-title">Database Connections</h3>
<div class="tab-content">
{% if not collector.connections %}
<div class="empty">
<p>There are no configured database connections.</p>
</div>
{% else %}
{{ helper.render_simple_table('Name', 'Service', collector.connections) }}
{% endif %}
</div>
</div>
<div class="tab {{ collector.managers is empty ? 'disabled' }}">
<h3 class="tab-title">Entity Managers</h3>
<div class="tab-content">
{% if not collector.managers %}
<div class="empty">
<p>There are no configured entity managers.</p>
</div>
{% else %}
{{ helper.render_simple_table('Name', 'Service', collector.managers) }}
{% endif %}
</div>
</div>
<div class="tab {{ not collector.cacheEnabled ? 'disabled' }}">
<h3 class="tab-title">Second Level Cache</h3>
<div class="tab-content">
{% if not collector.cacheEnabled %}
<div class="empty">
<p>Second Level Cache is not enabled.</p>
</div>
{% else %}
{% if not collector.cacheCounts %}
<div class="empty">
<p>Second level cache information is not available.</p>
</div>
{% else %}
<div class="metrics">
<div class="metric">
<span class="value">{{ collector.cacheCounts.hits }}</span>
<span class="label">Hits</span>
</div>
<div class="metric">
<span class="value">{{ collector.cacheCounts.misses }}</span>
<span class="label">Misses</span>
</div>
<div class="metric">
<span class="value">{{ collector.cacheCounts.puts }}</span>
<span class="label">Puts</span>
</div>
</div>
{% if collector.cacheRegions.hits %}
<h3>Number of cache hits</h3>
{{ helper.render_simple_table('Region', 'Hits', collector.cacheRegions.hits) }}
{% endif %}
{% if collector.cacheRegions.misses %}
<h3>Number of cache misses</h3>
{{ helper.render_simple_table('Region', 'Misses', collector.cacheRegions.misses) }}
{% endif %}
{% if collector.cacheRegions.puts %}
<h3>Number of cache puts</h3>
{{ helper.render_simple_table('Region', 'Puts', collector.cacheRegions.puts) }}
{% endif %}
{% endif %}
{% endif %}
</div>
</div>
<div class="tab">
<h3 class="tab-title">Managed Entities</h3>
<div class="tab-content">
{% if not collector.managedEntityCountByClass %}
<div class="empty">
<p>No managed entities.</p>
</div>
{% else %}
{% for manager, entityCounts in collector.managedEntityCountByClass %}
<h4>{{ manager }} <small>entity manager</small></h4>
{{ helper.render_simple_table('Class', 'Amount of managed objects', entityCounts) }}
{% endfor %}
{% endif %}
</div>
</div>
<div class="tab {{ not collector.entities ? 'disabled' }}">
<h3 class="tab-title">Entities Mapping</h3>
<div class="tab-content">
{% if not collector.entities %}
<div class="empty">
<p>No mapped entities.</p>
</div>
{% else %}
{% for manager, classes in collector.entities %}
{% if collector.managers|length > 1 %}
<h4>{{ manager }} <small>entity manager</small></h4>
{% endif %}
{% if classes is empty %}
<div class="empty">
<p>No loaded entities.</p>
</div>
{% else %}
<table>
<thead>
<tr>
<th scope="col">Class</th>
<th scope="col">Mapping errors</th>
</tr>
</thead>
<tbody>
{% for class in classes %}
{% set contains_errors = collector.mappingErrors[manager] is defined and collector.mappingErrors[manager][class.class] is defined %}
<tr class="{{ contains_errors ? 'status-error' }}">
<td>
<a href="{{ class.file|file_link(class.line) }}">{{ class. class}}</a>
</td>
<td class="font-normal">
{% if contains_errors %}
<ul>
{% for error in collector.mappingErrors[manager][class.class] %}
<li>{{ error }}</li>
{% endfor %}
</ul>
{% else %}
No errors.
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endif %}
{% endfor %}
{% endif %}
</div>
</div>
</div>
<script type="text/javascript">//<![CDATA[
function explain(link) {
"use strict";
var targetId = link.getAttribute('data-target-id');
var targetElement = document.getElementById(targetId);
if (targetElement.style.display != 'block') {
if (targetElement.getAttribute('data-sfurl') !== link.href) {
fetch(link.href, {
headers: {'X-Requested-With': 'XMLHttpRequest'}
}).then(async function (response) {
targetElement.innerHTML = await response.text()
targetElement.setAttribute('data-sfurl', link.href)
}, function () {
targetElement.innerHTML = 'An error occurred while loading the query explanation.';
})
}
targetElement.style.display = 'block';
link.innerHTML = 'Hide query explanation';
} else {
targetElement.style.display = 'none';
link.innerHTML = 'Explain query';
}
return false;
}
function sortTable(header, column, targetId) {
"use strict";
var direction = parseInt(header.getAttribute('data-sort-direction')) || 1,
items = [],
target = document.getElementById(targetId),
rows = target.children,
headers = header.parentElement.children,
i;
for (i = 0; i < rows.length; ++i) {
items.push(rows[i]);
}
for (i = 0; i < headers.length; ++i) {
headers[i].removeAttribute('data-sort-direction');
if (headers[i].children.length > 0) {
headers[i].children[0].innerHTML = '';
}
}
header.setAttribute('data-sort-direction', (-1*direction).toString());
header.children[0].innerHTML = direction > 0 ? '<span class="text-muted">&#9650;</span>' : '<span class="text-muted">&#9660;</span>';
items.sort(function(a, b) {
return direction * (parseFloat(a.children[column].innerHTML) - parseFloat(b.children[column].innerHTML));
});
for (i = 0; i < items.length; ++i) {
target.appendChild(items[i]);
}
}
if (navigator.clipboard) {
document.querySelectorAll('[data-clipboard-text]').forEach(function(button) {
button.classList.remove('hidden');
button.addEventListener('click', function() {
navigator.clipboard.writeText(button.getAttribute('data-clipboard-text'));
})
});
}
//]]></script>
{% endblock %}
{% macro render_simple_table(label1, label2, data) %}
<table>
<thead>
<tr>
<th scope="col" class="key">{{ label1 }}</th>
<th scope="col">{{ label2 }}</th>
</tr>
</thead>
<tbody>
{% for key, value in data %}
<tr>
<th scope="row">{{ key }}</th>
<td>{{ value }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endmacro %}

View File

@@ -0,0 +1,28 @@
{% if data[0]|length > 1 %}
{# The platform returns a table for the explanation (e.g. MySQL), display all columns #}
<table style="margin: 5px 0;">
<thead>
<tr>
{% for label in data[0]|keys %}
<th>{{ label }}</th>
{% endfor %}
</tr>
</thead>
<tbody>
{% for row in data %}
<tr>
{% for key, item in row %}
<td>{{ item|replace({',': ', '}) }}</td>
{% endfor %}
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
{# The Platform returns a single column for a textual explanation (e.g. PostgreSQL), display all lines #}
<pre style="margin: 5px 0;">
{%- for row in data -%}
{{ row|first }}{{ "\n" }}
{%- endfor -%}
</pre>
{% endif %}

View File

@@ -0,0 +1,4 @@
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" x="0px" y="0px" width="24" height="24" viewBox="0 0 24 24" enable-background="new 0 0 24 24" xml:space="preserve">
<path fill="#AAAAAA" d="M5,8h14c1.7,0,3-1.3,3-3s-1.3-3-3-3H5C3.3,2,2,3.3,2,5S3.3,8,5,8z M18,3.6c0.8,0,1.5,0.7,1.5,1.5S18.8,6.6,18,6.6s-1.5-0.7-1.5-1.5S17.2,3.6,18,3.6z M19,9H5c-1.7,0-3,1.3-3,3s1.3,3,3,3h14c1.7,0,3-1.3,3-3S20.7,9,19,9z M18,13.6
c-0.8,0-1.5-0.7-1.5-1.5s0.7-1.5,1.5-1.5s1.5,0.7,1.5,1.5S18.8,13.6,18,13.6z M19,16H5c-1.7,0-3,1.3-3,3s1.3,3,3,3h14c1.7,0,3-1.3,3-3S20.7,16,19,16z M18,20.6c-0.8,0-1.5-0.7-1.5-1.5s0.7-1.5,1.5-1.5s1.5,0.7,1.5,1.5S18.8,20.6,18,20.6z"/>
</svg>

After

Width:  |  Height:  |  Size: 659 B