welcome back to dyb-tech
This commit is contained in:
@@ -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']);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+49
@@ -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'));
|
||||
}
|
||||
}
|
||||
+124
@@ -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;
|
||||
}
|
||||
}
|
||||
+115
@@ -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;
|
||||
}
|
||||
}
|
||||
+106
@@ -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;
|
||||
}
|
||||
}
|
||||
+190
@@ -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);
|
||||
}
|
||||
}
|
||||
+58
@@ -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;
|
||||
}
|
||||
}
|
||||
+105
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
Vendored
+30
@@ -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();
|
||||
}
|
||||
}
|
||||
+49
@@ -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();
|
||||
}
|
||||
}
|
||||
+37
@@ -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;
|
||||
}
|
||||
}
|
||||
+23
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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();
|
||||
}
|
||||
}
|
||||
+1910
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;
|
||||
}
|
||||
}
|
||||
+15
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
+1040
File diff suppressed because it is too large
Load Diff
+478
@@ -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
@@ -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
@@ -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';
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user