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,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());
}
}