Skip to content

Commit

Permalink
Merge pull request #307 from lstrojny/custom-purger
Browse files Browse the repository at this point in the history
Support for custom purgers
  • Loading branch information
greg0ire committed Apr 3, 2020
2 parents fc785c6 + 18617bd commit 886cf2e
Show file tree
Hide file tree
Showing 11 changed files with 503 additions and 17 deletions.
55 changes: 47 additions & 8 deletions Command/LoadDataFixturesDoctrineCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,20 @@
namespace Doctrine\Bundle\FixturesBundle\Command;

use Doctrine\Bundle\DoctrineBundle\Command\DoctrineCommand;
use Doctrine\Bundle\FixturesBundle\DependencyInjection\CompilerPass\PurgerFactoryCompilerPass;
use Doctrine\Bundle\FixturesBundle\Loader\SymfonyFixturesLoader;
use Doctrine\Bundle\FixturesBundle\Purger\ORMPurgerFactory;
use Doctrine\Bundle\FixturesBundle\Purger\PurgerFactory;
use Doctrine\Common\DataFixtures\Executor\ORMExecutor;
use Doctrine\Common\DataFixtures\Purger\ORMPurger;
use Doctrine\Common\Persistence\ManagerRegistry;
use Doctrine\Common\Persistence\ManagerRegistry as DeprecatedManagerRegistry;
use Doctrine\DBAL\Sharding\PoolingShardConnection;
use Doctrine\Persistence\ManagerRegistry;
use LogicException;
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 TypeError;
use const E_USER_DEPRECATED;
use function implode;
use function sprintf;
Expand All @@ -28,19 +32,35 @@ class LoadDataFixturesDoctrineCommand extends DoctrineCommand
/** @var SymfonyFixturesLoader */
private $fixturesLoader;

public function __construct(SymfonyFixturesLoader $fixturesLoader, ?ManagerRegistry $doctrine = null)
/** @var PurgerFactory[] */
private $purgerFactories;

/**
* @param ManagerRegistry|DeprecatedManagerRegistry|null $doctrine
* @param PurgerFactory[] $purgerFactories
*/
public function __construct(SymfonyFixturesLoader $fixturesLoader, $doctrine = null, array $purgerFactories = [])
{
if ($doctrine === null) {
@trigger_error(sprintf(
'The "%s" constructor expects a "%s" instance as second argument, not passing it will throw a \TypeError in DoctrineFixturesBundle 4.0.',
static::class,
'Argument 2 of %s() expects an instance of %s or preferably %s, not passing it will throw a \TypeError in DoctrineFixturesBundle 4.0.',
__METHOD__,
DeprecatedManagerRegistry::class,
ManagerRegistry::class
), E_USER_DEPRECATED);
} elseif (! $doctrine instanceof ManagerRegistry && ! $doctrine instanceof DeprecatedManagerRegistry) {
throw new TypeError(sprintf(
'Argument 2 passed to %s() must implement interface %s or preferably %s',
__METHOD__,
DeprecatedManagerRegistry::class,
ManagerRegistry::class
));
}

parent::__construct($doctrine);

$this->fixturesLoader = $fixturesLoader;
$this->fixturesLoader = $fixturesLoader;
$this->purgerFactories = $purgerFactories;
}

// phpcs:ignore SlevomatCodingStandard.TypeHints.TypeHintDeclaration.MissingReturnTypeHint
Expand All @@ -52,6 +72,8 @@ protected function configure()
->addOption('append', null, InputOption::VALUE_NONE, 'Append the data fixtures instead of deleting all data from the database first.')
->addOption('group', null, InputOption::VALUE_IS_ARRAY|InputOption::VALUE_REQUIRED, 'Only load fixtures that belong to this group')
->addOption('em', null, InputOption::VALUE_REQUIRED, 'The entity manager to use for this command.')
->addOption('purger', null, InputOption::VALUE_REQUIRED, 'The purger to use for this command', 'default')
->addOption('purge-exclusions', null, InputOption::VALUE_IS_ARRAY | InputOption::VALUE_REQUIRED, 'List of database tables to ignore while purging')
->addOption('shard', null, InputOption::VALUE_REQUIRED, 'The shard connection to use for this command.')
->addOption('purge-with-truncate', null, InputOption::VALUE_NONE, 'Purge data by using a database-level TRUNCATE statement')
->setHelp(<<<EOT
Expand Down Expand Up @@ -118,8 +140,25 @@ protected function execute(InputInterface $input, OutputInterface $output)

return 1;
}
$purger = new ORMPurger($em);
$purger->setPurgeMode($input->getOption('purge-with-truncate') ? ORMPurger::PURGE_MODE_TRUNCATE : ORMPurger::PURGE_MODE_DELETE);

if (! isset($this->purgerFactories[$input->getOption('purger')])) {
$ui->warning(sprintf(
'Could not find purger factory with alias "%1$s", using default purger. Did you forget to register the %2$s implementation with tag "%3$s" and alias "%1$s"?',
$input->getOption('purger'),
PurgerFactory::class,
PurgerFactoryCompilerPass::PURGER_FACTORY_TAG
));
$factory = new ORMPurgerFactory();
} else {
$factory = $this->purgerFactories[$input->getOption('purger')];
}

$purger = $factory->createForEntityManager(
$input->getOption('em'),
$em,
$input->getOption('purge-exclusions'),
$input->getOption('purge-with-truncate')
);
$executor = new ORMExecutor($em, $purger);
$executor->setLogger(static function ($message) use ($ui) : void {
$ui->text(sprintf(' <comment>></comment> <info>%s</info>', $message));
Expand Down
35 changes: 35 additions & 0 deletions DependencyInjection/CompilerPass/PurgerFactoryCompilerPass.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<?php

declare(strict_types=1);

namespace Doctrine\Bundle\FixturesBundle\DependencyInjection\CompilerPass;

use LogicException;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Reference;
use function sprintf;

final class PurgerFactoryCompilerPass implements CompilerPassInterface
{
public const PURGER_FACTORY_TAG = 'doctrine.fixtures.purger_factory';

public function process(ContainerBuilder $container) : void
{
$definition = $container->getDefinition('doctrine.fixtures_load_command');
$taggedServices = $container->findTaggedServiceIds(self::PURGER_FACTORY_TAG);

$purgerFactories = [];
foreach ($taggedServices as $serviceId => $tags) {
foreach ($tags as $tagData) {
if (! isset($tagData['alias'])) {
throw new LogicException(sprintf('Proxy factory "%s" must define an alias', $serviceId));
}

$purgerFactories[$tagData['alias']] = new Reference($serviceId);
}
}

$definition->setArgument(2, $purgerFactories);
}
}
2 changes: 2 additions & 0 deletions DoctrineFixturesBundle.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
namespace Doctrine\Bundle\FixturesBundle;

use Doctrine\Bundle\FixturesBundle\DependencyInjection\CompilerPass\FixturesCompilerPass;
use Doctrine\Bundle\FixturesBundle\DependencyInjection\CompilerPass\PurgerFactoryCompilerPass;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\HttpKernel\Bundle\Bundle;

Expand All @@ -17,5 +18,6 @@ class DoctrineFixturesBundle extends Bundle
public function build(ContainerBuilder $container)
{
$container->addCompilerPass(new FixturesCompilerPass());
$container->addCompilerPass(new PurgerFactoryCompilerPass());
}
}
20 changes: 20 additions & 0 deletions Purger/ORMPurgerFactory.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?php

declare(strict_types=1);

namespace Doctrine\Bundle\FixturesBundle\Purger;

use Doctrine\Common\DataFixtures\Purger\ORMPurger;
use Doctrine\Common\DataFixtures\Purger\PurgerInterface;
use Doctrine\ORM\EntityManagerInterface;

final class ORMPurgerFactory implements PurgerFactory
{
public function createForEntityManager(?string $emName, EntityManagerInterface $em, array $excluded = [], bool $purgeWithTruncate = false) : PurgerInterface
{
$purger = new ORMPurger($em, $excluded);
$purger->setPurgeMode($purgeWithTruncate ? ORMPurger::PURGE_MODE_TRUNCATE : ORMPurger::PURGE_MODE_DELETE);

return $purger;
}
}
13 changes: 13 additions & 0 deletions Purger/PurgerFactory.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?php

declare(strict_types=1);

namespace Doctrine\Bundle\FixturesBundle\Purger;

use Doctrine\Common\DataFixtures\Purger\PurgerInterface;
use Doctrine\ORM\EntityManagerInterface;

interface PurgerFactory
{
public function createForEntityManager(?string $emName, EntityManagerInterface $em, array $excluded = [], bool $purgeWithTruncate = false) : PurgerInterface;
}
4 changes: 4 additions & 0 deletions Resources/config/services.xml
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,9 @@
<service id="doctrine.fixtures.loader" class="Doctrine\Bundle\FixturesBundle\Loader\SymfonyFixturesLoader" public="false">
<argument type="service" id="service_container" />
</service>

<service id="doctrine.fixtures.purger.orm_purger_factory" class="Doctrine\Bundle\FixturesBundle\Purger\ORMPurgerFactory" public="false">
<tag name="doctrine.fixtures.purger_factory" alias="default"/>
</service>
</services>
</container>
90 changes: 90 additions & 0 deletions Resources/doc/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -293,3 +293,93 @@ fixture using the ``UserFixtures`` group:
.. _`installation chapter`: https://getcomposer.org/doc/00-intro.md
.. _`Symfony Flex`: https://symfony.com/doc/current/setup/flex.html
.. _`default service configuration`: https://symfony.com/doc/current/service_container.html#service-container-services-load-example


Specifying purging behavior
---------------------------

By default all previously existing data is purged using ``DELETE FROM table`` statements. If you prefer to use
``TRUNCATE table`` statements for purging, use ``--purge-with-truncate``.

If you want to exclude a set of tables from being purged, e.g. because your schema comes with pre-populated,
semi-static data, pass the option ``--purge-exclusions``. Specify ``--purge-exclusions`` multiple times to exclude
multiple tables.

You can also customize purging behavior significantly more and implement a custom purger plus a custom purger factory.


// src/Purger/CustomPurger.php
namespace App\Purger;

use Doctrine\Common\DataFixtures\Purger\PurgerInterface;

// ...
class CustomPurger implements PurgerInterface
{
public function purge() : void
{
// ...
}
}

// src/Purger/CustomPurgerFactory.php
namespace App\Purger;
// ...
use Doctrine\Bundle\FixturesBundle\Purger\PurgerFactory;

class CustomPurgerFactory implements PurgerFactory
{
public function createForEntityManager(?string $emName, EntityManagerInterface $em, array $excluded = [], bool $purgeWithTruncate = false) : PurgerInterface;
{
return new CustomPurger($em);
}
}

The next step is to register our custom purger factory and specify its alias.

.. configuration-block::

.. code-block:: yaml
# config/services.yaml
services:
App\Purger\CustomPurgerFactory:
tags:
- { name: 'doctrine.fixtures.purger_factory', alias: 'my_purger' }
.. code-block:: xml
<!-- config/services.xml -->
<?xml version="1.0" encoding="UTF-8" ?>
<container xmlns="http://symfony.com/schema/dic/services"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://symfony.com/schema/dic/services
https://symfony.com/schema/dic/services/services-1.0.xsd">
<services>
<service id="App\Purger\CustomPurgerFactory">
<tag name="doctrine.fixtures.purger_factory" alias="my_purger"/>
</service>
</services>
</container>
.. code-block:: php
// config/services.php
namespace Symfony\Component\DependencyInjection\Loader\Configurator;
use App\Purger\CustomerPurgerFactory;
return function(ContainerConfigurator $configurator) : void {
$services = $configurator->services();
$services->set(CustomerPurgerFactory::class)
->tag('doctrine.fixtures.purger_factory', ['alias' => 'my_purger'])
;
};
With the ``--purger`` option we can now specify to use ``my_purger`` instead of the ``default`` purger.

.. code-block:: terminal
$ php bin/console doctrine:fixtures:load --purger=my_purger
9 changes: 5 additions & 4 deletions Tests/Command/LoadDataFixturesDoctrineCommandTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,17 @@

use Doctrine\Bundle\FixturesBundle\Command\LoadDataFixturesDoctrineCommand;
use Doctrine\Bundle\FixturesBundle\Loader\SymfonyFixturesLoader;
use Doctrine\Common\Persistence\ManagerRegistry;
use Doctrine\Bundle\FixturesBundle\Tests\DeprecationUtil;
use PHPUnit\Framework\TestCase;
use Symfony\Component\DependencyInjection\Container;
use TypeError;
use function sprintf;

class LoadDataFixturesDoctrineCommandTest extends TestCase
{
/**
* @group legacy
* @expectedDeprecation The "Doctrine\Bundle\FixturesBundle\Command\LoadDataFixturesDoctrineCommand" constructor expects a "Doctrine\Common\Persistence\ManagerRegistry" instance as second argument, not passing it will throw a \TypeError in DoctrineFixturesBundle 4.0.
* @expectedDeprecation Argument 2 of Doctrine\Bundle\FixturesBundle\Command\LoadDataFixturesDoctrineCommand::__construct() expects an instance of Doctrine\Common\Persistence\ManagerRegistry or preferably Doctrine\Persistence\ManagerRegistry, not passing it will throw a \TypeError in DoctrineFixturesBundle 4.0.
*/
public function testInstantiatingWithoutManagerRegistry() : void
{
Expand All @@ -24,7 +25,7 @@ public function testInstantiatingWithoutManagerRegistry() : void
try {
new LoadDataFixturesDoctrineCommand($loader);
} catch (TypeError $e) {
$this->expectExceptionMessage('Argument 1 passed to Doctrine\Bundle\DoctrineBundle\Command\DoctrineCommand::__construct() must be an instance of Doctrine\Common\Persistence\ManagerRegistry, null given');
$this->expectExceptionMessage(sprintf('Argument 1 passed to Doctrine\Bundle\DoctrineBundle\Command\DoctrineCommand::__construct() must be an instance of %s, null given', DeprecationUtil::getManagerRegistryClass()));

throw $e;
}
Expand All @@ -35,7 +36,7 @@ public function testInstantiatingWithoutManagerRegistry() : void
*/
public function testInstantiatingWithManagerRegistry() : void
{
$registry = $this->createMock(ManagerRegistry::class);
$registry = $this->createMock(DeprecationUtil::getManagerRegistryClass());
$loader = new SymfonyFixturesLoader(new Container());

new LoadDataFixturesDoctrineCommand($loader, $registry);
Expand Down
17 changes: 17 additions & 0 deletions Tests/DeprecationUtil.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?php

declare(strict_types=1);

namespace Doctrine\Bundle\FixturesBundle\Tests;

use Doctrine\Common\Persistence\ManagerRegistry as DeprecatedManagerRegistry;
use Doctrine\Persistence\ManagerRegistry;
use function interface_exists;

final class DeprecationUtil
{
public static function getManagerRegistryClass() : string
{
return interface_exists(ManagerRegistry::class) ? ManagerRegistry::class : DeprecatedManagerRegistry::class;
}
}
Loading

0 comments on commit 886cf2e

Please sign in to comment.