This commit is contained in:
Xes
2025-08-14 22:41:49 +02:00
parent 2de81ccc46
commit 8ce45119b6
39774 changed files with 4309466 additions and 0 deletions

View File

@@ -0,0 +1,79 @@
<?php
/*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* This software consists of voluntary contributions made by many individuals
* and is licensed under the MIT license. For more information, see
* <http://www.doctrine-project.org>.
*/
namespace Doctrine\ORM\Tools;
use Doctrine\ORM\Event\LoadClassMetadataEventArgs;
use Doctrine\ORM\Mapping\ClassMetadata;
/**
* Mechanism to programmatically attach entity listeners.
*
* @author Fabio B. SIlva <fabio.bat.silva@gmail.com>
*
* @since 2.5
*/
class AttachEntityListenersListener
{
/**
* @var array[]
*/
private $entityListeners = [];
/**
* Adds a entity listener for a specific entity.
*
* @param string $entityClass The entity to attach the listener.
* @param string $listenerClass The listener class.
* @param string $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.
*
* @param \Doctrine\ORM\Event\LoadClassMetadataEventArgs $event
*
* @return void
*/
public function loadClassMetadata(LoadClassMetadataEventArgs $event)
{
$metadata = $event->getClassMetadata();
if ( ! isset($this->entityListeners[$metadata->name])) {
return;
}
foreach ($this->entityListeners[$metadata->name] as $listener) {
$metadata->addEntityListener($listener['event'], $listener['class'], $listener['method']);
}
unset($this->entityListeners[$metadata->name]);
}
}

View File

@@ -0,0 +1,149 @@
<?php
/*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* This software consists of voluntary contributions made by many individuals
* and is licensed under the MIT license. For more information, see
* <http://www.doctrine-project.org>.
*/
namespace Doctrine\ORM\Tools\Console\Command\ClearCache;
use Doctrine\ORM\Cache;
use Doctrine\ORM\Cache\Region\DefaultRegion;
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;
/**
* Command to clear a collection cache region.
*
* @since 2.5
* @author Fabio B. Silva <fabio.bat.silva@gmail.com>
*/
class CollectionRegionCommand extends Command
{
/**
* {@inheritdoc}
*/
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('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
);
}
/**
* {@inheritdoc}
*/
protected function execute(InputInterface $input, OutputInterface $output)
{
$ui = new SymfonyStyle($input, $output);
$em = $this->getHelper('em')->getEntityManager();
$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')) {
$collectionRegion = $cache->getCollectionCacheRegion($ownerClass, $assoc);
if ( ! $collectionRegion instanceof DefaultRegion) {
throw new \InvalidArgumentException(sprintf(
'The option "--flush" expects a "Doctrine\ORM\Cache\Region\DefaultRegion", but got "%s".',
is_object($collectionRegion) ? get_class($collectionRegion) : gettype($collectionRegion)
));
}
$collectionRegion->getCache()->flushAll();
$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;
}
}

View File

@@ -0,0 +1,140 @@
<?php
/*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* This software consists of voluntary contributions made by many individuals
* and is licensed under the MIT license. For more information, see
* <http://www.doctrine-project.org>.
*/
namespace Doctrine\ORM\Tools\Console\Command\ClearCache;
use Doctrine\ORM\Cache;
use Doctrine\ORM\Cache\Region\DefaultRegion;
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;
/**
* Command to clear a entity cache region.
*
* @since 2.5
* @author Fabio B. Silva <fabio.bat.silva@gmail.com>
*/
class EntityRegionCommand extends Command
{
/**
* {@inheritdoc}
*/
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('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
);
}
/**
* {@inheritdoc}
*/
protected function execute(InputInterface $input, OutputInterface $output)
{
$ui = new SymfonyStyle($input, $output);
$em = $this->getHelper('em')->getEntityManager();
$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')) {
$entityRegion = $cache->getEntityCacheRegion($entityClass);
if ( ! $entityRegion instanceof DefaultRegion) {
throw new \InvalidArgumentException(sprintf(
'The option "--flush" expects a "Doctrine\ORM\Cache\Region\DefaultRegion", but got "%s".',
is_object($entityRegion) ? get_class($entityRegion) : gettype($entityRegion)
));
}
$entityRegion->getCache()->flushAll();
$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;
}
}

View File

@@ -0,0 +1,112 @@
<?php
/*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* This software consists of voluntary contributions made by many individuals
* and is licensed under the MIT license. For more information, see
* <http://www.doctrine-project.org>.
*/
namespace Doctrine\ORM\Tools\Console\Command\ClearCache;
use Doctrine\Common\Cache\ApcCache;
use Doctrine\Common\Cache\XcacheCache;
use Symfony\Component\Console\Command\Command;
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
* @since 2.0
* @author Benjamin Eberlei <kontakt@beberlei.de>
* @author Guilherme Blanco <guilhermeblanco@hotmail.com>
* @author Jonathan Wage <jonwage@gmail.com>
* @author Roman Borschel <roman@code-factory.org>
*/
class MetadataCommand extends Command
{
/**
* {@inheritdoc}
*/
protected function configure()
{
$this->setName('orm:clear-cache:metadata')
->setDescription('Clear all metadata cache of the various cache drivers')
->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.
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
);
}
/**
* {@inheritdoc}
*/
protected function execute(InputInterface $input, OutputInterface $output)
{
$ui = new SymfonyStyle($input, $output);
$em = $this->getHelper('em')->getEntityManager();
$cacheDriver = $em->getConfiguration()->getMetadataCacheImpl();
if ( ! $cacheDriver) {
throw new \InvalidArgumentException('No Metadata cache driver is configured on given EntityManager.');
}
if ($cacheDriver instanceof ApcCache) {
throw new \LogicException("Cannot clear APC Cache from Console, its shared in the Webserver memory and not accessible from the CLI.");
}
if ($cacheDriver instanceof XcacheCache) {
throw new \LogicException("Cannot clear XCache Cache from Console, its shared in the Webserver memory and not accessible from the CLI.");
}
$ui->comment('Clearing <info>all</info> Metadata cache entries');
$result = $cacheDriver->deleteAll();
$message = ($result) ? 'Successfully deleted cache entries.' : 'No cache entries were deleted.';
if (true === $input->getOption('flush')) {
$result = $cacheDriver->flushAll();
$message = ($result) ? 'Successfully flushed cache entries.' : $message;
}
if ( ! $result) {
$ui->error($message);
return 1;
}
$ui->success($message);
return 0;
}
}

View File

@@ -0,0 +1,111 @@
<?php
/*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* This software consists of voluntary contributions made by many individuals
* and is licensed under the MIT license. For more information, see
* <http://www.doctrine-project.org>.
*/
namespace Doctrine\ORM\Tools\Console\Command\ClearCache;
use Doctrine\Common\Cache\ApcCache;
use Doctrine\Common\Cache\XcacheCache;
use Symfony\Component\Console\Command\Command;
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 query cache of the various cache drivers.
*
* @link www.doctrine-project.org
* @since 2.0
* @author Benjamin Eberlei <kontakt@beberlei.de>
* @author Guilherme Blanco <guilhermeblanco@hotmail.com>
* @author Jonathan Wage <jonwage@gmail.com>
* @author Roman Borschel <roman@code-factory.org>
*/
class QueryCommand extends Command
{
/**
* {@inheritdoc}
*/
protected function configure()
{
$this->setName('orm:clear-cache:query')
->setDescription('Clear all query cache of the various cache drivers')
->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
);
}
/**
* {@inheritdoc}
*/
protected function execute(InputInterface $input, OutputInterface $output)
{
$ui = new SymfonyStyle($input, $output);
$em = $this->getHelper('em')->getEntityManager();
$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 APC Cache from Console, its shared in the Webserver memory and not accessible from the CLI.");
}
if ($cacheDriver instanceof XcacheCache) {
throw new \LogicException("Cannot clear XCache Cache from Console, its shared in the Webserver memory and not accessible from the CLI.");
}
$ui->comment('Clearing <info>all</info> Query cache entries');
$result = $cacheDriver->deleteAll();
$message = ($result) ? 'Successfully deleted cache entries.' : 'No cache entries were deleted.';
if (true === $input->getOption('flush')) {
$result = $cacheDriver->flushAll();
$message = ($result) ? 'Successfully flushed cache entries.' : $message;
}
if ( ! $result) {
$ui->error($message);
return 1;
}
$ui->success($message);
return 0;
}
}

View File

@@ -0,0 +1,131 @@
<?php
/*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* This software consists of voluntary contributions made by many individuals
* and is licensed under the MIT license. For more information, see
* <http://www.doctrine-project.org>.
*/
namespace Doctrine\ORM\Tools\Console\Command\ClearCache;
use Doctrine\ORM\Cache;
use Doctrine\ORM\Cache\Region\DefaultRegion;
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;
/**
* Command to clear a query cache region.
*
* @since 2.5
* @author Fabio B. Silva <fabio.bat.silva@gmail.com>
*/
class QueryRegionCommand extends Command
{
/**
* {@inheritdoc}
*/
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('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
);
}
/**
* {@inheritdoc}
*/
protected function execute(InputInterface $input, OutputInterface $output)
{
$ui = new SymfonyStyle($input, $output);
$em = $this->getHelper('em')->getEntityManager();
$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')) {
$queryCache = $cache->getQueryCache($name);
$queryRegion = $queryCache->getRegion();
if ( ! $queryRegion instanceof DefaultRegion) {
throw new \InvalidArgumentException(sprintf(
'The option "--flush" expects a "Doctrine\ORM\Cache\Region\DefaultRegion", but got "%s".',
is_object($queryRegion) ? get_class($queryRegion) : gettype($queryRegion)
));
}
$queryRegion->getCache()->flushAll();
$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;
}
}

View File

@@ -0,0 +1,112 @@
<?php
/*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* This software consists of voluntary contributions made by many individuals
* and is licensed under the MIT license. For more information, see
* <http://www.doctrine-project.org>.
*/
namespace Doctrine\ORM\Tools\Console\Command\ClearCache;
use Doctrine\Common\Cache\ApcCache;
use Doctrine\Common\Cache\XcacheCache;
use Symfony\Component\Console\Command\Command;
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 result cache of the various cache drivers.
*
* @link www.doctrine-project.org
* @since 2.0
* @author Benjamin Eberlei <kontakt@beberlei.de>
* @author Guilherme Blanco <guilhermeblanco@hotmail.com>
* @author Jonathan Wage <jonwage@gmail.com>
* @author Roman Borschel <roman@code-factory.org>
*/
class ResultCommand extends Command
{
/**
* {@inheritdoc}
*/
protected function configure()
{
$this->setName('orm:clear-cache:result')
->setDescription('Clear all result cache of the various cache drivers')
->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
);
}
/**
* {@inheritdoc}
*/
protected function execute(InputInterface $input, OutputInterface $output)
{
$ui = new SymfonyStyle($input, $output);
$em = $this->getHelper('em')->getEntityManager();
$cacheDriver = $em->getConfiguration()->getResultCacheImpl();
if ( ! $cacheDriver) {
throw new \InvalidArgumentException('No Result cache driver is configured on given EntityManager.');
}
if ($cacheDriver instanceof ApcCache) {
throw new \LogicException("Cannot clear APC Cache from Console, its shared in the Webserver memory and not accessible from the CLI.");
}
if ($cacheDriver instanceof XcacheCache) {
throw new \LogicException("Cannot clear XCache Cache from Console, its shared in the Webserver memory and not accessible from the CLI.");
}
$ui->comment('Clearing <info>all</info> Result cache entries');
$result = $cacheDriver->deleteAll();
$message = ($result) ? 'Successfully deleted cache entries.' : 'No cache entries were deleted.';
if (true === $input->getOption('flush')) {
$result = $cacheDriver->flushAll();
$message = ($result) ? 'Successfully flushed cache entries.' : $message;
}
if ( ! $result) {
$ui->error($message);
return 1;
}
$ui->success($message);
return 0;
}
}

View File

@@ -0,0 +1,215 @@
<?php
/*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* This software consists of voluntary contributions made by many individuals
* and is licensed under the MIT license. For more information, see
* <http://www.doctrine-project.org>.
*/
namespace Doctrine\ORM\Tools\Console\Command;
use Doctrine\ORM\Tools\ConvertDoctrine1Schema;
use Doctrine\ORM\Tools\EntityGenerator;
use Doctrine\ORM\Tools\Export\ClassMetadataExporter;
use Doctrine\ORM\Tools\Export\Driver\AnnotationExporter;
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;
/**
* Command to convert a Doctrine 1 schema to a Doctrine 2 mapping file.
*
* @link www.doctrine-project.org
* @since 2.0
* @author Benjamin Eberlei <kontakt@beberlei.de>
* @author Guilherme Blanco <guilhermeblanco@hotmail.com>
* @author Jonathan Wage <jonwage@gmail.com>
* @author Roman Borschel <roman@code-factory.org>
*
* @deprecated 2.7 This class is being removed from the ORM and won't have any replacement
*/
class ConvertDoctrine1SchemaCommand extends Command
{
/**
* @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;
}
/**
* @param EntityGenerator $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;
}
/**
* @param ClassMetadataExporter $metadataExporter
*
* @return void
*/
public function setMetadataExporter(ClassMetadataExporter $metadataExporter)
{
$this->metadataExporter = $metadataExporter;
}
/**
* {@inheritdoc}
*/
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.');
}
/**
* {@inheritdoc}
*/
protected function execute(InputInterface $input, OutputInterface $output)
{
$ui = new SymfonyStyle($input, $output);
$ui->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 array $fromPaths
* @param string $destPath
* @param string $toType
* @param int $numSpaces
* @param string|null $extend
* @param OutputInterface $output
*
* @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.');
}
}
}

View File

@@ -0,0 +1,187 @@
<?php
/*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* This software consists of voluntary contributions made by many individuals
* and is licensed under the MIT license. For more information, see
* <http://www.doctrine-project.org>.
*/
namespace Doctrine\ORM\Tools\Console\Command;
use Doctrine\ORM\Mapping\Driver\DatabaseDriver;
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\AnnotationExporter;
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;
/**
* Command to convert your mapping information between the various formats.
*
* @link www.doctrine-project.org
* @since 2.0
* @author Benjamin Eberlei <kontakt@beberlei.de>
* @author Guilherme Blanco <guilhermeblanco@hotmail.com>
* @author Jonathan Wage <jonwage@gmail.com>
* @author Roman Borschel <roman@code-factory.org>
*/
class ConvertMappingCommand extends Command
{
/**
* {@inheritdoc}
*/
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('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->setFilterSchemaAssetsExpression(\$regexp);
EOT
);
}
/**
* {@inheritdoc}
*/
protected function execute(InputInterface $input, OutputInterface $output)
{
$ui = new SymfonyStyle($input, $output);
$em = $this->getHelper('em')->getEntityManager();
if ($input->getOption('from-database') === true) {
$databaseDriver = new DatabaseDriver(
$em->getConnection()->getSchemaManager()
);
$em->getConfiguration()->setMetadataDriverImpl(
$databaseDriver
);
if (($namespace = $input->getOption('namespace')) !== null) {
$databaseDriver->setNamespace($namespace);
}
}
$cmf = new DisconnectedClassMetadataFactory();
$cmf->setEntityManager($em);
$metadata = $cmf->getAllMetadata();
$metadata = MetadataFilter::filter($metadata, $input->getOption('filter'));
// Process destination directory
if ( ! is_dir($destPath = $input->getArgument('dest-path'))) {
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'));
if (($extend = $input->getOption('extend')) !== null) {
$entityGenerator->setClassToExtend($extend);
}
}
if (empty($metadata)) {
$ui->success('No Metadata Classes to process.');
return;
}
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 \Doctrine\ORM\Tools\Export\Driver\AbstractExporter
*/
protected function getExporter($toType, $destPath)
{
$cme = new ClassMetadataExporter();
return $cme->getExporter($toType, $destPath);
}
}

View File

@@ -0,0 +1,78 @@
<?php
/*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* This software consists of voluntary contributions made by many individuals
* and is licensed under the MIT license. For more information, see
* <http://www.doctrine-project.org>.
*/
namespace Doctrine\ORM\Tools\Console\Command;
use Symfony\Component\Console\Command\Command;
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.
*
* @link www.doctrine-project.org
* @since 2.0
* @version $Revision$
* @author Benjamin Eberlei <kontakt@beberlei.de>
* @author Guilherme Blanco <guilhermeblanco@hotmail.com>
* @author Jonathan Wage <jonwage@gmail.com>
* @author Roman Borschel <roman@code-factory.org>
*/
class EnsureProductionSettingsCommand extends Command
{
/**
* {@inheritdoc}
*/
protected function configure()
{
$this->setName('orm:ensure-production-settings')
->setDescription('Verify that Doctrine is properly configured for a production environment')
->addOption('complete', null, InputOption::VALUE_NONE, 'Flag to also inspect database connection existence.')
->setHelp('Verify that Doctrine is properly configured for a production environment.');
}
/**
* {@inheritdoc}
*/
protected function execute(InputInterface $input, OutputInterface $output)
{
$ui = new SymfonyStyle($input, $output);
$em = $this->getHelper('em')->getEntityManager();
try {
$em->getConfiguration()->ensureProductionSettings();
if ($input->getOption('complete') !== null) {
$em->getConnection()->connect();
}
} catch (Throwable $e) {
$ui->error($e->getMessage());
return 1;
}
$ui->success('Environment is correctly configured for production.');
return 0;
}
}

View File

@@ -0,0 +1,147 @@
<?php
/*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* This software consists of voluntary contributions made by many individuals
* and is licensed under the MIT license. For more information, see
* <http://www.doctrine-project.org>.
*/
namespace Doctrine\ORM\Tools\Console\Command;
use Doctrine\ORM\Tools\Console\MetadataFilter;
use Doctrine\ORM\Tools\DisconnectedClassMetadataFactory;
use Doctrine\ORM\Tools\EntityGenerator;
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;
/**
* Command to generate entity classes and method stubs from your mapping information.
*
* @link www.doctrine-project.org
* @since 2.0
* @author Benjamin Eberlei <kontakt@beberlei.de>
* @author Guilherme Blanco <guilhermeblanco@hotmail.com>
* @author Jonathan Wage <jonwage@gmail.com>
* @author Roman Borschel <roman@code-factory.org>
*
* @deprecated 2.7 This class is being removed from the ORM and won't have any replacement
*/
class GenerateEntitiesCommand extends Command
{
/**
* {@inheritdoc}
*/
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('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
);
}
/**
* {@inheritdoc}
*/
protected function execute(InputInterface $input, OutputInterface $output)
{
$ui = new SymfonyStyle($input, $output);
$ui->warning('Command ' . $this->getName() . ' is deprecated and will be removed in Doctrine ORM 3.0.');
$em = $this->getHelper('em')->getEntityManager();
$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'));
if (($extend = $input->getOption('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;
}
}

View File

@@ -0,0 +1,110 @@
<?php
/*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* This software consists of voluntary contributions made by many individuals
* and is licensed under the MIT license. For more information, see
* <http://www.doctrine-project.org>.
*/
namespace Doctrine\ORM\Tools\Console\Command;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\Tools\Console\MetadataFilter;
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;
/**
* Command to (re)generate the proxy classes used by doctrine.
*
* @link www.doctrine-project.org
* @since 2.0
* @author Benjamin Eberlei <kontakt@beberlei.de>
* @author Guilherme Blanco <guilhermeblanco@hotmail.com>
* @author Jonathan Wage <jonwage@gmail.com>
* @author Roman Borschel <roman@code-factory.org>
*/
class GenerateProxiesCommand extends Command
{
/**
* {@inheritdoc}
*/
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('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.');
}
/**
* {@inheritdoc}
*/
protected function execute(InputInterface $input, OutputInterface $output)
{
$ui = new SymfonyStyle($input, $output);
/** @var EntityManagerInterface $em */
$em = $this->getHelper('em')->getEntityManager();
$metadatas = $em->getMetadataFactory()->getAllMetadata();
$metadatas = MetadataFilter::filter($metadatas, $input->getOption('filter'));
// Process destination directory
if (($destPath = $input->getArgument('dest-path')) === null) {
$destPath = $em->getConfiguration()->getProxyDir();
}
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;
}
}

View File

@@ -0,0 +1,119 @@
<?php
/*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* This software consists of voluntary contributions made by many individuals
* and is licensed under the MIT license. For more information, see
* <http://www.doctrine-project.org>.
*/
namespace Doctrine\ORM\Tools\Console\Command;
use Doctrine\ORM\Tools\Console\MetadataFilter;
use Doctrine\ORM\Tools\EntityRepositoryGenerator;
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;
/**
* Command to generate repository classes for mapping information.
*
* @link www.doctrine-project.org
* @since 2.0
* @author Benjamin Eberlei <kontakt@beberlei.de>
* @author Guilherme Blanco <guilhermeblanco@hotmail.com>
* @author Jonathan Wage <jonwage@gmail.com>
* @author Roman Borschel <roman@code-factory.org>
*
* @deprecated 2.7 This class is being removed from the ORM and won't have any replacement
*/
class GenerateRepositoriesCommand extends Command
{
/**
* {@inheritdoc}
*/
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('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.');
}
/**
* {@inheritdoc}
*/
protected function execute(InputInterface $input, OutputInterface $output)
{
$ui = new SymfonyStyle($input, $output);
$ui->warning('Command ' . $this->getName() . ' is deprecated and will be removed in Doctrine ORM 3.0.');
$em = $this->getHelper('em')->getEntityManager();
$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;
}
}

View File

@@ -0,0 +1,101 @@
<?php
/*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* This software consists of voluntary contributions made by many individuals
* and is licensed under the MIT license. For more information, see
* <http://www.doctrine-project.org>.
*/
namespace Doctrine\ORM\Tools\Console\Command;
use Doctrine\ORM\Mapping\MappingException;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
/**
* Show information about mapped entities.
*
* @link www.doctrine-project.org
* @since 2.1
* @author Benjamin Eberlei <kontakt@beberlei.de>
*/
class InfoCommand extends Command
{
/**
* {@inheritdoc}
*/
protected function configure()
{
$this->setName('orm:info')
->setDescription('Show basic information about all mapped entities')
->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
);
}
/**
* {@inheritdoc}
*/
protected function execute(InputInterface $input, OutputInterface $output)
{
$ui = new SymfonyStyle($input, $output);
/* @var $entityManager \Doctrine\ORM\EntityManager */
$entityManager = $this->getHelper('em')->getEntityManager();
$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;
}
}

View File

@@ -0,0 +1,290 @@
<?php
/*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* This software consists of voluntary contributions made by many individuals
* and is licensed under the MIT license. For more information, see
* <http://www.doctrine-project.org>.
*/
namespace Doctrine\ORM\Tools\Console\Command;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\Persistence\Mapping\MappingException;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use const JSON_PRETTY_PRINT;
use const JSON_UNESCAPED_SLASHES;
use const JSON_UNESCAPED_UNICODE;
use function json_encode;
/**
* Show information about mapped entities.
*
* @link www.doctrine-project.org
* @since 2.4
* @author Daniel Leech <daniel@dantleech.com>
*/
final class MappingDescribeCommand extends Command
{
/**
* {@inheritdoc}
*/
protected function configure()
{
$this->setName('orm:mapping:describe')
->addArgument('entityName', InputArgument::REQUIRED, 'Full or partial name of entity')
->setDescription('Display information about mapped objects')
->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
);
}
/**
* {@inheritdoc}
*/
protected function execute(InputInterface $input, OutputInterface $output)
{
$ui = new SymfonyStyle($input, $output);
/* @var $entityManager \Doctrine\ORM\EntityManagerInterface */
$entityManager = $this->getHelper('em')->getEntityManager();
$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
* @param EntityManagerInterface $entityManager
* @param SymfonyStyle $ui
*/
private function displayEntity($entityName, EntityManagerInterface $entityManager, SymfonyStyle $ui)
{
$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('Sequence generator definition', $metadata->sequenceGeneratorDefinition),
$this->formatField('Table generator definition', $metadata->tableGeneratorDefinition),
$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
*
* @param EntityManagerInterface $entityManager
*
* @return 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
* @param EntityManagerInterface $entityManager
*
* @return \Doctrine\ORM\Mapping\ClassMetadata
*/
private function getClassMetadata($entityName, EntityManagerInterface $entityManager)
{
try {
return $entityManager->getClassMetadata($entityName);
} catch (MappingException $e) {
}
$matches = array_filter(
$this->getMappedEntities($entityManager),
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
*
* @return string
*/
private function formatValue($value)
{
if ('' === $value) {
return '';
}
if (null === $value) {
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_class($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($label, $value) : array
{
if (null === $value) {
$value = '<comment>None</comment>';
}
return [sprintf('<info>%s</info>', $label), $this->formatValue($value)];
}
/**
* Format the association mappings
*
* @param array $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
*
* @param array $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));
}
}

View File

@@ -0,0 +1,116 @@
<?php
/*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* This software consists of voluntary contributions made by many individuals
* and is licensed under the MIT license. For more information, see
* <http://www.doctrine-project.org>.
*/
namespace Doctrine\ORM\Tools\Console\Command;
use Doctrine\Common\Util\Debug;
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;
/**
* Command to execute DQL queries in a given EntityManager.
*
* @link www.doctrine-project.org
* @since 2.0
* @author Benjamin Eberlei <kontakt@beberlei.de>
* @author Guilherme Blanco <guilhermeblanco@hotmail.com>
* @author Jonathan Wage <jonwage@gmail.com>
* @author Roman Borschel <roman@code-factory.org>
*/
class RunDqlCommand extends Command
{
/**
* {@inheritdoc}
*/
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('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('Executes arbitrary DQL directly from the command line.');
}
/**
* {@inheritdoc}
*/
protected function execute(InputInterface $input, OutputInterface $output)
{
$ui = new SymfonyStyle($input, $output);
/* @var $em \Doctrine\ORM\EntityManagerInterface */
$em = $this->getHelper('em')->getEntityManager();
if (($dql = $input->getArgument('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 = $input->getOption('hydrate');
$hydrationMode = 'Doctrine\ORM\Query::HYDRATE_' . strtoupper(str_replace('-', '_', $hydrationModeName));
if ( ! defined($hydrationMode)) {
throw new \RuntimeException(
"Hydration mode '$hydrationModeName' does not exist. It should be either: object. array, scalar or single-scalar."
);
}
$query = $em->createQuery($dql);
if (($firstResult = $input->getOption('first-result')) !== null) {
if ( ! is_numeric($firstResult)) {
throw new \LogicException("Option 'first-result' must contain an integer value");
}
$query->setFirstResult((int) $firstResult);
}
if (($maxResult = $input->getOption('max-result')) !== 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'), true, false));
return 0;
}
}

View File

@@ -0,0 +1,72 @@
<?php
/*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* This software consists of voluntary contributions made by many individuals
* and is licensed under the MIT license. For more information, see
* <http://www.doctrine-project.org>.
*/
namespace Doctrine\ORM\Tools\Console\Command\SchemaTool;
use Doctrine\ORM\Tools\SchemaTool;
use Symfony\Component\Console\Command\Command;
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
* @since 2.0
* @author Benjamin Eberlei <kontakt@beberlei.de>
* @author Guilherme Blanco <guilhermeblanco@hotmail.com>
* @author Jonathan Wage <jonwage@gmail.com>
* @author Roman Borschel <roman@code-factory.org>
*/
abstract class AbstractCommand extends Command
{
/**
* @param InputInterface $input
* @param OutputInterface $output
* @param SchemaTool $schemaTool
* @param array $metadatas
*
* @return null|int 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);
/**
* {@inheritdoc}
*/
protected function execute(InputInterface $input, OutputInterface $output)
{
$ui = new SymfonyStyle($input, $output);
$emHelper = $this->getHelper('em');
/* @var $em \Doctrine\ORM\EntityManager */
$em = $emHelper->getEntityManager();
$metadatas = $em->getMetadataFactory()->getAllMetadata();
if (empty($metadatas)) {
$ui->success('No Metadata Classes to process.');
return 0;
}
return $this->executeSchemaCommand($input, $output, new SchemaTool($em), $metadatas, $ui);
}
}

View File

@@ -0,0 +1,90 @@
<?php
/*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* This software consists of voluntary contributions made by many individuals
* and is licensed under the MIT license. For more information, see
* <http://www.doctrine-project.org>.
*/
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;
/**
* Command to create the database schema for a set of classes based on their mappings.
*
* @link www.doctrine-project.org
* @since 2.0
* @author Benjamin Eberlei <kontakt@beberlei.de>
* @author Guilherme Blanco <guilhermeblanco@hotmail.com>
* @author Jonathan Wage <jonwage@gmail.com>
* @author Roman Borschel <roman@code-factory.org>
*/
class CreateCommand extends AbstractCommand
{
/**
* {@inheritdoc}
*/
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('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->setFilterSchemaAssetsExpression(\$regexp);
EOT
);
}
/**
* {@inheritdoc}
*/
protected function executeSchemaCommand(InputInterface $input, OutputInterface $output, SchemaTool $schemaTool, array $metadatas, SymfonyStyle $ui)
{
$dumpSql = true === $input->getOption('dump-sql');
if ($dumpSql) {
$sqls = $schemaTool->getCreateSchemaSql($metadatas);
$ui->text('The following SQL statements will be executed:');
$ui->newLine();
foreach ($sqls as $sql) {
$ui->text(sprintf(' %s;', $sql));
}
return 0;
}
$ui->caution('This operation should not be executed in a production environment!');
$ui->text('Creating database schema...');
$ui->newLine();
$schemaTool->createSchema($metadatas);
$ui->success('Database schema created successfully!');
return 0;
}
}

View File

@@ -0,0 +1,130 @@
<?php
/*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* This software consists of voluntary contributions made by many individuals
* and is licensed under the MIT license. For more information, see
* <http://www.doctrine-project.org>.
*/
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;
/**
* Command to drop the database schema for a set of classes based on their mappings.
*
* @link www.doctrine-project.org
* @since 2.0
* @author Benjamin Eberlei <kontakt@beberlei.de>
* @author Guilherme Blanco <guilhermeblanco@hotmail.com>
* @author Jonathan Wage <jonwage@gmail.com>
* @author Roman Borschel <roman@code-factory.org>
*/
class DropCommand extends AbstractCommand
{
/**
* {@inheritdoc}
*/
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('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->setFilterSchemaAssetsExpression(\$regexp);
EOT
);
}
/**
* {@inheritdoc}
*/
protected function executeSchemaCommand(InputInterface $input, OutputInterface $output, SchemaTool $schemaTool, array $metadatas, SymfonyStyle $ui)
{
$isFullDatabaseDrop = $input->getOption('full-database');
$dumpSql = true === $input->getOption('dump-sql');
$force = true === $input->getOption('force');
if ($dumpSql) {
if ($isFullDatabaseDrop) {
$sqls = $schemaTool->getDropDatabaseSQL();
} else {
$sqls = $schemaTool->getDropSchemaSQL($metadatas);
}
$ui->text('The following SQL statements will be executed:');
$ui->newLine();
foreach ($sqls as $sql) {
$ui->text(sprintf(' %s;', $sql));
}
return 0;
}
if ($force) {
$ui->text('Dropping database schema...');
$ui->newLine();
if ($isFullDatabaseDrop) {
$schemaTool->dropDatabase();
} else {
$schemaTool->dropSchema($metadatas);
}
$ui->success('Database schema dropped successfully!');
return 0;
}
$ui->caution('This operation should not be executed in a production environment!');
if ($isFullDatabaseDrop) {
$sqls = $schemaTool->getDropDatabaseSQL();
} else {
$sqls = $schemaTool->getDropSchemaSQL($metadatas);
}
if (empty($sqls)) {
$ui->success('Nothing to drop. The database is empty!');
return 0;
}
$ui->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;
}
}

View File

@@ -0,0 +1,159 @@
<?php
/*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* This software consists of voluntary contributions made by many individuals
* and is licensed under the MIT license. For more information, see
* <http://www.doctrine-project.org>.
*/
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;
/**
* Command to generate the SQL needed to update the database schema to match
* the current mapping information.
*
* @link www.doctrine-project.org
* @since 2.0
* @author Benjamin Eberlei <kontakt@beberlei.de>
* @author Guilherme Blanco <guilhermeblanco@hotmail.com>
* @author Jonathan Wage <jonwage@gmail.com>
* @author Roman Borschel <roman@code-factory.org>
* @author Ryan Weaver <ryan@thatsquality.com>
*/
class UpdateCommand extends AbstractCommand
{
/**
* @var string
*/
protected $name = 'orm:schema-tool:update';
/**
* {@inheritdoc}
*/
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('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.
<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->setFilterSchemaAssetsExpression(\$regexp);
EOT
);
}
/**
* {@inheritdoc}
*/
protected function executeSchemaCommand(InputInterface $input, OutputInterface $output, SchemaTool $schemaTool, array $metadatas, SymfonyStyle $ui)
{
// Defining if update is complete or not (--complete not defined means $saveMode = true)
$saveMode = ! $input->getOption('complete');
$sqls = $schemaTool->getUpdateSchemaSql($metadatas, $saveMode);
if (empty($sqls)) {
$ui->success('Nothing to update - your database is already in sync with the current entity metadata.');
return 0;
}
$dumpSql = true === $input->getOption('dump-sql');
$force = true === $input->getOption('force');
if ($dumpSql) {
$ui->text('The following SQL statements will be executed:');
$ui->newLine();
foreach ($sqls as $sql) {
$ui->text(sprintf(' %s;', $sql));
}
}
if ($force) {
if ($dumpSql) {
$ui->newLine();
}
$ui->text('Updating database schema...');
$ui->newLine();
$schemaTool->updateSchema($metadatas, $saveMode);
$pluralization = (1 === count($sqls)) ? 'query was' : 'queries were';
$ui->text(sprintf(' <info>%s</info> %s executed', count($sqls), $pluralization));
$ui->success('Database schema updated successfully!');
}
if ($dumpSql || $force) {
return 0;
}
$ui->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.',
]
);
$ui->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;
}
}

View File

@@ -0,0 +1,100 @@
<?php
/*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* This software consists of voluntary contributions made by many individuals
* and is licensed under the MIT license. For more information, see
* <http://www.doctrine-project.org>.
*/
namespace Doctrine\ORM\Tools\Console\Command;
use Doctrine\ORM\Tools\SchemaValidator;
use Symfony\Component\Console\Command\Command;
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 validate that the current mapping is valid.
*
* @license http://www.opensource.org/licenses/mit-license.php MIT
* @link www.doctrine-project.com
* @since 1.0
* @author Benjamin Eberlei <kontakt@beberlei.de>
* @author Guilherme Blanco <guilhermeblanco@hotmail.com>
* @author Jonathan Wage <jonwage@gmail.com>
* @author Roman Borschel <roman@code-factory.org>
*/
class ValidateSchemaCommand extends Command
{
/**
* {@inheritdoc}
*/
protected function configure()
{
$this->setName('orm:validate-schema')
->setDescription('Validate the mapping files')
->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')
->setHelp('Validate that the mapping files are correct and in sync with the database.');
}
/**
* {@inheritdoc}
*/
protected function execute(InputInterface $input, OutputInterface $output)
{
$ui = new SymfonyStyle($input, $output);
$em = $this->getHelper('em')->getEntityManager();
$validator = new SchemaValidator($em);
$exit = 0;
$ui->section('Mapping');
if ($input->getOption('skip-mapping')) {
$ui->text('<comment>[SKIPPED] The mapping was not checked.</comment>');
} elseif ($errors = $validator->validateMapping()) {
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.');
$exit += 2;
} else {
$ui->success('The database schema is in sync with the mapping files.');
}
return $exit;
}
}

View File

@@ -0,0 +1,145 @@
<?php
/*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* This software consists of voluntary contributions made by many individuals
* and is licensed under the MIT license. For more information, see
* <http://www.doctrine-project.org>.
*/
namespace Doctrine\ORM\Tools\Console;
use Doctrine\DBAL\Tools\Console as DBALConsole;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\Tools\Console\Helper\EntityManagerHelper;
use OutOfBoundsException;
use PackageVersions\Versions;
use Symfony\Component\Console\Application;
use Symfony\Component\Console\Helper\HelperSet;
/**
* Handles running the Console Tools inside Symfony Console context.
*/
final class ConsoleRunner
{
/**
* Create a Symfony Console HelperSet
*
* @param EntityManagerInterface $entityManager
*
* @return HelperSet
*/
public static function createHelperSet(EntityManagerInterface $entityManager) : HelperSet
{
return new HelperSet(
[
'db' => new DBALConsole\Helper\ConnectionHelper($entityManager->getConnection()),
'em' => new EntityManagerHelper($entityManager),
]
);
}
/**
* Runs console with the given helper set.
*
* @param \Symfony\Component\Console\Helper\HelperSet $helperSet
* @param \Symfony\Component\Console\Command\Command[] $commands
*
* @return void
*/
public static function run(HelperSet $helperSet, array $commands = []) : void
{
$cli = self::createApplication($helperSet, $commands);
$cli->run();
}
/**
* Creates a console application with the given helperset and
* optional commands.
*
* @param \Symfony\Component\Console\Helper\HelperSet $helperSet
* @param array $commands
*
* @return \Symfony\Component\Console\Application
* @throws OutOfBoundsException
*/
public static function createApplication(HelperSet $helperSet, array $commands = []) : Application
{
$cli = new Application('Doctrine Command Line Interface', Versions::getVersion('doctrine/orm'));
$cli->setCatchExceptions(true);
$cli->setHelperSet($helperSet);
self::addCommands($cli);
$cli->addCommands($commands);
return $cli;
}
/**
* @param Application $cli
*
* @return void
*/
public static function addCommands(Application $cli) : void
{
$cli->addCommands(
[
// DBAL Commands
new DBALConsole\Command\ImportCommand(),
new DBALConsole\Command\ReservedWordsCommand(),
new DBALConsole\Command\RunSqlCommand(),
// ORM Commands
new Command\ClearCache\CollectionRegionCommand(),
new Command\ClearCache\EntityRegionCommand(),
new Command\ClearCache\MetadataCommand(),
new Command\ClearCache\QueryCommand(),
new Command\ClearCache\QueryRegionCommand(),
new Command\ClearCache\ResultCommand(),
new Command\SchemaTool\CreateCommand(),
new Command\SchemaTool\UpdateCommand(),
new Command\SchemaTool\DropCommand(),
new Command\EnsureProductionSettingsCommand(),
new Command\ConvertDoctrine1SchemaCommand(),
new Command\GenerateRepositoriesCommand(),
new Command\GenerateEntitiesCommand(),
new Command\GenerateProxiesCommand(),
new Command\ConvertMappingCommand(),
new Command\RunDqlCommand(),
new Command\ValidateSchemaCommand(),
new Command\InfoCommand(),
new Command\MappingDescribeCommand(),
]
);
}
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;
}
}

View File

@@ -0,0 +1,71 @@
<?php
/*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* This software consists of voluntary contributions made by many individuals
* and is licensed under the MIT license. For more information, see
* <http://www.doctrine-project.org>.
*/
namespace Doctrine\ORM\Tools\Console\Helper;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Console\Helper\Helper;
/**
* Doctrine CLI Connection Helper.
*
* @link www.doctrine-project.org
* @since 2.0
* @author Benjamin Eberlei <kontakt@beberlei.de>
* @author Guilherme Blanco <guilhermeblanco@hotmail.com>
* @author Jonathan Wage <jonwage@gmail.com>
* @author Roman Borschel <roman@code-factory.org>
*/
class EntityManagerHelper extends Helper
{
/**
* Doctrine ORM EntityManagerInterface.
*
* @var EntityManagerInterface
*/
protected $_em;
/**
* Constructor.
*
* @param EntityManagerInterface $em
*/
public function __construct(EntityManagerInterface $em)
{
$this->_em = $em;
}
/**
* Retrieves Doctrine ORM EntityManager.
*
* @return EntityManagerInterface
*/
public function getEntityManager()
{
return $this->_em;
}
/**
* {@inheritdoc}
*/
public function getName()
{
return 'entityManager';
}
}

View File

@@ -0,0 +1,104 @@
<?php
/*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* This software consists of voluntary contributions made by many individuals
* and is licensed under the MIT license. For more information, see
* <http://www.doctrine-project.org>.
*/
namespace Doctrine\ORM\Tools\Console;
use Doctrine\Persistence\Mapping\ClassMetadata;
/**
* Used by CLI Tools to restrict entity-based commands to given patterns.
*
* @license http://www.opensource.org/licenses/mit-license.php MIT
* @link www.doctrine-project.com
* @since 1.0
* @author Benjamin Eberlei <kontakt@beberlei.de>
* @author Guilherme Blanco <guilhermeblanco@hotmail.com>
* @author Jonathan Wage <jonwage@gmail.com>
* @author Roman Borschel <roman@code-factory.org>
*/
class MetadataFilter extends \FilterIterator implements \Countable
{
/**
* @var array
*/
private $filter = [];
/**
* Filter Metadatas by one or more filter options.
*
* @param ClassMetadata[] $metadatas
* @param string[]|string $filter
*
* @return ClassMetadata[]
*/
static public function filter(array $metadatas, $filter)
{
$metadatas = new MetadataFilter(new \ArrayIterator($metadatas), $filter);
return iterator_to_array($metadatas);
}
/**
* @param \ArrayIterator $metadata
* @param array|string $filter
*/
public function __construct(\ArrayIterator $metadata, $filter)
{
$this->filter = (array) $filter;
parent::__construct($metadata);
}
/**
* @return bool
*/
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->name);
if ($pregResult === false) {
throw new \RuntimeException(
sprintf("Error while evaluating regex '/%s/'.", $filter)
);
}
if ($pregResult) {
return true;
}
}
return false;
}
/**
* @return int
*/
public function count()
{
return count($this->getInnerIterator());
}
}

View File

@@ -0,0 +1,346 @@
<?php
/*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* This software consists of voluntary contributions made by many individuals
* and is licensed under the MIT license. For more information, see
* <http://www.doctrine-project.org>.
*/
namespace Doctrine\ORM\Tools;
use Doctrine\ORM\Mapping\ClassMetadataInfo;
use Doctrine\Common\Inflector\Inflector;
use Doctrine\DBAL\Types\Type;
use Symfony\Component\Yaml\Yaml;
/**
* Class to help with converting Doctrine 1 schema files to Doctrine 2 mapping files
*
*
* @link www.doctrine-project.org
* @since 2.0
* @author Guilherme Blanco <guilhermeblanco@hotmail.com>
* @author Jonathan Wage <jonwage@gmail.com>
* @author Roman Borschel <roman@code-factory.org>
*/
class ConvertDoctrine1Schema
{
/**
* @var array
*/
private $from;
/**
* @var array
*/
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 array $from
*
* @author Jonathan Wage
*/
public function __construct($from)
{
$this->from = (array) $from;
}
/**
* 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 string $className
* @param array $mappingInformation
*
* @return \Doctrine\ORM\Mapping\ClassMetadataInfo
*/
private function convertToClassMetadataInfo($className, $mappingInformation)
{
$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 string $className
* @param array $model
* @param ClassMetadataInfo $metadata
*
* @return void
*/
private function convertTableName($className, array $model, ClassMetadataInfo $metadata)
{
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 string $className
* @param array $model
* @param ClassMetadataInfo $metadata
*
* @return void
*/
private function convertColumns($className, array $model, ClassMetadataInfo $metadata)
{
$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 $className
* @param string $name
* @param string|array $column
* @param ClassMetadataInfo $metadata
*
* @return mixed[]
*
* @throws ToolsException
*/
private function convertColumn($className, $name, $column, ClassMetadataInfo $metadata)
{
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 = [];
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', 'notnull', 'version'];
foreach ($column as $key => $value) {
if (in_array($key, $allowed)) {
$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' => is_array($column['sequence']) ? $column['sequence']['name']:$column['sequence']
];
if (isset($column['sequence']['size'])) {
$definition['allocationSize'] = $column['sequence']['size'];
}
if (isset($column['sequence']['value'])) {
$definition['initialValue'] = $column['sequence']['value'];
}
$metadata->setSequenceGeneratorDefinition($definition);
}
return $fieldMapping;
}
/**
* @param string $className
* @param array $model
* @param ClassMetadataInfo $metadata
*
* @return void
*/
private function convertIndexes($className, array $model, ClassMetadataInfo $metadata)
{
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 string $className
* @param array $model
* @param ClassMetadataInfo $metadata
*
* @return void
*/
private function convertRelations($className, array $model, ClassMetadataInfo $metadata)
{
if (empty($model['relations'])) {
return;
}
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);
}
}
}

View File

@@ -0,0 +1,185 @@
<?php
/*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* This software consists of voluntary contributions made by many individuals
* and is licensed under the MIT license. For more information, see
* <http://www.doctrine-project.org>.
*/
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 Doctrine\Persistence\Proxy;
/**
* 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;
}
/**
* @param \Doctrine\ORM\Event\OnFlushEventArgs $args
*
* @return void
*/
public function onFlush(OnFlushEventArgs $args)
{
$this->dumpIdentityMap($args->getEntityManager());
}
/**
* Dumps the contents of the identity map into a stream.
*
* @param EntityManagerInterface $em
*
* @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_hash($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 ($value instanceof Proxy && !$value->__isInitialized()) {
fwrite($fh, "[PROXY] ");
}
fwrite($fh, $this->getIdString($value, $uow) . " " . spl_object_hash($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_hash($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_hash($obj)."\n");
}
}
}
}
}
}
fclose($fh);
}
/**
* @param mixed $var
*
* @return string
*/
private function getType($var)
{
if (is_object($var)) {
$refl = new \ReflectionObject($var);
return $refl->getShortName();
}
return gettype($var);
}
/**
* @param object $entity
* @param UnitOfWork $uow
*
* @return string
*/
private function getIdString($entity, UnitOfWork $uow)
{
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;
}
}

View File

@@ -0,0 +1,48 @@
<?php
/*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* This software consists of voluntary contributions made by many individuals
* and is licensed under the MIT license. For more information, see
* <http://www.doctrine-project.org>.
*/
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.
*
*
* @link www.doctrine-project.org
* @since 2.0
* @author Benjamin Eberlei <kontakt@beberlei.de>
* @author Guilherme Blanco <guilhermeblanco@hotmail.com>
* @author Jonathan Wage <jonwage@gmail.com>
* @author Roman Borschel <roman@code-factory.org>
*/
class DisconnectedClassMetadataFactory extends ClassMetadataFactory
{
/**
* @return StaticReflectionService
*/
public function getReflectionService()
{
return new StaticReflectionService();
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,182 @@
<?php
/*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* This software consists of voluntary contributions made by many individuals
* and is licensed under the MIT license. For more information, see
* <http://www.doctrine-project.org>.
*/
namespace Doctrine\ORM\Tools;
use Doctrine\ORM\EntityRepository;
use const E_USER_DEPRECATED;
use function trigger_error;
/**
* Class to generate entity repository classes
*
*
* @link www.doctrine-project.org
* @since 2.0
* @author Benjamin Eberlei <kontakt@beberlei.de>
* @author Guilherme Blanco <guilhermeblanco@hotmail.com>
* @author Jonathan Wage <jonwage@gmail.com>
* @author Roman Borschel <roman@code-factory.org>
*
* @deprecated 2.7 This class is being removed from the ORM and won't have any replacement
*/
class EntityRepositoryGenerator
{
private $repositoryName;
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()
{
@trigger_error(self::class . ' is deprecated and will be removed in Doctrine ORM 3.0', E_USER_DEPRECATED);
}
/**
* @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.
*
* @param string $fullClassName
*
* @return string $namespace
*/
private function getClassNamespace($fullClassName)
{
$namespace = substr($fullClassName, 0, strrpos($fullClassName, '\\'));
return $namespace;
}
/**
* Generates the class name
*
* @param string $fullClassName
*
* @return string
*/
private function generateClassName($fullClassName)
{
$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.
*
* @param string $fullClassName The full repository class name.
*
* @return string $namespace
*/
private function generateEntityRepositoryNamespace($fullClassName)
{
$namespace = $this->getClassNamespace($fullClassName);
return $namespace ? 'namespace ' . $namespace . ';' : '';
}
/**
* @param string $fullClassName
*
* @return string $repositoryName
*/
private function generateEntityRepositoryName($fullClassName)
{
$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 self
*/
public function setDefaultRepositoryName($repositoryName)
{
$this->repositoryName = $repositoryName;
return $this;
}
}

View File

@@ -0,0 +1,71 @@
<?php
/*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* This software consists of voluntary contributions made by many individuals
* and is licensed under the MIT license. For more information, see
* <http://www.doctrine-project.org>.
*/
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.
*
* @license http://www.opensource.org/licenses/mit-license.php MIT
* @link www.doctrine-project.com
* @since 1.0
* @author Benjamin Eberlei <kontakt@beberlei.de>
*/
class GenerateSchemaEventArgs extends EventArgs
{
/**
* @var \Doctrine\ORM\EntityManagerInterface
*/
private $em;
/**
* @var \Doctrine\DBAL\Schema\Schema
*/
private $schema;
/**
* @param EntityManagerInterface $em
* @param Schema $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;
}
}

View File

@@ -0,0 +1,86 @@
<?php
/*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* This software consists of voluntary contributions made by many individuals
* and is licensed under the MIT license. For more information, see
* <http://www.doctrine-project.org>.
*/
namespace Doctrine\ORM\Tools\Event;
use Doctrine\Common\EventArgs;
use Doctrine\ORM\Mapping\ClassMetadata;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\DBAL\Schema\Table;
/**
* Event Args used for the Events::postGenerateSchemaTable event.
*
* @license http://www.opensource.org/licenses/mit-license.php MIT
* @link www.doctrine-project.com
* @since 1.0
* @author Benjamin Eberlei <kontakt@beberlei.de>
*/
class GenerateSchemaTableEventArgs extends EventArgs
{
/**
* @var \Doctrine\ORM\Mapping\ClassMetadata
*/
private $classMetadata;
/**
* @var \Doctrine\DBAL\Schema\Schema
*/
private $schema;
/**
* @var \Doctrine\DBAL\Schema\Table
*/
private $classTable;
/**
* @param ClassMetadata $classMetadata
* @param Schema $schema
* @param Table $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;
}
}

View File

@@ -0,0 +1,86 @@
<?php
/*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* This software consists of voluntary contributions made by many individuals
* and is licensed under the MIT license. For more information, see
* <http://www.doctrine-project.org>.
*/
namespace Doctrine\ORM\Tools\Export;
use const E_USER_DEPRECATED;
use function trigger_error;
/**
* Class used for converting your mapping information between the
* supported formats: yaml, xml, and php/annotation.
*
* @link www.doctrine-project.org
* @since 2.0
* @author Jonathan Wage <jonwage@gmail.com>
*
* @deprecated 2.7 This class is being removed from the ORM and won't have any replacement
*/
class ClassMetadataExporter
{
/**
* @var array
*/
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()
{
@trigger_error(self::class . ' is deprecated and will be removed in Doctrine ORM 3.0', E_USER_DEPRECATED);
}
/**
* 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);
}
}

View File

@@ -0,0 +1,283 @@
<?php
/*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* This software consists of voluntary contributions made by many individuals
* and is licensed under the MIT license. For more information, see
* <http://www.doctrine-project.org>.
*/
namespace Doctrine\ORM\Tools\Export\Driver;
use Doctrine\ORM\Mapping\ClassMetadataInfo;
use Doctrine\ORM\Tools\Export\ExportException;
use const E_USER_DEPRECATED;
use function trigger_error;
/**
* Abstract base class which is to be used for the Exporter drivers
* which can be found in \Doctrine\ORM\Tools\Export\Driver.
*
* @link www.doctrine-project.org
* @since 2.0
* @author Jonathan Wage <jonwage@gmail.com>
*
* @deprecated 2.7 This class is being removed from the ORM and won't have any replacement
*/
abstract class AbstractExporter
{
/**
* @var array
*/
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)
{
@trigger_error(static::class . ' is deprecated and will be removed in Doctrine ORM 3.0', E_USER_DEPRECATED);
$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.
*
* @param ClassMetadataInfo $metadata
*
* @return string
*/
abstract public function exportClassMetadata(ClassMetadataInfo $metadata);
/**
* Sets the array of ClassMetadataInfo instances to export.
*
* @param array $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 \Doctrine\ORM\Tools\Export\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
if ($output = $this->exportClassMetadata($metadata)) {
$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.
*
* @param ClassMetadataInfo $metadata
*
* @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
*
* @return string
*
* @psalm-param ClassMetadataInfo::INHERITANCE_TYPE_* $type
*/
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
*
* @return string
*
* @psalm-param ClassMetadataInfo::FETCH_* $mode
*/
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
*
* @return string
*
* @psalm-param ClassMetadataInfo::CHANGETRACKING_* $policy
*/
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
*
* @return string
*
* @psalm-param ClassMetadataInfo::GENERATOR_TYPE_* $type
*/
protected function _getIdGeneratorTypeString($type)
{
switch ($type) {
case ClassMetadataInfo::GENERATOR_TYPE_AUTO:
return 'AUTO';
case ClassMetadataInfo::GENERATOR_TYPE_SEQUENCE:
return 'SEQUENCE';
case ClassMetadataInfo::GENERATOR_TYPE_TABLE:
return 'TABLE';
case ClassMetadataInfo::GENERATOR_TYPE_IDENTITY:
return 'IDENTITY';
case ClassMetadataInfo::GENERATOR_TYPE_UUID:
return 'UUID';
case ClassMetadataInfo::GENERATOR_TYPE_CUSTOM:
return 'CUSTOM';
}
}
}

View File

@@ -0,0 +1,82 @@
<?php
/*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* This software consists of voluntary contributions made by many individuals
* and is licensed under the MIT license. For more information, see
* <http://www.doctrine-project.org>.
*/
namespace Doctrine\ORM\Tools\Export\Driver;
use Doctrine\ORM\Mapping\ClassMetadataInfo;
use Doctrine\ORM\Tools\EntityGenerator;
/**
* ClassMetadata exporter for PHP classes with annotations.
*
* @link www.doctrine-project.org
* @since 2.0
* @author Jonathan Wage <jonwage@gmail.com>
*
* @deprecated 2.7 This class is being removed from the ORM and won't have any replacement
*/
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);
}
/**
* @param ClassMetadataInfo $metadata
*
* @return string
*/
protected function _generateOutputPath(ClassMetadataInfo $metadata)
{
return $this->_outputDir . '/' . str_replace('\\', '/', $metadata->name) . $this->_extension;
}
/**
* @param EntityGenerator $entityGenerator
*
* @return void
*/
public function setEntityGenerator(EntityGenerator $entityGenerator)
{
$this->_entityGenerator = $entityGenerator;
}
}

View File

@@ -0,0 +1,207 @@
<?php
/*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* This software consists of voluntary contributions made by many individuals
* and is licensed under the MIT license. For more information, see
* <http://www.doctrine-project.org>.
*/
namespace Doctrine\ORM\Tools\Export\Driver;
use Doctrine\ORM\Mapping\ClassMetadataInfo;
/**
* ClassMetadata exporter for PHP code.
*
* @link www.doctrine-project.org
* @since 2.0
* @author Jonathan Wage <jonwage@gmail.com>
*
* @deprecated 2.7 This class is being removed from the ORM and won't have any replacement
*/
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;';
}
if ($metadata->inheritanceType) {
$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[] = "\$metadata->addLifecycleCallback('$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)) {
$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;
}
}

View File

@@ -0,0 +1,496 @@
<?php
/*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* This software consists of voluntary contributions made by many individuals
* and is licensed under the MIT license. For more information, see
* <http://www.doctrine-project.org>.
*/
namespace Doctrine\ORM\Tools\Export\Driver;
use Doctrine\ORM\Mapping\ClassMetadataInfo;
use SimpleXMLElement;
/**
* ClassMetadata exporter for Doctrine XML mapping files.
*
* @link www.doctrine-project.org
* @since 2.0
* @author Jonathan Wage <jonwage@gmail.com>
*
* @deprecated 2.7 This class is being removed from the ORM and won't have any replacement
*/
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 && $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', $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', $value);
$discriminatorMappingXml->addAttribute('class', $className);
}
}
$trackingPolicy = $this->_getChangeTrackingPolicyString($metadata->changeTrackingPolicy);
if ( $trackingPolicy != 'DEFERRED_IMPLICIT') {
$root->addChild('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)) {
$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', $field['length']);
}
if (isset($field['associationKey']) && $field['associationKey']) {
$idXml->addAttribute('association-key', 'true');
}
if ($idGeneratorType = $this->_getIdGeneratorTypeString($metadata->generatorType)) {
$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']);
if (isset($field['columnName'])) {
$fieldXml->addAttribute('column', $field['columnName']);
}
if (isset($field['length'])) {
$fieldXml->addAttribute('length', $field['length']);
}
if (isset($field['precision'])) {
$fieldXml->addAttribute('precision', $field['precision']);
}
if (isset($field['scale'])) {
$fieldXml->addAttribute('scale', $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', $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');
}
}
}
$orderMap = [
ClassMetadataInfo::ONE_TO_ONE,
ClassMetadataInfo::ONE_TO_MANY,
ClassMetadataInfo::MANY_TO_ONE,
ClassMetadataInfo::MANY_TO_MANY,
];
uasort($metadata->associationMappings, function($m1, $m2) use (&$orderMap){
$a1 = array_search($m1['type'], $orderMap);
$a2 = array_search($m2['type'], $orderMap);
return strcmp($a1, $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']);
}
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']);
}
}
}
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 SimpleXMLElement $parentXml
* @param array $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
*
* @param SimpleXMLElement $identifierXmlNode
* @param ClassMetadataInfo $metadata
*
* @return void
*/
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 (0 === \count($metadata->entityListeners)) {
return;
}
$entityListenersXml = $root->addChild('entity-listeners');
$entityListenersXmlMap = [];
$this->generateEntityListenerXml($metadata, $entityListenersXmlMap, $entityListenersXml);
}
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']);
}
}
}
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;
}
}

View File

@@ -0,0 +1,265 @@
<?php
/*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* This software consists of voluntary contributions made by many individuals
* and is licensed under the MIT license. For more information, see
* <http://www.doctrine-project.org>.
*/
namespace Doctrine\ORM\Tools\Export\Driver;
use Symfony\Component\Yaml\Yaml;
use Doctrine\ORM\Mapping\ClassMetadataInfo;
/**
* ClassMetadata exporter for Doctrine YAML mapping files.
*
* @link www.doctrine-project.org
* @since 2.0
* @author Jonathan Wage <jonwage@gmail.com>
*
* @deprecated 2.7 This class is being removed from the ORM and won't have any replacement
*/
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);
}
if ($column = $metadata->discriminatorColumn) {
$array['discriminatorColumn'] = $column;
}
if ($map = $metadata->discriminatorMap) {
$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)) {
$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 array $array PHP array
* @param integer $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);
}
private function processEntityListeners(ClassMetadataInfo $metadata, array $array) : array
{
if (0 === \count($metadata->entityListeners)) {
return $array;
}
$array['entityListeners'] = [];
foreach ($metadata->entityListeners as $event => $entityListenerConfig) {
$array = $this->processEntityListenerConfig($array, $entityListenerConfig, $event);
}
return $array;
}
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;
}
}

View File

@@ -0,0 +1,58 @@
<?php
/*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* This software consists of voluntary contributions made by many individuals
* and is licensed under the MIT license. For more information, see
* <http://www.doctrine-project.org>.
*/
namespace Doctrine\ORM\Tools\Export;
use Doctrine\ORM\ORMException;
/**
* @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("The specified export driver '$type' does not exist");
}
/**
* @param string $type
*
* @return ExportException
*/
public static function invalidMappingDriverType($type)
{
return new self("The mapping driver '$type' does not exist");
}
/**
* @param string $file
*
* @return ExportException
*/
public static function attemptOverwriteExistingFile($file)
{
return new self("Attempting to overwrite an existing file '".$file."'.");
}
}

View File

@@ -0,0 +1,154 @@
<?php
/*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* This software consists of voluntary contributions made by many individuals
* and is licensed under the MIT license. For more information, see
* <http://www.doctrine-project.org>.
*/
namespace Doctrine\ORM\Tools\Pagination;
use Doctrine\ORM\Query\SqlWalker;
use Doctrine\ORM\Query\AST\SelectStatement;
/**
* 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`)
*
* @author Sander Marechal <s.marechal@jejik.com>
*/
class CountOutputWalker extends SqlWalker
{
/**
* @var \Doctrine\DBAL\Platforms\AbstractPlatform
*/
private $platform;
/**
* @var \Doctrine\ORM\Query\ResultSetMapping
*/
private $rsm;
/**
* @var array
*/
private $queryComponents;
/**
* Constructor.
*
* Stores various parameters that are otherwise unavailable
* because Doctrine\ORM\Query\SqlWalker keeps everything private without
* accessors.
*
* @param \Doctrine\ORM\Query $query
* @param \Doctrine\ORM\Query\ParserResult $parserResult
* @param array $queryComponents
*/
public function __construct($query, $parserResult, array $queryComponents)
{
$this->platform = $query->getEntityManager()->getConnection()->getDatabasePlatform();
$this->rsm = $parserResult->getResultSetMapping();
$this->queryComponents = $queryComponents;
parent::__construct($query, $parserResult, $queryComponents);
}
/**
* Walks down a SelectStatement AST node, wrapping it in a COUNT (SELECT DISTINCT).
*
* 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.
*
* @param SelectStatement $AST
*
* @return string
*
* @throws \RuntimeException
*/
public function walkSelectStatement(SelectStatement $AST)
{
if ($this->platform->getName() === "mssql") {
$AST->orderByClause = null;
}
$sql = parent::walkSelectStatement($AST);
if ($AST->groupByClause) {
return sprintf(
'SELECT %s AS dctrn_count FROM (%s) dctrn_table',
$this->platform->getCountExpression('*'),
$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->queryComponents[$rootAlias]['metadata'];
$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) 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) 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 %s AS dctrn_count FROM (SELECT DISTINCT %s FROM (%s) dctrn_result) dctrn_table',
$this->platform->getCountExpression('*'),
implode(', ', $sqlIdentifier),
$sql
);
}
}

View File

@@ -0,0 +1,94 @@
<?php
/*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* This software consists of voluntary contributions made by many individuals
* and is licensed under the MIT license. For more information, see
* <http://www.doctrine-project.org>.
*/
namespace Doctrine\ORM\Tools\Pagination;
use Doctrine\ORM\Query\TreeWalkerAdapter;
use Doctrine\ORM\Query\AST\SelectStatement;
use Doctrine\ORM\Query\AST\SelectExpression;
use Doctrine\ORM\Query\AST\PathExpression;
use Doctrine\ORM\Query\AST\AggregateExpression;
/**
* Replaces the selectClause of the AST with a COUNT statement.
*
* @category DoctrineExtensions
* @package DoctrineExtensions\Paginate
* @author David Abdemoulaie <dave@hobodave.com>
* @copyright Copyright (c) 2010 David Abdemoulaie (http://hobodave.com/)
* @license http://hobodave.com/license.txt New BSD License
*/
class CountWalker extends TreeWalkerAdapter
{
/**
* Distinct mode hint name.
*/
const HINT_DISTINCT = 'doctrine_paginator.distinct';
/**
* Walks down a SelectStatement AST node, modifying it to retrieve a COUNT.
*
* @param SelectStatement $AST
*
* @return void
*
* @throws \RuntimeException
*/
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');
}
$queryComponents = $this->_getQueryComponents();
// 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 = $queryComponents[$rootAlias]['metadata'];
$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;
}
}

View File

@@ -0,0 +1,607 @@
<?php
/*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* This software consists of voluntary contributions made by many individuals
* and is licensed under the MIT license. For more information, see
* <http://www.doctrine-project.org>.
*/
namespace Doctrine\ORM\Tools\Pagination;
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\Query\AST\OrderByClause;
use Doctrine\ORM\Query\AST\PartialObjectExpression;
use Doctrine\ORM\Query\AST\SelectExpression;
use Doctrine\ORM\Query\SqlWalker;
use Doctrine\ORM\Query\AST\SelectStatement;
/**
* 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`)
*
* @author Sander Marechal <s.marechal@jejik.com>
*/
class LimitSubqueryOutputWalker extends SqlWalker
{
private const ORDER_BY_PATH_EXPRESSION = '/(?<![a-z0-9_])%s\.%s(?![a-z0-9_])/i';
/**
* @var \Doctrine\DBAL\Platforms\AbstractPlatform
*/
private $platform;
/**
* @var \Doctrine\ORM\Query\ResultSetMapping
*/
private $rsm;
/**
* @var array
*/
private $queryComponents;
/**
* @var int
*/
private $firstResult;
/**
* @var int
*/
private $maxResults;
/**
* @var \Doctrine\ORM\EntityManager
*/
private $em;
/**
* The quote strategy.
*
* @var \Doctrine\ORM\Mapping\QuoteStrategy
*/
private $quoteStrategy;
/**
* @var array
*/
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;
/**
* Constructor.
*
* Stores various parameters that are otherwise unavailable
* because Doctrine\ORM\Query\SqlWalker keeps everything private without
* accessors.
*
* @param \Doctrine\ORM\Query $query
* @param \Doctrine\ORM\Query\ParserResult $parserResult
* @param array $queryComponents
*/
public function __construct($query, $parserResult, array $queryComponents)
{
$this->platform = $query->getEntityManager()->getConnection()->getDatabasePlatform();
$this->rsm = $parserResult->getResultSetMapping();
$this->queryComponents = $queryComponents;
// Reset limit and offset
$this->firstResult = $query->getFirstResult();
$this->maxResults = $query->getMaxResults();
$query->setFirstResult(null)->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.
*
* @return bool
*/
private function platformSupportsRowNumber()
{
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.
*
* @param SelectStatement $AST
*/
private function rebuildOrderByForRowNumber(SelectStatement $AST)
{
$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;
}
/**
* Walks down a SelectStatement AST node, wrapping it in a SELECT DISTINCT.
*
* @param SelectStatement $AST
*
* @return string
*
* @throws \RuntimeException
*/
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.
*
* @param SelectStatement $AST
*
* @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 SelectStatement $AST
* @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);
// http://www.doctrine-project.org/jira/browse/DDC-1958
$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.
*
* @param SelectStatement $AST
*/
private function addMissingItemsFromOrderByToSelect(SelectStatement $AST)
{
$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) {
$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(new PartialObjectExpression($idVar, array_keys($fields)), null, true);
}
}
/**
* Generates new SQL for statements with an order by clause
*
* @param array $sqlIdentifier
* @param string $innerSql
* @param string $sql
* @param OrderByClause|null $orderByClause
*
* @return string
*/
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 OrderByClause $orderByClause
* @param array $identifiers
* @param string $innerSql
*
* @return string
*/
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[][]
*/
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->queryComponents[$dqlAlias]['metadata'];
$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 array
*/
public function getOrderByPathExpressions()
{
return $this->orderByPathExpressions;
}
/**
* @param SelectStatement $AST
*
* @return string
*
* @throws \Doctrine\ORM\OptimisticLockException
* @throws \Doctrine\ORM\Query\QueryException
*/
private function getInnerSQL(SelectStatement $AST)
{
// 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;
}
/**
* @param SelectStatement $AST
*
* @return array-key[]
*
* @psalm-return array<array-key, array-key>
*/
private function getSQLIdentifier(SelectStatement $AST)
{
// 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->queryComponents[$rootAlias]['metadata'];
$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) 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) 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)) {
$this->orderByPathExpressions[] = $pathExpr;
}
return parent::walkPathExpression($pathExpr);
}
/**
* {@inheritdoc}
*/
public function walkSubSelect($subselect)
{
$this->inSubSelect = true;
$sql = parent::walkSubselect($subselect);
$this->inSubSelect = false;
return $sql;
}
}

View File

@@ -0,0 +1,175 @@
<?php
/*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* This software consists of voluntary contributions made by many individuals
* and is licensed under the MIT license. For more information, see
* <http://www.doctrine-project.org>.
*/
namespace Doctrine\ORM\Tools\Pagination;
use Doctrine\DBAL\Types\Type;
use Doctrine\ORM\Mapping\ClassMetadataInfo;
use Doctrine\ORM\Query;
use Doctrine\ORM\Query\TreeWalkerAdapter;
use Doctrine\ORM\Query\AST\Functions\IdentityFunction;
use Doctrine\ORM\Query\AST\PathExpression;
use Doctrine\ORM\Query\AST\SelectExpression;
use Doctrine\ORM\Query\AST\SelectStatement;
/**
* Replaces the selectClause of the AST with a SELECT DISTINCT root.id equivalent.
*
* @category DoctrineExtensions
* @package DoctrineExtensions\Paginate
* @author David Abdemoulaie <dave@hobodave.com>
* @copyright Copyright (c) 2010 David Abdemoulaie (http://hobodave.com/)
* @license http://hobodave.com/license.txt New BSD License
*/
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;
/**
* Walks down a SelectStatement AST node, modifying it to retrieve DISTINCT ids
* of the root Entity.
*
* @param SelectStatement $AST
*
* @return void
*
* @throws \RuntimeException
*/
public function walkSelectStatement(SelectStatement $AST)
{
$queryComponents = $this->_getQueryComponents();
// Get the root entity and alias from the AST fromClause
$from = $AST->fromClause->identificationVariableDeclarations;
$fromRoot = reset($from);
$rootAlias = $fromRoot->rangeVariableDeclaration->aliasIdentificationVariable;
$rootClass = $queryComponents[$rootAlias]['metadata'];
$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.");
}
$this->_getQuery()->setHint(
self::IDENTIFIER_TYPE,
Type::getType($rootClass->fieldMappings[$identifier]['type'])
);
$this->_getQuery()->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 = true;
if ( ! isset($AST->orderByClause)) {
return;
}
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.
*
* @param SelectStatement $AST
*/
private function validate(SelectStatement $AST)
{
// 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
&& null !== $query->getMaxResults()
&& $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'])
&& $queryComponent['relation']['type'] & ClassMetadataInfo::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)
{
if ($pathExpression->type === PathExpression::TYPE_SINGLE_VALUED_ASSOCIATION) {
$identity = new IdentityFunction('identity');
$identity->pathExpression = clone $pathExpression;
return $identity;
}
return clone $pathExpression;
}
}

View File

@@ -0,0 +1,288 @@
<?php
/*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* This software consists of voluntary contributions made by many individuals
* and is licensed under the MIT license. For more information, see
* <http://www.doctrine-project.org>.
*/
namespace Doctrine\ORM\Tools\Pagination;
use Doctrine\ORM\NoResultException;
use Doctrine\ORM\Query;
use Doctrine\ORM\Query\Parser;
use Doctrine\ORM\Query\ResultSetMapping;
use Doctrine\ORM\QueryBuilder;
use function array_map;
use function array_sum;
/**
* The paginator can handle various complex scenarios with DQL.
*
* @author Pablo Díez <pablodip@gmail.com>
* @author Benjamin Eberlei <kontakt@beberlei.de>
* @license New BSD
*/
class Paginator implements \Countable, \IteratorAggregate
{
/**
* @var Query
*/
private $query;
/**
* @var bool
*/
private $fetchJoinCollection;
/**
* @var bool|null
*/
private $useOutputWalkers;
/**
* @var int
*/
private $count;
/**
* Constructor.
*
* @param Query|QueryBuilder $query A Doctrine ORM query or query builder.
* @param boolean $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 boolean 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
*/
public function setUseOutputWalkers($useOutputWalkers)
{
$this->useOutputWalkers = $useOutputWalkers;
return $this;
}
/**
* {@inheritdoc}
*/
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}
*/
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_ID_COUNT, count($ids));
$whereInQuery->setFirstResult(null)->setMaxResults(null);
$whereInQuery->setParameter(WhereInWalker::PAGINATOR_ID_ALIAS, $ids);
$whereInQuery->setCacheable($this->query->isCacheable());
$whereInQuery->expireQueryCache();
$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);
}
/**
* Clones a query.
*
* @param Query $query The query.
*
* @return Query The cloned query.
*/
private function cloneQuery(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.
*
* @param Query $query The query.
*
* @return bool
*/
private function useOutputWalker(Query $query)
{
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.
*
* @param Query $query
* @param string $walkerClass
*/
private function appendTreeWalker(Query $query, $walkerClass)
{
$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.
*
* @return Query
*/
private function getCountQuery()
{
$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($platform->getSQLResultCasing('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(null)->setMaxResults(null);
return $countQuery;
}
private function unbindUnusedQueryParams(Query $query): void
{
$parser = new Parser($query);
$parameterMappings = $parser->parse()->getParameterMappings();
/* @var $parameters \Doctrine\Common\Collections\Collection|\Doctrine\ORM\Query\Parameter[] */
$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);
}
}

View File

@@ -0,0 +1,62 @@
<?php
/*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* This software consists of voluntary contributions made by many individuals
* and is licensed under the MIT license. For more information, see
* <http://www.doctrine-project.org>.
*/
namespace Doctrine\ORM\Tools\Pagination;
use Doctrine\ORM\ORMException;
use Doctrine\ORM\Query\AST\Functions\FunctionNode;
/**
* RowNumberOverFunction
*
* Provides ROW_NUMBER() OVER(ORDER BY...) construct for use in LimitSubqueryOutputWalker
*
* @since 2.5
* @author Bill Schaller <bill@zeroedin.com>
*/
class RowNumberOverFunction extends FunctionNode
{
/**
* @var \Doctrine\ORM\Query\AST\OrderByClause
*/
public $orderByClause;
/**
* @override
* @inheritdoc
*/
public function getSql(\Doctrine\ORM\Query\SqlWalker $sqlWalker)
{
return 'ROW_NUMBER() OVER(' . trim($sqlWalker->walkOrderByClause(
$this->orderByClause
)) . ')';
}
/**
* @override
* @inheritdoc
*
* @throws ORMException
*/
public function parse(\Doctrine\ORM\Query\Parser $parser)
{
throw new ORMException("The RowNumberOverFunction is not intended for, nor is it enabled for use in DQL.");
}
}

View File

@@ -0,0 +1,184 @@
<?php
/*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* This software consists of voluntary contributions made by many individuals
* and is licensed under the MIT license. For more information, see
* <http://www.doctrine-project.org>.
*/
namespace Doctrine\ORM\Tools\Pagination;
use Doctrine\ORM\Mapping\ClassMetadata;
use Doctrine\ORM\Query\AST\ArithmeticExpression;
use Doctrine\ORM\Query\AST\ConditionalExpression;
use Doctrine\ORM\Query\AST\ConditionalFactor;
use Doctrine\ORM\Query\AST\ConditionalPrimary;
use Doctrine\ORM\Query\AST\ConditionalTerm;
use Doctrine\ORM\Query\AST\InExpression;
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 Doctrine\ORM\Utility\PersisterHelper;
use function array_map;
use function assert;
use function is_array;
/**
* Replaces the whereClause of the AST with a WHERE id IN (:foo_1, :foo_2) equivalent.
*
* @category DoctrineExtensions
* @package DoctrineExtensions\Paginate
* @author David Abdemoulaie <dave@hobodave.com>
* @copyright Copyright (c) 2010 David Abdemoulaie (http://hobodave.com/)
* @license http://hobodave.com/license.txt New BSD License
*/
class WhereInWalker extends TreeWalkerAdapter
{
/**
* ID Count hint name.
*/
const HINT_PAGINATOR_ID_COUNT = 'doctrine.id.count';
/**
* Primary key alias for query.
*/
const PAGINATOR_ID_ALIAS = 'dpid';
/**
* Replaces the whereClause in the AST.
*
* Generates a clause equivalent to WHERE IN (:dpid_1, :dpid_2, ...)
*
* The parameter namespace (dpid) is defined by
* the PAGINATOR_ID_ALIAS
*
* The total number of parameters is retrieved from
* the HINT_PAGINATOR_ID_COUNT query hint.
*
* @param SelectStatement $AST
*
* @return void
*
* @throws \RuntimeException
*/
public function walkSelectStatement(SelectStatement $AST)
{
$queryComponents = $this->_getQueryComponents();
// 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;
/** @var ClassMetadata $rootClass */
$rootClass = $queryComponents[$rootAlias]['metadata'];
$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;
$count = $this->_getQuery()->getHint(self::HINT_PAGINATOR_ID_COUNT);
if ($count > 0) {
$arithmeticExpression = new ArithmeticExpression();
$arithmeticExpression->simpleArithmeticExpression = new SimpleArithmeticExpression(
[$pathExpression]
);
$expression = new InExpression($arithmeticExpression);
$expression->literals[] = new InputParameter(":" . self::PAGINATOR_ID_ALIAS);
$this->convertWhereInIdentifiersToDatabaseValue(
PersisterHelper::getTypeOfField(
$identifierFieldName,
$rootClass,
$this->_getQuery()
->getEntityManager()
)[0]
);
} else {
$expression = new NullComparisonExpression($pathExpression);
$expression->not = false;
}
$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
]
)
]
);
} elseif ($AST->whereClause->conditionalExpression instanceof ConditionalExpression
|| $AST->whereClause->conditionalExpression instanceof ConditionalFactor
) {
$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])
]
)
);
}
}
private function convertWhereInIdentifiersToDatabaseValue(string $type) : void
{
$query = $this->_getQuery();
$identifiersParameter = $query->getParameter(self::PAGINATOR_ID_ALIAS);
assert($identifiersParameter !== null);
$identifiers = $identifiersParameter->getValue();
assert(is_array($identifiers));
$connection = $this->_getQuery()
->getEntityManager()
->getConnection();
$query->setParameter(self::PAGINATOR_ID_ALIAS, array_map(static function ($id) use ($connection, $type) {
return $connection->convertToDatabaseValue($id, $type);
}, $identifiers));
}
}

View File

@@ -0,0 +1,143 @@
<?php
/*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* This software consists of voluntary contributions made by many individuals
* and is licensed under the MIT license. For more information, see
* <http://www.doctrine-project.org>.
*/
namespace Doctrine\ORM\Tools;
use Doctrine\ORM\Event\LoadClassMetadataEventArgs;
use Doctrine\ORM\Event\OnClassMetadataNotFoundEventArgs;
use Doctrine\ORM\Mapping\ClassMetadata;
use Doctrine\Common\EventSubscriber;
use Doctrine\ORM\Events;
/**
* ResolveTargetEntityListener
*
* Mechanism to overwrite interfaces or classes specified as association
* targets.
*
* @author Benjamin Eberlei <kontakt@beberlei.de>
* @since 2.2
*/
class ResolveTargetEntityListener implements EventSubscriber
{
/**
* @var array[] 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
* @param array $mapping
*
* @return void
*/
public function addResolveTargetEntity($originalEntity, $newEntity, array $mapping)
{
$mapping['targetEntity'] = ltrim($newEntity, "\\");
$this->resolveTargetEntities[ltrim($originalEntity, "\\")] = $mapping;
}
/**
* @param OnClassMetadataNotFoundEventArgs $args
*
* @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.
*
* @param LoadClassMetadataEventArgs $args
*
* @return void
*
* @internal this is an event callback, and should not be called directly
*/
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);
}
}
}
/**
* @param \Doctrine\ORM\Mapping\ClassMetadataInfo $classMetadata
* @param array $mapping
*
* @return void
*/
private function remapAssociation($classMetadata, $mapping)
{
$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;
}
}
}

View File

@@ -0,0 +1,937 @@
<?php
/*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* This software consists of voluntary contributions made by many individuals
* and is licensed under the MIT license. For more information, see
* <http://www.doctrine-project.org>.
*/
namespace Doctrine\ORM\Tools;
use Doctrine\DBAL\Schema\AbstractAsset;
use Doctrine\DBAL\Schema\Comparator;
use Doctrine\DBAL\Schema\Index;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\DBAL\Schema\Table;
use Doctrine\DBAL\Schema\Visitor\DropSchemaSqlCollector;
use Doctrine\DBAL\Schema\Visitor\RemoveNamespacedAssets;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\Mapping\ClassMetadata;
use Doctrine\ORM\ORMException;
use Doctrine\ORM\Tools\Event\GenerateSchemaTableEventArgs;
use Doctrine\ORM\Tools\Event\GenerateSchemaEventArgs;
/**
* The SchemaTool is a tool to create/drop/update database schemas based on
* <tt>ClassMetadata</tt> class descriptors.
*
* @link www.doctrine-project.org
* @since 2.0
* @author Guilherme Blanco <guilhermeblanco@hotmail.com>
* @author Jonathan Wage <jonwage@gmail.com>
* @author Roman Borschel <roman@code-factory.org>
* @author Benjamin Eberlei <kontakt@beberlei.de>
* @author Stefano Rodriguez <stefano.rodriguez@fubles.com>
*/
class SchemaTool
{
private const KNOWN_COLUMN_OPTIONS = ['comment', 'unsigned', 'fixed', 'default'];
/**
* @var \Doctrine\ORM\EntityManagerInterface
*/
private $em;
/**
* @var \Doctrine\DBAL\Platforms\AbstractPlatform
*/
private $platform;
/**
* The quote strategy.
*
* @var \Doctrine\ORM\Mapping\QuoteStrategy
*/
private $quoteStrategy;
/**
* Initializes a new SchemaTool instance that uses the connection of the
* provided EntityManager.
*
* @param \Doctrine\ORM\EntityManagerInterface $em
*/
public function __construct(EntityManagerInterface $em)
{
$this->em = $em;
$this->platform = $em->getConnection()->getDatabasePlatform();
$this->quoteStrategy = $em->getConfiguration()->getQuoteStrategy();
}
/**
* Creates the database schema for the given array of ClassMetadata instances.
*
* @param array $classes
*
* @return void
*
* @throws ToolsException
*/
public function createSchema(array $classes)
{
$createSchemaSql = $this->getCreateSchemaSql($classes);
$conn = $this->em->getConnection();
foreach ($createSchemaSql as $sql) {
try {
$conn->executeQuery($sql);
} catch (\Throwable $e) {
throw ToolsException::schemaToolFailure($sql, $e);
}
}
}
/**
* Gets the list of DDL statements that are required to create the database schema for
* the given list of ClassMetadata instances.
*
* @param array $classes
*
* @return string[] The SQL statements needed to create the schema for the classes.
*/
public function getCreateSchemaSql(array $classes)
{
$schema = $this->getSchemaFromMetadata($classes);
return $schema->toSql($this->platform);
}
/**
* Detects instances of ClassMetadata that don't need to be processed in the SchemaTool context.
*
* @param ClassMetadata $class
* @param array $processedClasses
*
* @return bool
*/
private function processingNotRequired($class, array $processedClasses)
{
return (
isset($processedClasses[$class->name]) ||
$class->isMappedSuperclass ||
$class->isEmbeddedClass ||
($class->isInheritanceTypeSingleTable() && $class->name != $class->rootEntityName)
);
}
/**
* Creates a Schema instance from a given set of metadata classes.
*
* @param array $classes
*
* @return Schema
*
* @throws \Doctrine\ORM\ORMException
*/
public function getSchemaFromMetadata(array $classes)
{
// Reminder for processed classes, used for hierarchies
$processedClasses = [];
$eventManager = $this->em->getEventManager();
$schemaManager = $this->em->getConnection()->getSchemaManager();
$metadataSchemaConfig = $schemaManager->createSchemaConfig();
$metadataSchemaConfig->setExplicitForeignKeyIndexes(false);
$schema = new Schema([], [], $metadataSchemaConfig);
$addedFks = [];
$blacklistedFks = [];
foreach ($classes as $class) {
/** @var \Doctrine\ORM\Mapping\ClassMetadata $class */
if ($this->processingNotRequired($class, $processedClasses)) {
continue;
}
$table = $schema->createTable($this->quoteStrategy->getTableName($class, $this->platform));
if ($class->isInheritanceTypeSingleTable()) {
$this->gatherColumns($class, $table);
$this->gatherRelationsSql($class, $table, $schema, $addedFks, $blacklistedFks);
// Add the discriminator column
$this->addDiscriminatorColumnDefinition($class, $table);
// Aggregate all the information from all classes in the hierarchy
foreach ($class->parentClasses as $parentClassName) {
// Parent class information is already contained in this class
$processedClasses[$parentClassName] = true;
}
foreach ($class->subClasses as $subClassName) {
$subClass = $this->em->getClassMetadata($subClassName);
$this->gatherColumns($subClass, $table);
$this->gatherRelationsSql($subClass, $table, $schema, $addedFks, $blacklistedFks);
$processedClasses[$subClassName] = true;
}
} elseif ($class->isInheritanceTypeJoined()) {
// Add all non-inherited fields as columns
foreach ($class->fieldMappings as $fieldName => $mapping) {
if ( ! isset($mapping['inherited'])) {
$this->gatherColumn($class, $mapping, $table);
}
}
$this->gatherRelationsSql($class, $table, $schema, $addedFks, $blacklistedFks);
// Add the discriminator column only to the root table
if ($class->name == $class->rootEntityName) {
$this->addDiscriminatorColumnDefinition($class, $table);
} else {
// Add an ID FK column to child tables
$pkColumns = [];
$inheritedKeyColumns = [];
foreach ($class->identifier as $identifierField) {
if (isset($class->fieldMappings[$identifierField]['inherited'])) {
$idMapping = $class->fieldMappings[$identifierField];
$this->gatherColumn($class, $idMapping, $table);
$columnName = $this->quoteStrategy->getColumnName(
$identifierField,
$class,
$this->platform
);
// TODO: This seems rather hackish, can we optimize it?
$table->getColumn($columnName)->setAutoincrement(false);
$pkColumns[] = $columnName;
$inheritedKeyColumns[] = $columnName;
continue;
}
if (isset($class->associationMappings[$identifierField]['inherited'])) {
$idMapping = $class->associationMappings[$identifierField];
$targetEntity = current(
array_filter(
$classes,
function (ClassMetadata $class) use ($idMapping) : bool {
return $class->name === $idMapping['targetEntity'];
}
)
);
foreach ($idMapping['joinColumns'] as $joinColumn) {
if (isset($targetEntity->fieldMappings[$joinColumn['referencedColumnName']])) {
$columnName = $this->quoteStrategy->getJoinColumnName(
$joinColumn,
$class,
$this->platform
);
$pkColumns[] = $columnName;
$inheritedKeyColumns[] = $columnName;
}
}
}
}
if ( ! empty($inheritedKeyColumns)) {
// Add a FK constraint on the ID column
$table->addForeignKeyConstraint(
$this->quoteStrategy->getTableName(
$this->em->getClassMetadata($class->rootEntityName),
$this->platform
),
$inheritedKeyColumns,
$inheritedKeyColumns,
['onDelete' => 'CASCADE']
);
}
if ( ! empty($pkColumns)) {
$table->setPrimaryKey($pkColumns);
}
}
} elseif ($class->isInheritanceTypeTablePerClass()) {
throw ORMException::notSupported();
} else {
$this->gatherColumns($class, $table);
$this->gatherRelationsSql($class, $table, $schema, $addedFks, $blacklistedFks);
}
$pkColumns = [];
foreach ($class->identifier as $identifierField) {
if (isset($class->fieldMappings[$identifierField])) {
$pkColumns[] = $this->quoteStrategy->getColumnName($identifierField, $class, $this->platform);
} elseif (isset($class->associationMappings[$identifierField])) {
/* @var $assoc \Doctrine\ORM\Mapping\OneToOne */
$assoc = $class->associationMappings[$identifierField];
foreach ($assoc['joinColumns'] as $joinColumn) {
$pkColumns[] = $this->quoteStrategy->getJoinColumnName($joinColumn, $class, $this->platform);
}
}
}
if ( ! $table->hasIndex('primary')) {
$table->setPrimaryKey($pkColumns);
}
// there can be unique indexes automatically created for join column
// if join column is also primary key we should keep only primary key on this column
// so, remove indexes overruled by primary key
$primaryKey = $table->getIndex('primary');
foreach ($table->getIndexes() as $idxKey => $existingIndex) {
if ($primaryKey->overrules($existingIndex)) {
$table->dropIndex($idxKey);
}
}
if (isset($class->table['indexes'])) {
foreach ($class->table['indexes'] as $indexName => $indexData) {
if ( ! isset($indexData['flags'])) {
$indexData['flags'] = [];
}
$table->addIndex($indexData['columns'], is_numeric($indexName) ? null : $indexName, (array) $indexData['flags'], $indexData['options'] ?? []);
}
}
if (isset($class->table['uniqueConstraints'])) {
foreach ($class->table['uniqueConstraints'] as $indexName => $indexData) {
$uniqIndex = new Index($indexName, $indexData['columns'], true, false, [], $indexData['options'] ?? []);
foreach ($table->getIndexes() as $tableIndexName => $tableIndex) {
if ($tableIndex->isFullfilledBy($uniqIndex)) {
$table->dropIndex($tableIndexName);
break;
}
}
$table->addUniqueIndex($indexData['columns'], is_numeric($indexName) ? null : $indexName, $indexData['options'] ?? []);
}
}
if (isset($class->table['options'])) {
foreach ($class->table['options'] as $key => $val) {
$table->addOption($key, $val);
}
}
$processedClasses[$class->name] = true;
if ($class->isIdGeneratorSequence() && $class->name == $class->rootEntityName) {
$seqDef = $class->sequenceGeneratorDefinition;
$quotedName = $this->quoteStrategy->getSequenceName($seqDef, $class, $this->platform);
if ( ! $schema->hasSequence($quotedName)) {
$schema->createSequence(
$quotedName,
$seqDef['allocationSize'],
$seqDef['initialValue']
);
}
}
if ($eventManager->hasListeners(ToolEvents::postGenerateSchemaTable)) {
$eventManager->dispatchEvent(
ToolEvents::postGenerateSchemaTable,
new GenerateSchemaTableEventArgs($class, $schema, $table)
);
}
}
if ( ! $this->platform->supportsSchemas() && ! $this->platform->canEmulateSchemas()) {
$schema->visit(new RemoveNamespacedAssets());
}
if ($eventManager->hasListeners(ToolEvents::postGenerateSchema)) {
$eventManager->dispatchEvent(
ToolEvents::postGenerateSchema,
new GenerateSchemaEventArgs($this->em, $schema)
);
}
return $schema;
}
/**
* Gets a portable column definition as required by the DBAL for the discriminator
* column of a class.
*
* @param ClassMetadata $class
* @param Table $table
*
* @return void
*/
private function addDiscriminatorColumnDefinition($class, Table $table)
{
$discrColumn = $class->discriminatorColumn;
if ( ! isset($discrColumn['type']) ||
(strtolower($discrColumn['type']) == 'string' && ! isset($discrColumn['length']))
) {
$discrColumn['type'] = 'string';
$discrColumn['length'] = 255;
}
$options = [
'length' => $discrColumn['length'] ?? null,
'notnull' => true
];
if (isset($discrColumn['columnDefinition'])) {
$options['columnDefinition'] = $discrColumn['columnDefinition'];
}
$table->addColumn($discrColumn['name'], $discrColumn['type'], $options);
}
/**
* Gathers the column definitions as required by the DBAL of all field mappings
* found in the given class.
*
* @param ClassMetadata $class
* @param Table $table
*
* @return void
*/
private function gatherColumns($class, Table $table)
{
$pkColumns = [];
foreach ($class->fieldMappings as $mapping) {
if ($class->isInheritanceTypeSingleTable() && isset($mapping['inherited'])) {
continue;
}
$this->gatherColumn($class, $mapping, $table);
if ($class->isIdentifier($mapping['fieldName'])) {
$pkColumns[] = $this->quoteStrategy->getColumnName($mapping['fieldName'], $class, $this->platform);
}
}
}
/**
* Creates a column definition as required by the DBAL from an ORM field mapping definition.
*
* @param ClassMetadata $class The class that owns the field mapping.
* @param array $mapping The field mapping.
* @param Table $table
*
* @return void
*/
private function gatherColumn($class, array $mapping, Table $table)
{
$columnName = $this->quoteStrategy->getColumnName($mapping['fieldName'], $class, $this->platform);
$columnType = $mapping['type'];
$options = [];
$options['length'] = $mapping['length'] ?? null;
$options['notnull'] = isset($mapping['nullable']) ? ! $mapping['nullable'] : true;
if ($class->isInheritanceTypeSingleTable() && $class->parentClasses) {
$options['notnull'] = false;
}
$options['platformOptions'] = [];
$options['platformOptions']['version'] = $class->isVersioned && $class->versionField === $mapping['fieldName'];
if (strtolower($columnType) === 'string' && null === $options['length']) {
$options['length'] = 255;
}
if (isset($mapping['precision'])) {
$options['precision'] = $mapping['precision'];
}
if (isset($mapping['scale'])) {
$options['scale'] = $mapping['scale'];
}
if (isset($mapping['default'])) {
$options['default'] = $mapping['default'];
}
if (isset($mapping['columnDefinition'])) {
$options['columnDefinition'] = $mapping['columnDefinition'];
}
// the 'default' option can be overwritten here
$options = $this->gatherColumnOptions($mapping) + $options;
if ($class->isIdGeneratorIdentity() && $class->getIdentifierFieldNames() == [$mapping['fieldName']]) {
$options['autoincrement'] = true;
}
if ($class->isInheritanceTypeJoined() && $class->name !== $class->rootEntityName) {
$options['autoincrement'] = false;
}
if ($table->hasColumn($columnName)) {
// required in some inheritance scenarios
$table->changeColumn($columnName, $options);
} else {
$table->addColumn($columnName, $columnType, $options);
}
$isUnique = $mapping['unique'] ?? false;
if ($isUnique) {
$table->addUniqueIndex([$columnName]);
}
}
/**
* Gathers the SQL for properly setting up the relations of the given class.
* This includes the SQL for foreign key constraints and join tables.
*
* @param ClassMetadata $class
* @param Table $table
* @param Schema $schema
* @param array $addedFks
* @param array $blacklistedFks
*
* @return void
*
* @throws \Doctrine\ORM\ORMException
*/
private function gatherRelationsSql($class, $table, $schema, &$addedFks, &$blacklistedFks)
{
foreach ($class->associationMappings as $id => $mapping) {
if (isset($mapping['inherited']) && ! \in_array($id, $class->identifier, true)) {
continue;
}
$foreignClass = $this->em->getClassMetadata($mapping['targetEntity']);
if ($mapping['type'] & ClassMetadata::TO_ONE && $mapping['isOwningSide']) {
$primaryKeyColumns = []; // PK is unnecessary for this relation-type
$this->gatherRelationJoinColumns(
$mapping['joinColumns'],
$table,
$foreignClass,
$mapping,
$primaryKeyColumns,
$addedFks,
$blacklistedFks
);
} elseif ($mapping['type'] == ClassMetadata::ONE_TO_MANY && $mapping['isOwningSide']) {
//... create join table, one-many through join table supported later
throw ORMException::notSupported();
} elseif ($mapping['type'] == ClassMetadata::MANY_TO_MANY && $mapping['isOwningSide']) {
// create join table
$joinTable = $mapping['joinTable'];
$theJoinTable = $schema->createTable(
$this->quoteStrategy->getJoinTableName($mapping, $foreignClass, $this->platform)
);
$primaryKeyColumns = [];
// Build first FK constraint (relation table => source table)
$this->gatherRelationJoinColumns(
$joinTable['joinColumns'],
$theJoinTable,
$class,
$mapping,
$primaryKeyColumns,
$addedFks,
$blacklistedFks
);
// Build second FK constraint (relation table => target table)
$this->gatherRelationJoinColumns(
$joinTable['inverseJoinColumns'],
$theJoinTable,
$foreignClass,
$mapping,
$primaryKeyColumns,
$addedFks,
$blacklistedFks
);
$theJoinTable->setPrimaryKey($primaryKeyColumns);
}
}
}
/**
* Gets the class metadata that is responsible for the definition of the referenced column name.
*
* Previously this was a simple task, but with DDC-117 this problem is actually recursive. If its
* not a simple field, go through all identifier field names that are associations recursively and
* find that referenced column name.
*
* TODO: Is there any way to make this code more pleasing?
*
* @param ClassMetadata $class
* @param string $referencedColumnName
*
* @return array (ClassMetadata, referencedFieldName)
*/
private function getDefiningClass($class, $referencedColumnName)
{
$referencedFieldName = $class->getFieldName($referencedColumnName);
if ($class->hasField($referencedFieldName)) {
return [$class, $referencedFieldName];
}
if (in_array($referencedColumnName, $class->getIdentifierColumnNames())) {
// it seems to be an entity as foreign key
foreach ($class->getIdentifierFieldNames() as $fieldName) {
if ($class->hasAssociation($fieldName)
&& $class->getSingleAssociationJoinColumnName($fieldName) == $referencedColumnName) {
return $this->getDefiningClass(
$this->em->getClassMetadata($class->associationMappings[$fieldName]['targetEntity']),
$class->getSingleAssociationReferencedJoinColumnName($fieldName)
);
}
}
}
return null;
}
/**
* Gathers columns and fk constraints that are required for one part of relationship.
*
* @param array $joinColumns
* @param Table $theJoinTable
* @param ClassMetadata $class
* @param array $mapping
* @param array $primaryKeyColumns
* @param array $addedFks
* @param array $blacklistedFks
*
* @return void
*
* @throws \Doctrine\ORM\ORMException
*/
private function gatherRelationJoinColumns(
$joinColumns,
$theJoinTable,
$class,
$mapping,
&$primaryKeyColumns,
&$addedFks,
&$blacklistedFks
)
{
$localColumns = [];
$foreignColumns = [];
$fkOptions = [];
$foreignTableName = $this->quoteStrategy->getTableName($class, $this->platform);
$uniqueConstraints = [];
foreach ($joinColumns as $joinColumn) {
[$definingClass, $referencedFieldName] = $this->getDefiningClass(
$class,
$joinColumn['referencedColumnName']
);
if ( ! $definingClass) {
throw new \Doctrine\ORM\ORMException(
'Column name `' . $joinColumn['referencedColumnName'] . '` referenced for relation from '
. $mapping['sourceEntity'] . ' towards ' . $mapping['targetEntity'] . ' does not exist.'
);
}
$quotedColumnName = $this->quoteStrategy->getJoinColumnName($joinColumn, $class, $this->platform);
$quotedRefColumnName = $this->quoteStrategy->getReferencedJoinColumnName(
$joinColumn,
$class,
$this->platform
);
$primaryKeyColumns[] = $quotedColumnName;
$localColumns[] = $quotedColumnName;
$foreignColumns[] = $quotedRefColumnName;
if ( ! $theJoinTable->hasColumn($quotedColumnName)) {
// Only add the column to the table if it does not exist already.
// It might exist already if the foreign key is mapped into a regular
// property as well.
$fieldMapping = $definingClass->getFieldMapping($referencedFieldName);
$columnDef = null;
if (isset($joinColumn['columnDefinition'])) {
$columnDef = $joinColumn['columnDefinition'];
} elseif (isset($fieldMapping['columnDefinition'])) {
$columnDef = $fieldMapping['columnDefinition'];
}
$columnOptions = ['notnull' => false, 'columnDefinition' => $columnDef];
if (isset($joinColumn['nullable'])) {
$columnOptions['notnull'] = ! $joinColumn['nullable'];
}
$columnOptions = $columnOptions + $this->gatherColumnOptions($fieldMapping);
if ($fieldMapping['type'] == "string" && isset($fieldMapping['length'])) {
$columnOptions['length'] = $fieldMapping['length'];
} elseif ($fieldMapping['type'] == "decimal") {
$columnOptions['scale'] = $fieldMapping['scale'];
$columnOptions['precision'] = $fieldMapping['precision'];
}
$theJoinTable->addColumn($quotedColumnName, $fieldMapping['type'], $columnOptions);
}
if (isset($joinColumn['unique']) && $joinColumn['unique'] == true) {
$uniqueConstraints[] = ['columns' => [$quotedColumnName]];
}
if (isset($joinColumn['onDelete'])) {
$fkOptions['onDelete'] = $joinColumn['onDelete'];
}
}
// Prefer unique constraints over implicit simple indexes created for foreign keys.
// Also avoids index duplication.
foreach ($uniqueConstraints as $indexName => $unique) {
$theJoinTable->addUniqueIndex($unique['columns'], is_numeric($indexName) ? null : $indexName);
}
$compositeName = $theJoinTable->getName().'.'.implode('', $localColumns);
if (isset($addedFks[$compositeName])
&& ($foreignTableName != $addedFks[$compositeName]['foreignTableName']
|| 0 < count(array_diff($foreignColumns, $addedFks[$compositeName]['foreignColumns'])))
) {
foreach ($theJoinTable->getForeignKeys() as $fkName => $key) {
if (0 === count(array_diff($key->getLocalColumns(), $localColumns))
&& (($key->getForeignTableName() != $foreignTableName)
|| 0 < count(array_diff($key->getForeignColumns(), $foreignColumns)))
) {
$theJoinTable->removeForeignKey($fkName);
break;
}
}
$blacklistedFks[$compositeName] = true;
} elseif ( ! isset($blacklistedFks[$compositeName])) {
$addedFks[$compositeName] = ['foreignTableName' => $foreignTableName, 'foreignColumns' => $foreignColumns];
$theJoinTable->addUnnamedForeignKeyConstraint(
$foreignTableName,
$localColumns,
$foreignColumns,
$fkOptions
);
}
}
/**
* @param mixed[] $mapping
*
* @return mixed[]
*/
private function gatherColumnOptions(array $mapping) : array
{
if (! isset($mapping['options'])) {
return [];
}
$options = array_intersect_key($mapping['options'], array_flip(self::KNOWN_COLUMN_OPTIONS));
$options['customSchemaOptions'] = array_diff_key($mapping['options'], $options);
return $options;
}
/**
* Drops the database schema for the given classes.
*
* In any way when an exception is thrown it is suppressed since drop was
* issued for all classes of the schema and some probably just don't exist.
*
* @param array $classes
*
* @return void
*/
public function dropSchema(array $classes)
{
$dropSchemaSql = $this->getDropSchemaSQL($classes);
$conn = $this->em->getConnection();
foreach ($dropSchemaSql as $sql) {
try {
$conn->executeQuery($sql);
} catch (\Throwable $e) {
// ignored
}
}
}
/**
* Drops all elements in the database of the current connection.
*
* @return void
*/
public function dropDatabase()
{
$dropSchemaSql = $this->getDropDatabaseSQL();
$conn = $this->em->getConnection();
foreach ($dropSchemaSql as $sql) {
$conn->executeQuery($sql);
}
}
/**
* Gets the SQL needed to drop the database schema for the connections database.
*
* @return string[]
*/
public function getDropDatabaseSQL()
{
$sm = $this->em->getConnection()->getSchemaManager();
$schema = $sm->createSchema();
$visitor = new DropSchemaSqlCollector($this->platform);
$schema->visit($visitor);
return $visitor->getQueries();
}
/**
* Gets SQL to drop the tables defined by the passed classes.
*
* @param array $classes
*
* @return string[]
*/
public function getDropSchemaSQL(array $classes)
{
$visitor = new DropSchemaSqlCollector($this->platform);
$schema = $this->getSchemaFromMetadata($classes);
$sm = $this->em->getConnection()->getSchemaManager();
$fullSchema = $sm->createSchema();
foreach ($fullSchema->getTables() as $table) {
if ( ! $schema->hasTable($table->getName())) {
foreach ($table->getForeignKeys() as $foreignKey) {
if ($schema->hasTable($foreignKey->getForeignTableName())) {
$visitor->acceptForeignKey($table, $foreignKey);
}
}
} else {
$visitor->acceptTable($table);
foreach ($table->getForeignKeys() as $foreignKey) {
$visitor->acceptForeignKey($table, $foreignKey);
}
}
}
if ($this->platform->supportsSequences()) {
foreach ($schema->getSequences() as $sequence) {
$visitor->acceptSequence($sequence);
}
foreach ($schema->getTables() as $table) {
/* @var $sequence Table */
if ($table->hasPrimaryKey()) {
$columns = $table->getPrimaryKey()->getColumns();
if (count($columns) == 1) {
$checkSequence = $table->getName() . '_' . $columns[0] . '_seq';
if ($fullSchema->hasSequence($checkSequence)) {
$visitor->acceptSequence($fullSchema->getSequence($checkSequence));
}
}
}
}
}
return $visitor->getQueries();
}
/**
* Updates the database schema of the given classes by comparing the ClassMetadata
* instances to the current database schema that is inspected.
*
* @param array $classes
* @param boolean $saveMode If TRUE, only performs a partial update
* without dropping assets which are scheduled for deletion.
*
* @return void
*/
public function updateSchema(array $classes, $saveMode = false)
{
$updateSchemaSql = $this->getUpdateSchemaSql($classes, $saveMode);
$conn = $this->em->getConnection();
foreach ($updateSchemaSql as $sql) {
$conn->executeQuery($sql);
}
}
/**
* Gets the sequence of SQL statements that need to be performed in order
* to bring the given class mappings in-synch with the relational schema.
*
* @param array $classes The classes to consider.
* @param boolean $saveMode If TRUE, only generates SQL for a partial update
* that does not include SQL for dropping assets which are scheduled for deletion.
*
* @return string[] The sequence of SQL statements.
*/
public function getUpdateSchemaSql(array $classes, $saveMode = false)
{
$toSchema = $this->getSchemaFromMetadata($classes);
$fromSchema = $this->createSchemaForComparison($toSchema);
$comparator = new Comparator();
$schemaDiff = $comparator->compare($fromSchema, $toSchema);
if ($saveMode) {
return $schemaDiff->toSaveSql($this->platform);
}
return $schemaDiff->toSql($this->platform);
}
/**
* Creates the schema from the database, ensuring tables from the target schema are whitelisted for comparison.
*/
private function createSchemaForComparison(Schema $toSchema) : Schema
{
$connection = $this->em->getConnection();
$schemaManager = $connection->getSchemaManager();
// backup schema assets filter
$config = $connection->getConfiguration();
$previousFilter = $config->getSchemaAssetsFilter();
if ($previousFilter === null) {
return $schemaManager->createSchema();
}
// whitelist assets we already know about in $toSchema, use the existing filter otherwise
$config->setSchemaAssetsFilter(static function ($asset) use ($previousFilter, $toSchema) : bool {
$assetName = $asset instanceof AbstractAsset ? $asset->getName() : $asset;
return $toSchema->hasTable($assetName) || $toSchema->hasSequence($assetName) || $previousFilter($asset);
});
try {
return $schemaManager->createSchema();
} finally {
// restore schema assets filter
$config->setSchemaAssetsFilter($previousFilter);
}
}
}

View File

@@ -0,0 +1,275 @@
<?php
/*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* This software consists of voluntary contributions made by many individuals
* and is licensed under the MIT license. For more information, see
* <http://www.doctrine-project.org>.
*/
namespace Doctrine\ORM\Tools;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\Mapping\ClassMetadataInfo;
use Doctrine\DBAL\Types\Type;
/**
* Performs strict validation of the mapping schema
*
* @license http://www.opensource.org/licenses/mit-license.php MIT
* @link www.doctrine-project.com
* @since 1.0
* @author Benjamin Eberlei <kontakt@beberlei.de>
* @author Guilherme Blanco <guilhermeblanco@hotmail.com>
* @author Jonathan Wage <jonwage@gmail.com>
* @author Roman Borschel <roman@code-factory.org>
*/
class SchemaValidator
{
/**
* @var EntityManagerInterface
*/
private $em;
/**
* @param EntityManagerInterface $em
*/
public function __construct(EntityManagerInterface $em)
{
$this->em = $em;
}
/**
* 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.
*
* @return array
*/
public function validateMapping()
{
$errors = [];
$cmf = $this->em->getMetadataFactory();
$classes = $cmf->getAllMetadata();
foreach ($classes as $class) {
if ($ce = $this->validateClass($class)) {
$errors[$class->name] = $ce;
}
}
return $errors;
}
/**
* Validates a single class of the current.
*
* @param ClassMetadataInfo $class
*
* @return string[]
*
* @psalm-return list<string>
*/
public function validateClass(ClassMetadataInfo $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'] . "'.";
}
}
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;
}
if ($assoc['mappedBy'] && $assoc['inversedBy']) {
$ce[] = "The association " . $class . "#" . $fieldName . " cannot be defined as both inverse and owning.";
}
$targetMetadata = $cmf->getMetadataFor($assoc['targetEntity']);
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 mappedBy association on the target-entity ".
$assoc['targetEntity'] . "#" . $assoc['mappedBy'] . " does not contain the required ".
"'inversedBy' 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'] == ClassMetadataInfo::ONE_TO_ONE && $targetAssoc['type'] !== ClassMetadataInfo::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'] == ClassMetadataInfo::MANY_TO_ONE && $targetAssoc['type'] !== ClassMetadataInfo::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'] == ClassMetadataInfo::MANY_TO_MANY && $targetAssoc['type'] !== ClassMetadataInfo::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'] == ClassMetadataInfo::MANY_TO_MANY) {
$identifierColumns = $class->getIdentifierColumnNames();
foreach ($assoc['joinTable']['joinColumns'] as $joinColumn) {
if (!in_array($joinColumn['referencedColumnName'], $identifierColumns)) {
$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)) {
$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'] & ClassMetadataInfo::TO_ONE) {
$identifierColumns = $targetMetadata->getIdentifierColumnNames();
foreach ($assoc['joinColumns'] as $joinColumn) {
if (!in_array($joinColumn['referencedColumnName'], $identifierColumns)) {
$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;
}
}
}
}
foreach ($class->subClasses as $subClass) {
if (!in_array($class->name, class_parents($subClass))) {
$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()
{
$schemaTool = new SchemaTool($this->em);
$allMetadata = $this->em->getMetadataFactory()->getAllMetadata();
return count($schemaTool->getUpdateSchemaSql($allMetadata, true)) == 0;
}
}

View File

@@ -0,0 +1,195 @@
<?php
/*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* This software consists of voluntary contributions made by many individuals
* and is licensed under the MIT license. For more information, see
* <http://www.doctrine-project.org>.
*/
namespace Doctrine\ORM\Tools;
use Doctrine\Common\ClassLoader;
use Doctrine\Common\Cache\Cache;
use Doctrine\Common\Cache\CacheProvider;
use Doctrine\Common\Cache\ArrayCache;
use Doctrine\ORM\Configuration;
use Doctrine\ORM\Mapping\Driver\XmlDriver;
use Doctrine\ORM\Mapping\Driver\YamlDriver;
/**
* Convenience class for setting up Doctrine from different installations and configurations.
*
* @author Benjamin Eberlei <kontakt@beberlei.de>
*/
class Setup
{
/**
* Use this method to register all autoloads for a downloaded Doctrine library.
* Pick the directory the library was uncompressed into.
*
* @param string $directory
*
* @return void
*/
public static function registerAutoloadDirectory($directory)
{
if (!class_exists('Doctrine\Common\ClassLoader', false)) {
require_once $directory . "/Doctrine/Common/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 array $paths
* @param boolean $isDevMode
* @param string $proxyDir
* @param Cache $cache
* @param bool $useSimpleAnnotationReader
*
* @return Configuration
*/
public static function createAnnotationMetadataConfiguration(array $paths, $isDevMode = false, $proxyDir = null, Cache $cache = null, $useSimpleAnnotationReader = true)
{
$config = self::createConfiguration($isDevMode, $proxyDir, $cache);
$config->setMetadataDriverImpl($config->newDefaultAnnotationDriver($paths, $useSimpleAnnotationReader));
return $config;
}
/**
* Creates a configuration with a xml metadata driver.
*
* @param array $paths
* @param boolean $isDevMode
* @param string $proxyDir
* @param Cache $cache
*
* @return Configuration
*/
public static function createXMLMetadataConfiguration(array $paths, $isDevMode = false, $proxyDir = null, Cache $cache = null)
{
$config = self::createConfiguration($isDevMode, $proxyDir, $cache);
$config->setMetadataDriverImpl(new XmlDriver($paths));
return $config;
}
/**
* Creates a configuration with a yaml metadata driver.
*
* @param array $paths
* @param boolean $isDevMode
* @param string $proxyDir
* @param Cache $cache
*
* @return Configuration
*/
public static function createYAMLMetadataConfiguration(array $paths, $isDevMode = false, $proxyDir = null, Cache $cache = null)
{
$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 $proxyDir
* @param Cache $cache
*
* @return Configuration
*/
public static function createConfiguration($isDevMode = false, $proxyDir = null, Cache $cache = null)
{
$proxyDir = $proxyDir ?: sys_get_temp_dir();
$cache = self::createCacheConfiguration($isDevMode, $proxyDir, $cache);
$config = new Configuration();
$config->setMetadataCacheImpl($cache);
$config->setQueryCacheImpl($cache);
$config->setResultCacheImpl($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 ($isDevMode === true) {
return new ArrayCache();
}
if (extension_loaded('apcu')) {
return new \Doctrine\Common\Cache\ApcuCache();
}
if (extension_loaded('memcached')) {
$memcached = new \Memcached();
$memcached->addServer('127.0.0.1', 11211);
$cache = new \Doctrine\Common\Cache\MemcachedCache();
$cache->setMemcached($memcached);
return $cache;
}
if (extension_loaded('redis')) {
$redis = new \Redis();
$redis->connect('127.0.0.1');
$cache = new \Doctrine\Common\Cache\RedisCache();
$cache->setRedis($redis);
return $cache;
}
return new ArrayCache();
}
}

View File

@@ -0,0 +1,42 @@
<?php
/*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* This software consists of voluntary contributions made by many individuals
* and is licensed under the MIT license. For more information, see
* <http://www.doctrine-project.org>.
*/
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.
*
* @var string
*/
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.
*
* @var string
*/
const postGenerateSchema = 'postGenerateSchema';
}

View File

@@ -0,0 +1,46 @@
<?php
/*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* This software consists of voluntary contributions made by many individuals
* and is licensed under the MIT license. For more information, see
* <http://www.doctrine-project.org>.
*/
namespace Doctrine\ORM\Tools;
use Doctrine\ORM\ORMException;
use Throwable;
/**
* Tools related Exceptions.
*
* @author Benjamin Eberlei <kontakt@beberlei.de>
*/
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("Could not map doctrine 1 type '$type'!");
}
}