welcome back to dyb-tech

This commit is contained in:
Daniel Guzman
2024-05-18 02:28:01 +02:00
parent 9513cdba09
commit 9f30bc98c7
6149 changed files with 668407 additions and 0 deletions
@@ -0,0 +1,60 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Tools;
use Doctrine\ORM\Event\LoadClassMetadataEventArgs;
use Doctrine\ORM\Mapping\Builder\EntityListenerBuilder;
use function ltrim;
/**
* Mechanism to programmatically attach entity listeners.
*/
class AttachEntityListenersListener
{
/** @var mixed[][] */
private $entityListeners = [];
/**
* Adds an entity listener for a specific entity.
*
* @param string $entityClass The entity to attach the listener.
* @param string $listenerClass The listener class.
* @param string|null $eventName The entity lifecycle event.
* @param string|null $listenerCallback The listener callback method or NULL to use $eventName.
*
* @return void
*/
public function addEntityListener($entityClass, $listenerClass, $eventName, $listenerCallback = null)
{
$this->entityListeners[ltrim($entityClass, '\\')][] = [
'event' => $eventName,
'class' => $listenerClass,
'method' => $listenerCallback ?: $eventName,
];
}
/**
* Processes event and attach the entity listener.
*
* @return void
*/
public function loadClassMetadata(LoadClassMetadataEventArgs $event)
{
$metadata = $event->getClassMetadata();
if (! isset($this->entityListeners[$metadata->name])) {
return;
}
foreach ($this->entityListeners[$metadata->name] as $listener) {
if ($listener['event'] === null) {
EntityListenerBuilder::bindEntityListener($metadata, $listener['class']);
} else {
$metadata->addEntityListener($listener['event'], $listener['class'], $listener['method']);
}
}
}
}
@@ -0,0 +1,49 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Tools\Console\Command;
use Doctrine\Deprecations\Deprecation;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\Tools\Console\EntityManagerProvider;
use Doctrine\ORM\Tools\Console\Helper\EntityManagerHelper;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use function assert;
abstract class AbstractEntityManagerCommand extends Command
{
/** @var EntityManagerProvider|null */
private $entityManagerProvider;
public function __construct(?EntityManagerProvider $entityManagerProvider = null)
{
parent::__construct();
$this->entityManagerProvider = $entityManagerProvider;
}
final protected function getEntityManager(InputInterface $input): EntityManagerInterface
{
// This is a backwards compatibility required check for commands extending Doctrine ORM commands
if (! $input->hasOption('em') || $this->entityManagerProvider === null) {
Deprecation::trigger(
'doctrine/orm',
'https://github.com/doctrine/orm/issues/8327',
'Not passing EntityManagerProvider as a dependency to command class "%s" is deprecated',
static::class
);
$helper = $this->getHelper('em');
assert($helper instanceof EntityManagerHelper);
return $helper->getEntityManager();
}
return $input->getOption('em') === null
? $this->entityManagerProvider->getDefaultManager()
: $this->entityManagerProvider->getManager($input->getOption('em'));
}
}
@@ -0,0 +1,124 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Tools\Console\Command\ClearCache;
use Doctrine\ORM\Cache;
use Doctrine\ORM\Tools\Console\Command\AbstractEntityManagerCommand;
use Doctrine\ORM\Tools\Console\CommandCompatibility;
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 Symfony\Component\Console\Style\SymfonyStyle;
use function sprintf;
/**
* Command to clear a collection cache region.
*/
class CollectionRegionCommand extends AbstractEntityManagerCommand
{
use CommandCompatibility;
/** @return void */
protected function configure()
{
$this->setName('orm:clear-cache:region:collection')
->setDescription('Clear a second-level cache collection region')
->addArgument('owner-class', InputArgument::OPTIONAL, 'The owner entity name.')
->addArgument('association', InputArgument::OPTIONAL, 'The association collection name.')
->addArgument('owner-id', InputArgument::OPTIONAL, 'The owner identifier.')
->addOption('em', null, InputOption::VALUE_REQUIRED, 'Name of the entity manager to operate on')
->addOption('all', null, InputOption::VALUE_NONE, 'If defined, all entity regions will be deleted/invalidated.')
->addOption('flush', null, InputOption::VALUE_NONE, 'If defined, all cache entries will be flushed.')
->setHelp(<<<'EOT'
The <info>%command.name%</info> command is meant to clear a second-level cache collection regions for an associated Entity Manager.
It is possible to delete/invalidate all collection region, a specific collection region or flushes the cache provider.
The execution type differ on how you execute the command.
If you want to invalidate all entries for an collection region this command would do the work:
<info>%command.name% 'Entities\MyEntity' 'collectionName'</info>
To invalidate a specific entry you should use :
<info>%command.name% 'Entities\MyEntity' 'collectionName' 1</info>
If you want to invalidate all entries for the all collection regions:
<info>%command.name% --all</info>
Alternatively, if you want to flush the configured cache provider for an collection region use this command:
<info>%command.name% 'Entities\MyEntity' 'collectionName' --flush</info>
Finally, be aware that if <info>--flush</info> option is passed,
not all cache providers are able to flush entries, because of a limitation of its execution nature.
EOT
);
}
private function doExecute(InputInterface $input, OutputInterface $output): int
{
$ui = (new SymfonyStyle($input, $output))->getErrorStyle();
$em = $this->getEntityManager($input);
$ownerClass = $input->getArgument('owner-class');
$assoc = $input->getArgument('association');
$ownerId = $input->getArgument('owner-id');
$cache = $em->getCache();
if (! $cache instanceof Cache) {
throw new InvalidArgumentException('No second-level cache is configured on the given EntityManager.');
}
if (( ! $ownerClass || ! $assoc) && ! $input->getOption('all')) {
throw new InvalidArgumentException('Missing arguments "--owner-class" "--association"');
}
if ($input->getOption('flush')) {
$cache->getCollectionCacheRegion($ownerClass, $assoc)
->evictAll();
$ui->comment(
sprintf(
'Flushing cache provider configured for <info>"%s#%s"</info>',
$ownerClass,
$assoc
)
);
return 0;
}
if ($input->getOption('all')) {
$ui->comment('Clearing <info>all</info> second-level cache collection regions');
$cache->evictEntityRegions();
return 0;
}
if ($ownerId) {
$ui->comment(
sprintf(
'Clearing second-level cache entry for collection <info>"%s#%s"</info> owner entity identified by <info>"%s"</info>',
$ownerClass,
$assoc,
$ownerId
)
);
$cache->evictCollection($ownerClass, $assoc, $ownerId);
return 0;
}
$ui->comment(sprintf('Clearing second-level cache for collection <info>"%s#%s"</info>', $ownerClass, $assoc));
$cache->evictCollectionRegion($ownerClass, $assoc);
return 0;
}
}
@@ -0,0 +1,115 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Tools\Console\Command\ClearCache;
use Doctrine\ORM\Cache;
use Doctrine\ORM\Tools\Console\Command\AbstractEntityManagerCommand;
use Doctrine\ORM\Tools\Console\CommandCompatibility;
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 Symfony\Component\Console\Style\SymfonyStyle;
use function sprintf;
/**
* Command to clear a entity cache region.
*/
class EntityRegionCommand extends AbstractEntityManagerCommand
{
use CommandCompatibility;
/** @return void */
protected function configure()
{
$this->setName('orm:clear-cache:region:entity')
->setDescription('Clear a second-level cache entity region')
->addArgument('entity-class', InputArgument::OPTIONAL, 'The entity name.')
->addArgument('entity-id', InputArgument::OPTIONAL, 'The entity identifier.')
->addOption('em', null, InputOption::VALUE_REQUIRED, 'Name of the entity manager to operate on')
->addOption('all', null, InputOption::VALUE_NONE, 'If defined, all entity regions will be deleted/invalidated.')
->addOption('flush', null, InputOption::VALUE_NONE, 'If defined, all cache entries will be flushed.')
->setHelp(<<<'EOT'
The <info>%command.name%</info> command is meant to clear a second-level cache entity region for an associated Entity Manager.
It is possible to delete/invalidate all entity region, a specific entity region or flushes the cache provider.
The execution type differ on how you execute the command.
If you want to invalidate all entries for an entity region this command would do the work:
<info>%command.name% 'Entities\MyEntity'</info>
To invalidate a specific entry you should use :
<info>%command.name% 'Entities\MyEntity' 1</info>
If you want to invalidate all entries for the all entity regions:
<info>%command.name% --all</info>
Alternatively, if you want to flush the configured cache provider for an entity region use this command:
<info>%command.name% 'Entities\MyEntity' --flush</info>
Finally, be aware that if <info>--flush</info> option is passed,
not all cache providers are able to flush entries, because of a limitation of its execution nature.
EOT
);
}
private function doExecute(InputInterface $input, OutputInterface $output): int
{
$ui = (new SymfonyStyle($input, $output))->getErrorStyle();
$em = $this->getEntityManager($input);
$entityClass = $input->getArgument('entity-class');
$entityId = $input->getArgument('entity-id');
$cache = $em->getCache();
if (! $cache instanceof Cache) {
throw new InvalidArgumentException('No second-level cache is configured on the given EntityManager.');
}
if (! $entityClass && ! $input->getOption('all')) {
throw new InvalidArgumentException('Invalid argument "--entity-class"');
}
if ($input->getOption('flush')) {
$cache->getEntityCacheRegion($entityClass)
->evictAll();
$ui->comment(sprintf('Flushing cache provider configured for entity named <info>"%s"</info>', $entityClass));
return 0;
}
if ($input->getOption('all')) {
$ui->comment('Clearing <info>all</info> second-level cache entity regions');
$cache->evictEntityRegions();
return 0;
}
if ($entityId) {
$ui->comment(
sprintf(
'Clearing second-level cache entry for entity <info>"%s"</info> identified by <info>"%s"</info>',
$entityClass,
$entityId
)
);
$cache->evictEntity($entityClass, $entityId);
return 0;
}
$ui->comment(sprintf('Clearing second-level cache for entity <info>"%s"</info>', $entityClass));
$cache->evictEntityRegion($entityClass);
return 0;
}
}
@@ -0,0 +1,57 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Tools\Console\Command\ClearCache;
use Doctrine\ORM\Tools\Console\Command\AbstractEntityManagerCommand;
use Doctrine\ORM\Tools\Console\CommandCompatibility;
use InvalidArgumentException;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
/**
* Command to clear the metadata cache of the various cache drivers.
*
* @link www.doctrine-project.org
*/
class MetadataCommand extends AbstractEntityManagerCommand
{
use CommandCompatibility;
/** @return void */
protected function configure()
{
$this->setName('orm:clear-cache:metadata')
->setDescription('Clear all metadata cache of the various cache drivers')
->addOption('em', null, InputOption::VALUE_REQUIRED, 'Name of the entity manager to operate on')
->addOption('flush', null, InputOption::VALUE_NONE, 'If defined, cache entries will be flushed instead of deleted/invalidated.')
->setHelp(<<<'EOT'
The <info>%command.name%</info> command is meant to clear the metadata cache of associated Entity Manager.
EOT
);
}
private function doExecute(InputInterface $input, OutputInterface $output): int
{
$ui = (new SymfonyStyle($input, $output))->getErrorStyle();
$em = $this->getEntityManager($input);
$cacheDriver = $em->getConfiguration()->getMetadataCache();
if (! $cacheDriver) {
throw new InvalidArgumentException('No Metadata cache driver is configured on given EntityManager.');
}
$ui->comment('Clearing <info>all</info> Metadata cache entries');
$result = $cacheDriver->clear();
$message = $result ? 'Successfully deleted cache entries.' : 'No cache entries were deleted.';
$ui->success($message);
return 0;
}
}
@@ -0,0 +1,123 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Tools\Console\Command\ClearCache;
use Doctrine\Common\Cache\ApcCache;
use Doctrine\Common\Cache\ClearableCache;
use Doctrine\Common\Cache\FlushableCache;
use Doctrine\Common\Cache\XcacheCache;
use Doctrine\ORM\Tools\Console\Command\AbstractEntityManagerCommand;
use Doctrine\ORM\Tools\Console\CommandCompatibility;
use InvalidArgumentException;
use LogicException;
use Symfony\Component\Cache\Adapter\ApcuAdapter;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use function assert;
use function get_debug_type;
use function sprintf;
/**
* Command to clear the query cache of the various cache drivers.
*
* @link www.doctrine-project.org
*/
class QueryCommand extends AbstractEntityManagerCommand
{
use CommandCompatibility;
/** @return void */
protected function configure()
{
$this->setName('orm:clear-cache:query')
->setDescription('Clear all query cache of the various cache drivers')
->addOption('em', null, InputOption::VALUE_REQUIRED, 'Name of the entity manager to operate on')
->addOption('flush', null, InputOption::VALUE_NONE, 'If defined, cache entries will be flushed instead of deleted/invalidated.')
->setHelp(<<<'EOT'
The <info>%command.name%</info> command is meant to clear the query cache of associated Entity Manager.
It is possible to invalidate all cache entries at once - called delete -, or flushes the cache provider
instance completely.
The execution type differ on how you execute the command.
If you want to invalidate the entries (and not delete from cache instance), this command would do the work:
<info>%command.name%</info>
Alternatively, if you want to flush the cache provider using this command:
<info>%command.name% --flush</info>
Finally, be aware that if <info>--flush</info> option is passed, not all cache providers are able to flush entries,
because of a limitation of its execution nature.
EOT
);
}
private function doExecute(InputInterface $input, OutputInterface $output): int
{
$ui = (new SymfonyStyle($input, $output))->getErrorStyle();
$em = $this->getEntityManager($input);
$cache = $em->getConfiguration()->getQueryCache();
if ($cache instanceof ApcuAdapter) {
throw new LogicException('Cannot clear APCu Cache from Console, it\'s shared in the Webserver memory and not accessible from the CLI.');
}
$cacheDriver = null;
if (! $cache) {
$cacheDriver = $em->getConfiguration()->getQueryCacheImpl();
if (! $cacheDriver) {
throw new InvalidArgumentException('No Query cache driver is configured on given EntityManager.');
}
if ($cacheDriver instanceof ApcCache) {
throw new LogicException('Cannot clear APCu Cache from Console, it\'s shared in the Webserver memory and not accessible from the CLI.');
}
if ($cacheDriver instanceof XcacheCache) {
throw new LogicException('Cannot clear XCache Cache from Console, it\'s shared in the Webserver memory and not accessible from the CLI.');
}
if (! ($cacheDriver instanceof ClearableCache)) {
throw new LogicException(sprintf(
'Can only clear cache when ClearableCache interface is implemented, %s does not implement.',
get_debug_type($cacheDriver)
));
}
}
$ui->comment('Clearing <info>all</info> Query cache entries');
if ($cache) {
$result = $cache->clear();
} else {
assert($cacheDriver !== null);
$result = $cacheDriver->deleteAll();
}
$message = $result ? 'Successfully deleted cache entries.' : 'No cache entries were deleted.';
if ($input->getOption('flush') === true && ! $cache) {
if (! ($cacheDriver instanceof FlushableCache)) {
throw new LogicException(sprintf(
'Can only clear cache when FlushableCache interface is implemented, %s does not implement.',
get_debug_type($cacheDriver)
));
}
$result = $cacheDriver->flushAll();
$message = $result ? 'Successfully flushed cache entries.' : $message;
}
$ui->success($message);
return 0;
}
}
@@ -0,0 +1,106 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Tools\Console\Command\ClearCache;
use Doctrine\ORM\Cache;
use Doctrine\ORM\Tools\Console\Command\AbstractEntityManagerCommand;
use Doctrine\ORM\Tools\Console\CommandCompatibility;
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 Symfony\Component\Console\Style\SymfonyStyle;
use function sprintf;
/**
* Command to clear a query cache region.
*/
class QueryRegionCommand extends AbstractEntityManagerCommand
{
use CommandCompatibility;
/** @return void */
protected function configure()
{
$this->setName('orm:clear-cache:region:query')
->setDescription('Clear a second-level cache query region')
->addArgument('region-name', InputArgument::OPTIONAL, 'The query region to clear.')
->addOption('em', null, InputOption::VALUE_REQUIRED, 'Name of the entity manager to operate on')
->addOption('all', null, InputOption::VALUE_NONE, 'If defined, all query regions will be deleted/invalidated.')
->addOption('flush', null, InputOption::VALUE_NONE, 'If defined, all cache entries will be flushed.')
->setHelp(<<<'EOT'
The <info>%command.name%</info> command is meant to clear a second-level cache query region for an associated Entity Manager.
It is possible to delete/invalidate all query region, a specific query region or flushes the cache provider.
The execution type differ on how you execute the command.
If you want to invalidate all entries for the default query region this command would do the work:
<info>%command.name%</info>
To invalidate entries for a specific query region you should use :
<info>%command.name% my_region_name</info>
If you want to invalidate all entries for the all query region:
<info>%command.name% --all</info>
Alternatively, if you want to flush the configured cache provider use this command:
<info>%command.name% my_region_name --flush</info>
Finally, be aware that if <info>--flush</info> option is passed,
not all cache providers are able to flush entries, because of a limitation of its execution nature.
EOT
);
}
private function doExecute(InputInterface $input, OutputInterface $output): int
{
$ui = (new SymfonyStyle($input, $output))->getErrorStyle();
$em = $this->getEntityManager($input);
$name = $input->getArgument('region-name');
$cache = $em->getCache();
if ($name === null) {
$name = Cache::DEFAULT_QUERY_REGION_NAME;
}
if (! $cache instanceof Cache) {
throw new InvalidArgumentException('No second-level cache is configured on the given EntityManager.');
}
if ($input->getOption('flush')) {
$cache->getQueryCache($name)
->getRegion()
->evictAll();
$ui->comment(
sprintf(
'Flushing cache provider configured for second-level cache query region named <info>"%s"</info>',
$name
)
);
return 0;
}
if ($input->getOption('all')) {
$ui->comment('Clearing <info>all</info> second-level cache query regions');
$cache->evictQueryRegions();
return 0;
}
$ui->comment(sprintf('Clearing second-level cache query region named <info>"%s"</info>', $name));
$cache->evictQueryRegion($name);
return 0;
}
}
@@ -0,0 +1,110 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Tools\Console\Command\ClearCache;
use Doctrine\Common\Cache\ApcCache;
use Doctrine\Common\Cache\ClearableCache;
use Doctrine\Common\Cache\FlushableCache;
use Doctrine\Common\Cache\XcacheCache;
use Doctrine\ORM\Configuration;
use Doctrine\ORM\Tools\Console\Command\AbstractEntityManagerCommand;
use Doctrine\ORM\Tools\Console\CommandCompatibility;
use InvalidArgumentException;
use LogicException;
use Symfony\Component\Cache\Adapter\ApcuAdapter;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use function get_debug_type;
use function method_exists;
use function sprintf;
/**
* Command to clear the result cache of the various cache drivers.
*
* @link www.doctrine-project.org
*/
class ResultCommand extends AbstractEntityManagerCommand
{
use CommandCompatibility;
/** @return void */
protected function configure()
{
$this->setName('orm:clear-cache:result')
->setDescription('Clear all result cache of the various cache drivers')
->addOption('em', null, InputOption::VALUE_REQUIRED, 'Name of the entity manager to operate on')
->addOption('flush', null, InputOption::VALUE_NONE, 'If defined, cache entries will be flushed instead of deleted/invalidated.')
->setHelp(<<<'EOT'
The <info>%command.name%</info> command is meant to clear the result cache of associated Entity Manager.
It is possible to invalidate all cache entries at once - called delete -, or flushes the cache provider
instance completely.
The execution type differ on how you execute the command.
If you want to invalidate the entries (and not delete from cache instance), this command would do the work:
<info>%command.name%</info>
Alternatively, if you want to flush the cache provider using this command:
<info>%command.name% --flush</info>
Finally, be aware that if <info>--flush</info> option is passed, not all cache providers are able to flush entries,
because of a limitation of its execution nature.
EOT
);
}
private function doExecute(InputInterface $input, OutputInterface $output): int
{
$ui = (new SymfonyStyle($input, $output))->getErrorStyle();
$em = $this->getEntityManager($input);
$cache = $em->getConfiguration()->getResultCache();
$cacheDriver = method_exists(Configuration::class, 'getResultCacheImpl') ? $em->getConfiguration()->getResultCacheImpl() : null;
if (! $cacheDriver && ! $cache) {
throw new InvalidArgumentException('No Result cache driver is configured on given EntityManager.');
}
if ($cacheDriver instanceof ApcCache || $cache instanceof ApcuAdapter) {
throw new LogicException('Cannot clear APCu Cache from Console, it\'s shared in the Webserver memory and not accessible from the CLI.');
}
if ($cacheDriver instanceof XcacheCache) {
throw new LogicException('Cannot clear XCache Cache from Console, it\'s shared in the Webserver memory and not accessible from the CLI.');
}
if (! $cache && ! ($cacheDriver instanceof ClearableCache)) {
throw new LogicException(sprintf(
'Can only clear cache when ClearableCache interface is implemented, %s does not implement.',
get_debug_type($cacheDriver)
));
}
$ui->comment('Clearing <info>all</info> Result cache entries');
$result = $cache ? $cache->clear() : $cacheDriver->deleteAll();
$message = $result ? 'Successfully deleted cache entries.' : 'No cache entries were deleted.';
if ($input->getOption('flush') === true && ! $cache) {
if (! ($cacheDriver instanceof FlushableCache)) {
throw new LogicException(sprintf(
'Can only clear cache when FlushableCache interface is implemented, %s does not implement.',
get_debug_type($cacheDriver)
));
}
$result = $cacheDriver->flushAll();
$message = $result ? 'Successfully flushed cache entries.' : $message;
}
$ui->success($message);
return 0;
}
}
@@ -0,0 +1,190 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Tools\Console\Command;
use Doctrine\ORM\Tools\Console\CommandCompatibility;
use Doctrine\ORM\Tools\ConvertDoctrine1Schema;
use Doctrine\ORM\Tools\EntityGenerator;
use Doctrine\ORM\Tools\Export\ClassMetadataExporter;
use Doctrine\ORM\Tools\Export\Driver\AnnotationExporter;
use InvalidArgumentException;
use Symfony\Component\Console\Command\Command;
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 Symfony\Component\Console\Style\SymfonyStyle;
use function array_merge;
use function file_exists;
use function is_readable;
use function is_writable;
use function realpath;
use function sprintf;
use const PHP_EOL;
/**
* Command to convert a Doctrine 1 schema to a Doctrine 2 mapping file.
*
* @deprecated 2.7 This class is being removed from the ORM and won't have any replacement
*
* @link www.doctrine-project.org
*/
class ConvertDoctrine1SchemaCommand extends Command
{
use CommandCompatibility;
/** @var EntityGenerator|null */
private $entityGenerator = null;
/** @var ClassMetadataExporter|null */
private $metadataExporter = null;
/** @return EntityGenerator */
public function getEntityGenerator()
{
if ($this->entityGenerator === null) {
$this->entityGenerator = new EntityGenerator();
}
return $this->entityGenerator;
}
/** @return void */
public function setEntityGenerator(EntityGenerator $entityGenerator)
{
$this->entityGenerator = $entityGenerator;
}
/** @return ClassMetadataExporter */
public function getMetadataExporter()
{
if ($this->metadataExporter === null) {
$this->metadataExporter = new ClassMetadataExporter();
}
return $this->metadataExporter;
}
/** @return void */
public function setMetadataExporter(ClassMetadataExporter $metadataExporter)
{
$this->metadataExporter = $metadataExporter;
}
/** @return void */
protected function configure()
{
$this->setName('orm:convert-d1-schema')
->setAliases(['orm:convert:d1-schema'])
->setDescription('Converts Doctrine 1.x schema into a Doctrine 2.x schema')
->addArgument('from-path', InputArgument::REQUIRED, 'The path of Doctrine 1.X schema information.')
->addArgument('to-type', InputArgument::REQUIRED, 'The destination Doctrine 2.X mapping type.')
->addArgument('dest-path', InputArgument::REQUIRED, 'The path to generate your Doctrine 2.X mapping information.')
->addOption('from', null, InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 'Optional paths of Doctrine 1.X schema information.', [])
->addOption('extend', null, InputOption::VALUE_OPTIONAL, 'Defines a base class to be extended by generated entity classes.')
->addOption('num-spaces', null, InputOption::VALUE_OPTIONAL, 'Defines the number of indentation spaces', 4)
->setHelp('Converts Doctrine 1.x schema into a Doctrine 2.x schema.');
}
private function doExecute(InputInterface $input, OutputInterface $output): int
{
$ui = new SymfonyStyle($input, $output);
$ui->getErrorStyle()->warning('Command ' . $this->getName() . ' is deprecated and will be removed in Doctrine ORM 3.0.');
// Process source directories
$fromPaths = array_merge([$input->getArgument('from-path')], $input->getOption('from'));
// Process destination directory
$destPath = realpath($input->getArgument('dest-path'));
$toType = $input->getArgument('to-type');
$extend = $input->getOption('extend');
$numSpaces = (int) $input->getOption('num-spaces');
$this->convertDoctrine1Schema($fromPaths, $destPath, $toType, $numSpaces, $extend, $output);
return 0;
}
/**
* @param mixed[] $fromPaths
* @param string $destPath
* @param string $toType
* @param int $numSpaces
* @param string|null $extend
*
* @return void
*
* @throws InvalidArgumentException
*/
public function convertDoctrine1Schema(array $fromPaths, $destPath, $toType, $numSpaces, $extend, OutputInterface $output)
{
foreach ($fromPaths as &$dirName) {
$dirName = realpath($dirName);
if (! file_exists($dirName)) {
throw new InvalidArgumentException(
sprintf("Doctrine 1.X schema directory '<info>%s</info>' does not exist.", $dirName)
);
}
if (! is_readable($dirName)) {
throw new InvalidArgumentException(
sprintf("Doctrine 1.X schema directory '<info>%s</info>' does not have read permissions.", $dirName)
);
}
}
if (! file_exists($destPath)) {
throw new InvalidArgumentException(
sprintf("Doctrine 2.X mapping destination directory '<info>%s</info>' does not exist.", $destPath)
);
}
if (! is_writable($destPath)) {
throw new InvalidArgumentException(
sprintf("Doctrine 2.X mapping destination directory '<info>%s</info>' does not have write permissions.", $destPath)
);
}
$cme = $this->getMetadataExporter();
$exporter = $cme->getExporter($toType, $destPath);
if ($exporter instanceof AnnotationExporter) {
$entityGenerator = $this->getEntityGenerator();
$exporter->setEntityGenerator($entityGenerator);
$entityGenerator->setNumSpaces($numSpaces);
if ($extend !== null) {
$entityGenerator->setClassToExtend($extend);
}
}
$converter = new ConvertDoctrine1Schema($fromPaths);
$metadata = $converter->getMetadata();
if ($metadata) {
$output->writeln('');
foreach ($metadata as $class) {
$output->writeln(sprintf('Processing entity "<info>%s</info>"', $class->name));
}
$exporter->setMetadata($metadata);
$exporter->export();
$output->writeln(PHP_EOL . sprintf(
'Converting Doctrine 1.X schema to "<info>%s</info>" mapping type in "<info>%s</info>"',
$toType,
$destPath
));
} else {
$output->writeln('No Metadata Classes to process.');
}
}
}
@@ -0,0 +1,193 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Tools\Console\Command;
use Doctrine\DBAL\Connection;
use Doctrine\ORM\Mapping\Driver\DatabaseDriver;
use Doctrine\ORM\Tools\Console\CommandCompatibility;
use Doctrine\ORM\Tools\Console\MetadataFilter;
use Doctrine\ORM\Tools\DisconnectedClassMetadataFactory;
use Doctrine\ORM\Tools\EntityGenerator;
use Doctrine\ORM\Tools\Export\ClassMetadataExporter;
use Doctrine\ORM\Tools\Export\Driver\AbstractExporter;
use Doctrine\ORM\Tools\Export\Driver\AnnotationExporter;
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 Symfony\Component\Console\Style\SymfonyStyle;
use function file_exists;
use function is_dir;
use function is_writable;
use function method_exists;
use function mkdir;
use function realpath;
use function sprintf;
use function strtolower;
/**
* Command to convert your mapping information between the various formats.
*
* @deprecated 2.7 This class is being removed from the ORM and won't have any replacement
*
* @link www.doctrine-project.org
*/
class ConvertMappingCommand extends AbstractEntityManagerCommand
{
use CommandCompatibility;
/** @return void */
protected function configure()
{
$this->setName('orm:convert-mapping')
->setAliases(['orm:convert:mapping'])
->setDescription('Convert mapping information between supported formats')
->addArgument('to-type', InputArgument::REQUIRED, 'The mapping type to be converted.')
->addArgument('dest-path', InputArgument::REQUIRED, 'The path to generate your entities classes.')
->addOption('em', null, InputOption::VALUE_REQUIRED, 'Name of the entity manager to operate on')
->addOption('filter', null, InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 'A string pattern used to match entities that should be processed.')
->addOption('force', 'f', InputOption::VALUE_NONE, 'Force to overwrite existing mapping files.')
->addOption('from-database', null, null, 'Whether or not to convert mapping information from existing database.')
->addOption('extend', null, InputOption::VALUE_OPTIONAL, 'Defines a base class to be extended by generated entity classes.')
->addOption('num-spaces', null, InputOption::VALUE_OPTIONAL, 'Defines the number of indentation spaces', 4)
->addOption('namespace', null, InputOption::VALUE_OPTIONAL, 'Defines a namespace for the generated entity classes, if converted from database.')
->setHelp(<<<'EOT'
Convert mapping information between supported formats.
This is an execute <info>one-time</info> command. It should not be necessary for
you to call this method multiple times, especially when using the <comment>--from-database</comment>
flag.
Converting an existing database schema into mapping files only solves about 70-80%
of the necessary mapping information. Additionally the detection from an existing
database cannot detect inverse associations, inheritance types,
entities with foreign keys as primary keys and many of the
semantical operations on associations such as cascade.
<comment>Hint:</comment> There is no need to convert YAML or XML mapping files to annotations
every time you make changes. All mapping drivers are first class citizens
in Doctrine 2 and can be used as runtime mapping for the ORM.
<comment>Hint:</comment> If you have a database with tables that should not be managed
by the ORM, you can use a DBAL functionality to filter the tables and sequences down
on a global level:
$config->setSchemaAssetsFilter(function (string|AbstractAsset $assetName): bool {
if ($assetName instanceof AbstractAsset) {
$assetName = $assetName->getName();
}
return !str_starts_with($assetName, 'audit_');
});
EOT
);
}
private function doExecute(InputInterface $input, OutputInterface $output): int
{
$ui = new SymfonyStyle($input, $output);
$ui->getErrorStyle()->warning('Command ' . $this->getName() . ' is deprecated and will be removed in Doctrine ORM 3.0.');
$em = $this->getEntityManager($input);
if ($input->getOption('from-database') === true) {
$databaseDriver = new DatabaseDriver(
method_exists(Connection::class, 'createSchemaManager')
? $em->getConnection()->createSchemaManager()
: $em->getConnection()->getSchemaManager()
);
$em->getConfiguration()->setMetadataDriverImpl(
$databaseDriver
);
$namespace = $input->getOption('namespace');
if ($namespace !== null) {
$databaseDriver->setNamespace($namespace);
}
}
$cmf = new DisconnectedClassMetadataFactory();
$cmf->setEntityManager($em);
$metadata = $cmf->getAllMetadata();
$metadata = MetadataFilter::filter($metadata, $input->getOption('filter'));
// Process destination directory
$destPath = $input->getArgument('dest-path');
if (! is_dir($destPath)) {
mkdir($destPath, 0775, true);
}
$destPath = realpath($destPath);
if (! file_exists($destPath)) {
throw new InvalidArgumentException(
sprintf("Mapping destination directory '<info>%s</info>' does not exist.", $input->getArgument('dest-path'))
);
}
if (! is_writable($destPath)) {
throw new InvalidArgumentException(
sprintf("Mapping destination directory '<info>%s</info>' does not have write permissions.", $destPath)
);
}
$toType = strtolower($input->getArgument('to-type'));
$exporter = $this->getExporter($toType, $destPath);
$exporter->setOverwriteExistingFiles($input->getOption('force'));
if ($exporter instanceof AnnotationExporter) {
$entityGenerator = new EntityGenerator();
$exporter->setEntityGenerator($entityGenerator);
$entityGenerator->setNumSpaces((int) $input->getOption('num-spaces'));
$extend = $input->getOption('extend');
if ($extend !== null) {
$entityGenerator->setClassToExtend($extend);
}
}
if (empty($metadata)) {
$ui->success('No Metadata Classes to process.');
return 0;
}
foreach ($metadata as $class) {
$ui->text(sprintf('Processing entity "<info>%s</info>"', $class->name));
}
$exporter->setMetadata($metadata);
$exporter->export();
$ui->newLine();
$ui->text(
sprintf(
'Exporting "<info>%s</info>" mapping information to "<info>%s</info>"',
$toType,
$destPath
)
);
return 0;
}
/**
* @param string $toType
* @param string $destPath
*
* @return AbstractExporter
*/
protected function getExporter($toType, $destPath)
{
$cme = new ClassMetadataExporter();
return $cme->getExporter($toType, $destPath);
}
}
@@ -0,0 +1,58 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Tools\Console\Command;
use Doctrine\ORM\Tools\Console\CommandCompatibility;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use Throwable;
/**
* Command to ensure that Doctrine is properly configured for a production environment.
*
* @deprecated
*
* @link www.doctrine-project.org
*/
class EnsureProductionSettingsCommand extends AbstractEntityManagerCommand
{
use CommandCompatibility;
/** @return void */
protected function configure()
{
$this->setName('orm:ensure-production-settings')
->setDescription('Verify that Doctrine is properly configured for a production environment')
->addOption('em', null, InputOption::VALUE_REQUIRED, 'Name of the entity manager to operate on')
->addOption('complete', null, InputOption::VALUE_NONE, 'Flag to also inspect database connection existence.')
->setHelp('Verify that Doctrine is properly configured for a production environment.');
}
private function doExecute(InputInterface $input, OutputInterface $output): int
{
$ui = (new SymfonyStyle($input, $output))->getErrorStyle();
$ui->warning('This console command has been deprecated and will be removed in a future version of Doctrine ORM.');
$em = $this->getEntityManager($input);
try {
$em->getConfiguration()->ensureProductionSettings();
if ($input->getOption('complete') === true) {
$em->getConnection()->connect();
}
} catch (Throwable $e) {
$ui->error($e->getMessage());
return 1;
}
$ui->success('Environment is correctly configured for production.');
return 0;
}
}
@@ -0,0 +1,133 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Tools\Console\Command;
use Doctrine\ORM\Tools\Console\CommandCompatibility;
use Doctrine\ORM\Tools\Console\MetadataFilter;
use Doctrine\ORM\Tools\DisconnectedClassMetadataFactory;
use Doctrine\ORM\Tools\EntityGenerator;
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 Symfony\Component\Console\Style\SymfonyStyle;
use function file_exists;
use function is_writable;
use function realpath;
use function sprintf;
/**
* Command to generate entity classes and method stubs from your mapping information.
*
* @deprecated 2.7 This class is being removed from the ORM and won't have any replacement
*
* @link www.doctrine-project.org
*/
class GenerateEntitiesCommand extends AbstractEntityManagerCommand
{
use CommandCompatibility;
/** @return void */
protected function configure()
{
$this->setName('orm:generate-entities')
->setAliases(['orm:generate:entities'])
->setDescription('Generate entity classes and method stubs from your mapping information')
->addArgument('dest-path', InputArgument::REQUIRED, 'The path to generate your entity classes.')
->addOption('em', null, InputOption::VALUE_REQUIRED, 'Name of the entity manager to operate on')
->addOption('filter', null, InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 'A string pattern used to match entities that should be processed.')
->addOption('generate-annotations', null, InputOption::VALUE_OPTIONAL, 'Flag to define if generator should generate annotation metadata on entities.', false)
->addOption('generate-methods', null, InputOption::VALUE_OPTIONAL, 'Flag to define if generator should generate stub methods on entities.', true)
->addOption('regenerate-entities', null, InputOption::VALUE_OPTIONAL, 'Flag to define if generator should regenerate entity if it exists.', false)
->addOption('update-entities', null, InputOption::VALUE_OPTIONAL, 'Flag to define if generator should only update entity if it exists.', true)
->addOption('extend', null, InputOption::VALUE_REQUIRED, 'Defines a base class to be extended by generated entity classes.')
->addOption('num-spaces', null, InputOption::VALUE_REQUIRED, 'Defines the number of indentation spaces', 4)
->addOption('no-backup', null, InputOption::VALUE_NONE, 'Flag to define if generator should avoid backuping existing entity file if it exists.')
->setHelp(<<<'EOT'
Generate entity classes and method stubs from your mapping information.
If you use the <comment>--update-entities</comment> or <comment>--regenerate-entities</comment> flags your existing
code gets overwritten. The EntityGenerator will only append new code to your
file and will not delete the old code. However this approach may still be prone
to error and we suggest you use code repositories such as GIT or SVN to make
backups of your code.
It makes sense to generate the entity code if you are using entities as Data
Access Objects only and don't put much additional logic on them. If you are
however putting much more logic on the entities you should refrain from using
the entity-generator and code your entities manually.
<error>Important:</error> Even if you specified Inheritance options in your
XML or YAML Mapping files the generator cannot generate the base and
child classes for you correctly, because it doesn't know which
class is supposed to extend which. You have to adjust the entity
code manually for inheritance to work!
EOT
);
}
private function doExecute(InputInterface $input, OutputInterface $output): int
{
$ui = (new SymfonyStyle($input, $output))->getErrorStyle();
$ui->warning('Command ' . $this->getName() . ' is deprecated and will be removed in Doctrine ORM 3.0.');
$em = $this->getEntityManager($input);
$cmf = new DisconnectedClassMetadataFactory();
$cmf->setEntityManager($em);
$metadatas = $cmf->getAllMetadata();
$metadatas = MetadataFilter::filter($metadatas, $input->getOption('filter'));
// Process destination directory
$destPath = realpath($input->getArgument('dest-path'));
if (! file_exists($destPath)) {
throw new InvalidArgumentException(
sprintf("Entities destination directory '<info>%s</info>' does not exist.", $input->getArgument('dest-path'))
);
}
if (! is_writable($destPath)) {
throw new InvalidArgumentException(
sprintf("Entities destination directory '<info>%s</info>' does not have write permissions.", $destPath)
);
}
if (empty($metadatas)) {
$ui->success('No Metadata Classes to process.');
return 0;
}
$entityGenerator = new EntityGenerator();
$entityGenerator->setGenerateAnnotations($input->getOption('generate-annotations'));
$entityGenerator->setGenerateStubMethods($input->getOption('generate-methods'));
$entityGenerator->setRegenerateEntityIfExists($input->getOption('regenerate-entities'));
$entityGenerator->setUpdateEntityIfExists($input->getOption('update-entities'));
$entityGenerator->setNumSpaces((int) $input->getOption('num-spaces'));
$entityGenerator->setBackupExisting(! $input->getOption('no-backup'));
$extend = $input->getOption('extend');
if ($extend !== null) {
$entityGenerator->setClassToExtend($extend);
}
foreach ($metadatas as $metadata) {
$ui->text(sprintf('Processing entity "<info>%s</info>"', $metadata->name));
}
// Generating Entities
$entityGenerator->generate($metadatas, $destPath);
// Outputting information message
$ui->newLine();
$ui->success(sprintf('Entity classes generated to "%s"', $destPath));
return 0;
}
}
@@ -0,0 +1,100 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Tools\Console\Command;
use Doctrine\ORM\Tools\Console\CommandCompatibility;
use Doctrine\ORM\Tools\Console\MetadataFilter;
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 Symfony\Component\Console\Style\SymfonyStyle;
use function file_exists;
use function is_dir;
use function is_writable;
use function mkdir;
use function realpath;
use function sprintf;
/**
* Command to (re)generate the proxy classes used by doctrine.
*
* @link www.doctrine-project.org
*/
class GenerateProxiesCommand extends AbstractEntityManagerCommand
{
use CommandCompatibility;
/** @return void */
protected function configure()
{
$this->setName('orm:generate-proxies')
->setAliases(['orm:generate:proxies'])
->setDescription('Generates proxy classes for entity classes')
->addArgument('dest-path', InputArgument::OPTIONAL, 'The path to generate your proxy classes. If none is provided, it will attempt to grab from configuration.')
->addOption('em', null, InputOption::VALUE_REQUIRED, 'Name of the entity manager to operate on')
->addOption('filter', null, InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 'A string pattern used to match entities that should be processed.')
->setHelp('Generates proxy classes for entity classes.');
}
private function doExecute(InputInterface $input, OutputInterface $output): int
{
$ui = (new SymfonyStyle($input, $output))->getErrorStyle();
$em = $this->getEntityManager($input);
$metadatas = $em->getMetadataFactory()->getAllMetadata();
$metadatas = MetadataFilter::filter($metadatas, $input->getOption('filter'));
// Process destination directory
$destPath = $input->getArgument('dest-path');
if ($destPath === null) {
$destPath = $em->getConfiguration()->getProxyDir();
if ($destPath === null) {
throw new InvalidArgumentException('Proxy directory cannot be null');
}
}
if (! is_dir($destPath)) {
mkdir($destPath, 0775, true);
}
$destPath = realpath($destPath);
if (! file_exists($destPath)) {
throw new InvalidArgumentException(
sprintf("Proxies destination directory '<info>%s</info>' does not exist.", $em->getConfiguration()->getProxyDir())
);
}
if (! is_writable($destPath)) {
throw new InvalidArgumentException(
sprintf("Proxies destination directory '<info>%s</info>' does not have write permissions.", $destPath)
);
}
if (empty($metadatas)) {
$ui->success('No Metadata Classes to process.');
return 0;
}
foreach ($metadatas as $metadata) {
$ui->text(sprintf('Processing entity "<info>%s</info>"', $metadata->name));
}
// Generating Proxies
$em->getProxyFactory()->generateProxyClasses($metadatas, $destPath);
// Outputting information message
$ui->newLine();
$ui->text(sprintf('Proxy classes generated to "<info>%s</info>"', $destPath));
return 0;
}
}
@@ -0,0 +1,105 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Tools\Console\Command;
use Doctrine\ORM\Tools\Console\CommandCompatibility;
use Doctrine\ORM\Tools\Console\MetadataFilter;
use Doctrine\ORM\Tools\EntityRepositoryGenerator;
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 Symfony\Component\Console\Style\SymfonyStyle;
use function file_exists;
use function is_writable;
use function realpath;
use function sprintf;
/**
* Command to generate repository classes for mapping information.
*
* @deprecated 2.7 This class is being removed from the ORM and won't have any replacement
*
* @link www.doctrine-project.org
*/
class GenerateRepositoriesCommand extends AbstractEntityManagerCommand
{
use CommandCompatibility;
/** @return void */
protected function configure()
{
$this->setName('orm:generate-repositories')
->setAliases(['orm:generate:repositories'])
->setDescription('Generate repository classes from your mapping information')
->addArgument('dest-path', InputArgument::REQUIRED, 'The path to generate your repository classes.')
->addOption('em', null, InputOption::VALUE_REQUIRED, 'Name of the entity manager to operate on')
->addOption('filter', null, InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 'A string pattern used to match entities that should be processed.')
->setHelp('Generate repository classes from your mapping information.');
}
private function doExecute(InputInterface $input, OutputInterface $output): int
{
$ui = (new SymfonyStyle($input, $output))->getErrorStyle();
$ui->warning('Command ' . $this->getName() . ' is deprecated and will be removed in Doctrine ORM 3.0.');
$em = $this->getEntityManager($input);
$metadatas = $em->getMetadataFactory()->getAllMetadata();
$metadatas = MetadataFilter::filter($metadatas, $input->getOption('filter'));
$repositoryName = $em->getConfiguration()->getDefaultRepositoryClassName();
// Process destination directory
$destPath = realpath($input->getArgument('dest-path'));
if (! file_exists($destPath)) {
throw new InvalidArgumentException(
sprintf("Entities destination directory '<info>%s</info>' does not exist.", $input->getArgument('dest-path'))
);
}
if (! is_writable($destPath)) {
throw new InvalidArgumentException(
sprintf("Entities destination directory '<info>%s</info>' does not have write permissions.", $destPath)
);
}
if (empty($metadatas)) {
$ui->success('No Metadata Classes to process.');
return 0;
}
$numRepositories = 0;
$generator = new EntityRepositoryGenerator();
$generator->setDefaultRepositoryName($repositoryName);
foreach ($metadatas as $metadata) {
if ($metadata->customRepositoryClassName) {
$ui->text(sprintf('Processing repository "<info>%s</info>"', $metadata->customRepositoryClassName));
$generator->writeEntityRepositoryClass($metadata->customRepositoryClassName, $destPath);
++$numRepositories;
}
}
if ($numRepositories === 0) {
$ui->text('No Repository classes were found to be processed.');
return 0;
}
// Outputting information message
$ui->newLine();
$ui->text(sprintf('Repository classes generated to "<info>%s</info>"', $destPath));
return 0;
}
}
@@ -0,0 +1,85 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Tools\Console\Command;
use Doctrine\ORM\Mapping\MappingException;
use Doctrine\ORM\Tools\Console\CommandCompatibility;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use function count;
use function sprintf;
/**
* Show information about mapped entities.
*
* @link www.doctrine-project.org
*/
class InfoCommand extends AbstractEntityManagerCommand
{
use CommandCompatibility;
/** @return void */
protected function configure()
{
$this->setName('orm:info')
->setDescription('Show basic information about all mapped entities')
->addOption('em', null, InputOption::VALUE_REQUIRED, 'Name of the entity manager to operate on')
->setHelp(<<<'EOT'
The <info>%command.name%</info> shows basic information about which
entities exist and possibly if their mapping information contains errors or
not.
EOT
);
}
private function doExecute(InputInterface $input, OutputInterface $output): int
{
$ui = (new SymfonyStyle($input, $output))->getErrorStyle();
$entityManager = $this->getEntityManager($input);
$entityClassNames = $entityManager->getConfiguration()
->getMetadataDriverImpl()
->getAllClassNames();
if (! $entityClassNames) {
$ui->caution(
[
'You do not have any mapped Doctrine ORM entities according to the current configuration.',
'If you have entities or mapping files you should check your mapping configuration for errors.',
]
);
return 1;
}
$ui->text(sprintf('Found <info>%d</info> mapped entities:', count($entityClassNames)));
$ui->newLine();
$failure = false;
foreach ($entityClassNames as $entityClassName) {
try {
$entityManager->getClassMetadata($entityClassName);
$ui->text(sprintf('<info>[OK]</info> %s', $entityClassName));
} catch (MappingException $e) {
$ui->text(
[
sprintf('<error>[FAIL]</error> %s', $entityClassName),
sprintf('<comment>%s</comment>', $e->getMessage()),
'',
]
);
$failure = true;
}
}
return $failure ? 1 : 0;
}
}
@@ -0,0 +1,284 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Tools\Console\Command;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\Mapping\ClassMetadata;
use Doctrine\Persistence\Mapping\MappingException;
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 Symfony\Component\Console\Style\SymfonyStyle;
use function array_filter;
use function array_map;
use function array_merge;
use function count;
use function current;
use function get_debug_type;
use function implode;
use function is_array;
use function is_bool;
use function is_object;
use function is_scalar;
use function json_encode;
use function preg_match;
use function preg_quote;
use function print_r;
use function sprintf;
use const JSON_PRETTY_PRINT;
use const JSON_UNESCAPED_SLASHES;
use const JSON_UNESCAPED_UNICODE;
/**
* Show information about mapped entities.
*
* @link www.doctrine-project.org
*
* @psalm-import-type AssociationMapping from ClassMetadata
* @psalm-import-type FieldMapping from ClassMetadata
*/
final class MappingDescribeCommand extends AbstractEntityManagerCommand
{
protected function configure(): void
{
$this->setName('orm:mapping:describe')
->addArgument('entityName', InputArgument::REQUIRED, 'Full or partial name of entity')
->setDescription('Display information about mapped objects')
->addOption('em', null, InputOption::VALUE_REQUIRED, 'Name of the entity manager to operate on')
->setHelp(<<<'EOT'
The %command.full_name% command describes the metadata for the given full or partial entity class name.
<info>%command.full_name%</info> My\Namespace\Entity\MyEntity
Or:
<info>%command.full_name%</info> MyEntity
EOT
);
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$ui = (new SymfonyStyle($input, $output))->getErrorStyle();
$entityManager = $this->getEntityManager($input);
$this->displayEntity($input->getArgument('entityName'), $entityManager, $ui);
return 0;
}
/**
* Display all the mapping information for a single Entity.
*
* @param string $entityName Full or partial entity class name
*/
private function displayEntity(
string $entityName,
EntityManagerInterface $entityManager,
SymfonyStyle $ui
): void {
$metadata = $this->getClassMetadata($entityName, $entityManager);
$ui->table(
['Field', 'Value'],
array_merge(
[
$this->formatField('Name', $metadata->name),
$this->formatField('Root entity name', $metadata->rootEntityName),
$this->formatField('Custom generator definition', $metadata->customGeneratorDefinition),
$this->formatField('Custom repository class', $metadata->customRepositoryClassName),
$this->formatField('Mapped super class?', $metadata->isMappedSuperclass),
$this->formatField('Embedded class?', $metadata->isEmbeddedClass),
$this->formatField('Parent classes', $metadata->parentClasses),
$this->formatField('Sub classes', $metadata->subClasses),
$this->formatField('Embedded classes', $metadata->subClasses),
$this->formatField('Named queries', $metadata->namedQueries),
$this->formatField('Named native queries', $metadata->namedNativeQueries),
$this->formatField('SQL result set mappings', $metadata->sqlResultSetMappings),
$this->formatField('Identifier', $metadata->identifier),
$this->formatField('Inheritance type', $metadata->inheritanceType),
$this->formatField('Discriminator column', $metadata->discriminatorColumn),
$this->formatField('Discriminator value', $metadata->discriminatorValue),
$this->formatField('Discriminator map', $metadata->discriminatorMap),
$this->formatField('Generator type', $metadata->generatorType),
$this->formatField('Table', $metadata->table),
$this->formatField('Composite identifier?', $metadata->isIdentifierComposite),
$this->formatField('Foreign identifier?', $metadata->containsForeignIdentifier),
$this->formatField('Enum identifier?', $metadata->containsEnumIdentifier),
$this->formatField('Sequence generator definition', $metadata->sequenceGeneratorDefinition),
$this->formatField('Change tracking policy', $metadata->changeTrackingPolicy),
$this->formatField('Versioned?', $metadata->isVersioned),
$this->formatField('Version field', $metadata->versionField),
$this->formatField('Read only?', $metadata->isReadOnly),
$this->formatEntityListeners($metadata->entityListeners),
],
[$this->formatField('Association mappings:', '')],
$this->formatMappings($metadata->associationMappings),
[$this->formatField('Field mappings:', '')],
$this->formatMappings($metadata->fieldMappings)
)
);
}
/**
* Return all mapped entity class names
*
* @return string[]
* @psalm-return class-string[]
*/
private function getMappedEntities(EntityManagerInterface $entityManager): array
{
$entityClassNames = $entityManager->getConfiguration()
->getMetadataDriverImpl()
->getAllClassNames();
if (! $entityClassNames) {
throw new InvalidArgumentException(
'You do not have any mapped Doctrine ORM entities according to the current configuration. ' .
'If you have entities or mapping files you should check your mapping configuration for errors.'
);
}
return $entityClassNames;
}
/**
* Return the class metadata for the given entity
* name
*
* @param string $entityName Full or partial entity name
*/
private function getClassMetadata(
string $entityName,
EntityManagerInterface $entityManager
): ClassMetadata {
try {
return $entityManager->getClassMetadata($entityName);
} catch (MappingException $e) {
}
$matches = array_filter(
$this->getMappedEntities($entityManager),
static function ($mappedEntity) use ($entityName) {
return preg_match('{' . preg_quote($entityName) . '}', $mappedEntity);
}
);
if (! $matches) {
throw new InvalidArgumentException(sprintf(
'Could not find any mapped Entity classes matching "%s"',
$entityName
));
}
if (count($matches) > 1) {
throw new InvalidArgumentException(sprintf(
'Entity name "%s" is ambiguous, possible matches: "%s"',
$entityName,
implode(', ', $matches)
));
}
return $entityManager->getClassMetadata(current($matches));
}
/**
* Format the given value for console output
*
* @param mixed $value
*/
private function formatValue($value): string
{
if ($value === '') {
return '';
}
if ($value === null) {
return '<comment>Null</comment>';
}
if (is_bool($value)) {
return '<comment>' . ($value ? 'True' : 'False') . '</comment>';
}
if (empty($value)) {
return '<comment>Empty</comment>';
}
if (is_array($value)) {
return json_encode($value, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT);
}
if (is_object($value)) {
return sprintf('<%s>', get_debug_type($value));
}
if (is_scalar($value)) {
return (string) $value;
}
throw new InvalidArgumentException(sprintf('Do not know how to format value "%s"', print_r($value, true)));
}
/**
* Add the given label and value to the two column table output
*
* @param string $label Label for the value
* @param mixed $value A Value to show
*
* @return string[]
* @psalm-return array{0: string, 1: string}
*/
private function formatField(string $label, $value): array
{
if ($value === null) {
$value = '<comment>None</comment>';
}
return [sprintf('<info>%s</info>', $label), $this->formatValue($value)];
}
/**
* Format the association mappings
*
* @psalm-param array<string, FieldMapping|AssociationMapping> $propertyMappings
*
* @return string[][]
* @psalm-return list<array{0: string, 1: string}>
*/
private function formatMappings(array $propertyMappings): array
{
$output = [];
foreach ($propertyMappings as $propertyName => $mapping) {
$output[] = $this->formatField(sprintf(' %s', $propertyName), '');
foreach ($mapping as $field => $value) {
$output[] = $this->formatField(sprintf(' %s', $field), $this->formatValue($value));
}
}
return $output;
}
/**
* Format the entity listeners
*
* @psalm-param list<object> $entityListeners
*
* @return string[]
* @psalm-return array{0: string, 1: string}
*/
private function formatEntityListeners(array $entityListeners): array
{
return $this->formatField('Entity listeners', array_map('get_class', $entityListeners));
}
}
@@ -0,0 +1,123 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Tools\Console\Command;
use Doctrine\ORM\Tools\Console\CommandCompatibility;
use Doctrine\ORM\Tools\Debug;
use LogicException;
use RuntimeException;
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 Symfony\Component\Console\Style\SymfonyStyle;
use function constant;
use function defined;
use function is_numeric;
use function sprintf;
use function str_replace;
use function strtoupper;
/**
* Command to execute DQL queries in a given EntityManager.
*
* @link www.doctrine-project.org
*/
class RunDqlCommand extends AbstractEntityManagerCommand
{
use CommandCompatibility;
/** @return void */
protected function configure()
{
$this->setName('orm:run-dql')
->setDescription('Executes arbitrary DQL directly from the command line')
->addArgument('dql', InputArgument::REQUIRED, 'The DQL to execute.')
->addOption('em', null, InputOption::VALUE_REQUIRED, 'Name of the entity manager to operate on')
->addOption('hydrate', null, InputOption::VALUE_REQUIRED, 'Hydration mode of result set. Should be either: object, array, scalar or single-scalar.', 'object')
->addOption('first-result', null, InputOption::VALUE_REQUIRED, 'The first result in the result set.')
->addOption('max-result', null, InputOption::VALUE_REQUIRED, 'The maximum number of results in the result set.')
->addOption('depth', null, InputOption::VALUE_REQUIRED, 'Dumping depth of Entity graph.', 7)
->addOption('show-sql', null, InputOption::VALUE_NONE, 'Dump generated SQL instead of executing query')
->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 App\Entity\User u"</info>
You can also optionally specify some additional options like what type of
hydration to use when executing the query:
<info>php %command.full_name% "SELECT u FROM App\Entity\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 App\Entity\User u" --first-result=0 --max-result=30</info>
EOT
);
}
private function doExecute(InputInterface $input, OutputInterface $output): int
{
$ui = new SymfonyStyle($input, $output);
$em = $this->getEntityManager($input);
$dql = $input->getArgument('dql');
if ($dql === null) {
throw new RuntimeException("Argument 'dql' is required in order to execute this command correctly.");
}
$depth = $input->getOption('depth');
if (! is_numeric($depth)) {
throw new LogicException("Option 'depth' must contain an integer value");
}
$hydrationModeName = (string) $input->getOption('hydrate');
$hydrationMode = 'Doctrine\ORM\Query::HYDRATE_' . strtoupper(str_replace('-', '_', $hydrationModeName));
if (! defined($hydrationMode)) {
throw new RuntimeException(sprintf(
"Hydration mode '%s' does not exist. It should be either: object. array, scalar or single-scalar.",
$hydrationModeName
));
}
$query = $em->createQuery($dql);
$firstResult = $input->getOption('first-result');
if ($firstResult !== null) {
if (! is_numeric($firstResult)) {
throw new LogicException("Option 'first-result' must contain an integer value");
}
$query->setFirstResult((int) $firstResult);
}
$maxResult = $input->getOption('max-result');
if ($maxResult !== null) {
if (! is_numeric($maxResult)) {
throw new LogicException("Option 'max-result' must contain an integer value");
}
$query->setMaxResults((int) $maxResult);
}
if ($input->getOption('show-sql')) {
$ui->text($query->getSQL());
return 0;
}
$resultSet = $query->execute([], constant($hydrationMode));
$ui->text(Debug::dump($resultSet, (int) $input->getOption('depth')));
return 0;
}
}
@@ -0,0 +1,46 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Tools\Console\Command\SchemaTool;
use Doctrine\ORM\Tools\Console\Command\AbstractEntityManagerCommand;
use Doctrine\ORM\Tools\Console\CommandCompatibility;
use Doctrine\ORM\Tools\SchemaTool;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
/**
* Base class for CreateCommand, DropCommand and UpdateCommand.
*
* @link www.doctrine-project.org
*/
abstract class AbstractCommand extends AbstractEntityManagerCommand
{
use CommandCompatibility;
/**
* @param mixed[] $metadatas
*
* @return int|null Null or 0 if everything went fine, or an error code.
*/
abstract protected function executeSchemaCommand(InputInterface $input, OutputInterface $output, SchemaTool $schemaTool, array $metadatas, SymfonyStyle $ui);
private function doExecute(InputInterface $input, OutputInterface $output): int
{
$ui = new SymfonyStyle($input, $output);
$em = $this->getEntityManager($input);
$metadatas = $em->getMetadataFactory()->getAllMetadata();
if (empty($metadatas)) {
$ui->getErrorStyle()->success('No Metadata Classes to process.');
return 0;
}
return $this->executeSchemaCommand($input, $output, new SchemaTool($em), $metadatas, $ui);
}
}
@@ -0,0 +1,77 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Tools\Console\Command\SchemaTool;
use Doctrine\ORM\Tools\SchemaTool;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use function sprintf;
/**
* Command to create the database schema for a set of classes based on their mappings.
*
* @link www.doctrine-project.org
*/
class CreateCommand extends AbstractCommand
{
/** @return void */
protected function configure()
{
$this->setName('orm:schema-tool:create')
->setDescription('Processes the schema and either create it directly on EntityManager Storage Connection or generate the SQL output')
->addOption('em', null, InputOption::VALUE_REQUIRED, 'Name of the entity manager to operate on')
->addOption('dump-sql', null, InputOption::VALUE_NONE, 'Instead of trying to apply generated SQLs into EntityManager Storage Connection, output them.')
->setHelp(<<<'EOT'
Processes the schema and either create it directly on EntityManager Storage Connection or generate the SQL output.
<comment>Hint:</comment> If you have a database with tables that should not be managed
by the ORM, you can use a DBAL functionality to filter the tables and sequences down
on a global level:
$config->setSchemaAssetsFilter(function (string|AbstractAsset $assetName): bool {
if ($assetName instanceof AbstractAsset) {
$assetName = $assetName->getName();
}
return !str_starts_with($assetName, 'audit_');
});
EOT
);
}
/**
* {@inheritDoc}
*/
protected function executeSchemaCommand(InputInterface $input, OutputInterface $output, SchemaTool $schemaTool, array $metadatas, SymfonyStyle $ui)
{
$dumpSql = $input->getOption('dump-sql') === true;
if ($dumpSql) {
$sqls = $schemaTool->getCreateSchemaSql($metadatas);
foreach ($sqls as $sql) {
$ui->writeln(sprintf('%s;', $sql));
}
return 0;
}
$notificationUi = $ui->getErrorStyle();
$notificationUi->caution('This operation should not be executed in a production environment!');
$notificationUi->text('Creating database schema...');
$notificationUi->newLine();
$schemaTool->createSchema($metadatas);
$notificationUi->success('Database schema created successfully!');
return 0;
}
}
@@ -0,0 +1,118 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Tools\Console\Command\SchemaTool;
use Doctrine\ORM\Tools\SchemaTool;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use function count;
use function sprintf;
/**
* Command to drop the database schema for a set of classes based on their mappings.
*
* @link www.doctrine-project.org
*/
class DropCommand extends AbstractCommand
{
/** @return void */
protected function configure()
{
$this->setName('orm:schema-tool:drop')
->setDescription('Drop the complete database schema of EntityManager Storage Connection or generate the corresponding SQL output')
->addOption('em', null, InputOption::VALUE_REQUIRED, 'Name of the entity manager to operate on')
->addOption('dump-sql', null, InputOption::VALUE_NONE, 'Instead of trying to apply generated SQLs into EntityManager Storage Connection, output them.')
->addOption('force', 'f', InputOption::VALUE_NONE, "Don't ask for the deletion of the database, but force the operation to run.")
->addOption('full-database', null, InputOption::VALUE_NONE, 'Instead of using the Class Metadata to detect the database table schema, drop ALL assets that the database contains.')
->setHelp(<<<'EOT'
Processes the schema and either drop the database schema of EntityManager Storage Connection or generate the SQL output.
Beware that the complete database is dropped by this command, even tables that are not relevant to your metadata model.
<comment>Hint:</comment> If you have a database with tables that should not be managed
by the ORM, you can use a DBAL functionality to filter the tables and sequences down
on a global level:
$config->setSchemaAssetsFilter(function (string|AbstractAsset $assetName): bool {
if ($assetName instanceof AbstractAsset) {
$assetName = $assetName->getName();
}
return !str_starts_with($assetName, 'audit_');
});
EOT
);
}
/**
* {@inheritDoc}
*/
protected function executeSchemaCommand(InputInterface $input, OutputInterface $output, SchemaTool $schemaTool, array $metadatas, SymfonyStyle $ui)
{
$isFullDatabaseDrop = $input->getOption('full-database');
$dumpSql = $input->getOption('dump-sql') === true;
$force = $input->getOption('force') === true;
if ($dumpSql) {
if ($isFullDatabaseDrop) {
$sqls = $schemaTool->getDropDatabaseSQL();
} else {
$sqls = $schemaTool->getDropSchemaSQL($metadatas);
}
foreach ($sqls as $sql) {
$ui->writeln(sprintf('%s;', $sql));
}
return 0;
}
$notificationUi = $ui->getErrorStyle();
if ($force) {
$notificationUi->text('Dropping database schema...');
$notificationUi->newLine();
if ($isFullDatabaseDrop) {
$schemaTool->dropDatabase();
} else {
$schemaTool->dropSchema($metadatas);
}
$notificationUi->success('Database schema dropped successfully!');
return 0;
}
$notificationUi->caution('This operation should not be executed in a production environment!');
if ($isFullDatabaseDrop) {
$sqls = $schemaTool->getDropDatabaseSQL();
} else {
$sqls = $schemaTool->getDropSchemaSQL($metadatas);
}
if (empty($sqls)) {
$notificationUi->success('Nothing to drop. The database is empty!');
return 0;
}
$notificationUi->text(
[
sprintf('The Schema-Tool would execute <info>"%s"</info> queries to update the database.', count($sqls)),
'',
'Please run the operation by passing one - or both - of the following options:',
'',
sprintf(' <info>%s --force</info> to execute the command', $this->getName()),
sprintf(' <info>%s --dump-sql</info> to dump the SQL statements to the screen', $this->getName()),
]
);
return 1;
}
}
@@ -0,0 +1,151 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Tools\Console\Command\SchemaTool;
use Doctrine\ORM\Tools\SchemaTool;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use function count;
use function sprintf;
/**
* Command to generate the SQL needed to update the database schema to match
* the current mapping information.
*
* @link www.doctrine-project.org
*/
class UpdateCommand extends AbstractCommand
{
/** @var string */
protected $name = 'orm:schema-tool:update';
/** @return void */
protected function configure()
{
$this->setName($this->name)
->setDescription('Executes (or dumps) the SQL needed to update the database schema to match the current mapping metadata')
->addOption('em', null, InputOption::VALUE_REQUIRED, 'Name of the entity manager to operate on')
->addOption('complete', null, InputOption::VALUE_NONE, 'If defined, all assets of the database which are not relevant to the current metadata will be dropped.')
->addOption('dump-sql', null, InputOption::VALUE_NONE, 'Dumps the generated SQL statements to the screen (does not execute them).')
->addOption('force', 'f', InputOption::VALUE_NONE, 'Causes the generated SQL statements to be physically executed against your database.')
->setHelp(<<<'EOT'
The <info>%command.name%</info> command generates the SQL needed to
synchronize the database schema with the current mapping metadata of the
default entity manager.
For example, if you add metadata for a new column to an entity, this command
would generate and output the SQL needed to add the new column to the database:
<info>%command.name% --dump-sql</info>
Alternatively, you can execute the generated queries:
<info>%command.name% --force</info>
If both options are specified, the queries are output and then executed:
<info>%command.name% --dump-sql --force</info>
Finally, be aware that if the <info>--complete</info> option is passed, this
task will drop all database assets (e.g. tables, etc) that are *not* described
by the current metadata. In other words, without this option, this task leaves
untouched any "extra" tables that exist in the database, but which aren't
described by any metadata. Not passing that option is deprecated.
<comment>Hint:</comment> If you have a database with tables that should not be managed
by the ORM, you can use a DBAL functionality to filter the tables and sequences down
on a global level:
$config->setSchemaAssetsFilter(function (string|AbstractAsset $assetName): bool {
if ($assetName instanceof AbstractAsset) {
$assetName = $assetName->getName();
}
return !str_starts_with($assetName, 'audit_');
});
EOT
);
}
/**
* {@inheritDoc}
*/
protected function executeSchemaCommand(InputInterface $input, OutputInterface $output, SchemaTool $schemaTool, array $metadatas, SymfonyStyle $ui)
{
$notificationUi = $ui->getErrorStyle();
// Defining if update is complete or not (--complete not defined means $saveMode = true)
$saveMode = ! $input->getOption('complete');
if ($saveMode) {
$notificationUi->warning(sprintf(
'Not passing the "--complete" option to "%s" is deprecated and will not be supported when using doctrine/dbal 4',
$this->getName() ?? $this->name
));
}
$sqls = $schemaTool->getUpdateSchemaSql($metadatas, $saveMode);
if (empty($sqls)) {
$notificationUi->success('Nothing to update - your database is already in sync with the current entity metadata.');
return 0;
}
$dumpSql = $input->getOption('dump-sql') === true;
$force = $input->getOption('force') === true;
if ($dumpSql) {
foreach ($sqls as $sql) {
$ui->writeln(sprintf('%s;', $sql));
}
}
if ($force) {
if ($dumpSql) {
$notificationUi->newLine();
}
$notificationUi->text('Updating database schema...');
$notificationUi->newLine();
$schemaTool->updateSchema($metadatas, $saveMode);
$pluralization = count($sqls) === 1 ? 'query was' : 'queries were';
$notificationUi->text(sprintf(' <info>%s</info> %s executed', count($sqls), $pluralization));
$notificationUi->success('Database schema updated successfully!');
}
if ($dumpSql || $force) {
return 0;
}
$notificationUi->caution(
[
'This operation should not be executed in a production environment!',
'',
'Use the incremental update to detect changes during development and use',
'the SQL DDL provided to manually update your database in production.',
]
);
$notificationUi->text(
[
sprintf('The Schema-Tool would execute <info>"%s"</info> queries to update the database.', count($sqls)),
'',
'Please run the operation by passing one - or both - of the following options:',
'',
sprintf(' <info>%s --force</info> to execute the command', $this->getName()),
sprintf(' <info>%s --dump-sql</info> to dump the SQL statements to the screen', $this->getName()),
]
);
return 1;
}
}
@@ -0,0 +1,93 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Tools\Console\Command;
use Doctrine\ORM\Tools\Console\CommandCompatibility;
use Doctrine\ORM\Tools\SchemaValidator;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use function count;
use function sprintf;
/**
* Command to validate that the current mapping is valid.
*
* @link www.doctrine-project.com
*/
class ValidateSchemaCommand extends AbstractEntityManagerCommand
{
use CommandCompatibility;
/** @return void */
protected function configure()
{
$this->setName('orm:validate-schema')
->setDescription('Validate the mapping files')
->addOption('em', null, InputOption::VALUE_REQUIRED, 'Name of the entity manager to operate on')
->addOption('skip-mapping', null, InputOption::VALUE_NONE, 'Skip the mapping validation check')
->addOption('skip-sync', null, InputOption::VALUE_NONE, 'Skip checking if the mapping is in sync with the database')
->addOption('skip-property-types', null, InputOption::VALUE_NONE, 'Skip checking if property types match the Doctrine types')
->setHelp('Validate that the mapping files are correct and in sync with the database.');
}
private function doExecute(InputInterface $input, OutputInterface $output): int
{
$ui = (new SymfonyStyle($input, $output))->getErrorStyle();
$em = $this->getEntityManager($input);
$validator = new SchemaValidator($em, ! $input->getOption('skip-property-types'));
$exit = 0;
$ui->section('Mapping');
if ($input->getOption('skip-mapping')) {
$ui->text('<comment>[SKIPPED] The mapping was not checked.</comment>');
} else {
$errors = $validator->validateMapping();
if ($errors) {
foreach ($errors as $className => $errorMessages) {
$ui->text(
sprintf(
'<error>[FAIL]</error> The entity-class <comment>%s</comment> mapping is invalid:',
$className
)
);
$ui->listing($errorMessages);
$ui->newLine();
}
++$exit;
} else {
$ui->success('The mapping files are correct.');
}
}
$ui->section('Database');
if ($input->getOption('skip-sync')) {
$ui->text('<comment>[SKIPPED] The database was not checked for synchronicity.</comment>');
} elseif (! $validator->schemaInSyncWithMetadata()) {
$ui->error('The database schema is not in sync with the current mapping file.');
if ($output->getVerbosity() >= OutputInterface::VERBOSITY_VERBOSE) {
$sqls = $validator->getUpdateSchemaList();
$ui->comment(sprintf('<info>%d</info> schema diff(s) detected:', count($sqls)));
foreach ($sqls as $sql) {
$ui->text(sprintf(' %s;', $sql));
}
}
$exit += 2;
} else {
$ui->success('The database schema is in sync with the mapping files.');
}
return $exit;
}
}
@@ -0,0 +1,35 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Tools\Console;
use ReflectionMethod;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
if ((new ReflectionMethod(Command::class, 'execute'))->hasReturnType()) {
/** @internal */
trait CommandCompatibility
{
protected function execute(InputInterface $input, OutputInterface $output): int
{
return $this->doExecute($input, $output);
}
}
} else {
/** @internal */
trait CommandCompatibility
{
/**
* {@inheritDoc}
*
* @return int
*/
protected function execute(InputInterface $input, OutputInterface $output)
{
return $this->doExecute($input, $output);
}
}
}
+146
View File
@@ -0,0 +1,146 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Tools\Console;
use Composer\InstalledVersions;
use Doctrine\DBAL\Tools\Console as DBALConsole;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\Tools\Console\EntityManagerProvider\ConnectionFromManagerProvider;
use Doctrine\ORM\Tools\Console\EntityManagerProvider\HelperSetManagerProvider;
use Doctrine\ORM\Tools\Console\Helper\EntityManagerHelper;
use OutOfBoundsException;
use Symfony\Component\Console\Application;
use Symfony\Component\Console\Command\Command as SymfonyCommand;
use Symfony\Component\Console\Helper\HelperSet;
use function assert;
use function class_exists;
/**
* Handles running the Console Tools inside Symfony Console context.
*/
final class ConsoleRunner
{
/**
* Create a Symfony Console HelperSet
*
* @deprecated This method will be removed in ORM 3.0 without replacement.
*/
public static function createHelperSet(EntityManagerInterface $entityManager): HelperSet
{
$helpers = ['em' => new EntityManagerHelper($entityManager)];
if (class_exists(DBALConsole\Helper\ConnectionHelper::class)) {
$helpers['db'] = new DBALConsole\Helper\ConnectionHelper($entityManager->getConnection());
}
return new HelperSet($helpers);
}
/**
* Runs console with the given helper set.
*
* @param HelperSet|EntityManagerProvider $helperSetOrProvider
* @param SymfonyCommand[] $commands
*/
public static function run($helperSetOrProvider, array $commands = []): void
{
$cli = self::createApplication($helperSetOrProvider, $commands);
$cli->run();
}
/**
* Creates a console application with the given helperset and
* optional commands.
*
* @param HelperSet|EntityManagerProvider $helperSetOrProvider
* @param SymfonyCommand[] $commands
*
* @throws OutOfBoundsException
*/
public static function createApplication($helperSetOrProvider, array $commands = []): Application
{
$version = InstalledVersions::getVersion('doctrine/orm');
assert($version !== null);
$cli = new Application('Doctrine Command Line Interface', $version);
$cli->setCatchExceptions(true);
if ($helperSetOrProvider instanceof HelperSet) {
$cli->setHelperSet($helperSetOrProvider);
$helperSetOrProvider = new HelperSetManagerProvider($helperSetOrProvider);
}
self::addCommands($cli, $helperSetOrProvider);
$cli->addCommands($commands);
return $cli;
}
public static function addCommands(Application $cli, ?EntityManagerProvider $entityManagerProvider = null): void
{
if ($entityManagerProvider === null) {
$entityManagerProvider = new HelperSetManagerProvider($cli->getHelperSet());
}
$connectionProvider = new ConnectionFromManagerProvider($entityManagerProvider);
if (class_exists(DBALConsole\Command\ImportCommand::class)) {
$cli->add(new DBALConsole\Command\ImportCommand());
}
$cli->addCommands(
[
// DBAL Commands
new DBALConsole\Command\ReservedWordsCommand($connectionProvider),
new DBALConsole\Command\RunSqlCommand($connectionProvider),
// ORM Commands
new Command\ClearCache\CollectionRegionCommand($entityManagerProvider),
new Command\ClearCache\EntityRegionCommand($entityManagerProvider),
new Command\ClearCache\MetadataCommand($entityManagerProvider),
new Command\ClearCache\QueryCommand($entityManagerProvider),
new Command\ClearCache\QueryRegionCommand($entityManagerProvider),
new Command\ClearCache\ResultCommand($entityManagerProvider),
new Command\SchemaTool\CreateCommand($entityManagerProvider),
new Command\SchemaTool\UpdateCommand($entityManagerProvider),
new Command\SchemaTool\DropCommand($entityManagerProvider),
new Command\EnsureProductionSettingsCommand($entityManagerProvider),
new Command\ConvertDoctrine1SchemaCommand(),
new Command\GenerateRepositoriesCommand($entityManagerProvider),
new Command\GenerateEntitiesCommand($entityManagerProvider),
new Command\GenerateProxiesCommand($entityManagerProvider),
new Command\ConvertMappingCommand($entityManagerProvider),
new Command\RunDqlCommand($entityManagerProvider),
new Command\ValidateSchemaCommand($entityManagerProvider),
new Command\InfoCommand($entityManagerProvider),
new Command\MappingDescribeCommand($entityManagerProvider),
]
);
}
/** @deprecated This method will be removed in ORM 3.0 without replacement. */
public static function printCliConfigTemplate(): void
{
echo <<<'HELP'
You are missing a "cli-config.php" or "config/cli-config.php" file in your
project, which is required to get the Doctrine Console working. You can use the
following sample as a template:
<?php
use Doctrine\ORM\Tools\Console\ConsoleRunner;
// replace with file to your own project bootstrap
require_once 'bootstrap.php';
// replace with mechanism to retrieve EntityManager in your app
$entityManager = GetEntityManager();
return ConsoleRunner::createHelperSet($entityManager);
HELP;
}
}
@@ -0,0 +1,14 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Tools\Console;
use Doctrine\ORM\EntityManagerInterface;
interface EntityManagerProvider
{
public function getDefaultManager(): EntityManagerInterface;
public function getManager(string $name): EntityManagerInterface;
}
@@ -0,0 +1,30 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Tools\Console\EntityManagerProvider;
use Doctrine\DBAL\Connection;
use Doctrine\DBAL\Tools\Console\ConnectionProvider;
use Doctrine\ORM\Tools\Console\EntityManagerProvider;
final class ConnectionFromManagerProvider implements ConnectionProvider
{
/** @var EntityManagerProvider */
private $entityManagerProvider;
public function __construct(EntityManagerProvider $entityManagerProvider)
{
$this->entityManagerProvider = $entityManagerProvider;
}
public function getDefaultConnection(): Connection
{
return $this->entityManagerProvider->getDefaultManager()->getConnection();
}
public function getConnection(string $name): Connection
{
return $this->entityManagerProvider->getManager($name)->getConnection();
}
}
@@ -0,0 +1,49 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Tools\Console\EntityManagerProvider;
use Doctrine\Deprecations\Deprecation;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\Tools\Console\EntityManagerProvider;
use Doctrine\ORM\Tools\Console\Helper\EntityManagerHelper;
use Symfony\Component\Console\Helper\HelperSet;
use function assert;
/** @deprecated This class will be removed in ORM 3.0 without replacement. */
final class HelperSetManagerProvider implements EntityManagerProvider
{
/** @var HelperSet */
private $helperSet;
public function __construct(HelperSet $helperSet)
{
$this->helperSet = $helperSet;
Deprecation::trigger(
'doctrine/orm',
'https://github.com/doctrine/orm/issues/8327',
'Use of a HelperSet and the HelperSetManagerProvider is deprecated and will be removed in ORM 3.0'
);
}
public function getManager(string $name): EntityManagerInterface
{
if ($name !== 'default') {
throw UnknownManagerException::unknownManager($name, ['default']);
}
return $this->getDefaultManager();
}
public function getDefaultManager(): EntityManagerInterface
{
$helper = $this->helperSet->get('entityManager');
assert($helper instanceof EntityManagerHelper);
return $helper->getEntityManager();
}
}
@@ -0,0 +1,37 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Tools\Console\EntityManagerProvider;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\Tools\Console\EntityManagerProvider;
final class SingleManagerProvider implements EntityManagerProvider
{
/** @var EntityManagerInterface */
private $entityManager;
/** @var string */
private $defaultManagerName;
public function __construct(EntityManagerInterface $entityManager, string $defaultManagerName = 'default')
{
$this->entityManager = $entityManager;
$this->defaultManagerName = $defaultManagerName;
}
public function getDefaultManager(): EntityManagerInterface
{
return $this->entityManager;
}
public function getManager(string $name): EntityManagerInterface
{
if ($name !== $this->defaultManagerName) {
throw UnknownManagerException::unknownManager($name, [$this->defaultManagerName]);
}
return $this->entityManager;
}
}
@@ -0,0 +1,23 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Tools\Console\EntityManagerProvider;
use OutOfBoundsException;
use function implode;
use function sprintf;
final class UnknownManagerException extends OutOfBoundsException
{
/** @psalm-param list<string> $knownManagers */
public static function unknownManager(string $unknownManager, array $knownManagers = []): self
{
return new self(sprintf(
'Requested unknown entity manager: %s, known managers: %s',
$unknownManager,
implode(', ', $knownManagers)
));
}
}
@@ -0,0 +1,75 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Tools\Console\Helper;
use Doctrine\Deprecations\Deprecation;
use Doctrine\ORM\EntityManagerInterface;
use ReflectionMethod;
use Symfony\Component\Console\Helper\Helper;
use Symfony\Component\Console\Helper\HelperInterface;
if ((new ReflectionMethod(HelperInterface::class, 'getName'))->hasReturnType()) {
/** @internal */
trait EntityManagerHelperCompatibility
{
public function getName(): string
{
return 'entityManager';
}
}
} else {
/** @internal */
trait EntityManagerHelperCompatibility
{
/**
* {@inheritDoc}
*
* @return string
*/
public function getName()
{
return 'entityManager';
}
}
}
/**
* Doctrine CLI Connection Helper.
*
* @deprecated This class will be removed in ORM 3.0 without replacement.
*/
class EntityManagerHelper extends Helper
{
use EntityManagerHelperCompatibility;
/**
* Doctrine ORM EntityManagerInterface.
*
* @var EntityManagerInterface
*/
protected $_em;
public function __construct(EntityManagerInterface $em)
{
Deprecation::triggerIfCalledFromOutside(
'doctrine/orm',
'https://github.com/doctrine/orm/pull/9641',
'The %s class is deprecated and will be removed in ORM 3.0',
self::class
);
$this->_em = $em;
}
/**
* Retrieves Doctrine ORM EntityManager.
*
* @return EntityManagerInterface
*/
public function getEntityManager()
{
return $this->_em;
}
}
@@ -0,0 +1,98 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Tools\Console;
use ArrayIterator;
use Countable;
use Doctrine\Persistence\Mapping\ClassMetadata;
use FilterIterator;
use ReturnTypeWillChange;
use RuntimeException;
use function assert;
use function count;
use function iterator_to_array;
use function preg_match;
use function sprintf;
/**
* Used by CLI Tools to restrict entity-based commands to given patterns.
*
* @link www.doctrine-project.com
*/
class MetadataFilter extends FilterIterator implements Countable
{
/** @var mixed[] */
private $filter = [];
/**
* Filter Metadatas by one or more filter options.
*
* @param ClassMetadata[] $metadatas
* @param string[]|string $filter
*
* @return ClassMetadata[]
*/
public static function filter(array $metadatas, $filter)
{
$metadatas = new MetadataFilter(new ArrayIterator($metadatas), $filter);
return iterator_to_array($metadatas);
}
/** @param mixed[]|string $filter */
public function __construct(ArrayIterator $metadata, $filter)
{
$this->filter = (array) $filter;
parent::__construct($metadata);
}
/** @return bool */
#[ReturnTypeWillChange]
public function accept()
{
if (count($this->filter) === 0) {
return true;
}
$it = $this->getInnerIterator();
$metadata = $it->current();
foreach ($this->filter as $filter) {
$pregResult = preg_match('/' . $filter . '/', $metadata->getName());
if ($pregResult === false) {
throw new RuntimeException(
sprintf("Error while evaluating regex '/%s/'.", $filter)
);
}
if ($pregResult) {
return true;
}
}
return false;
}
/** @return ArrayIterator<int, ClassMetadata> */
#[ReturnTypeWillChange]
public function getInnerIterator()
{
$innerIterator = parent::getInnerIterator();
assert($innerIterator instanceof ArrayIterator);
return $innerIterator;
}
/** @return int */
#[ReturnTypeWillChange]
public function count()
{
return count($this->getInnerIterator());
}
}
+336
View File
@@ -0,0 +1,336 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Tools;
use Doctrine\DBAL\Types\Type;
use Doctrine\Deprecations\Deprecation;
use Doctrine\Inflector\InflectorFactory;
use Doctrine\ORM\Mapping\ClassMetadataInfo;
use Symfony\Component\Yaml\Yaml;
use function array_merge;
use function count;
use function explode;
use function file_get_contents;
use function glob;
use function in_array;
use function is_array;
use function is_dir;
use function is_string;
use function preg_match;
use function strtolower;
/**
* Class to help with converting Doctrine 1 schema files to Doctrine 2 mapping files
*
* @deprecated This class is being removed from the ORM and won't have any replacement
*
* @link www.doctrine-project.org
*/
class ConvertDoctrine1Schema
{
/** @var mixed[] */
private $from;
/** @var array<string,string> */
private $legacyTypeMap = [
// TODO: This list may need to be updated
'clob' => 'text',
'timestamp' => 'datetime',
'enum' => 'string',
];
/**
* Constructor passes the directory or array of directories
* to convert the Doctrine 1 schema files from.
*
* @param string[]|string $from
* @psalm-param list<string>|string $from
*/
public function __construct($from)
{
$this->from = (array) $from;
Deprecation::trigger(
'doctrine/orm',
'https://github.com/doctrine/orm/issues/8458',
'%s is deprecated with no replacement',
self::class
);
}
/**
* Gets an array of ClassMetadataInfo instances from the passed
* Doctrine 1 schema.
*
* @return ClassMetadataInfo[] An array of ClassMetadataInfo instances
* @psalm-return list<ClassMetadataInfo>
*/
public function getMetadata()
{
$schema = [];
foreach ($this->from as $path) {
if (is_dir($path)) {
$files = glob($path . '/*.yml');
foreach ($files as $file) {
$schema = array_merge($schema, (array) Yaml::parse(file_get_contents($file)));
}
} else {
$schema = array_merge($schema, (array) Yaml::parse(file_get_contents($path)));
}
}
$metadatas = [];
foreach ($schema as $className => $mappingInformation) {
$metadatas[] = $this->convertToClassMetadataInfo($className, $mappingInformation);
}
return $metadatas;
}
/**
* @param mixed[] $mappingInformation
* @psalm-param class-string $className
*/
private function convertToClassMetadataInfo(
string $className,
array $mappingInformation
): ClassMetadataInfo {
$metadata = new ClassMetadataInfo($className);
$this->convertTableName($className, $mappingInformation, $metadata);
$this->convertColumns($className, $mappingInformation, $metadata);
$this->convertIndexes($className, $mappingInformation, $metadata);
$this->convertRelations($className, $mappingInformation, $metadata);
return $metadata;
}
/** @param mixed[] $model */
private function convertTableName(string $className, array $model, ClassMetadataInfo $metadata): void
{
if (isset($model['tableName']) && $model['tableName']) {
$e = explode('.', $model['tableName']);
if (count($e) > 1) {
$metadata->table['schema'] = $e[0];
$metadata->table['name'] = $e[1];
} else {
$metadata->table['name'] = $e[0];
}
}
}
/** @param mixed[] $model */
private function convertColumns(
string $className,
array $model,
ClassMetadataInfo $metadata
): void {
$id = false;
if (isset($model['columns']) && $model['columns']) {
foreach ($model['columns'] as $name => $column) {
$fieldMapping = $this->convertColumn($className, $name, $column, $metadata);
if (isset($fieldMapping['id']) && $fieldMapping['id']) {
$id = true;
}
}
}
if (! $id) {
$fieldMapping = [
'fieldName' => 'id',
'columnName' => 'id',
'type' => 'integer',
'id' => true,
];
$metadata->mapField($fieldMapping);
$metadata->setIdGeneratorType(ClassMetadataInfo::GENERATOR_TYPE_AUTO);
}
}
/**
* @param string|mixed[] $column
*
* @return mixed[]
*
* @throws ToolsException
*/
private function convertColumn(
string $className,
string $name,
$column,
ClassMetadataInfo $metadata
): array {
if (is_string($column)) {
$string = $column;
$column = [];
$column['type'] = $string;
}
if (! isset($column['name'])) {
$column['name'] = $name;
}
// check if a column alias was used (column_name as field_name)
if (preg_match('/(\w+)\sas\s(\w+)/i', $column['name'], $matches)) {
$name = $matches[1];
$column['name'] = $name;
$column['alias'] = $matches[2];
}
if (preg_match('/([a-zA-Z]+)\(([0-9]+)\)/', $column['type'], $matches)) {
$column['type'] = $matches[1];
$column['length'] = $matches[2];
}
$column['type'] = strtolower($column['type']);
// check if legacy column type (1.x) needs to be mapped to a 2.0 one
if (isset($this->legacyTypeMap[$column['type']])) {
$column['type'] = $this->legacyTypeMap[$column['type']];
}
if (! Type::hasType($column['type'])) {
throw ToolsException::couldNotMapDoctrine1Type($column['type']);
}
$fieldMapping = [
'nullable' => ! ($column['notnull'] ?? true), // Doctrine 1 columns are nullable by default
];
if (isset($column['primary'])) {
$fieldMapping['id'] = true;
}
$fieldMapping['fieldName'] = $column['alias'] ?? $name;
$fieldMapping['columnName'] = $column['name'];
$fieldMapping['type'] = $column['type'];
if (isset($column['length'])) {
$fieldMapping['length'] = $column['length'];
}
$allowed = ['precision', 'scale', 'unique', 'options', 'version'];
foreach ($column as $key => $value) {
if (in_array($key, $allowed, true)) {
$fieldMapping[$key] = $value;
}
}
$metadata->mapField($fieldMapping);
if (isset($column['autoincrement'])) {
$metadata->setIdGeneratorType(ClassMetadataInfo::GENERATOR_TYPE_AUTO);
} elseif (isset($column['sequence'])) {
$metadata->setIdGeneratorType(ClassMetadataInfo::GENERATOR_TYPE_SEQUENCE);
$definition = [
'sequenceName' => (string) (is_array($column['sequence']) ? $column['sequence']['name'] : $column['sequence']),
];
if (isset($column['sequence']['size'])) {
$definition['allocationSize'] = (int) $column['sequence']['size'];
}
if (isset($column['sequence']['value'])) {
$definition['initialValue'] = (int) $column['sequence']['value'];
}
$metadata->setSequenceGeneratorDefinition($definition);
}
return $fieldMapping;
}
/** @param mixed[] $model */
private function convertIndexes(
string $className,
array $model,
ClassMetadataInfo $metadata
): void {
if (empty($model['indexes'])) {
return;
}
foreach ($model['indexes'] as $name => $index) {
$type = isset($index['type']) && $index['type'] === 'unique'
? 'uniqueConstraints' : 'indexes';
$metadata->table[$type][$name] = [
'columns' => $index['fields'],
];
}
}
/** @param mixed[] $model */
private function convertRelations(
string $className,
array $model,
ClassMetadataInfo $metadata
): void {
if (empty($model['relations'])) {
return;
}
$inflector = InflectorFactory::create()->build();
foreach ($model['relations'] as $name => $relation) {
if (! isset($relation['alias'])) {
$relation['alias'] = $name;
}
if (! isset($relation['class'])) {
$relation['class'] = $name;
}
if (! isset($relation['local'])) {
$relation['local'] = $inflector->tableize($relation['class']);
}
if (! isset($relation['foreign'])) {
$relation['foreign'] = 'id';
}
if (! isset($relation['foreignAlias'])) {
$relation['foreignAlias'] = $className;
}
if (isset($relation['refClass'])) {
$type = 'many';
$foreignType = 'many';
$joinColumns = [];
} else {
$type = $relation['type'] ?? 'one';
$foreignType = $relation['foreignType'] ?? 'many';
$joinColumns = [
[
'name' => $relation['local'],
'referencedColumnName' => $relation['foreign'],
'onDelete' => $relation['onDelete'] ?? null,
],
];
}
if ($type === 'one' && $foreignType === 'one') {
$method = 'mapOneToOne';
} elseif ($type === 'many' && $foreignType === 'many') {
$method = 'mapManyToMany';
} else {
$method = 'mapOneToMany';
}
$associationMapping = [];
$associationMapping['fieldName'] = $relation['alias'];
$associationMapping['targetEntity'] = $relation['class'];
$associationMapping['mappedBy'] = $relation['foreignAlias'];
$associationMapping['joinColumns'] = $joinColumns;
$metadata->$method($associationMapping);
}
}
}
+168
View File
@@ -0,0 +1,168 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Tools;
use ArrayIterator;
use ArrayObject;
use DateTimeInterface;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Proxy\DefaultProxyClassNameResolver;
use Doctrine\Persistence\Proxy;
use stdClass;
use function array_keys;
use function count;
use function end;
use function explode;
use function extension_loaded;
use function get_class;
use function html_entity_decode;
use function ini_get;
use function ini_set;
use function is_array;
use function is_object;
use function ob_end_clean;
use function ob_get_contents;
use function ob_start;
use function strip_tags;
use function var_dump;
/**
* Static class containing most used debug methods.
*
* @internal
*
* @link www.doctrine-project.org
*/
final class Debug
{
/**
* Private constructor (prevents instantiation).
*/
private function __construct()
{
}
/**
* Prints a dump of the public, protected and private properties of $var.
*
* @link https://xdebug.org/
*
* @param mixed $var The variable to dump.
* @param int $maxDepth The maximum nesting level for object properties.
*/
public static function dump($var, int $maxDepth = 2): string
{
$html = ini_get('html_errors');
if ($html !== '1') {
ini_set('html_errors', 'on');
}
if (extension_loaded('xdebug')) {
$previousDepth = ini_get('xdebug.var_display_max_depth');
ini_set('xdebug.var_display_max_depth', (string) $maxDepth);
}
try {
$var = self::export($var, $maxDepth);
ob_start();
var_dump($var);
$dump = ob_get_contents();
ob_end_clean();
$dumpText = strip_tags(html_entity_decode($dump));
} finally {
ini_set('html_errors', $html);
if (isset($previousDepth)) {
ini_set('xdebug.var_display_max_depth', $previousDepth);
}
}
return $dumpText;
}
/**
* @param mixed $var
*
* @return mixed
*/
public static function export($var, int $maxDepth)
{
if ($var instanceof Collection) {
$var = $var->toArray();
}
if (! $maxDepth) {
return is_object($var) ? get_class($var)
: (is_array($var) ? 'Array(' . count($var) . ')' : $var);
}
if (is_array($var)) {
$return = [];
foreach ($var as $k => $v) {
$return[$k] = self::export($v, $maxDepth - 1);
}
return $return;
}
if (! is_object($var)) {
return $var;
}
$return = new stdClass();
if ($var instanceof DateTimeInterface) {
$return->__CLASS__ = get_class($var);
$return->date = $var->format('c');
$return->timezone = $var->getTimezone()->getName();
return $return;
}
$return->__CLASS__ = DefaultProxyClassNameResolver::getClass($var);
if ($var instanceof Proxy) {
$return->__IS_PROXY__ = true;
$return->__PROXY_INITIALIZED__ = $var->__isInitialized();
}
if ($var instanceof ArrayObject || $var instanceof ArrayIterator) {
$return->__STORAGE__ = self::export($var->getArrayCopy(), $maxDepth - 1);
}
return self::fillReturnWithClassAttributes($var, $return, $maxDepth);
}
/**
* Fill the $return variable with class attributes
* Based on obj2array function from {@see https://secure.php.net/manual/en/function.get-object-vars.php#47075}
*
* @param object $var
*
* @return mixed
*/
private static function fillReturnWithClassAttributes($var, stdClass $return, int $maxDepth)
{
$clone = (array) $var;
foreach (array_keys($clone) as $key) {
$aux = explode("\0", (string) $key);
$name = end($aux);
if ($aux[0] === '') {
$name .= ':' . ($aux[1] === '*' ? 'protected' : $aux[1] . ':private');
}
$return->$name = self::export($clone[$key], $maxDepth - 1);
}
return $return;
}
}
@@ -0,0 +1,159 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Tools;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\Event\OnFlushEventArgs;
use Doctrine\ORM\Mapping\ClassMetadata;
use Doctrine\ORM\PersistentCollection;
use Doctrine\ORM\UnitOfWork;
use ReflectionObject;
use function count;
use function fclose;
use function fopen;
use function fwrite;
use function gettype;
use function is_object;
use function spl_object_id;
/**
* Use this logger to dump the identity map during the onFlush event. This is useful for debugging
* weird UnitOfWork behavior with complex operations.
*/
class DebugUnitOfWorkListener
{
/** @var string */
private $file;
/** @var string */
private $context;
/**
* Pass a stream and context information for the debugging session.
*
* The stream can be php://output to print to the screen.
*
* @param string $file
* @param string $context
*/
public function __construct($file = 'php://output', $context = '')
{
$this->file = $file;
$this->context = $context;
}
/** @return void */
public function onFlush(OnFlushEventArgs $args)
{
$this->dumpIdentityMap($args->getObjectManager());
}
/**
* Dumps the contents of the identity map into a stream.
*
* @return void
*/
public function dumpIdentityMap(EntityManagerInterface $em)
{
$uow = $em->getUnitOfWork();
$identityMap = $uow->getIdentityMap();
$fh = fopen($this->file, 'xb+');
if (count($identityMap) === 0) {
fwrite($fh, 'Flush Operation [' . $this->context . "] - Empty identity map.\n");
return;
}
fwrite($fh, 'Flush Operation [' . $this->context . "] - Dumping identity map:\n");
foreach ($identityMap as $className => $map) {
fwrite($fh, 'Class: ' . $className . "\n");
foreach ($map as $entity) {
fwrite($fh, ' Entity: ' . $this->getIdString($entity, $uow) . ' ' . spl_object_id($entity) . "\n");
fwrite($fh, " Associations:\n");
$cm = $em->getClassMetadata($className);
foreach ($cm->associationMappings as $field => $assoc) {
fwrite($fh, ' ' . $field . ' ');
$value = $cm->getFieldValue($entity, $field);
if ($assoc['type'] & ClassMetadata::TO_ONE) {
if ($value === null) {
fwrite($fh, " NULL\n");
} else {
if ($uow->isUninitializedObject($value)) {
fwrite($fh, '[PROXY] ');
}
fwrite($fh, $this->getIdString($value, $uow) . ' ' . spl_object_id($value) . "\n");
}
} else {
$initialized = ! ($value instanceof PersistentCollection) || $value->isInitialized();
if ($value === null) {
fwrite($fh, " NULL\n");
} elseif ($initialized) {
fwrite($fh, '[INITIALIZED] ' . $this->getType($value) . ' ' . count($value) . " elements\n");
foreach ($value as $obj) {
fwrite($fh, ' ' . $this->getIdString($obj, $uow) . ' ' . spl_object_id($obj) . "\n");
}
} else {
fwrite($fh, '[PROXY] ' . $this->getType($value) . " unknown element size\n");
foreach ($value->unwrap() as $obj) {
fwrite($fh, ' ' . $this->getIdString($obj, $uow) . ' ' . spl_object_id($obj) . "\n");
}
}
}
}
}
}
fclose($fh);
}
/** @param mixed $var */
private function getType($var): string
{
if (is_object($var)) {
$refl = new ReflectionObject($var);
return $refl->getShortName();
}
return gettype($var);
}
/** @param object $entity */
private function getIdString($entity, UnitOfWork $uow): string
{
if ($uow->isInIdentityMap($entity)) {
$ids = $uow->getEntityIdentifier($entity);
$idstring = '';
foreach ($ids as $k => $v) {
$idstring .= $k . '=' . $v;
}
} else {
$idstring = 'NEWOBJECT ';
}
$state = $uow->getEntityState($entity);
if ($state === UnitOfWork::STATE_NEW) {
$idstring .= ' [NEW]';
} elseif ($state === UnitOfWork::STATE_REMOVED) {
$idstring .= ' [REMOVED]';
} elseif ($state === UnitOfWork::STATE_MANAGED) {
$idstring .= ' [MANAGED]';
} elseif ($state === UnitOfWork::STATE_DETACHED) {
$idstring .= ' [DETACHED]';
}
return $idstring;
}
}
@@ -0,0 +1,27 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Tools;
use Doctrine\ORM\Mapping\ClassMetadataFactory;
use Doctrine\Persistence\Mapping\StaticReflectionService;
/**
* The DisconnectedClassMetadataFactory is used to create ClassMetadataInfo objects
* that do not require the entity class actually exist. This allows us to
* load some mapping information and use it to do things like generate code
* from the mapping information.
*
* @deprecated This class is being removed from the ORM and will be removed in 3.0.
*
* @link www.doctrine-project.org
*/
class DisconnectedClassMetadataFactory extends ClassMetadataFactory
{
/** @return StaticReflectionService */
public function getReflectionService()
{
return new StaticReflectionService();
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,168 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Tools;
use Doctrine\Deprecations\Deprecation;
use Doctrine\ORM\EntityRepository;
use function array_keys;
use function array_values;
use function chmod;
use function dirname;
use function file_exists;
use function file_put_contents;
use function is_dir;
use function mkdir;
use function str_replace;
use function strlen;
use function strrpos;
use function substr;
use const DIRECTORY_SEPARATOR;
/**
* Class to generate entity repository classes
*
* @deprecated 2.7 This class is being removed from the ORM and won't have any replacement
*
* @link www.doctrine-project.org
*/
class EntityRepositoryGenerator
{
/** @psalm-var class-string|null */
private $repositoryName;
/** @var string */
protected static $_template =
'<?php
<namespace>
/**
* <className>
*
* This class was generated by the Doctrine ORM. Add your own custom
* repository methods below.
*/
class <className> extends <repositoryName>
{
}
';
public function __construct()
{
Deprecation::trigger(
'doctrine/orm',
'https://github.com/doctrine/orm/issues/8458',
'%s is deprecated and will be removed in Doctrine ORM 3.0',
self::class
);
}
/**
* @param string $fullClassName
*
* @return string
*/
public function generateEntityRepositoryClass($fullClassName)
{
$variables = [
'<namespace>' => $this->generateEntityRepositoryNamespace($fullClassName),
'<repositoryName>' => $this->generateEntityRepositoryName($fullClassName),
'<className>' => $this->generateClassName($fullClassName),
];
return str_replace(array_keys($variables), array_values($variables), self::$_template);
}
/**
* Generates the namespace, if class do not have namespace, return empty string instead.
*
* @psalm-param class-string $fullClassName
*/
private function getClassNamespace(string $fullClassName): string
{
return substr($fullClassName, 0, (int) strrpos($fullClassName, '\\'));
}
/**
* Generates the class name
*
* @psalm-param class-string $fullClassName
*/
private function generateClassName(string $fullClassName): string
{
$namespace = $this->getClassNamespace($fullClassName);
$className = $fullClassName;
if ($namespace) {
$className = substr($fullClassName, strrpos($fullClassName, '\\') + 1, strlen($fullClassName));
}
return $className;
}
/**
* Generates the namespace statement, if class do not have namespace, return empty string instead.
*
* @psalm-param class-string $fullClassName The full repository class name.
*/
private function generateEntityRepositoryNamespace(string $fullClassName): string
{
$namespace = $this->getClassNamespace($fullClassName);
return $namespace ? 'namespace ' . $namespace . ';' : '';
}
private function generateEntityRepositoryName(string $fullClassName): string
{
$namespace = $this->getClassNamespace($fullClassName);
$repositoryName = $this->repositoryName ?: EntityRepository::class;
if ($namespace && $repositoryName[0] !== '\\') {
$repositoryName = '\\' . $repositoryName;
}
return $repositoryName;
}
/**
* @param string $fullClassName
* @param string $outputDirectory
*
* @return void
*/
public function writeEntityRepositoryClass($fullClassName, $outputDirectory)
{
$code = $this->generateEntityRepositoryClass($fullClassName);
$path = $outputDirectory . DIRECTORY_SEPARATOR
. str_replace('\\', DIRECTORY_SEPARATOR, $fullClassName) . '.php';
$dir = dirname($path);
if (! is_dir($dir)) {
mkdir($dir, 0775, true);
}
if (! file_exists($path)) {
file_put_contents($path, $code);
chmod($path, 0664);
}
}
/**
* @param string $repositoryName
*
* @return $this
*/
public function setDefaultRepositoryName($repositoryName)
{
$this->repositoryName = $repositoryName;
return $this;
}
}
@@ -0,0 +1,41 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Tools\Event;
use Doctrine\Common\EventArgs;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\ORM\EntityManagerInterface;
/**
* Event Args used for the Events::postGenerateSchema event.
*
* @link www.doctrine-project.com
*/
class GenerateSchemaEventArgs extends EventArgs
{
/** @var EntityManagerInterface */
private $em;
/** @var Schema */
private $schema;
public function __construct(EntityManagerInterface $em, Schema $schema)
{
$this->em = $em;
$this->schema = $schema;
}
/** @return EntityManagerInterface */
public function getEntityManager()
{
return $this->em;
}
/** @return Schema */
public function getSchema()
{
return $this->schema;
}
}
@@ -0,0 +1,52 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Tools\Event;
use Doctrine\Common\EventArgs;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\DBAL\Schema\Table;
use Doctrine\ORM\Mapping\ClassMetadata;
/**
* Event Args used for the Events::postGenerateSchemaTable event.
*
* @link www.doctrine-project.com
*/
class GenerateSchemaTableEventArgs extends EventArgs
{
/** @var ClassMetadata */
private $classMetadata;
/** @var Schema */
private $schema;
/** @var Table */
private $classTable;
public function __construct(ClassMetadata $classMetadata, Schema $schema, Table $classTable)
{
$this->classMetadata = $classMetadata;
$this->schema = $schema;
$this->classTable = $classTable;
}
/** @return ClassMetadata */
public function getClassMetadata()
{
return $this->classMetadata;
}
/** @return Schema */
public function getSchema()
{
return $this->schema;
}
/** @return Table */
public function getClassTable()
{
return $this->classTable;
}
}
@@ -0,0 +1,22 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Tools\Exception;
use Doctrine\ORM\Exception\ORMException;
use function sprintf;
final class MissingColumnException extends ORMException
{
public static function fromColumnSourceAndTarget(string $column, string $source, string $target): self
{
return new self(sprintf(
'Column name "%s" referenced for relation from %s towards %s does not exist.',
$column,
$source,
$target
));
}
}
@@ -0,0 +1,16 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Tools\Exception;
use Doctrine\ORM\Exception\ORMException;
use Doctrine\ORM\Exception\SchemaToolException;
final class NotSupported extends ORMException implements SchemaToolException
{
public static function create(): self
{
return new self('This behaviour is (currently) not supported by Doctrine 2');
}
}
@@ -0,0 +1,71 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Tools\Export;
use Doctrine\Deprecations\Deprecation;
/**
* Class used for converting your mapping information between the
* supported formats: yaml, xml, and php/annotation.
*
* @deprecated 2.7 This class is being removed from the ORM and won't have any replacement
*
* @link www.doctrine-project.org
*/
class ClassMetadataExporter
{
/** @var array<string,string> */
private static $_exporterDrivers = [
'xml' => Driver\XmlExporter::class,
'yaml' => Driver\YamlExporter::class,
'yml' => Driver\YamlExporter::class,
'php' => Driver\PhpExporter::class,
'annotation' => Driver\AnnotationExporter::class,
];
public function __construct()
{
Deprecation::trigger(
'doctrine/orm',
'https://github.com/doctrine/orm/issues/8458',
'%s is deprecated with no replacement',
self::class
);
}
/**
* Registers a new exporter driver class under a specified name.
*
* @param string $name
* @param string $class
*
* @return void
*/
public static function registerExportDriver($name, $class)
{
self::$_exporterDrivers[$name] = $class;
}
/**
* Gets an exporter driver instance.
*
* @param string $type The type to get (yml, xml, etc.).
* @param string|null $dest The directory where the exporter will export to.
*
* @return Driver\AbstractExporter
*
* @throws ExportException
*/
public function getExporter($type, $dest = null)
{
if (! isset(self::$_exporterDrivers[$type])) {
throw ExportException::invalidExporterDriverType($type);
}
$class = self::$_exporterDrivers[$type];
return new $class($dest);
}
}
@@ -0,0 +1,261 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Tools\Export\Driver;
use Doctrine\Deprecations\Deprecation;
use Doctrine\ORM\Mapping\ClassMetadata;
use Doctrine\ORM\Mapping\ClassMetadataInfo;
use Doctrine\ORM\Tools\Export\ExportException;
use function chmod;
use function dirname;
use function file_exists;
use function file_put_contents;
use function is_dir;
use function mkdir;
use function str_replace;
/**
* Abstract base class which is to be used for the Exporter drivers
* which can be found in \Doctrine\ORM\Tools\Export\Driver.
*
* @deprecated 2.7 This class is being removed from the ORM and won't have any replacement
*
* @link www.doctrine-project.org
*/
abstract class AbstractExporter
{
/** @var ClassMetadata[] */
protected $_metadata = [];
/** @var string|null */
protected $_outputDir;
/** @var string|null */
protected $_extension;
/** @var bool */
protected $_overwriteExistingFiles = false;
/** @param string|null $dir */
public function __construct($dir = null)
{
Deprecation::trigger(
'doctrine/orm',
'https://github.com/doctrine/orm/issues/8458',
'%s is deprecated with no replacement',
self::class
);
$this->_outputDir = $dir;
}
/**
* @param bool $overwrite
*
* @return void
*/
public function setOverwriteExistingFiles($overwrite)
{
$this->_overwriteExistingFiles = $overwrite;
}
/**
* Converts a single ClassMetadata instance to the exported format
* and returns it.
*
* @return string
*/
abstract public function exportClassMetadata(ClassMetadataInfo $metadata);
/**
* Sets the array of ClassMetadata instances to export.
*
* @psalm-param list<ClassMetadata> $metadata
*
* @return void
*/
public function setMetadata(array $metadata)
{
$this->_metadata = $metadata;
}
/**
* Gets the extension used to generated the path to a class.
*
* @return string|null
*/
public function getExtension()
{
return $this->_extension;
}
/**
* Sets the directory to output the mapping files to.
*
* [php]
* $exporter = new YamlExporter($metadata);
* $exporter->setOutputDir(__DIR__ . '/yaml');
* $exporter->export();
*
* @param string $dir
*
* @return void
*/
public function setOutputDir($dir)
{
$this->_outputDir = $dir;
}
/**
* Exports each ClassMetadata instance to a single Doctrine Mapping file
* named after the entity.
*
* @return void
*
* @throws ExportException
*/
public function export()
{
if (! is_dir($this->_outputDir)) {
mkdir($this->_outputDir, 0775, true);
}
foreach ($this->_metadata as $metadata) {
// In case output is returned, write it to a file, skip otherwise
$output = $this->exportClassMetadata($metadata);
if ($output) {
$path = $this->_generateOutputPath($metadata);
$dir = dirname($path);
if (! is_dir($dir)) {
mkdir($dir, 0775, true);
}
if (file_exists($path) && ! $this->_overwriteExistingFiles) {
throw ExportException::attemptOverwriteExistingFile($path);
}
file_put_contents($path, $output);
chmod($path, 0664);
}
}
}
/**
* Generates the path to write the class for the given ClassMetadataInfo instance.
*
* @return string
*/
protected function _generateOutputPath(ClassMetadataInfo $metadata)
{
return $this->_outputDir . '/' . str_replace('\\', '.', $metadata->name) . $this->_extension;
}
/**
* Sets the directory to output the mapping files to.
*
* [php]
* $exporter = new YamlExporter($metadata, __DIR__ . '/yaml');
* $exporter->setExtension('.yml');
* $exporter->export();
*
* @param string $extension
*
* @return void
*/
public function setExtension($extension)
{
$this->_extension = $extension;
}
/**
* @param int $type
* @psalm-param ClassMetadataInfo::INHERITANCE_TYPE_* $type
*
* @return string
*/
protected function _getInheritanceTypeString($type)
{
switch ($type) {
case ClassMetadataInfo::INHERITANCE_TYPE_NONE:
return 'NONE';
case ClassMetadataInfo::INHERITANCE_TYPE_JOINED:
return 'JOINED';
case ClassMetadataInfo::INHERITANCE_TYPE_SINGLE_TABLE:
return 'SINGLE_TABLE';
case ClassMetadataInfo::INHERITANCE_TYPE_TABLE_PER_CLASS:
return 'PER_CLASS';
}
}
/**
* @param int $mode
* @psalm-param ClassMetadataInfo::FETCH_* $mode
*
* @return string
*/
protected function _getFetchModeString($mode)
{
switch ($mode) {
case ClassMetadataInfo::FETCH_EAGER:
return 'EAGER';
case ClassMetadataInfo::FETCH_EXTRA_LAZY:
return 'EXTRA_LAZY';
case ClassMetadataInfo::FETCH_LAZY:
return 'LAZY';
}
}
/**
* @param int $policy
* @psalm-param ClassMetadataInfo::CHANGETRACKING_* $policy
*
* @return string
*/
protected function _getChangeTrackingPolicyString($policy)
{
switch ($policy) {
case ClassMetadataInfo::CHANGETRACKING_DEFERRED_IMPLICIT:
return 'DEFERRED_IMPLICIT';
case ClassMetadataInfo::CHANGETRACKING_DEFERRED_EXPLICIT:
return 'DEFERRED_EXPLICIT';
case ClassMetadataInfo::CHANGETRACKING_NOTIFY:
return 'NOTIFY';
}
}
/**
* @param int $type
* @psalm-param ClassMetadataInfo::GENERATOR_TYPE_* $type
*
* @return string
*/
protected function _getIdGeneratorTypeString($type)
{
switch ($type) {
case ClassMetadataInfo::GENERATOR_TYPE_AUTO:
return 'AUTO';
case ClassMetadataInfo::GENERATOR_TYPE_SEQUENCE:
return 'SEQUENCE';
case ClassMetadataInfo::GENERATOR_TYPE_IDENTITY:
return 'IDENTITY';
case ClassMetadataInfo::GENERATOR_TYPE_UUID:
return 'UUID';
case ClassMetadataInfo::GENERATOR_TYPE_CUSTOM:
return 'CUSTOM';
}
}
}
@@ -0,0 +1,56 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Tools\Export\Driver;
use Doctrine\ORM\Mapping\ClassMetadataInfo;
use Doctrine\ORM\Tools\EntityGenerator;
use RuntimeException;
use function str_replace;
/**
* ClassMetadata exporter for PHP classes with annotations.
*
* @deprecated 2.7 This class is being removed from the ORM and won't have any replacement
*
* @link www.doctrine-project.org
*/
class AnnotationExporter extends AbstractExporter
{
/** @var string */
protected $_extension = '.php';
/** @var EntityGenerator|null */
private $entityGenerator;
/**
* {@inheritDoc}
*/
public function exportClassMetadata(ClassMetadataInfo $metadata)
{
if (! $this->entityGenerator) {
throw new RuntimeException('For the AnnotationExporter you must set an EntityGenerator instance with the setEntityGenerator() method.');
}
$this->entityGenerator->setGenerateAnnotations(true);
$this->entityGenerator->setGenerateStubMethods(false);
$this->entityGenerator->setRegenerateEntityIfExists(false);
$this->entityGenerator->setUpdateEntityIfExists(false);
return $this->entityGenerator->generateEntityClass($metadata);
}
/** @return string */
protected function _generateOutputPath(ClassMetadataInfo $metadata)
{
return $this->_outputDir . '/' . str_replace('\\', '/', $metadata->name) . $this->_extension;
}
/** @return void */
public function setEntityGenerator(EntityGenerator $entityGenerator)
{
$this->entityGenerator = $entityGenerator;
}
}
@@ -0,0 +1,201 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Tools\Export\Driver;
use Doctrine\ORM\Mapping\ClassMetadataInfo;
use function array_merge;
use function count;
use function implode;
use function sprintf;
use function str_repeat;
use function str_replace;
use function ucfirst;
use function var_export;
use const PHP_EOL;
/**
* ClassMetadata exporter for PHP code.
*
* @deprecated 2.7 This class is being removed from the ORM and won't have any replacement
*
* @link www.doctrine-project.org
*/
class PhpExporter extends AbstractExporter
{
/** @var string */
protected $_extension = '.php';
/**
* {@inheritDoc}
*/
public function exportClassMetadata(ClassMetadataInfo $metadata)
{
$lines = [];
$lines[] = '<?php';
$lines[] = null;
$lines[] = 'use Doctrine\ORM\Mapping\ClassMetadataInfo;';
$lines[] = null;
if ($metadata->isMappedSuperclass) {
$lines[] = '$metadata->isMappedSuperclass = true;';
}
$lines[] = '$metadata->setInheritanceType(ClassMetadataInfo::INHERITANCE_TYPE_' . $this->_getInheritanceTypeString($metadata->inheritanceType) . ');';
if ($metadata->customRepositoryClassName) {
$lines[] = "\$metadata->customRepositoryClassName = '" . $metadata->customRepositoryClassName . "';";
}
if ($metadata->table) {
$lines[] = '$metadata->setPrimaryTable(' . $this->_varExport($metadata->table) . ');';
}
if ($metadata->discriminatorColumn) {
$lines[] = '$metadata->setDiscriminatorColumn(' . $this->_varExport($metadata->discriminatorColumn) . ');';
}
if ($metadata->discriminatorMap) {
$lines[] = '$metadata->setDiscriminatorMap(' . $this->_varExport($metadata->discriminatorMap) . ');';
}
if ($metadata->changeTrackingPolicy) {
$lines[] = '$metadata->setChangeTrackingPolicy(ClassMetadataInfo::CHANGETRACKING_' . $this->_getChangeTrackingPolicyString($metadata->changeTrackingPolicy) . ');';
}
if ($metadata->lifecycleCallbacks) {
foreach ($metadata->lifecycleCallbacks as $event => $callbacks) {
foreach ($callbacks as $callback) {
$lines[] = sprintf("\$metadata->addLifecycleCallback('%s', '%s');", $callback, $event);
}
}
}
$lines = array_merge($lines, $this->processEntityListeners($metadata));
foreach ($metadata->fieldMappings as $fieldMapping) {
$lines[] = '$metadata->mapField(' . $this->_varExport($fieldMapping) . ');';
}
if (! $metadata->isIdentifierComposite) {
$generatorType = $this->_getIdGeneratorTypeString($metadata->generatorType);
if ($generatorType) {
$lines[] = '$metadata->setIdGeneratorType(ClassMetadataInfo::GENERATOR_TYPE_' . $generatorType . ');';
}
}
foreach ($metadata->associationMappings as $associationMapping) {
$cascade = ['remove', 'persist', 'refresh', 'merge', 'detach'];
foreach ($cascade as $key => $value) {
if (! $associationMapping['isCascade' . ucfirst($value)]) {
unset($cascade[$key]);
}
}
if (count($cascade) === 5) {
$cascade = ['all'];
}
$method = null;
$associationMappingArray = [
'fieldName' => $associationMapping['fieldName'],
'targetEntity' => $associationMapping['targetEntity'],
'cascade' => $cascade,
];
if (isset($associationMapping['fetch'])) {
$associationMappingArray['fetch'] = $associationMapping['fetch'];
}
if ($associationMapping['type'] & ClassMetadataInfo::TO_ONE) {
$method = 'mapOneToOne';
$oneToOneMappingArray = [
'mappedBy' => $associationMapping['mappedBy'],
'inversedBy' => $associationMapping['inversedBy'],
'joinColumns' => $associationMapping['isOwningSide'] ? $associationMapping['joinColumns'] : [],
'orphanRemoval' => $associationMapping['orphanRemoval'],
];
$associationMappingArray = array_merge($associationMappingArray, $oneToOneMappingArray);
} elseif ($associationMapping['type'] === ClassMetadataInfo::ONE_TO_MANY) {
$method = 'mapOneToMany';
$potentialAssociationMappingIndexes = [
'mappedBy',
'orphanRemoval',
'orderBy',
];
$oneToManyMappingArray = [];
foreach ($potentialAssociationMappingIndexes as $index) {
if (isset($associationMapping[$index])) {
$oneToManyMappingArray[$index] = $associationMapping[$index];
}
}
$associationMappingArray = array_merge($associationMappingArray, $oneToManyMappingArray);
} elseif ($associationMapping['type'] === ClassMetadataInfo::MANY_TO_MANY) {
$method = 'mapManyToMany';
$potentialAssociationMappingIndexes = [
'mappedBy',
'joinTable',
'orderBy',
];
$manyToManyMappingArray = [];
foreach ($potentialAssociationMappingIndexes as $index) {
if (isset($associationMapping[$index])) {
$manyToManyMappingArray[$index] = $associationMapping[$index];
}
}
$associationMappingArray = array_merge($associationMappingArray, $manyToManyMappingArray);
}
$lines[] = '$metadata->' . $method . '(' . $this->_varExport($associationMappingArray) . ');';
}
return implode("\n", $lines);
}
/**
* @param mixed $var
*
* @return string
*/
protected function _varExport($var)
{
$export = var_export($var, true);
$export = str_replace("\n", PHP_EOL . str_repeat(' ', 8), $export);
$export = str_replace(' ', ' ', $export);
$export = str_replace('array (', 'array(', $export);
$export = str_replace('array( ', 'array(', $export);
$export = str_replace(',)', ')', $export);
$export = str_replace(', )', ')', $export);
$export = str_replace(' ', ' ', $export);
return $export;
}
/**
* @return string[]
* @psalm-return list<string>
*/
private function processEntityListeners(ClassMetadataInfo $metadata): array
{
$lines = [];
foreach ($metadata->entityListeners as $event => $entityListenerConfig) {
foreach ($entityListenerConfig as $entityListener) {
$lines[] = sprintf(
'$metadata->addEntityListener(%s, %s, %s);',
var_export($event, true),
var_export($entityListener['class'], true),
var_export($entityListener['method'], true)
);
}
}
return $lines;
}
}
@@ -0,0 +1,501 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Tools\Export\Driver;
use Doctrine\ORM\Mapping\ClassMetadataInfo;
use DOMDocument;
use SimpleXMLElement;
use function array_search;
use function count;
use function implode;
use function is_array;
use function strcmp;
use function uasort;
/**
* ClassMetadata exporter for Doctrine XML mapping files.
*
* @deprecated 2.7 This class is being removed from the ORM and won't have any replacement
*
* @link www.doctrine-project.org
*/
class XmlExporter extends AbstractExporter
{
/** @var string */
protected $_extension = '.dcm.xml';
/**
* {@inheritDoc}
*/
public function exportClassMetadata(ClassMetadataInfo $metadata)
{
$xml = new SimpleXMLElement('<?xml version="1.0" encoding="utf-8"?><doctrine-mapping ' .
'xmlns="http://doctrine-project.org/schemas/orm/doctrine-mapping" ' .
'xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" ' .
'xsi:schemaLocation="http://doctrine-project.org/schemas/orm/doctrine-mapping https://www.doctrine-project.org/schemas/orm/doctrine-mapping.xsd" />');
if ($metadata->isMappedSuperclass) {
$root = $xml->addChild('mapped-superclass');
} else {
$root = $xml->addChild('entity');
}
if ($metadata->customRepositoryClassName) {
$root->addAttribute('repository-class', $metadata->customRepositoryClassName);
}
$root->addAttribute('name', $metadata->name);
if (isset($metadata->table['name'])) {
$root->addAttribute('table', $metadata->table['name']);
}
if (isset($metadata->table['schema'])) {
$root->addAttribute('schema', $metadata->table['schema']);
}
if ($metadata->inheritanceType !== ClassMetadataInfo::INHERITANCE_TYPE_NONE) {
$root->addAttribute('inheritance-type', $this->_getInheritanceTypeString($metadata->inheritanceType));
}
if (isset($metadata->table['options'])) {
$optionsXml = $root->addChild('options');
$this->exportTableOptions($optionsXml, $metadata->table['options']);
}
if ($metadata->discriminatorColumn) {
$discriminatorColumnXml = $root->addChild('discriminator-column');
$discriminatorColumnXml->addAttribute('name', $metadata->discriminatorColumn['name']);
$discriminatorColumnXml->addAttribute('type', $metadata->discriminatorColumn['type']);
if (isset($metadata->discriminatorColumn['length'])) {
$discriminatorColumnXml->addAttribute('length', (string) $metadata->discriminatorColumn['length']);
}
}
if ($metadata->discriminatorMap) {
$discriminatorMapXml = $root->addChild('discriminator-map');
foreach ($metadata->discriminatorMap as $value => $className) {
$discriminatorMappingXml = $discriminatorMapXml->addChild('discriminator-mapping');
$discriminatorMappingXml->addAttribute('value', (string) $value);
$discriminatorMappingXml->addAttribute('class', $className);
}
}
$trackingPolicy = $this->_getChangeTrackingPolicyString($metadata->changeTrackingPolicy);
if ($trackingPolicy !== 'DEFERRED_IMPLICIT') {
$root->addAttribute('change-tracking-policy', $trackingPolicy);
}
if (isset($metadata->table['indexes'])) {
$indexesXml = $root->addChild('indexes');
foreach ($metadata->table['indexes'] as $name => $index) {
$indexXml = $indexesXml->addChild('index');
$indexXml->addAttribute('name', $name);
$indexXml->addAttribute('columns', implode(',', $index['columns']));
if (isset($index['flags'])) {
$indexXml->addAttribute('flags', implode(',', $index['flags']));
}
}
}
if (isset($metadata->table['uniqueConstraints'])) {
$uniqueConstraintsXml = $root->addChild('unique-constraints');
foreach ($metadata->table['uniqueConstraints'] as $name => $unique) {
$uniqueConstraintXml = $uniqueConstraintsXml->addChild('unique-constraint');
$uniqueConstraintXml->addAttribute('name', $name);
$uniqueConstraintXml->addAttribute('columns', implode(',', $unique['columns']));
}
}
$fields = $metadata->fieldMappings;
$id = [];
foreach ($fields as $name => $field) {
if (isset($field['id']) && $field['id']) {
$id[$name] = $field;
unset($fields[$name]);
}
}
foreach ($metadata->associationMappings as $name => $assoc) {
if (isset($assoc['id']) && $assoc['id']) {
$id[$name] = [
'fieldName' => $name,
'associationKey' => true,
];
}
}
if (! $metadata->isIdentifierComposite) {
$idGeneratorType = $this->_getIdGeneratorTypeString($metadata->generatorType);
if ($idGeneratorType) {
$id[$metadata->getSingleIdentifierFieldName()]['generator']['strategy'] = $idGeneratorType;
}
}
if ($id) {
foreach ($id as $field) {
$idXml = $root->addChild('id');
$idXml->addAttribute('name', $field['fieldName']);
if (isset($field['type'])) {
$idXml->addAttribute('type', $field['type']);
}
if (isset($field['columnName'])) {
$idXml->addAttribute('column', $field['columnName']);
}
if (isset($field['length'])) {
$idXml->addAttribute('length', (string) $field['length']);
}
if (isset($field['associationKey']) && $field['associationKey']) {
$idXml->addAttribute('association-key', 'true');
}
$idGeneratorType = $this->_getIdGeneratorTypeString($metadata->generatorType);
if ($idGeneratorType) {
$generatorXml = $idXml->addChild('generator');
$generatorXml->addAttribute('strategy', $idGeneratorType);
$this->exportSequenceInformation($idXml, $metadata);
}
}
}
if ($fields) {
foreach ($fields as $field) {
$fieldXml = $root->addChild('field');
$fieldXml->addAttribute('name', $field['fieldName']);
$fieldXml->addAttribute('type', $field['type']);
$fieldXml->addAttribute('column', $field['columnName']);
if (isset($field['length'])) {
$fieldXml->addAttribute('length', (string) $field['length']);
}
if (isset($field['precision'])) {
$fieldXml->addAttribute('precision', (string) $field['precision']);
}
if (isset($field['scale'])) {
$fieldXml->addAttribute('scale', (string) $field['scale']);
}
if (isset($field['unique']) && $field['unique']) {
$fieldXml->addAttribute('unique', 'true');
}
if (isset($field['options'])) {
$optionsXml = $fieldXml->addChild('options');
foreach ($field['options'] as $key => $value) {
$optionXml = $optionsXml->addChild('option', (string) $value);
$optionXml->addAttribute('name', $key);
}
}
if (isset($field['version'])) {
$fieldXml->addAttribute('version', $field['version']);
}
if (isset($field['columnDefinition'])) {
$fieldXml->addAttribute('column-definition', $field['columnDefinition']);
}
if (isset($field['nullable'])) {
$fieldXml->addAttribute('nullable', $field['nullable'] ? 'true' : 'false');
}
if (isset($field['notInsertable'])) {
$fieldXml->addAttribute('insertable', 'false');
}
if (isset($field['notUpdatable'])) {
$fieldXml->addAttribute('updatable', 'false');
}
}
}
$orderMap = [
ClassMetadataInfo::ONE_TO_ONE,
ClassMetadataInfo::ONE_TO_MANY,
ClassMetadataInfo::MANY_TO_ONE,
ClassMetadataInfo::MANY_TO_MANY,
];
uasort($metadata->associationMappings, static function ($m1, $m2) use (&$orderMap) {
$a1 = array_search($m1['type'], $orderMap, true);
$a2 = array_search($m2['type'], $orderMap, true);
return strcmp((string) $a1, (string) $a2);
});
foreach ($metadata->associationMappings as $associationMapping) {
$associationMappingXml = null;
if ($associationMapping['type'] === ClassMetadataInfo::ONE_TO_ONE) {
$associationMappingXml = $root->addChild('one-to-one');
} elseif ($associationMapping['type'] === ClassMetadataInfo::MANY_TO_ONE) {
$associationMappingXml = $root->addChild('many-to-one');
} elseif ($associationMapping['type'] === ClassMetadataInfo::ONE_TO_MANY) {
$associationMappingXml = $root->addChild('one-to-many');
} elseif ($associationMapping['type'] === ClassMetadataInfo::MANY_TO_MANY) {
$associationMappingXml = $root->addChild('many-to-many');
}
$associationMappingXml->addAttribute('field', $associationMapping['fieldName']);
$associationMappingXml->addAttribute('target-entity', $associationMapping['targetEntity']);
if (isset($associationMapping['mappedBy'])) {
$associationMappingXml->addAttribute('mapped-by', $associationMapping['mappedBy']);
}
if (isset($associationMapping['inversedBy'])) {
$associationMappingXml->addAttribute('inversed-by', $associationMapping['inversedBy']);
}
if (isset($associationMapping['indexBy'])) {
$associationMappingXml->addAttribute('index-by', $associationMapping['indexBy']);
}
if (isset($associationMapping['orphanRemoval']) && $associationMapping['orphanRemoval'] !== false) {
$associationMappingXml->addAttribute('orphan-removal', 'true');
}
if (isset($associationMapping['fetch'])) {
$associationMappingXml->addAttribute('fetch', $this->_getFetchModeString($associationMapping['fetch']));
}
$cascade = [];
if ($associationMapping['isCascadeRemove']) {
$cascade[] = 'cascade-remove';
}
if ($associationMapping['isCascadePersist']) {
$cascade[] = 'cascade-persist';
}
if ($associationMapping['isCascadeRefresh']) {
$cascade[] = 'cascade-refresh';
}
if ($associationMapping['isCascadeMerge']) {
$cascade[] = 'cascade-merge';
}
if ($associationMapping['isCascadeDetach']) {
$cascade[] = 'cascade-detach';
}
if (count($cascade) === 5) {
$cascade = ['cascade-all'];
}
if ($cascade) {
$cascadeXml = $associationMappingXml->addChild('cascade');
foreach ($cascade as $type) {
$cascadeXml->addChild($type);
}
}
if (isset($associationMapping['joinTable']) && $associationMapping['joinTable']) {
$joinTableXml = $associationMappingXml->addChild('join-table');
$joinTableXml->addAttribute('name', $associationMapping['joinTable']['name']);
$joinColumnsXml = $joinTableXml->addChild('join-columns');
foreach ($associationMapping['joinTable']['joinColumns'] as $joinColumn) {
$joinColumnXml = $joinColumnsXml->addChild('join-column');
$joinColumnXml->addAttribute('name', $joinColumn['name']);
$joinColumnXml->addAttribute('referenced-column-name', $joinColumn['referencedColumnName']);
if (isset($joinColumn['onDelete'])) {
$joinColumnXml->addAttribute('on-delete', $joinColumn['onDelete']);
}
}
$inverseJoinColumnsXml = $joinTableXml->addChild('inverse-join-columns');
foreach ($associationMapping['joinTable']['inverseJoinColumns'] as $inverseJoinColumn) {
$inverseJoinColumnXml = $inverseJoinColumnsXml->addChild('join-column');
$inverseJoinColumnXml->addAttribute('name', $inverseJoinColumn['name']);
$inverseJoinColumnXml->addAttribute('referenced-column-name', $inverseJoinColumn['referencedColumnName']);
if (isset($inverseJoinColumn['onDelete'])) {
$inverseJoinColumnXml->addAttribute('on-delete', $inverseJoinColumn['onDelete']);
}
if (isset($inverseJoinColumn['columnDefinition'])) {
$inverseJoinColumnXml->addAttribute('column-definition', $inverseJoinColumn['columnDefinition']);
}
if (isset($inverseJoinColumn['nullable'])) {
$inverseJoinColumnXml->addAttribute('nullable', $inverseJoinColumn['nullable'] ? 'true' : 'false');
}
if (isset($inverseJoinColumn['orderBy'])) {
$inverseJoinColumnXml->addAttribute('order-by', $inverseJoinColumn['orderBy']);
}
}
}
if (isset($associationMapping['joinColumns'])) {
$joinColumnsXml = $associationMappingXml->addChild('join-columns');
foreach ($associationMapping['joinColumns'] as $joinColumn) {
$joinColumnXml = $joinColumnsXml->addChild('join-column');
$joinColumnXml->addAttribute('name', $joinColumn['name']);
$joinColumnXml->addAttribute('referenced-column-name', $joinColumn['referencedColumnName']);
if (isset($joinColumn['onDelete'])) {
$joinColumnXml->addAttribute('on-delete', $joinColumn['onDelete']);
}
if (isset($joinColumn['columnDefinition'])) {
$joinColumnXml->addAttribute('column-definition', $joinColumn['columnDefinition']);
}
if (isset($joinColumn['nullable'])) {
$joinColumnXml->addAttribute('nullable', $joinColumn['nullable'] ? 'true' : 'false');
}
}
}
if (isset($associationMapping['orderBy'])) {
$orderByXml = $associationMappingXml->addChild('order-by');
foreach ($associationMapping['orderBy'] as $name => $direction) {
$orderByFieldXml = $orderByXml->addChild('order-by-field');
$orderByFieldXml->addAttribute('name', $name);
$orderByFieldXml->addAttribute('direction', $direction);
}
}
}
if (isset($metadata->lifecycleCallbacks) && count($metadata->lifecycleCallbacks) > 0) {
$lifecycleCallbacksXml = $root->addChild('lifecycle-callbacks');
foreach ($metadata->lifecycleCallbacks as $name => $methods) {
foreach ($methods as $method) {
$lifecycleCallbackXml = $lifecycleCallbacksXml->addChild('lifecycle-callback');
$lifecycleCallbackXml->addAttribute('type', $name);
$lifecycleCallbackXml->addAttribute('method', $method);
}
}
}
$this->processEntityListeners($metadata, $root);
return $this->asXml($xml);
}
/**
* Exports (nested) option elements.
*
* @param mixed[] $options
*/
private function exportTableOptions(SimpleXMLElement $parentXml, array $options): void
{
foreach ($options as $name => $option) {
$isArray = is_array($option);
$optionXml = $isArray
? $parentXml->addChild('option')
: $parentXml->addChild('option', (string) $option);
$optionXml->addAttribute('name', (string) $name);
if ($isArray) {
$this->exportTableOptions($optionXml, $option);
}
}
}
/**
* Export sequence information (if available/configured) into the current identifier XML node
*/
private function exportSequenceInformation(SimpleXMLElement $identifierXmlNode, ClassMetadataInfo $metadata): void
{
$sequenceDefinition = $metadata->sequenceGeneratorDefinition;
if (! ($metadata->generatorType === ClassMetadataInfo::GENERATOR_TYPE_SEQUENCE && $sequenceDefinition)) {
return;
}
$sequenceGeneratorXml = $identifierXmlNode->addChild('sequence-generator');
$sequenceGeneratorXml->addAttribute('sequence-name', $sequenceDefinition['sequenceName']);
$sequenceGeneratorXml->addAttribute('allocation-size', $sequenceDefinition['allocationSize']);
$sequenceGeneratorXml->addAttribute('initial-value', $sequenceDefinition['initialValue']);
}
private function asXml(SimpleXMLElement $simpleXml): string
{
$dom = new DOMDocument('1.0', 'UTF-8');
$dom->loadXML($simpleXml->asXML());
$dom->formatOutput = true;
return $dom->saveXML();
}
private function processEntityListeners(ClassMetadataInfo $metadata, SimpleXMLElement $root): void
{
if (count($metadata->entityListeners) === 0) {
return;
}
$entityListenersXml = $root->addChild('entity-listeners');
$entityListenersXmlMap = [];
$this->generateEntityListenerXml($metadata, $entityListenersXmlMap, $entityListenersXml);
}
/** @param mixed[] $entityListenersXmlMap */
private function generateEntityListenerXml(
ClassMetadataInfo $metadata,
array $entityListenersXmlMap,
SimpleXMLElement $entityListenersXml
): void {
foreach ($metadata->entityListeners as $event => $entityListenerConfig) {
foreach ($entityListenerConfig as $entityListener) {
$entityListenerXml = $this->addClassToMapIfExists(
$entityListenersXmlMap,
$entityListener,
$entityListenersXml
);
$entityListenerCallbackXml = $entityListenerXml->addChild('lifecycle-callback');
$entityListenerCallbackXml->addAttribute('type', $event);
$entityListenerCallbackXml->addAttribute('method', $entityListener['method']);
}
}
}
/**
* @param mixed[] $entityListenersXmlMap
* @param mixed[] $entityListener
*/
private function addClassToMapIfExists(
array $entityListenersXmlMap,
array $entityListener,
SimpleXMLElement $entityListenersXml
): SimpleXMLElement {
if (isset($entityListenersXmlMap[$entityListener['class']])) {
return $entityListenersXmlMap[$entityListener['class']];
}
$entityListenerXml = $entityListenersXml->addChild('entity-listener');
$entityListenerXml->addAttribute('class', $entityListener['class']);
$entityListenersXmlMap[$entityListener['class']] = $entityListenerXml;
return $entityListenerXml;
}
}
@@ -0,0 +1,271 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Tools\Export\Driver;
use Doctrine\ORM\Mapping\ClassMetadataInfo;
use Symfony\Component\Yaml\Yaml;
use function array_merge;
use function count;
/**
* ClassMetadata exporter for Doctrine YAML mapping files.
*
* @deprecated 2.7 This class is being removed from the ORM and won't have any replacement
*
* @link www.doctrine-project.org
*/
class YamlExporter extends AbstractExporter
{
/** @var string */
protected $_extension = '.dcm.yml';
/**
* {@inheritDoc}
*/
public function exportClassMetadata(ClassMetadataInfo $metadata)
{
$array = [];
if ($metadata->isMappedSuperclass) {
$array['type'] = 'mappedSuperclass';
} else {
$array['type'] = 'entity';
}
$metadataTable = $metadata->table ?? ['name' => null];
$array['table'] = $metadataTable['name'];
if (isset($metadataTable['schema'])) {
$array['schema'] = $metadataTable['schema'];
}
$inheritanceType = $metadata->inheritanceType;
if ($inheritanceType !== ClassMetadataInfo::INHERITANCE_TYPE_NONE) {
$array['inheritanceType'] = $this->_getInheritanceTypeString($inheritanceType);
}
$column = $metadata->discriminatorColumn;
if ($column) {
$array['discriminatorColumn'] = $column;
}
$map = $metadata->discriminatorMap;
if ($map) {
$array['discriminatorMap'] = $map;
}
if ($metadata->changeTrackingPolicy !== ClassMetadataInfo::CHANGETRACKING_DEFERRED_IMPLICIT) {
$array['changeTrackingPolicy'] = $this->_getChangeTrackingPolicyString($metadata->changeTrackingPolicy);
}
if (isset($metadataTable['indexes'])) {
$array['indexes'] = $metadataTable['indexes'];
}
if ($metadata->customRepositoryClassName) {
$array['repositoryClass'] = $metadata->customRepositoryClassName;
}
if (isset($metadataTable['uniqueConstraints'])) {
$array['uniqueConstraints'] = $metadataTable['uniqueConstraints'];
}
if (isset($metadataTable['options'])) {
$array['options'] = $metadataTable['options'];
}
$fieldMappings = $metadata->fieldMappings;
$ids = [];
foreach ($fieldMappings as $name => $fieldMapping) {
$fieldMapping['column'] = $fieldMapping['columnName'];
unset($fieldMapping['columnName'], $fieldMapping['fieldName']);
if ($fieldMapping['column'] === $name) {
unset($fieldMapping['column']);
}
if (isset($fieldMapping['id']) && $fieldMapping['id']) {
$ids[$name] = $fieldMapping;
unset($fieldMappings[$name]);
continue;
}
$fieldMappings[$name] = $fieldMapping;
}
if (! $metadata->isIdentifierComposite) {
$idGeneratorType = $this->_getIdGeneratorTypeString($metadata->generatorType);
if ($idGeneratorType) {
$ids[$metadata->getSingleIdentifierFieldName()]['generator']['strategy'] = $idGeneratorType;
}
}
$array['id'] = $ids;
if ($fieldMappings) {
$array['fields'] = $fieldMappings;
}
foreach ($metadata->associationMappings as $name => $associationMapping) {
$cascade = [];
if ($associationMapping['isCascadeRemove']) {
$cascade[] = 'remove';
}
if ($associationMapping['isCascadePersist']) {
$cascade[] = 'persist';
}
if ($associationMapping['isCascadeRefresh']) {
$cascade[] = 'refresh';
}
if ($associationMapping['isCascadeMerge']) {
$cascade[] = 'merge';
}
if ($associationMapping['isCascadeDetach']) {
$cascade[] = 'detach';
}
if (count($cascade) === 5) {
$cascade = ['all'];
}
$associationMappingArray = [
'targetEntity' => $associationMapping['targetEntity'],
'cascade' => $cascade,
];
if (isset($associationMapping['fetch'])) {
$associationMappingArray['fetch'] = $this->_getFetchModeString($associationMapping['fetch']);
}
if (isset($associationMapping['id']) && $associationMapping['id'] === true) {
$array['id'][$name]['associationKey'] = true;
}
if ($associationMapping['type'] & ClassMetadataInfo::TO_ONE) {
$joinColumns = $associationMapping['isOwningSide'] ? $associationMapping['joinColumns'] : [];
$newJoinColumns = [];
foreach ($joinColumns as $joinColumn) {
$newJoinColumns[$joinColumn['name']]['referencedColumnName'] = $joinColumn['referencedColumnName'];
if (isset($joinColumn['onDelete'])) {
$newJoinColumns[$joinColumn['name']]['onDelete'] = $joinColumn['onDelete'];
}
}
$oneToOneMappingArray = [
'mappedBy' => $associationMapping['mappedBy'],
'inversedBy' => $associationMapping['inversedBy'],
'joinColumns' => $newJoinColumns,
'orphanRemoval' => $associationMapping['orphanRemoval'],
];
$associationMappingArray = array_merge($associationMappingArray, $oneToOneMappingArray);
if ($associationMapping['type'] & ClassMetadataInfo::ONE_TO_ONE) {
$array['oneToOne'][$name] = $associationMappingArray;
} else {
$array['manyToOne'][$name] = $associationMappingArray;
}
} elseif ($associationMapping['type'] === ClassMetadataInfo::ONE_TO_MANY) {
$oneToManyMappingArray = [
'mappedBy' => $associationMapping['mappedBy'],
'inversedBy' => $associationMapping['inversedBy'],
'orphanRemoval' => $associationMapping['orphanRemoval'],
'orderBy' => $associationMapping['orderBy'] ?? null,
];
$associationMappingArray = array_merge($associationMappingArray, $oneToManyMappingArray);
$array['oneToMany'][$name] = $associationMappingArray;
} elseif ($associationMapping['type'] === ClassMetadataInfo::MANY_TO_MANY) {
$manyToManyMappingArray = [
'mappedBy' => $associationMapping['mappedBy'],
'inversedBy' => $associationMapping['inversedBy'],
'joinTable' => $associationMapping['joinTable'] ?? null,
'orderBy' => $associationMapping['orderBy'] ?? null,
];
$associationMappingArray = array_merge($associationMappingArray, $manyToManyMappingArray);
$array['manyToMany'][$name] = $associationMappingArray;
}
}
if (isset($metadata->lifecycleCallbacks)) {
$array['lifecycleCallbacks'] = $metadata->lifecycleCallbacks;
}
$array = $this->processEntityListeners($metadata, $array);
return $this->yamlDump([$metadata->name => $array], 10);
}
/**
* Dumps a PHP array to a YAML string.
*
* The yamlDump method, when supplied with an array, will do its best
* to convert the array into friendly YAML.
*
* @param mixed[] $array PHP array
* @param int $inline [optional] The level where you switch to inline YAML
*
* @return string A YAML string representing the original PHP array
*/
protected function yamlDump($array, $inline = 2)
{
return Yaml::dump($array, $inline);
}
/**
* @psalm-param array<string, mixed> $array
*
* @psalm-return array<string, mixed>&array{entityListeners: array<class-string, array<string, array{string}>>}
*/
private function processEntityListeners(ClassMetadataInfo $metadata, array $array): array
{
if (count($metadata->entityListeners) === 0) {
return $array;
}
$array['entityListeners'] = [];
foreach ($metadata->entityListeners as $event => $entityListenerConfig) {
$array = $this->processEntityListenerConfig($array, $entityListenerConfig, $event);
}
return $array;
}
/**
* @psalm-param array{entityListeners: array<class-string, array<string, array{string}>>} $array
* @psalm-param list<array{class: class-string, method: string}> $entityListenerConfig
*
* @psalm-return array{entityListeners: array<class-string, array<string, array{string}>>}
*/
private function processEntityListenerConfig(
array $array,
array $entityListenerConfig,
string $event
): array {
foreach ($entityListenerConfig as $entityListener) {
if (! isset($array['entityListeners'][$entityListener['class']])) {
$array['entityListeners'][$entityListener['class']] = [];
}
$array['entityListeners'][$entityListener['class']][$event] = [$entityListener['method']];
}
return $array;
}
}
@@ -0,0 +1,49 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Tools\Export;
use Doctrine\ORM\Exception\ORMException;
use function sprintf;
/** @deprecated 2.7 This class is being removed from the ORM and won't have any replacement */
class ExportException extends ORMException
{
/**
* @param string $type
*
* @return ExportException
*/
public static function invalidExporterDriverType($type)
{
return new self(sprintf(
"The specified export driver '%s' does not exist",
$type
));
}
/**
* @param string $type
*
* @return ExportException
*/
public static function invalidMappingDriverType($type)
{
return new self(sprintf(
"The mapping driver '%s' does not exist",
$type
));
}
/**
* @param string $file
*
* @return ExportException
*/
public static function attemptOverwriteExistingFile($file)
{
return new self("Attempting to overwrite an existing file '" . $file . "'.");
}
}
@@ -0,0 +1,135 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Tools\Pagination;
use Doctrine\DBAL\Platforms\AbstractPlatform;
use Doctrine\DBAL\Platforms\SQLServerPlatform;
use Doctrine\ORM\Query;
use Doctrine\ORM\Query\AST\SelectStatement;
use Doctrine\ORM\Query\Parser;
use Doctrine\ORM\Query\ParserResult;
use Doctrine\ORM\Query\ResultSetMapping;
use Doctrine\ORM\Query\SqlWalker;
use RuntimeException;
use function array_diff;
use function array_keys;
use function count;
use function implode;
use function reset;
use function sprintf;
/**
* Wraps the query in order to accurately count the root objects.
*
* Given a DQL like `SELECT u FROM User u` it will generate an SQL query like:
* SELECT COUNT(*) (SELECT DISTINCT <id> FROM (<original SQL>))
*
* Works with composite keys but cannot deal with queries that have multiple
* root entities (e.g. `SELECT f, b from Foo, Bar`)
*
* Note that the ORDER BY clause is not removed. Many SQL implementations (e.g. MySQL)
* are able to cache subqueries. By keeping the ORDER BY clause intact, the limitSubQuery
* that will most likely be executed next can be read from the native SQL cache.
*
* @psalm-import-type QueryComponent from Parser
*/
class CountOutputWalker extends SqlWalker
{
/** @var AbstractPlatform */
private $platform;
/** @var ResultSetMapping */
private $rsm;
/**
* Stores various parameters that are otherwise unavailable
* because Doctrine\ORM\Query\SqlWalker keeps everything private without
* accessors.
*
* @param Query $query
* @param ParserResult $parserResult
* @param mixed[] $queryComponents
* @psalm-param array<string, QueryComponent> $queryComponents
*/
public function __construct($query, $parserResult, array $queryComponents)
{
$this->platform = $query->getEntityManager()->getConnection()->getDatabasePlatform();
$this->rsm = $parserResult->getResultSetMapping();
parent::__construct($query, $parserResult, $queryComponents);
}
/**
* {@inheritDoc}
*/
public function walkSelectStatement(SelectStatement $AST)
{
if ($this->platform instanceof SQLServerPlatform) {
$AST->orderByClause = null;
}
$sql = parent::walkSelectStatement($AST);
if ($AST->groupByClause) {
return sprintf(
'SELECT COUNT(*) AS dctrn_count FROM (%s) dctrn_table',
$sql
);
}
// Find out the SQL alias of the identifier column of the root entity
// It may be possible to make this work with multiple root entities but that
// would probably require issuing multiple queries or doing a UNION SELECT
// so for now, It's not supported.
// Get the root entity and alias from the AST fromClause
$from = $AST->fromClause->identificationVariableDeclarations;
if (count($from) > 1) {
throw new RuntimeException('Cannot count query which selects two FROM components, cannot make distinction');
}
$fromRoot = reset($from);
$rootAlias = $fromRoot->rangeVariableDeclaration->aliasIdentificationVariable;
$rootClass = $this->getMetadataForDqlAlias($rootAlias);
$rootIdentifier = $rootClass->identifier;
// For every identifier, find out the SQL alias by combing through the ResultSetMapping
$sqlIdentifier = [];
foreach ($rootIdentifier as $property) {
if (isset($rootClass->fieldMappings[$property])) {
foreach (array_keys($this->rsm->fieldMappings, $property, true) as $alias) {
if ($this->rsm->columnOwnerMap[$alias] === $rootAlias) {
$sqlIdentifier[$property] = $alias;
}
}
}
if (isset($rootClass->associationMappings[$property])) {
$joinColumn = $rootClass->associationMappings[$property]['joinColumns'][0]['name'];
foreach (array_keys($this->rsm->metaMappings, $joinColumn, true) as $alias) {
if ($this->rsm->columnOwnerMap[$alias] === $rootAlias) {
$sqlIdentifier[$property] = $alias;
}
}
}
}
if (count($rootIdentifier) !== count($sqlIdentifier)) {
throw new RuntimeException(sprintf(
'Not all identifier properties can be found in the ResultSetMapping: %s',
implode(', ', array_diff($rootIdentifier, array_keys($sqlIdentifier)))
));
}
// Build the counter query
return sprintf(
'SELECT COUNT(*) AS dctrn_count FROM (SELECT DISTINCT %s FROM (%s) dctrn_result) dctrn_table',
implode(', ', $sqlIdentifier),
$sql
);
}
}
@@ -0,0 +1,68 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Tools\Pagination;
use Doctrine\ORM\Query\AST\AggregateExpression;
use Doctrine\ORM\Query\AST\PathExpression;
use Doctrine\ORM\Query\AST\SelectExpression;
use Doctrine\ORM\Query\AST\SelectStatement;
use Doctrine\ORM\Query\TreeWalkerAdapter;
use RuntimeException;
use function count;
use function reset;
/**
* Replaces the selectClause of the AST with a COUNT statement.
*/
class CountWalker extends TreeWalkerAdapter
{
/**
* Distinct mode hint name.
*/
public const HINT_DISTINCT = 'doctrine_paginator.distinct';
public function walkSelectStatement(SelectStatement $AST)
{
if ($AST->havingClause) {
throw new RuntimeException('Cannot count query that uses a HAVING clause. Use the output walkers for pagination');
}
// Get the root entity and alias from the AST fromClause
$from = $AST->fromClause->identificationVariableDeclarations;
if (count($from) > 1) {
throw new RuntimeException('Cannot count query which selects two FROM components, cannot make distinction');
}
$fromRoot = reset($from);
$rootAlias = $fromRoot->rangeVariableDeclaration->aliasIdentificationVariable;
$rootClass = $this->getMetadataForDqlAlias($rootAlias);
$identifierFieldName = $rootClass->getSingleIdentifierFieldName();
$pathType = PathExpression::TYPE_STATE_FIELD;
if (isset($rootClass->associationMappings[$identifierFieldName])) {
$pathType = PathExpression::TYPE_SINGLE_VALUED_ASSOCIATION;
}
$pathExpression = new PathExpression(
PathExpression::TYPE_STATE_FIELD | PathExpression::TYPE_SINGLE_VALUED_ASSOCIATION,
$rootAlias,
$identifierFieldName
);
$pathExpression->type = $pathType;
$distinct = $this->_getQuery()->getHint(self::HINT_DISTINCT);
$AST->selectClause->selectExpressions = [
new SelectExpression(
new AggregateExpression('count', $pathExpression, $distinct),
null
),
];
// ORDER BY is not needed, only increases query execution through unnecessary sorting.
$AST->orderByClause = null;
}
}
@@ -0,0 +1,15 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Tools\Pagination\Exception;
use Doctrine\ORM\Exception\ORMException;
final class RowNumberOverFunctionNotEnabled extends ORMException
{
public static function create(): self
{
return new self('The RowNumberOverFunction is not intended for, nor is it enabled for use in DQL.');
}
}
@@ -0,0 +1,573 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Tools\Pagination;
use Doctrine\DBAL\Platforms\AbstractPlatform;
use Doctrine\DBAL\Platforms\DB2Platform;
use Doctrine\DBAL\Platforms\OraclePlatform;
use Doctrine\DBAL\Platforms\PostgreSQLPlatform;
use Doctrine\DBAL\Platforms\SQLAnywherePlatform;
use Doctrine\DBAL\Platforms\SQLServerPlatform;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\Mapping\QuoteStrategy;
use Doctrine\ORM\OptimisticLockException;
use Doctrine\ORM\Query;
use Doctrine\ORM\Query\AST\OrderByClause;
use Doctrine\ORM\Query\AST\PathExpression;
use Doctrine\ORM\Query\AST\SelectExpression;
use Doctrine\ORM\Query\AST\SelectStatement;
use Doctrine\ORM\Query\Parser;
use Doctrine\ORM\Query\ParserResult;
use Doctrine\ORM\Query\QueryException;
use Doctrine\ORM\Query\ResultSetMapping;
use Doctrine\ORM\Query\SqlWalker;
use RuntimeException;
use function array_diff;
use function array_keys;
use function assert;
use function count;
use function implode;
use function in_array;
use function is_string;
use function method_exists;
use function preg_replace;
use function reset;
use function sprintf;
use function strrpos;
use function substr;
/**
* Wraps the query in order to select root entity IDs for pagination.
*
* Given a DQL like `SELECT u FROM User u` it will generate an SQL query like:
* SELECT DISTINCT <id> FROM (<original SQL>) LIMIT x OFFSET y
*
* Works with composite keys but cannot deal with queries that have multiple
* root entities (e.g. `SELECT f, b from Foo, Bar`)
*
* @psalm-import-type QueryComponent from Parser
*/
class LimitSubqueryOutputWalker extends SqlWalker
{
private const ORDER_BY_PATH_EXPRESSION = '/(?<![a-z0-9_])%s\.%s(?![a-z0-9_])/i';
/** @var AbstractPlatform */
private $platform;
/** @var ResultSetMapping */
private $rsm;
/** @var int */
private $firstResult;
/** @var int */
private $maxResults;
/** @var EntityManagerInterface */
private $em;
/**
* The quote strategy.
*
* @var QuoteStrategy
*/
private $quoteStrategy;
/** @var list<PathExpression> */
private $orderByPathExpressions = [];
/**
* @var bool We don't want to add path expressions from sub-selects into the select clause of the containing query.
* This state flag simply keeps track on whether we are walking on a subquery or not
*/
private $inSubSelect = false;
/**
* Stores various parameters that are otherwise unavailable
* because Doctrine\ORM\Query\SqlWalker keeps everything private without
* accessors.
*
* @param Query $query
* @param ParserResult $parserResult
* @param mixed[] $queryComponents
* @psalm-param array<string, QueryComponent> $queryComponents
*/
public function __construct($query, $parserResult, array $queryComponents)
{
$this->platform = $query->getEntityManager()->getConnection()->getDatabasePlatform();
$this->rsm = $parserResult->getResultSetMapping();
// Reset limit and offset
$this->firstResult = $query->getFirstResult();
$this->maxResults = $query->getMaxResults();
$query->setFirstResult(0)->setMaxResults(null);
$this->em = $query->getEntityManager();
$this->quoteStrategy = $this->em->getConfiguration()->getQuoteStrategy();
parent::__construct($query, $parserResult, $queryComponents);
}
/**
* Check if the platform supports the ROW_NUMBER window function.
*/
private function platformSupportsRowNumber(): bool
{
return $this->platform instanceof PostgreSQLPlatform
|| $this->platform instanceof SQLServerPlatform
|| $this->platform instanceof OraclePlatform
|| $this->platform instanceof SQLAnywherePlatform
|| $this->platform instanceof DB2Platform
|| (method_exists($this->platform, 'supportsRowNumberFunction')
&& $this->platform->supportsRowNumberFunction());
}
/**
* Rebuilds a select statement's order by clause for use in a
* ROW_NUMBER() OVER() expression.
*/
private function rebuildOrderByForRowNumber(SelectStatement $AST): void
{
$orderByClause = $AST->orderByClause;
$selectAliasToExpressionMap = [];
// Get any aliases that are available for select expressions.
foreach ($AST->selectClause->selectExpressions as $selectExpression) {
$selectAliasToExpressionMap[$selectExpression->fieldIdentificationVariable] = $selectExpression->expression;
}
// Rebuild string orderby expressions to use the select expression they're referencing
foreach ($orderByClause->orderByItems as $orderByItem) {
if (is_string($orderByItem->expression) && isset($selectAliasToExpressionMap[$orderByItem->expression])) {
$orderByItem->expression = $selectAliasToExpressionMap[$orderByItem->expression];
}
}
$func = new RowNumberOverFunction('dctrn_rownum');
$func->orderByClause = $AST->orderByClause;
$AST->selectClause->selectExpressions[] = new SelectExpression($func, 'dctrn_rownum', true);
// No need for an order by clause, we'll order by rownum in the outer query.
$AST->orderByClause = null;
}
/**
* {@inheritDoc}
*/
public function walkSelectStatement(SelectStatement $AST)
{
if ($this->platformSupportsRowNumber()) {
return $this->walkSelectStatementWithRowNumber($AST);
}
return $this->walkSelectStatementWithoutRowNumber($AST);
}
/**
* Walks down a SelectStatement AST node, wrapping it in a SELECT DISTINCT.
* This method is for use with platforms which support ROW_NUMBER.
*
* @return string
*
* @throws RuntimeException
*/
public function walkSelectStatementWithRowNumber(SelectStatement $AST)
{
$hasOrderBy = false;
$outerOrderBy = ' ORDER BY dctrn_minrownum ASC';
$orderGroupBy = '';
if ($AST->orderByClause instanceof OrderByClause) {
$hasOrderBy = true;
$this->rebuildOrderByForRowNumber($AST);
}
$innerSql = $this->getInnerSQL($AST);
$sqlIdentifier = $this->getSQLIdentifier($AST);
if ($hasOrderBy) {
$orderGroupBy = ' GROUP BY ' . implode(', ', $sqlIdentifier);
$sqlIdentifier[] = 'MIN(' . $this->walkResultVariable('dctrn_rownum') . ') AS dctrn_minrownum';
}
// Build the counter query
$sql = sprintf(
'SELECT DISTINCT %s FROM (%s) dctrn_result',
implode(', ', $sqlIdentifier),
$innerSql
);
if ($hasOrderBy) {
$sql .= $orderGroupBy . $outerOrderBy;
}
// Apply the limit and offset.
$sql = $this->platform->modifyLimitQuery(
$sql,
$this->maxResults,
$this->firstResult
);
// Add the columns to the ResultSetMapping. It's not really nice but
// it works. Preferably I'd clear the RSM or simply create a new one
// but that is not possible from inside the output walker, so we dirty
// up the one we have.
foreach ($sqlIdentifier as $property => $alias) {
$this->rsm->addScalarResult($alias, $property);
}
return $sql;
}
/**
* Walks down a SelectStatement AST node, wrapping it in a SELECT DISTINCT.
* This method is for platforms which DO NOT support ROW_NUMBER.
*
* @param bool $addMissingItemsFromOrderByToSelect
*
* @return string
*
* @throws RuntimeException
*/
public function walkSelectStatementWithoutRowNumber(SelectStatement $AST, $addMissingItemsFromOrderByToSelect = true)
{
// We don't want to call this recursively!
if ($AST->orderByClause instanceof OrderByClause && $addMissingItemsFromOrderByToSelect) {
// In the case of ordering a query by columns from joined tables, we
// must add those columns to the select clause of the query BEFORE
// the SQL is generated.
$this->addMissingItemsFromOrderByToSelect($AST);
}
// Remove order by clause from the inner query
// It will be re-appended in the outer select generated by this method
$orderByClause = $AST->orderByClause;
$AST->orderByClause = null;
$innerSql = $this->getInnerSQL($AST);
$sqlIdentifier = $this->getSQLIdentifier($AST);
// Build the counter query
$sql = sprintf(
'SELECT DISTINCT %s FROM (%s) dctrn_result',
implode(', ', $sqlIdentifier),
$innerSql
);
// https://github.com/doctrine/orm/issues/2630
$sql = $this->preserveSqlOrdering($sqlIdentifier, $innerSql, $sql, $orderByClause);
// Apply the limit and offset.
$sql = $this->platform->modifyLimitQuery(
$sql,
$this->maxResults,
$this->firstResult
);
// Add the columns to the ResultSetMapping. It's not really nice but
// it works. Preferably I'd clear the RSM or simply create a new one
// but that is not possible from inside the output walker, so we dirty
// up the one we have.
foreach ($sqlIdentifier as $property => $alias) {
$this->rsm->addScalarResult($alias, $property);
}
// Restore orderByClause
$AST->orderByClause = $orderByClause;
return $sql;
}
/**
* Finds all PathExpressions in an AST's OrderByClause, and ensures that
* the referenced fields are present in the SelectClause of the passed AST.
*/
private function addMissingItemsFromOrderByToSelect(SelectStatement $AST): void
{
$this->orderByPathExpressions = [];
// We need to do this in another walker because otherwise we'll end up
// polluting the state of this one.
$walker = clone $this;
// This will populate $orderByPathExpressions via
// LimitSubqueryOutputWalker::walkPathExpression, which will be called
// as the select statement is walked. We'll end up with an array of all
// path expressions referenced in the query.
$walker->walkSelectStatementWithoutRowNumber($AST, false);
$orderByPathExpressions = $walker->getOrderByPathExpressions();
// Get a map of referenced identifiers to field names.
$selects = [];
foreach ($orderByPathExpressions as $pathExpression) {
assert($pathExpression->field !== null);
$idVar = $pathExpression->identificationVariable;
$field = $pathExpression->field;
if (! isset($selects[$idVar])) {
$selects[$idVar] = [];
}
$selects[$idVar][$field] = true;
}
// Loop the select clause of the AST and exclude items from $select
// that are already being selected in the query.
foreach ($AST->selectClause->selectExpressions as $selectExpression) {
if ($selectExpression instanceof SelectExpression) {
$idVar = $selectExpression->expression;
if (! is_string($idVar)) {
continue;
}
$field = $selectExpression->fieldIdentificationVariable;
if ($field === null) {
// No need to add this select, as we're already fetching the whole object.
unset($selects[$idVar]);
} else {
unset($selects[$idVar][$field]);
}
}
}
// Add select items which were not excluded to the AST's select clause.
foreach ($selects as $idVar => $fields) {
$AST->selectClause->selectExpressions[] = new SelectExpression($idVar, null, true);
}
}
/**
* Generates new SQL for statements with an order by clause
*
* @param mixed[] $sqlIdentifier
*/
private function preserveSqlOrdering(
array $sqlIdentifier,
string $innerSql,
string $sql,
?OrderByClause $orderByClause
): string {
// If the sql statement has an order by clause, we need to wrap it in a new select distinct statement
if (! $orderByClause) {
return $sql;
}
// now only select distinct identifier
return sprintf(
'SELECT DISTINCT %s FROM (%s) dctrn_result',
implode(', ', $sqlIdentifier),
$this->recreateInnerSql($orderByClause, $sqlIdentifier, $innerSql)
);
}
/**
* Generates a new SQL statement for the inner query to keep the correct sorting
*
* @param mixed[] $identifiers
*/
private function recreateInnerSql(
OrderByClause $orderByClause,
array $identifiers,
string $innerSql
): string {
[$searchPatterns, $replacements] = $this->generateSqlAliasReplacements();
$orderByItems = [];
foreach ($orderByClause->orderByItems as $orderByItem) {
// Walk order by item to get string representation of it and
// replace path expressions in the order by clause with their column alias
$orderByItemString = preg_replace(
$searchPatterns,
$replacements,
$this->walkOrderByItem($orderByItem)
);
$orderByItems[] = $orderByItemString;
$identifier = substr($orderByItemString, 0, strrpos($orderByItemString, ' '));
if (! in_array($identifier, $identifiers, true)) {
$identifiers[] = $identifier;
}
}
return $sql = sprintf(
'SELECT DISTINCT %s FROM (%s) dctrn_result_inner ORDER BY %s',
implode(', ', $identifiers),
$innerSql,
implode(', ', $orderByItems)
);
}
/**
* @return string[][]
* @psalm-return array{0: list<non-empty-string>, 1: list<string>}
*/
private function generateSqlAliasReplacements(): array
{
$aliasMap = $searchPatterns = $replacements = $metadataList = [];
// Generate DQL alias -> SQL table alias mapping
foreach (array_keys($this->rsm->aliasMap) as $dqlAlias) {
$metadataList[$dqlAlias] = $class = $this->getMetadataForDqlAlias($dqlAlias);
$aliasMap[$dqlAlias] = $this->getSQLTableAlias($class->getTableName(), $dqlAlias);
}
// Generate search patterns for each field's path expression in the order by clause
foreach ($this->rsm->fieldMappings as $fieldAlias => $fieldName) {
$dqlAliasForFieldAlias = $this->rsm->columnOwnerMap[$fieldAlias];
$class = $metadataList[$dqlAliasForFieldAlias];
// If the field is from a joined child table, we won't be ordering on it.
if (! isset($class->fieldMappings[$fieldName])) {
continue;
}
$fieldMapping = $class->fieldMappings[$fieldName];
// Get the proper column name as will appear in the select list
$columnName = $this->quoteStrategy->getColumnName(
$fieldName,
$metadataList[$dqlAliasForFieldAlias],
$this->em->getConnection()->getDatabasePlatform()
);
// Get the SQL table alias for the entity and field
$sqlTableAliasForFieldAlias = $aliasMap[$dqlAliasForFieldAlias];
if (isset($fieldMapping['declared']) && $fieldMapping['declared'] !== $class->name) {
// Field was declared in a parent class, so we need to get the proper SQL table alias
// for the joined parent table.
$otherClassMetadata = $this->em->getClassMetadata($fieldMapping['declared']);
if (! $otherClassMetadata->isMappedSuperclass) {
$sqlTableAliasForFieldAlias = $this->getSQLTableAlias($otherClassMetadata->getTableName(), $dqlAliasForFieldAlias);
}
}
// Compose search and replace patterns
$searchPatterns[] = sprintf(self::ORDER_BY_PATH_EXPRESSION, $sqlTableAliasForFieldAlias, $columnName);
$replacements[] = $fieldAlias;
}
return [$searchPatterns, $replacements];
}
/**
* getter for $orderByPathExpressions
*
* @return list<PathExpression>
*/
public function getOrderByPathExpressions()
{
return $this->orderByPathExpressions;
}
/**
* @throws OptimisticLockException
* @throws QueryException
*/
private function getInnerSQL(SelectStatement $AST): string
{
// Set every select expression as visible(hidden = false) to
// make $AST have scalar mappings properly - this is relevant for referencing selected
// fields from outside the subquery, for example in the ORDER BY segment
$hiddens = [];
foreach ($AST->selectClause->selectExpressions as $idx => $expr) {
$hiddens[$idx] = $expr->hiddenAliasResultVariable;
$expr->hiddenAliasResultVariable = false;
}
$innerSql = parent::walkSelectStatement($AST);
// Restore hiddens
foreach ($AST->selectClause->selectExpressions as $idx => $expr) {
$expr->hiddenAliasResultVariable = $hiddens[$idx];
}
return $innerSql;
}
/** @return string[] */
private function getSQLIdentifier(SelectStatement $AST): array
{
// Find out the SQL alias of the identifier column of the root entity.
// It may be possible to make this work with multiple root entities but that
// would probably require issuing multiple queries or doing a UNION SELECT.
// So for now, it's not supported.
// Get the root entity and alias from the AST fromClause.
$from = $AST->fromClause->identificationVariableDeclarations;
if (count($from) !== 1) {
throw new RuntimeException('Cannot count query which selects two FROM components, cannot make distinction');
}
$fromRoot = reset($from);
$rootAlias = $fromRoot->rangeVariableDeclaration->aliasIdentificationVariable;
$rootClass = $this->getMetadataForDqlAlias($rootAlias);
$rootIdentifier = $rootClass->identifier;
// For every identifier, find out the SQL alias by combing through the ResultSetMapping
$sqlIdentifier = [];
foreach ($rootIdentifier as $property) {
if (isset($rootClass->fieldMappings[$property])) {
foreach (array_keys($this->rsm->fieldMappings, $property, true) as $alias) {
if ($this->rsm->columnOwnerMap[$alias] === $rootAlias) {
$sqlIdentifier[$property] = $alias;
}
}
}
if (isset($rootClass->associationMappings[$property])) {
$joinColumn = $rootClass->associationMappings[$property]['joinColumns'][0]['name'];
foreach (array_keys($this->rsm->metaMappings, $joinColumn, true) as $alias) {
if ($this->rsm->columnOwnerMap[$alias] === $rootAlias) {
$sqlIdentifier[$property] = $alias;
}
}
}
}
if (count($sqlIdentifier) === 0) {
throw new RuntimeException('The Paginator does not support Queries which only yield ScalarResults.');
}
if (count($rootIdentifier) !== count($sqlIdentifier)) {
throw new RuntimeException(sprintf(
'Not all identifier properties can be found in the ResultSetMapping: %s',
implode(', ', array_diff($rootIdentifier, array_keys($sqlIdentifier)))
));
}
return $sqlIdentifier;
}
/**
* {@inheritDoc}
*/
public function walkPathExpression($pathExpr)
{
if (! $this->inSubSelect && ! $this->platformSupportsRowNumber() && ! in_array($pathExpr, $this->orderByPathExpressions, true)) {
$this->orderByPathExpressions[] = $pathExpr;
}
return parent::walkPathExpression($pathExpr);
}
/**
* {@inheritDoc}
*/
public function walkSubSelect($subselect)
{
$this->inSubSelect = true;
$sql = parent::walkSubselect($subselect);
$this->inSubSelect = false;
return $sql;
}
}
@@ -0,0 +1,158 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Tools\Pagination;
use Doctrine\DBAL\Types\Type;
use Doctrine\ORM\Mapping\ClassMetadata;
use Doctrine\ORM\Query;
use Doctrine\ORM\Query\AST\Functions\IdentityFunction;
use Doctrine\ORM\Query\AST\Node;
use Doctrine\ORM\Query\AST\PathExpression;
use Doctrine\ORM\Query\AST\SelectExpression;
use Doctrine\ORM\Query\AST\SelectStatement;
use Doctrine\ORM\Query\TreeWalkerAdapter;
use RuntimeException;
use function count;
use function is_string;
use function reset;
/**
* Replaces the selectClause of the AST with a SELECT DISTINCT root.id equivalent.
*/
class LimitSubqueryWalker extends TreeWalkerAdapter
{
public const IDENTIFIER_TYPE = 'doctrine_paginator.id.type';
public const FORCE_DBAL_TYPE_CONVERSION = 'doctrine_paginator.scalar_result.force_dbal_type_conversion';
/**
* Counter for generating unique order column aliases.
*
* @var int
*/
private $aliasCounter = 0;
public function walkSelectStatement(SelectStatement $AST)
{
// Get the root entity and alias from the AST fromClause
$from = $AST->fromClause->identificationVariableDeclarations;
$fromRoot = reset($from);
$rootAlias = $fromRoot->rangeVariableDeclaration->aliasIdentificationVariable;
$rootClass = $this->getMetadataForDqlAlias($rootAlias);
$this->validate($AST);
$identifier = $rootClass->getSingleIdentifierFieldName();
if (isset($rootClass->associationMappings[$identifier])) {
throw new RuntimeException('Paginating an entity with foreign key as identifier only works when using the Output Walkers. Call Paginator#setUseOutputWalkers(true) before iterating the paginator.');
}
$query = $this->_getQuery();
$query->setHint(
self::IDENTIFIER_TYPE,
Type::getType($rootClass->fieldMappings[$identifier]['type'])
);
$query->setHint(self::FORCE_DBAL_TYPE_CONVERSION, true);
$pathExpression = new PathExpression(
PathExpression::TYPE_STATE_FIELD | PathExpression::TYPE_SINGLE_VALUED_ASSOCIATION,
$rootAlias,
$identifier
);
$pathExpression->type = PathExpression::TYPE_STATE_FIELD;
$AST->selectClause->selectExpressions = [new SelectExpression($pathExpression, '_dctrn_id')];
$AST->selectClause->isDistinct = ($query->getHints()[Paginator::HINT_ENABLE_DISTINCT] ?? true) === true;
if (! isset($AST->orderByClause)) {
return;
}
$queryComponents = $this->_getQueryComponents();
foreach ($AST->orderByClause->orderByItems as $item) {
if ($item->expression instanceof PathExpression) {
$AST->selectClause->selectExpressions[] = new SelectExpression(
$this->createSelectExpressionItem($item->expression),
'_dctrn_ord' . $this->aliasCounter++
);
continue;
}
if (is_string($item->expression) && isset($queryComponents[$item->expression])) {
$qComp = $queryComponents[$item->expression];
if (isset($qComp['resultVariable'])) {
$AST->selectClause->selectExpressions[] = new SelectExpression(
$qComp['resultVariable'],
$item->expression
);
}
}
}
}
/**
* Validate the AST to ensure that this walker is able to properly manipulate it.
*/
private function validate(SelectStatement $AST): void
{
// Prevent LimitSubqueryWalker from being used with queries that include
// a limit, a fetched to-many join, and an order by condition that
// references a column from the fetch joined table.
$queryComponents = $this->getQueryComponents();
$query = $this->_getQuery();
$from = $AST->fromClause->identificationVariableDeclarations;
$fromRoot = reset($from);
if (
$query instanceof Query
&& $query->getMaxResults() !== null
&& $AST->orderByClause
&& count($fromRoot->joins)
) {
// Check each orderby item.
// TODO: check complex orderby items too...
foreach ($AST->orderByClause->orderByItems as $orderByItem) {
$expression = $orderByItem->expression;
if (
$orderByItem->expression instanceof PathExpression
&& isset($queryComponents[$expression->identificationVariable])
) {
$queryComponent = $queryComponents[$expression->identificationVariable];
if (
isset($queryComponent['parent'])
&& isset($queryComponent['relation'])
&& $queryComponent['relation']['type'] & ClassMetadata::TO_MANY
) {
throw new RuntimeException('Cannot select distinct identifiers from query with LIMIT and ORDER BY on a column from a fetch joined to-many association. Use output walkers.');
}
}
}
}
}
/**
* Retrieve either an IdentityFunction (IDENTITY(u.assoc)) or a state field (u.name).
*
* @return IdentityFunction|PathExpression
*/
private function createSelectExpressionItem(PathExpression $pathExpression): Node
{
if ($pathExpression->type === PathExpression::TYPE_SINGLE_VALUED_ASSOCIATION) {
$identity = new IdentityFunction('identity');
$identity->pathExpression = clone $pathExpression;
return $identity;
}
return clone $pathExpression;
}
}
+291
View File
@@ -0,0 +1,291 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Tools\Pagination;
use ArrayIterator;
use Countable;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Internal\SQLResultCasing;
use Doctrine\ORM\NoResultException;
use Doctrine\ORM\Query;
use Doctrine\ORM\Query\Parameter;
use Doctrine\ORM\Query\Parser;
use Doctrine\ORM\Query\ResultSetMapping;
use Doctrine\ORM\QueryBuilder;
use IteratorAggregate;
use ReturnTypeWillChange;
use Traversable;
use function array_key_exists;
use function array_map;
use function array_sum;
use function assert;
use function is_string;
/**
* The paginator can handle various complex scenarios with DQL.
*
* @template-covariant T
* @implements IteratorAggregate<array-key, T>
*/
class Paginator implements Countable, IteratorAggregate
{
use SQLResultCasing;
public const HINT_ENABLE_DISTINCT = 'paginator.distinct.enable';
/** @var Query */
private $query;
/** @var bool */
private $fetchJoinCollection;
/** @var bool|null */
private $useOutputWalkers;
/** @var int|null */
private $count;
/**
* @param Query|QueryBuilder $query A Doctrine ORM query or query builder.
* @param bool $fetchJoinCollection Whether the query joins a collection (true by default).
*/
public function __construct($query, $fetchJoinCollection = true)
{
if ($query instanceof QueryBuilder) {
$query = $query->getQuery();
}
$this->query = $query;
$this->fetchJoinCollection = (bool) $fetchJoinCollection;
}
/**
* Returns the query.
*
* @return Query
*/
public function getQuery()
{
return $this->query;
}
/**
* Returns whether the query joins a collection.
*
* @return bool Whether the query joins a collection.
*/
public function getFetchJoinCollection()
{
return $this->fetchJoinCollection;
}
/**
* Returns whether the paginator will use an output walker.
*
* @return bool|null
*/
public function getUseOutputWalkers()
{
return $this->useOutputWalkers;
}
/**
* Sets whether the paginator will use an output walker.
*
* @param bool|null $useOutputWalkers
*
* @return $this
* @psalm-return static<T>
*/
public function setUseOutputWalkers($useOutputWalkers)
{
$this->useOutputWalkers = $useOutputWalkers;
return $this;
}
/**
* {@inheritDoc}
*
* @return int
*/
#[ReturnTypeWillChange]
public function count()
{
if ($this->count === null) {
try {
$this->count = (int) array_sum(array_map('current', $this->getCountQuery()->getScalarResult()));
} catch (NoResultException $e) {
$this->count = 0;
}
}
return $this->count;
}
/**
* {@inheritDoc}
*
* @return Traversable
* @psalm-return Traversable<array-key, T>
*/
#[ReturnTypeWillChange]
public function getIterator()
{
$offset = $this->query->getFirstResult();
$length = $this->query->getMaxResults();
if ($this->fetchJoinCollection && $length !== null) {
$subQuery = $this->cloneQuery($this->query);
if ($this->useOutputWalker($subQuery)) {
$subQuery->setHint(Query::HINT_CUSTOM_OUTPUT_WALKER, LimitSubqueryOutputWalker::class);
} else {
$this->appendTreeWalker($subQuery, LimitSubqueryWalker::class);
$this->unbindUnusedQueryParams($subQuery);
}
$subQuery->setFirstResult($offset)->setMaxResults($length);
$foundIdRows = $subQuery->getScalarResult();
// don't do this for an empty id array
if ($foundIdRows === []) {
return new ArrayIterator([]);
}
$whereInQuery = $this->cloneQuery($this->query);
$ids = array_map('current', $foundIdRows);
$this->appendTreeWalker($whereInQuery, WhereInWalker::class);
$whereInQuery->setHint(WhereInWalker::HINT_PAGINATOR_HAS_IDS, true);
$whereInQuery->setFirstResult(0)->setMaxResults(null);
$whereInQuery->setCacheable($this->query->isCacheable());
$databaseIds = $this->convertWhereInIdentifiersToDatabaseValues($ids);
$whereInQuery->setParameter(WhereInWalker::PAGINATOR_ID_ALIAS, $databaseIds);
$result = $whereInQuery->getResult($this->query->getHydrationMode());
} else {
$result = $this->cloneQuery($this->query)
->setMaxResults($length)
->setFirstResult($offset)
->setCacheable($this->query->isCacheable())
->getResult($this->query->getHydrationMode());
}
return new ArrayIterator($result);
}
private function cloneQuery(Query $query): Query
{
$cloneQuery = clone $query;
$cloneQuery->setParameters(clone $query->getParameters());
$cloneQuery->setCacheable(false);
foreach ($query->getHints() as $name => $value) {
$cloneQuery->setHint($name, $value);
}
return $cloneQuery;
}
/**
* Determines whether to use an output walker for the query.
*/
private function useOutputWalker(Query $query): bool
{
if ($this->useOutputWalkers === null) {
return (bool) $query->getHint(Query::HINT_CUSTOM_OUTPUT_WALKER) === false;
}
return $this->useOutputWalkers;
}
/**
* Appends a custom tree walker to the tree walkers hint.
*
* @psalm-param class-string $walkerClass
*/
private function appendTreeWalker(Query $query, string $walkerClass): void
{
$hints = $query->getHint(Query::HINT_CUSTOM_TREE_WALKERS);
if ($hints === false) {
$hints = [];
}
$hints[] = $walkerClass;
$query->setHint(Query::HINT_CUSTOM_TREE_WALKERS, $hints);
}
/**
* Returns Query prepared to count.
*/
private function getCountQuery(): Query
{
$countQuery = $this->cloneQuery($this->query);
if (! $countQuery->hasHint(CountWalker::HINT_DISTINCT)) {
$countQuery->setHint(CountWalker::HINT_DISTINCT, true);
}
if ($this->useOutputWalker($countQuery)) {
$platform = $countQuery->getEntityManager()->getConnection()->getDatabasePlatform(); // law of demeter win
$rsm = new ResultSetMapping();
$rsm->addScalarResult($this->getSQLResultCasing($platform, 'dctrn_count'), 'count');
$countQuery->setHint(Query::HINT_CUSTOM_OUTPUT_WALKER, CountOutputWalker::class);
$countQuery->setResultSetMapping($rsm);
} else {
$this->appendTreeWalker($countQuery, CountWalker::class);
$this->unbindUnusedQueryParams($countQuery);
}
$countQuery->setFirstResult(0)->setMaxResults(null);
return $countQuery;
}
private function unbindUnusedQueryParams(Query $query): void
{
$parser = new Parser($query);
$parameterMappings = $parser->parse()->getParameterMappings();
/** @var Collection|Parameter[] $parameters */
$parameters = $query->getParameters();
foreach ($parameters as $key => $parameter) {
$parameterName = $parameter->getName();
if (! (isset($parameterMappings[$parameterName]) || array_key_exists($parameterName, $parameterMappings))) {
unset($parameters[$key]);
}
}
$query->setParameters($parameters);
}
/**
* @param mixed[] $identifiers
*
* @return mixed[]
*/
private function convertWhereInIdentifiersToDatabaseValues(array $identifiers): array
{
$query = $this->cloneQuery($this->query);
$query->setHint(Query::HINT_CUSTOM_OUTPUT_WALKER, RootTypeWalker::class);
$connection = $this->query->getEntityManager()->getConnection();
$type = $query->getSQL();
assert(is_string($type));
return array_map(static function ($id) use ($connection, $type) {
return $connection->convertToDatabaseValue($id, $type);
}, $identifiers);
}
}
@@ -0,0 +1,48 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Tools\Pagination;
use Doctrine\ORM\Query\AST;
use Doctrine\ORM\Query\SqlWalker;
use Doctrine\ORM\Utility\PersisterHelper;
use RuntimeException;
use function count;
use function reset;
/**
* Infers the DBAL type of the #Id (identifier) column of the given query's root entity, and
* returns it in place of a real SQL statement.
*
* Obtaining this type is a necessary intermediate step for \Doctrine\ORM\Tools\Pagination\Paginator.
* We can best do this from a tree walker because it gives us access to the AST.
*
* Returning the type instead of a "real" SQL statement is a slight hack. However, it has the
* benefit that the DQL -> root entity id type resolution can be cached in the query cache.
*/
final class RootTypeWalker extends SqlWalker
{
public function walkSelectStatement(AST\SelectStatement $AST): string
{
// Get the root entity and alias from the AST fromClause
$from = $AST->fromClause->identificationVariableDeclarations;
if (count($from) > 1) {
throw new RuntimeException('Can only process queries that select only one FROM component');
}
$fromRoot = reset($from);
$rootAlias = $fromRoot->rangeVariableDeclaration->aliasIdentificationVariable;
$rootClass = $this->getMetadataForDqlAlias($rootAlias);
$identifierFieldName = $rootClass->getSingleIdentifierFieldName();
return PersisterHelper::getTypeOfField(
$identifierFieldName,
$rootClass,
$this->getQuery()
->getEntityManager()
)[0];
}
}
@@ -0,0 +1,42 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Tools\Pagination;
use Doctrine\ORM\Query\AST\Functions\FunctionNode;
use Doctrine\ORM\Query\AST\OrderByClause;
use Doctrine\ORM\Query\Parser;
use Doctrine\ORM\Query\SqlWalker;
use Doctrine\ORM\Tools\Pagination\Exception\RowNumberOverFunctionNotEnabled;
use function trim;
/**
* RowNumberOverFunction
*
* Provides ROW_NUMBER() OVER(ORDER BY...) construct for use in LimitSubqueryOutputWalker
*/
class RowNumberOverFunction extends FunctionNode
{
/** @var OrderByClause */
public $orderByClause;
/** @inheritDoc */
public function getSql(SqlWalker $sqlWalker)
{
return 'ROW_NUMBER() OVER(' . trim($sqlWalker->walkOrderByClause(
$this->orderByClause
)) . ')';
}
/**
* @throws RowNumberOverFunctionNotEnabled
*
* @inheritDoc
*/
public function parse(Parser $parser)
{
throw RowNumberOverFunctionNotEnabled::create();
}
}
@@ -0,0 +1,116 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Tools\Pagination;
use Doctrine\ORM\Query\AST\ArithmeticExpression;
use Doctrine\ORM\Query\AST\ConditionalExpression;
use Doctrine\ORM\Query\AST\ConditionalPrimary;
use Doctrine\ORM\Query\AST\ConditionalTerm;
use Doctrine\ORM\Query\AST\InListExpression;
use Doctrine\ORM\Query\AST\InputParameter;
use Doctrine\ORM\Query\AST\NullComparisonExpression;
use Doctrine\ORM\Query\AST\PathExpression;
use Doctrine\ORM\Query\AST\SelectStatement;
use Doctrine\ORM\Query\AST\SimpleArithmeticExpression;
use Doctrine\ORM\Query\AST\WhereClause;
use Doctrine\ORM\Query\TreeWalkerAdapter;
use RuntimeException;
use function count;
use function reset;
/**
* Appends a condition equivalent to "WHERE IN (:dpid_1, :dpid_2, ...)" to the whereClause of the AST.
*
* The parameter namespace (dpid) is defined by
* the PAGINATOR_ID_ALIAS
*
* The HINT_PAGINATOR_HAS_IDS query hint indicates whether there are
* any ids in the parameter at all.
*/
class WhereInWalker extends TreeWalkerAdapter
{
/**
* ID Count hint name.
*/
public const HINT_PAGINATOR_HAS_IDS = 'doctrine.paginator_has_ids';
/**
* Primary key alias for query.
*/
public const PAGINATOR_ID_ALIAS = 'dpid';
public function walkSelectStatement(SelectStatement $AST)
{
// Get the root entity and alias from the AST fromClause
$from = $AST->fromClause->identificationVariableDeclarations;
if (count($from) > 1) {
throw new RuntimeException('Cannot count query which selects two FROM components, cannot make distinction');
}
$fromRoot = reset($from);
$rootAlias = $fromRoot->rangeVariableDeclaration->aliasIdentificationVariable;
$rootClass = $this->getMetadataForDqlAlias($rootAlias);
$identifierFieldName = $rootClass->getSingleIdentifierFieldName();
$pathType = PathExpression::TYPE_STATE_FIELD;
if (isset($rootClass->associationMappings[$identifierFieldName])) {
$pathType = PathExpression::TYPE_SINGLE_VALUED_ASSOCIATION;
}
$pathExpression = new PathExpression(PathExpression::TYPE_STATE_FIELD | PathExpression::TYPE_SINGLE_VALUED_ASSOCIATION, $rootAlias, $identifierFieldName);
$pathExpression->type = $pathType;
$hasIds = $this->_getQuery()->getHint(self::HINT_PAGINATOR_HAS_IDS);
if ($hasIds) {
$arithmeticExpression = new ArithmeticExpression();
$arithmeticExpression->simpleArithmeticExpression = new SimpleArithmeticExpression(
[$pathExpression]
);
$expression = new InListExpression(
$arithmeticExpression,
[new InputParameter(':' . self::PAGINATOR_ID_ALIAS)]
);
} else {
$expression = new NullComparisonExpression($pathExpression);
}
$conditionalPrimary = new ConditionalPrimary();
$conditionalPrimary->simpleConditionalExpression = $expression;
if ($AST->whereClause) {
if ($AST->whereClause->conditionalExpression instanceof ConditionalTerm) {
$AST->whereClause->conditionalExpression->conditionalFactors[] = $conditionalPrimary;
} elseif ($AST->whereClause->conditionalExpression instanceof ConditionalPrimary) {
$AST->whereClause->conditionalExpression = new ConditionalExpression(
[
new ConditionalTerm(
[
$AST->whereClause->conditionalExpression,
$conditionalPrimary,
]
),
]
);
} else {
$tmpPrimary = new ConditionalPrimary();
$tmpPrimary->conditionalExpression = $AST->whereClause->conditionalExpression;
$AST->whereClause->conditionalExpression = new ConditionalTerm(
[
$tmpPrimary,
$conditionalPrimary,
]
);
}
} else {
$AST->whereClause = new WhereClause(
new ConditionalExpression(
[new ConditionalTerm([$conditionalPrimary])]
)
);
}
}
}
@@ -0,0 +1,126 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Tools;
use Doctrine\Common\EventSubscriber;
use Doctrine\ORM\Event\LoadClassMetadataEventArgs;
use Doctrine\ORM\Event\OnClassMetadataNotFoundEventArgs;
use Doctrine\ORM\Events;
use Doctrine\ORM\Mapping\ClassMetadata;
use function array_key_exists;
use function array_replace_recursive;
use function ltrim;
/**
* ResolveTargetEntityListener
*
* Mechanism to overwrite interfaces or classes specified as association
* targets.
*
* @psalm-import-type AssociationMapping from ClassMetadata
*/
class ResolveTargetEntityListener implements EventSubscriber
{
/** @var mixed[][] indexed by original entity name */
private $resolveTargetEntities = [];
/**
* {@inheritDoc}
*/
public function getSubscribedEvents()
{
return [
Events::loadClassMetadata,
Events::onClassMetadataNotFound,
];
}
/**
* Adds a target-entity class name to resolve to a new class name.
*
* @param string $originalEntity
* @param string $newEntity
* @psalm-param array<string, mixed> $mapping
*
* @return void
*/
public function addResolveTargetEntity($originalEntity, $newEntity, array $mapping)
{
$mapping['targetEntity'] = ltrim($newEntity, '\\');
$this->resolveTargetEntities[ltrim($originalEntity, '\\')] = $mapping;
}
/**
* @internal this is an event callback, and should not be called directly
*
* @return void
*/
public function onClassMetadataNotFound(OnClassMetadataNotFoundEventArgs $args)
{
if (array_key_exists($args->getClassName(), $this->resolveTargetEntities)) {
$args->setFoundMetadata(
$args
->getObjectManager()
->getClassMetadata($this->resolveTargetEntities[$args->getClassName()]['targetEntity'])
);
}
}
/**
* Processes event and resolves new target entity names.
*
* @internal this is an event callback, and should not be called directly
*
* @return void
*/
public function loadClassMetadata(LoadClassMetadataEventArgs $args)
{
$cm = $args->getClassMetadata();
foreach ($cm->associationMappings as $mapping) {
if (isset($this->resolveTargetEntities[$mapping['targetEntity']])) {
$this->remapAssociation($cm, $mapping);
}
}
foreach ($this->resolveTargetEntities as $interface => $data) {
if ($data['targetEntity'] === $cm->getName()) {
$args->getEntityManager()->getMetadataFactory()->setMetadataFor($interface, $cm);
}
}
foreach ($cm->discriminatorMap as $value => $class) {
if (isset($this->resolveTargetEntities[$class])) {
$cm->addDiscriminatorMapClass($value, $this->resolveTargetEntities[$class]['targetEntity']);
}
}
}
/** @param AssociationMapping $mapping */
private function remapAssociation(ClassMetadata $classMetadata, array $mapping): void
{
$newMapping = $this->resolveTargetEntities[$mapping['targetEntity']];
$newMapping = array_replace_recursive($mapping, $newMapping);
$newMapping['fieldName'] = $mapping['fieldName'];
unset($classMetadata->associationMappings[$mapping['fieldName']]);
switch ($mapping['type']) {
case ClassMetadata::MANY_TO_MANY:
$classMetadata->mapManyToMany($newMapping);
break;
case ClassMetadata::MANY_TO_ONE:
$classMetadata->mapManyToOne($newMapping);
break;
case ClassMetadata::ONE_TO_MANY:
$classMetadata->mapOneToMany($newMapping);
break;
case ClassMetadata::ONE_TO_ONE:
$classMetadata->mapOneToOne($newMapping);
break;
}
}
}
File diff suppressed because it is too large Load Diff
+478
View File
@@ -0,0 +1,478 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Tools;
use BackedEnum;
use Doctrine\DBAL\Types\AsciiStringType;
use Doctrine\DBAL\Types\BigIntType;
use Doctrine\DBAL\Types\BooleanType;
use Doctrine\DBAL\Types\DecimalType;
use Doctrine\DBAL\Types\FloatType;
use Doctrine\DBAL\Types\GuidType;
use Doctrine\DBAL\Types\IntegerType;
use Doctrine\DBAL\Types\JsonType;
use Doctrine\DBAL\Types\SimpleArrayType;
use Doctrine\DBAL\Types\SmallIntType;
use Doctrine\DBAL\Types\StringType;
use Doctrine\DBAL\Types\TextType;
use Doctrine\DBAL\Types\Type;
use Doctrine\Deprecations\Deprecation;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\Mapping\ClassMetadata;
use Doctrine\ORM\Mapping\ClassMetadataInfo;
use ReflectionEnum;
use ReflectionNamedType;
use function array_diff;
use function array_filter;
use function array_key_exists;
use function array_map;
use function array_push;
use function array_search;
use function array_values;
use function assert;
use function class_exists;
use function class_parents;
use function count;
use function get_class;
use function implode;
use function in_array;
use function interface_exists;
use function is_a;
use function sprintf;
use const PHP_VERSION_ID;
/**
* Performs strict validation of the mapping schema
*
* @link www.doctrine-project.com
*
* @psalm-import-type FieldMapping from ClassMetadata
*/
class SchemaValidator
{
/** @var EntityManagerInterface */
private $em;
/** @var bool */
private $validatePropertyTypes;
/**
* It maps built-in Doctrine types to PHP types
*/
private const BUILTIN_TYPES_MAP = [
AsciiStringType::class => 'string',
BigIntType::class => 'string',
BooleanType::class => 'bool',
DecimalType::class => 'string',
FloatType::class => 'float',
GuidType::class => 'string',
IntegerType::class => 'int',
JsonType::class => 'array',
SimpleArrayType::class => 'array',
SmallIntType::class => 'int',
StringType::class => 'string',
TextType::class => 'string',
];
public function __construct(EntityManagerInterface $em, bool $validatePropertyTypes = true)
{
$this->em = $em;
$this->validatePropertyTypes = $validatePropertyTypes;
}
/**
* Checks the internal consistency of all mapping files.
*
* There are several checks that can't be done at runtime or are too expensive, which can be verified
* with this command. For example:
*
* 1. Check if a relation with "mappedBy" is actually connected to that specified field.
* 2. Check if "mappedBy" and "inversedBy" are consistent to each other.
* 3. Check if "referencedColumnName" attributes are really pointing to primary key columns.
*
* @psalm-return array<string, list<string>>
*/
public function validateMapping()
{
$errors = [];
$cmf = $this->em->getMetadataFactory();
$classes = $cmf->getAllMetadata();
foreach ($classes as $class) {
$ce = $this->validateClass($class);
if ($ce) {
$errors[$class->name] = $ce;
}
}
return $errors;
}
/**
* Validates a single class of the current.
*
* @return string[]
* @psalm-return list<string>
*/
public function validateClass(ClassMetadataInfo $class)
{
if (! $class instanceof ClassMetadata) {
Deprecation::trigger(
'doctrine/orm',
'https://github.com/doctrine/orm/pull/249',
'Passing an instance of %s to %s is deprecated, please pass a ClassMetadata instance instead.',
get_class($class),
__METHOD__,
ClassMetadata::class
);
}
$ce = [];
$cmf = $this->em->getMetadataFactory();
foreach ($class->fieldMappings as $fieldName => $mapping) {
if (! Type::hasType($mapping['type'])) {
$ce[] = "The field '" . $class->name . '#' . $fieldName . "' uses a non-existent type '" . $mapping['type'] . "'.";
}
}
// PHP 7.4 introduces the ability to type properties, so we can't validate them in previous versions
if (PHP_VERSION_ID >= 70400 && $this->validatePropertyTypes) {
array_push($ce, ...$this->validatePropertiesTypes($class));
}
if ($class->isEmbeddedClass && count($class->associationMappings) > 0) {
$ce[] = "Embeddable '" . $class->name . "' does not support associations";
return $ce;
}
foreach ($class->associationMappings as $fieldName => $assoc) {
if (! class_exists($assoc['targetEntity']) || $cmf->isTransient($assoc['targetEntity'])) {
$ce[] = "The target entity '" . $assoc['targetEntity'] . "' specified on " . $class->name . '#' . $fieldName . ' is unknown or not an entity.';
return $ce;
}
$targetMetadata = $cmf->getMetadataFor($assoc['targetEntity']);
if ($targetMetadata->isMappedSuperclass) {
$ce[] = "The target entity '" . $assoc['targetEntity'] . "' specified on " . $class->name . '#' . $fieldName . ' is a mapped superclass. This is not possible since there is no table that a foreign key could refer to.';
return $ce;
}
if ($assoc['mappedBy'] && $assoc['inversedBy']) {
$ce[] = 'The association ' . $class . '#' . $fieldName . ' cannot be defined as both inverse and owning.';
}
if (isset($assoc['id']) && $targetMetadata->containsForeignIdentifier) {
$ce[] = "Cannot map association '" . $class->name . '#' . $fieldName . ' as identifier, because ' .
"the target entity '" . $targetMetadata->name . "' also maps an association as identifier.";
}
if ($assoc['mappedBy']) {
if ($targetMetadata->hasField($assoc['mappedBy'])) {
$ce[] = 'The association ' . $class->name . '#' . $fieldName . ' refers to the owning side ' .
'field ' . $assoc['targetEntity'] . '#' . $assoc['mappedBy'] . ' which is not defined as association, but as field.';
}
if (! $targetMetadata->hasAssociation($assoc['mappedBy'])) {
$ce[] = 'The association ' . $class->name . '#' . $fieldName . ' refers to the owning side ' .
'field ' . $assoc['targetEntity'] . '#' . $assoc['mappedBy'] . ' which does not exist.';
} elseif ($targetMetadata->associationMappings[$assoc['mappedBy']]['inversedBy'] === null) {
$ce[] = 'The field ' . $class->name . '#' . $fieldName . ' is on the inverse side of a ' .
'bi-directional relationship, but the specified mappedBy association on the target-entity ' .
$assoc['targetEntity'] . '#' . $assoc['mappedBy'] . ' does not contain the required ' .
"'inversedBy=\"" . $fieldName . "\"' attribute.";
} elseif ($targetMetadata->associationMappings[$assoc['mappedBy']]['inversedBy'] !== $fieldName) {
$ce[] = 'The mappings ' . $class->name . '#' . $fieldName . ' and ' .
$assoc['targetEntity'] . '#' . $assoc['mappedBy'] . ' are ' .
'inconsistent with each other.';
}
}
if ($assoc['inversedBy']) {
if ($targetMetadata->hasField($assoc['inversedBy'])) {
$ce[] = 'The association ' . $class->name . '#' . $fieldName . ' refers to the inverse side ' .
'field ' . $assoc['targetEntity'] . '#' . $assoc['inversedBy'] . ' which is not defined as association.';
}
if (! $targetMetadata->hasAssociation($assoc['inversedBy'])) {
$ce[] = 'The association ' . $class->name . '#' . $fieldName . ' refers to the inverse side ' .
'field ' . $assoc['targetEntity'] . '#' . $assoc['inversedBy'] . ' which does not exist.';
} elseif ($targetMetadata->associationMappings[$assoc['inversedBy']]['mappedBy'] === null) {
$ce[] = 'The field ' . $class->name . '#' . $fieldName . ' is on the owning side of a ' .
'bi-directional relationship, but the specified inversedBy association on the target-entity ' .
$assoc['targetEntity'] . '#' . $assoc['inversedBy'] . ' does not contain the required ' .
"'mappedBy=\"" . $fieldName . "\"' attribute.";
} elseif ($targetMetadata->associationMappings[$assoc['inversedBy']]['mappedBy'] !== $fieldName) {
$ce[] = 'The mappings ' . $class->name . '#' . $fieldName . ' and ' .
$assoc['targetEntity'] . '#' . $assoc['inversedBy'] . ' are ' .
'inconsistent with each other.';
}
// Verify inverse side/owning side match each other
if (array_key_exists($assoc['inversedBy'], $targetMetadata->associationMappings)) {
$targetAssoc = $targetMetadata->associationMappings[$assoc['inversedBy']];
if ($assoc['type'] === ClassMetadata::ONE_TO_ONE && $targetAssoc['type'] !== ClassMetadata::ONE_TO_ONE) {
$ce[] = 'If association ' . $class->name . '#' . $fieldName . ' is one-to-one, then the inversed ' .
'side ' . $targetMetadata->name . '#' . $assoc['inversedBy'] . ' has to be one-to-one as well.';
} elseif ($assoc['type'] === ClassMetadata::MANY_TO_ONE && $targetAssoc['type'] !== ClassMetadata::ONE_TO_MANY) {
$ce[] = 'If association ' . $class->name . '#' . $fieldName . ' is many-to-one, then the inversed ' .
'side ' . $targetMetadata->name . '#' . $assoc['inversedBy'] . ' has to be one-to-many.';
} elseif ($assoc['type'] === ClassMetadata::MANY_TO_MANY && $targetAssoc['type'] !== ClassMetadata::MANY_TO_MANY) {
$ce[] = 'If association ' . $class->name . '#' . $fieldName . ' is many-to-many, then the inversed ' .
'side ' . $targetMetadata->name . '#' . $assoc['inversedBy'] . ' has to be many-to-many as well.';
}
}
}
if ($assoc['isOwningSide']) {
if ($assoc['type'] === ClassMetadata::MANY_TO_MANY) {
$identifierColumns = $class->getIdentifierColumnNames();
foreach ($assoc['joinTable']['joinColumns'] as $joinColumn) {
if (! in_array($joinColumn['referencedColumnName'], $identifierColumns, true)) {
$ce[] = "The referenced column name '" . $joinColumn['referencedColumnName'] . "' " .
"has to be a primary key column on the target entity class '" . $class->name . "'.";
break;
}
}
$identifierColumns = $targetMetadata->getIdentifierColumnNames();
foreach ($assoc['joinTable']['inverseJoinColumns'] as $inverseJoinColumn) {
if (! in_array($inverseJoinColumn['referencedColumnName'], $identifierColumns, true)) {
$ce[] = "The referenced column name '" . $inverseJoinColumn['referencedColumnName'] . "' " .
"has to be a primary key column on the target entity class '" . $targetMetadata->name . "'.";
break;
}
}
if (count($targetMetadata->getIdentifierColumnNames()) !== count($assoc['joinTable']['inverseJoinColumns'])) {
$ce[] = "The inverse join columns of the many-to-many table '" . $assoc['joinTable']['name'] . "' " .
"have to contain to ALL identifier columns of the target entity '" . $targetMetadata->name . "', " .
"however '" . implode(', ', array_diff($targetMetadata->getIdentifierColumnNames(), array_values($assoc['relationToTargetKeyColumns']))) .
"' are missing.";
}
if (count($class->getIdentifierColumnNames()) !== count($assoc['joinTable']['joinColumns'])) {
$ce[] = "The join columns of the many-to-many table '" . $assoc['joinTable']['name'] . "' " .
"have to contain to ALL identifier columns of the source entity '" . $class->name . "', " .
"however '" . implode(', ', array_diff($class->getIdentifierColumnNames(), array_values($assoc['relationToSourceKeyColumns']))) .
"' are missing.";
}
} elseif ($assoc['type'] & ClassMetadata::TO_ONE) {
$identifierColumns = $targetMetadata->getIdentifierColumnNames();
foreach ($assoc['joinColumns'] as $joinColumn) {
if (! in_array($joinColumn['referencedColumnName'], $identifierColumns, true)) {
$ce[] = "The referenced column name '" . $joinColumn['referencedColumnName'] . "' " .
"has to be a primary key column on the target entity class '" . $targetMetadata->name . "'.";
}
}
if (count($identifierColumns) !== count($assoc['joinColumns'])) {
$ids = [];
foreach ($assoc['joinColumns'] as $joinColumn) {
$ids[] = $joinColumn['name'];
}
$ce[] = "The join columns of the association '" . $assoc['fieldName'] . "' " .
"have to match to ALL identifier columns of the target entity '" . $targetMetadata->name . "', " .
"however '" . implode(', ', array_diff($targetMetadata->getIdentifierColumnNames(), $ids)) .
"' are missing.";
}
}
}
if (isset($assoc['orderBy']) && $assoc['orderBy'] !== null) {
foreach ($assoc['orderBy'] as $orderField => $orientation) {
if (! $targetMetadata->hasField($orderField) && ! $targetMetadata->hasAssociation($orderField)) {
$ce[] = 'The association ' . $class->name . '#' . $fieldName . ' is ordered by a foreign field ' .
$orderField . ' that is not a field on the target entity ' . $targetMetadata->name . '.';
continue;
}
if ($targetMetadata->isCollectionValuedAssociation($orderField)) {
$ce[] = 'The association ' . $class->name . '#' . $fieldName . ' is ordered by a field ' .
$orderField . ' on ' . $targetMetadata->name . ' that is a collection-valued association.';
continue;
}
if ($targetMetadata->isAssociationInverseSide($orderField)) {
$ce[] = 'The association ' . $class->name . '#' . $fieldName . ' is ordered by a field ' .
$orderField . ' on ' . $targetMetadata->name . ' that is the inverse side of an association.';
continue;
}
}
}
}
if (
! $class->isInheritanceTypeNone()
&& ! $class->isRootEntity()
&& ($class->reflClass !== null && ! $class->reflClass->isAbstract())
&& ! $class->isMappedSuperclass
&& array_search($class->name, $class->discriminatorMap, true) === false
) {
$ce[] = "Entity class '" . $class->name . "' is part of inheritance hierarchy, but is " .
"not mapped in the root entity '" . $class->rootEntityName . "' discriminator map. " .
'All subclasses must be listed in the discriminator map.';
}
foreach ($class->subClasses as $subClass) {
if (! in_array($class->name, class_parents($subClass), true)) {
$ce[] = "According to the discriminator map class '" . $subClass . "' has to be a child " .
"of '" . $class->name . "' but these entities are not related through inheritance.";
}
}
return $ce;
}
/**
* Checks if the Database Schema is in sync with the current metadata state.
*
* @return bool
*/
public function schemaInSyncWithMetadata()
{
return count($this->getUpdateSchemaList()) === 0;
}
/**
* Returns the list of missing Database Schema updates.
*
* @return array<string>
*/
public function getUpdateSchemaList(): array
{
$schemaTool = new SchemaTool($this->em);
$allMetadata = $this->em->getMetadataFactory()->getAllMetadata();
return $schemaTool->getUpdateSchemaSql($allMetadata, true);
}
/** @return list<string> containing the found issues */
private function validatePropertiesTypes(ClassMetadataInfo $class): array
{
return array_values(
array_filter(
array_map(
/** @param FieldMapping $fieldMapping */
function (array $fieldMapping) use ($class): ?string {
$fieldName = $fieldMapping['fieldName'];
assert(isset($class->reflFields[$fieldName]));
$propertyType = $class->reflFields[$fieldName]->getType();
// If the field type is not a built-in type, we cannot check it
if (! Type::hasType($fieldMapping['type'])) {
return null;
}
// If the property type is not a named type, we cannot check it
if (! ($propertyType instanceof ReflectionNamedType) || $propertyType->getName() === 'mixed') {
return null;
}
$metadataFieldType = $this->findBuiltInType(Type::getType($fieldMapping['type']));
//If the metadata field type is not a mapped built-in type, we cannot check it
if ($metadataFieldType === null) {
return null;
}
$propertyType = $propertyType->getName();
// If the property type is the same as the metadata field type, we are ok
if ($propertyType === $metadataFieldType) {
return null;
}
if (is_a($propertyType, BackedEnum::class, true)) {
$backingType = (string) (new ReflectionEnum($propertyType))->getBackingType();
if ($metadataFieldType !== $backingType) {
return sprintf(
"The field '%s#%s' has the property type '%s' with a backing type of '%s' that differs from the metadata field type '%s'.",
$class->name,
$fieldName,
$propertyType,
$backingType,
$metadataFieldType
);
}
if (! isset($fieldMapping['enumType']) || $propertyType === $fieldMapping['enumType']) {
return null;
}
return sprintf(
"The field '%s#%s' has the property type '%s' that differs from the metadata enumType '%s'.",
$class->name,
$fieldName,
$propertyType,
$fieldMapping['enumType']
);
}
if (
isset($fieldMapping['enumType'])
&& $propertyType !== $fieldMapping['enumType']
&& interface_exists($propertyType)
&& is_a($fieldMapping['enumType'], $propertyType, true)
) {
$backingType = (string) (new ReflectionEnum($fieldMapping['enumType']))->getBackingType();
if ($metadataFieldType === $backingType) {
return null;
}
return sprintf(
"The field '%s#%s' has the metadata enumType '%s' with a backing type of '%s' that differs from the metadata field type '%s'.",
$class->name,
$fieldName,
$fieldMapping['enumType'],
$backingType,
$metadataFieldType
);
}
if (
$fieldMapping['type'] === 'json'
&& in_array($propertyType, ['string', 'int', 'float', 'bool', 'true', 'false', 'null'], true)
) {
return null;
}
return sprintf(
"The field '%s#%s' has the property type '%s' that differs from the metadata field type '%s' returned by the '%s' DBAL type.",
$class->name,
$fieldName,
$propertyType,
$metadataFieldType,
$fieldMapping['type']
);
},
$class->fieldMappings
)
)
);
}
/**
* The exact DBAL type must be used (no subclasses), since consumers of doctrine/orm may have their own
* customization around field types.
*/
private function findBuiltInType(Type $type): ?string
{
$typeName = get_class($type);
return self::BUILTIN_TYPES_MAP[$typeName] ?? null;
}
}
+268
View File
@@ -0,0 +1,268 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Tools;
use Doctrine\Common\Cache\ApcuCache;
use Doctrine\Common\Cache\ArrayCache;
use Doctrine\Common\Cache\Cache;
use Doctrine\Common\Cache\CacheProvider;
use Doctrine\Common\Cache\MemcachedCache;
use Doctrine\Common\Cache\Psr6\CacheAdapter;
use Doctrine\Common\Cache\Psr6\DoctrineProvider;
use Doctrine\Common\Cache\RedisCache;
use Doctrine\Common\ClassLoader;
use Doctrine\Deprecations\Deprecation;
use Doctrine\ORM\Configuration;
use Doctrine\ORM\Mapping\Driver\AttributeDriver;
use Doctrine\ORM\Mapping\Driver\XmlDriver;
use Doctrine\ORM\Mapping\Driver\YamlDriver;
use Doctrine\ORM\ORMSetup;
use Memcached;
use Redis;
use RuntimeException;
use Symfony\Component\Cache\Adapter\ApcuAdapter;
use Symfony\Component\Cache\Adapter\ArrayAdapter;
use Symfony\Component\Cache\Adapter\MemcachedAdapter;
use Symfony\Component\Cache\Adapter\RedisAdapter;
use function apcu_enabled;
use function class_exists;
use function dirname;
use function extension_loaded;
use function file_exists;
use function md5;
use function sys_get_temp_dir;
/**
* Convenience class for setting up Doctrine from different installations and configurations.
*
* @deprecated Use {@see ORMSetup} instead.
*/
class Setup
{
/**
* Use this method to register all autoloads for a downloaded Doctrine library.
* Pick the directory the library was uncompressed into.
*
* @deprecated Use Composer's autoloader instead.
*
* @param string $directory
*
* @return void
*/
public static function registerAutoloadDirectory($directory)
{
if (! class_exists('Doctrine\Common\ClassLoader', false)) {
if (file_exists($directory . '/Doctrine/Common/ClassLoader.php')) {
require_once $directory . '/Doctrine/Common/ClassLoader.php';
} elseif (file_exists(dirname($directory) . '/src/ClassLoader.php')) {
require_once dirname($directory) . '/src/ClassLoader.php';
}
}
$loader = new ClassLoader('Doctrine', $directory);
$loader->register();
$loader = new ClassLoader('Symfony\Component', $directory . '/Doctrine');
$loader->register();
}
/**
* Creates a configuration with an annotation metadata driver.
*
* @param string[] $paths
* @param bool $isDevMode
* @param string|null $proxyDir
* @param bool $useSimpleAnnotationReader
*
* @return Configuration
*/
public static function createAnnotationMetadataConfiguration(array $paths, $isDevMode = false, $proxyDir = null, ?Cache $cache = null, $useSimpleAnnotationReader = true)
{
Deprecation::trigger(
'doctrine/orm',
'https://github.com/doctrine/orm/pull/9443',
'%s is deprecated and will be removed in Doctrine 3.0, please use %s instead.',
self::class,
ORMSetup::class
);
$config = self::createConfiguration($isDevMode, $proxyDir, $cache);
$config->setMetadataDriverImpl($config->newDefaultAnnotationDriver($paths, $useSimpleAnnotationReader));
return $config;
}
/**
* Creates a configuration with an attribute metadata driver.
*
* @param string[] $paths
* @param bool $isDevMode
* @param string|null $proxyDir
*/
public static function createAttributeMetadataConfiguration(
array $paths,
$isDevMode = false,
$proxyDir = null,
?Cache $cache = null
): Configuration {
Deprecation::trigger(
'doctrine/orm',
'https://github.com/doctrine/orm/pull/9443',
'%s is deprecated and will be removed in Doctrine 3.0, please use %s instead.',
self::class,
ORMSetup::class
);
$config = self::createConfiguration($isDevMode, $proxyDir, $cache);
$config->setMetadataDriverImpl(new AttributeDriver($paths));
return $config;
}
/**
* Creates a configuration with an XML metadata driver.
*
* @param string[] $paths
* @param bool $isDevMode
* @param string|null $proxyDir
*
* @return Configuration
*/
public static function createXMLMetadataConfiguration(array $paths, $isDevMode = false, $proxyDir = null, ?Cache $cache = null)
{
Deprecation::trigger(
'doctrine/orm',
'https://github.com/doctrine/orm/pull/9443',
'%s is deprecated and will be removed in Doctrine 3.0, please use %s instead.',
self::class,
ORMSetup::class
);
$config = self::createConfiguration($isDevMode, $proxyDir, $cache);
$config->setMetadataDriverImpl(new XmlDriver($paths));
return $config;
}
/**
* Creates a configuration with a YAML metadata driver.
*
* @deprecated YAML metadata mapping is deprecated and will be removed in 3.0
*
* @param string[] $paths
* @param bool $isDevMode
* @param string|null $proxyDir
*
* @return Configuration
*/
public static function createYAMLMetadataConfiguration(array $paths, $isDevMode = false, $proxyDir = null, ?Cache $cache = null)
{
Deprecation::trigger(
'doctrine/orm',
'https://github.com/doctrine/orm/issues/8465',
'YAML mapping driver is deprecated and will be removed in Doctrine ORM 3.0, please migrate to attribute or XML driver.'
);
$config = self::createConfiguration($isDevMode, $proxyDir, $cache);
$config->setMetadataDriverImpl(new YamlDriver($paths));
return $config;
}
/**
* Creates a configuration without a metadata driver.
*
* @param bool $isDevMode
* @param string|null $proxyDir
*
* @return Configuration
*/
public static function createConfiguration($isDevMode = false, $proxyDir = null, ?Cache $cache = null)
{
Deprecation::triggerIfCalledFromOutside(
'doctrine/orm',
'https://github.com/doctrine/orm/pull/9443',
'%s is deprecated and will be removed in Doctrine 3.0, please use %s instead.',
self::class,
ORMSetup::class
);
$proxyDir = $proxyDir ?: sys_get_temp_dir();
$cache = self::createCacheConfiguration($isDevMode, $proxyDir, $cache);
$config = new Configuration();
$config->setMetadataCache(CacheAdapter::wrap($cache));
$config->setQueryCache(CacheAdapter::wrap($cache));
$config->setResultCache(CacheAdapter::wrap($cache));
$config->setProxyDir($proxyDir);
$config->setProxyNamespace('DoctrineProxies');
$config->setAutoGenerateProxyClasses($isDevMode);
return $config;
}
private static function createCacheConfiguration(bool $isDevMode, string $proxyDir, ?Cache $cache): Cache
{
$cache = self::createCacheInstance($isDevMode, $cache);
if (! $cache instanceof CacheProvider) {
return $cache;
}
$namespace = $cache->getNamespace();
if ($namespace !== '') {
$namespace .= ':';
}
$cache->setNamespace($namespace . 'dc2_' . md5($proxyDir) . '_'); // to avoid collisions
return $cache;
}
private static function createCacheInstance(bool $isDevMode, ?Cache $cache): Cache
{
if ($cache !== null) {
return $cache;
}
if (! class_exists(ArrayCache::class) && ! class_exists(ArrayAdapter::class)) {
throw new RuntimeException('Setup tool cannot configure caches without doctrine/cache 1.11 or symfony/cache. Please add an explicit dependency to either library.');
}
if ($isDevMode === true) {
$cache = class_exists(ArrayCache::class) ? new ArrayCache() : new ArrayAdapter();
} elseif (extension_loaded('apcu') && apcu_enabled()) {
$cache = class_exists(ApcuCache::class) ? new ApcuCache() : new ApcuAdapter();
} elseif (extension_loaded('memcached') && (class_exists(MemcachedCache::class) || MemcachedAdapter::isSupported())) {
$memcached = new Memcached();
$memcached->addServer('127.0.0.1', 11211);
if (class_exists(MemcachedCache::class)) {
$cache = new MemcachedCache();
$cache->setMemcached($memcached);
} else {
$cache = new MemcachedAdapter($memcached);
}
} elseif (extension_loaded('redis')) {
$redis = new Redis();
$redis->connect('127.0.0.1');
if (class_exists(RedisCache::class)) {
$cache = new RedisCache();
$cache->setRedis($redis);
} else {
$cache = new RedisAdapter($redis);
}
} else {
$cache = class_exists(ArrayCache::class) ? new ArrayCache() : new ArrayAdapter();
}
return $cache instanceof Cache ? $cache : DoctrineProvider::wrap($cache);
}
}
+23
View File
@@ -0,0 +1,23 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Tools;
class ToolEvents
{
/**
* The postGenerateSchemaTable event occurs in SchemaTool#getSchemaFromMetadata()
* whenever an entity class is transformed into its table representation. It receives
* the current non-complete Schema instance, the Entity Metadata Class instance and
* the Schema Table instance of this entity.
*/
public const postGenerateSchemaTable = 'postGenerateSchemaTable';
/**
* The postGenerateSchema event is triggered in SchemaTool#getSchemaFromMetadata()
* after all entity classes have been transformed into the related Schema structure.
* The EventArgs contain the EntityManager and the created Schema instance.
*/
public const postGenerateSchema = 'postGenerateSchema';
}
+35
View File
@@ -0,0 +1,35 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Tools;
use Doctrine\ORM\Exception\ORMException;
use Throwable;
use function sprintf;
/**
* Tools related Exceptions.
*/
class ToolsException extends ORMException
{
public static function schemaToolFailure(string $sql, Throwable $e): self
{
return new self(
"Schema-Tool failed with Error '" . $e->getMessage() . "' while executing DDL: " . $sql,
0,
$e
);
}
/**
* @param string $type
*
* @return ToolsException
*/
public static function couldNotMapDoctrine1Type($type)
{
return new self(sprintf("Could not map doctrine 1 type '%s'!", $type));
}
}