Actualización

This commit is contained in:
Xes
2025-04-10 12:24:57 +02:00
parent 8969cc929d
commit 45420b6f0d
39760 changed files with 4303286 additions and 0 deletions

View File

@@ -0,0 +1,97 @@
<?php
declare(strict_types=1);
namespace Doctrine\Common\DataFixtures;
use BadMethodCallException;
/**
* Abstract Fixture class helps to manage references
* between fixture classes in order to set relations
* among other fixtures
*/
abstract class AbstractFixture implements SharedFixtureInterface
{
/**
* Fixture reference repository
*
* @var ReferenceRepository
*/
protected $referenceRepository;
/**
* {@inheritdoc}
*/
public function setReferenceRepository(ReferenceRepository $referenceRepository)
{
$this->referenceRepository = $referenceRepository;
}
/**
* Set the reference entry identified by $name
* and referenced to managed $object. If $name
* already is set, it overrides it
*
* @see Doctrine\Common\DataFixtures\ReferenceRepository::setReference
*
* @param string $name
* @param object $object - managed object
*
* @return void
*/
public function setReference($name, $object)
{
$this->referenceRepository->setReference($name, $object);
}
/**
* Set the reference entry identified by $name
* and referenced to managed $object. If $name
* already is set, it throws a
* BadMethodCallException exception
*
* @see Doctrine\Common\DataFixtures\ReferenceRepository::addReference
*
* @param string $name
* @param object $object - managed object
*
* @return void
*
* @throws BadMethodCallException - if repository already has a reference by $name.
*/
public function addReference($name, $object)
{
$this->referenceRepository->addReference($name, $object);
}
/**
* Loads an object using stored reference
* named by $name
*
* @see Doctrine\Common\DataFixtures\ReferenceRepository::getReference
*
* @param string $name
*
* @return object
*/
public function getReference($name)
{
return $this->referenceRepository->getReference($name);
}
/**
* Check if an object is stored using reference
* named by $name
*
* @see Doctrine\Common\DataFixtures\ReferenceRepository::hasReference
*
* @param string $name
*
* @return bool
*/
public function hasReference($name)
{
return $this->referenceRepository->hasReference($name);
}
}

View File

@@ -0,0 +1,19 @@
<?php
declare(strict_types=1);
namespace Doctrine\Common\DataFixtures;
/**
* DependentFixtureInterface needs to be implemented by fixtures which depend on other fixtures
*/
interface DependentFixtureInterface
{
/**
* This method must return an array of fixtures classes
* on which the implementing class depends on
*
* @psalm-return array<class-string<FixtureInterface>>
*/
public function getDependencies();
}

View File

@@ -0,0 +1,55 @@
<?php
declare(strict_types=1);
namespace Doctrine\Common\DataFixtures\Event\Listener;
use Doctrine\Common\DataFixtures\ReferenceRepository;
use Doctrine\Common\EventSubscriber;
use Doctrine\ODM\MongoDB\Event\LifecycleEventArgs;
/**
* Reference Listener populates identities for
* stored references
*/
final class MongoDBReferenceListener implements EventSubscriber
{
/** @var ReferenceRepository */
private $referenceRepository;
public function __construct(ReferenceRepository $referenceRepository)
{
$this->referenceRepository = $referenceRepository;
}
/**
* {@inheritdoc}
*/
public function getSubscribedEvents()
{
return ['postPersist'];
}
/**
* Populates identities for stored references
*
* @return void
*/
public function postPersist(LifecycleEventArgs $args)
{
$object = $args->getDocument();
$names = $this->referenceRepository->getReferenceNames($object);
if ($names === false) {
return;
}
foreach ($names as $name) {
$identity = $args->getDocumentManager()
->getUnitOfWork()
->getDocumentIdentifier($object);
$this->referenceRepository->setReferenceIdentity($name, $identity);
}
}
}

View File

@@ -0,0 +1,56 @@
<?php
declare(strict_types=1);
namespace Doctrine\Common\DataFixtures\Event\Listener;
use Doctrine\Common\DataFixtures\ReferenceRepository;
use Doctrine\Common\EventSubscriber;
use Doctrine\ORM\Event\LifecycleEventArgs;
/**
* Reference Listener populates identities for
* stored references
*/
final class ORMReferenceListener implements EventSubscriber
{
/** @var ReferenceRepository */
private $referenceRepository;
public function __construct(ReferenceRepository $referenceRepository)
{
$this->referenceRepository = $referenceRepository;
}
/**
* {@inheritdoc}
*/
public function getSubscribedEvents()
{
// would be better to use onClear, but it is supported only in 2.1
return ['postPersist'];
}
/**
* Populates identities for stored references
*
* @return void
*/
public function postPersist(LifecycleEventArgs $args)
{
$object = $args->getEntity();
$names = $this->referenceRepository->getReferenceNames($object);
if ($names === false) {
return;
}
foreach ($names as $name) {
$identity = $args->getEntityManager()
->getUnitOfWork()
->getEntityIdentifier($object);
$this->referenceRepository->setReferenceIdentity($name, $identity);
}
}
}

View File

@@ -0,0 +1,11 @@
<?php
declare(strict_types=1);
namespace Doctrine\Common\DataFixtures\Exception;
use Doctrine\Common\CommonException;
class CircularReferenceException extends CommonException
{
}

View File

@@ -0,0 +1,161 @@
<?php
declare(strict_types=1);
namespace Doctrine\Common\DataFixtures\Executor;
use Doctrine\Common\DataFixtures\FixtureInterface;
use Doctrine\Common\DataFixtures\OrderedFixtureInterface;
use Doctrine\Common\DataFixtures\Purger\PurgerInterface;
use Doctrine\Common\DataFixtures\ReferenceRepository;
use Doctrine\Common\DataFixtures\SharedFixtureInterface;
use Doctrine\Persistence\ObjectManager;
use Exception;
use function get_class;
use function interface_exists;
use function sprintf;
/**
* Abstract fixture executor.
*/
abstract class AbstractExecutor
{
/**
* Purger instance for purging database before loading data fixtures
*
* @var PurgerInterface
*/
protected $purger;
/**
* Logger callback for logging messages when loading data fixtures
*
* @var callable
*/
protected $logger;
/**
* Fixture reference repository
*
* @var ReferenceRepository
*/
protected $referenceRepository;
public function __construct(ObjectManager $manager)
{
$this->referenceRepository = new ReferenceRepository($manager);
}
/** @return ReferenceRepository */
public function getReferenceRepository()
{
return $this->referenceRepository;
}
public function setReferenceRepository(ReferenceRepository $referenceRepository)
{
$this->referenceRepository = $referenceRepository;
}
/**
* Sets the Purger instance to use for this executor instance.
*
* @return void
*/
public function setPurger(PurgerInterface $purger)
{
$this->purger = $purger;
}
/** @return PurgerInterface */
public function getPurger()
{
return $this->purger;
}
/**
* Set the logger callable to execute with the log() method.
*
* @param callable $logger
*
* @return void
*/
public function setLogger($logger)
{
$this->logger = $logger;
}
/**
* Logs a message using the logger.
*
* @param string $message
*
* @return void
*/
public function log($message)
{
$logger = $this->logger;
$logger($message);
}
/**
* Load a fixture with the given persistence manager.
*
* @return void
*/
public function load(ObjectManager $manager, FixtureInterface $fixture)
{
if ($this->logger) {
$prefix = '';
if ($fixture instanceof OrderedFixtureInterface) {
$prefix = sprintf('[%d] ', $fixture->getOrder());
}
$this->log('loading ' . $prefix . get_class($fixture));
}
// additionally pass the instance of reference repository to shared fixtures
if ($fixture instanceof SharedFixtureInterface) {
$fixture->setReferenceRepository($this->referenceRepository);
}
$fixture->load($manager);
$manager->clear();
}
/**
* Purges the database before loading.
*
* @return void
*
* @throws Exception if the purger is not defined.
*/
public function purge()
{
if ($this->purger === null) {
throw new Exception(
PurgerInterface::class .
' instance is required if you want to purge the database before loading your data fixtures.'
);
}
if ($this->logger) {
$this->log('purging database');
}
$this->purger->purge();
}
/**
* Executes the given array of data fixtures.
*
* @param FixtureInterface[] $fixtures Array of fixtures to execute.
* @param bool $append Whether to append the data fixtures or purge the database before loading.
*
* @return void
*/
abstract public function execute(array $fixtures, $append = false);
}
interface_exists(ObjectManager::class);

View File

@@ -0,0 +1,75 @@
<?php
declare(strict_types=1);
namespace Doctrine\Common\DataFixtures\Executor;
use Doctrine\Common\DataFixtures\Event\Listener\MongoDBReferenceListener;
use Doctrine\Common\DataFixtures\Purger\MongoDBPurger;
use Doctrine\Common\DataFixtures\ReferenceRepository;
use Doctrine\ODM\MongoDB\DocumentManager;
/**
* Class responsible for executing data fixtures.
*/
class MongoDBExecutor extends AbstractExecutor
{
/** @var DocumentManager */
private $dm;
/** @var MongoDBReferenceListener */
private $listener;
/**
* Construct new fixtures loader instance.
*
* @param DocumentManager $dm DocumentManager instance used for persistence.
*/
public function __construct(DocumentManager $dm, ?MongoDBPurger $purger = null)
{
$this->dm = $dm;
if ($purger !== null) {
$this->purger = $purger;
$this->purger->setDocumentManager($dm);
}
parent::__construct($dm);
$this->listener = new MongoDBReferenceListener($this->referenceRepository);
$dm->getEventManager()->addEventSubscriber($this->listener);
}
/**
* Retrieve the DocumentManager instance this executor instance is using.
*
* @return DocumentManager
*/
public function getObjectManager()
{
return $this->dm;
}
/** @inheritDoc */
public function setReferenceRepository(ReferenceRepository $referenceRepository)
{
$this->dm->getEventManager()->removeEventListener(
$this->listener->getSubscribedEvents(),
$this->listener
);
$this->referenceRepository = $referenceRepository;
$this->listener = new MongoDBReferenceListener($this->referenceRepository);
$this->dm->getEventManager()->addEventSubscriber($this->listener);
}
/** @inheritDoc */
public function execute(array $fixtures, $append = false)
{
if ($append === false) {
$this->purge();
}
foreach ($fixtures as $fixture) {
$this->load($this->dm, $fixture);
}
}
}

View File

@@ -0,0 +1,78 @@
<?php
declare(strict_types=1);
namespace Doctrine\Common\DataFixtures\Executor;
use Doctrine\Common\DataFixtures\Event\Listener\ORMReferenceListener;
use Doctrine\Common\DataFixtures\Purger\ORMPurgerInterface;
use Doctrine\Common\DataFixtures\ReferenceRepository;
use Doctrine\ORM\EntityManagerInterface;
/**
* Class responsible for executing data fixtures.
*/
class ORMExecutor extends AbstractExecutor
{
/** @var EntityManagerInterface */
private $em;
/** @var ORMReferenceListener */
private $listener;
/**
* Construct new fixtures loader instance.
*
* @param EntityManagerInterface $em EntityManagerInterface instance used for persistence.
*/
public function __construct(EntityManagerInterface $em, ?ORMPurgerInterface $purger = null)
{
$this->em = $em;
if ($purger !== null) {
$this->purger = $purger;
$this->purger->setEntityManager($em);
}
parent::__construct($em);
$this->listener = new ORMReferenceListener($this->referenceRepository);
$em->getEventManager()->addEventSubscriber($this->listener);
}
/**
* Retrieve the EntityManagerInterface instance this executor instance is using.
*
* @return EntityManagerInterface
*/
public function getObjectManager()
{
return $this->em;
}
/** @inheritDoc */
public function setReferenceRepository(ReferenceRepository $referenceRepository)
{
$this->em->getEventManager()->removeEventListener(
$this->listener->getSubscribedEvents(),
$this->listener
);
$this->referenceRepository = $referenceRepository;
$this->listener = new ORMReferenceListener($this->referenceRepository);
$this->em->getEventManager()->addEventSubscriber($this->listener);
}
/** @inheritDoc */
public function execute(array $fixtures, $append = false)
{
$executor = $this;
$this->em->transactional(static function (EntityManagerInterface $em) use ($executor, $fixtures, $append) {
if ($append === false) {
$executor->purge();
}
foreach ($fixtures as $fixture) {
$executor->load($em, $fixture);
}
});
}
}

View File

@@ -0,0 +1,64 @@
<?php
declare(strict_types=1);
namespace Doctrine\Common\DataFixtures\Executor;
use Doctrine\Common\DataFixtures\Purger\PHPCRPurger;
use Doctrine\ODM\PHPCR\DocumentManagerInterface;
use function method_exists;
/**
* Class responsible for executing data fixtures.
*/
class PHPCRExecutor extends AbstractExecutor
{
/** @var DocumentManagerInterface */
private $dm;
/**
* @param DocumentManagerInterface $dm manager instance used for persisting the fixtures
* @param PHPCRPurger|null $purger to remove the current data if append is false
*/
public function __construct(DocumentManagerInterface $dm, ?PHPCRPurger $purger = null)
{
parent::__construct($dm);
$this->dm = $dm;
if ($purger === null) {
return;
}
$purger->setDocumentManager($dm);
$this->setPurger($purger);
}
/** @return DocumentManagerInterface */
public function getObjectManager()
{
return $this->dm;
}
/** @inheritDoc */
public function execute(array $fixtures, $append = false)
{
$that = $this;
$function = static function ($dm) use ($append, $that, $fixtures) {
if ($append === false) {
$that->purge();
}
foreach ($fixtures as $fixture) {
$that->load($dm, $fixture);
}
};
if (method_exists($this->dm, 'transactional')) {
$this->dm->transactional($function);
} else {
$function($this->dm);
}
}
}

View File

@@ -0,0 +1,22 @@
<?php
declare(strict_types=1);
namespace Doctrine\Common\DataFixtures;
use Doctrine\Persistence\ObjectManager;
use function interface_exists;
/**
* Interface contract for fixture classes to implement.
*/
interface FixtureInterface
{
/**
* Load data fixtures with the passed EntityManager
*/
public function load(ObjectManager $manager);
}
interface_exists(ObjectManager::class);

View File

@@ -0,0 +1,458 @@
<?php
declare(strict_types=1);
namespace Doctrine\Common\DataFixtures;
use ArrayIterator;
use Doctrine\Common\DataFixtures\Exception\CircularReferenceException;
use InvalidArgumentException;
use Iterator;
use RecursiveDirectoryIterator;
use RecursiveIteratorIterator;
use ReflectionClass;
use RuntimeException;
use SplFileInfo;
use function array_keys;
use function array_merge;
use function asort;
use function class_exists;
use function class_implements;
use function count;
use function get_class;
use function get_declared_classes;
use function implode;
use function in_array;
use function is_array;
use function is_dir;
use function is_readable;
use function realpath;
use function sort;
use function sprintf;
use function usort;
/**
* Class responsible for loading data fixture classes.
*/
class Loader
{
/**
* Array of fixture object instances to execute.
*
* @psalm-var array<class-string<FixtureInterface>, FixtureInterface>
*/
private $fixtures = [];
/**
* Array of ordered fixture object instances.
*
* @psalm-var array<class-string<OrderedFixtureInterface>, OrderedFixtureInterface>|list<OrderedFixtureInterface>
*/
private $orderedFixtures = [];
/**
* Determines if we must order fixtures by number
*
* @var bool
*/
private $orderFixturesByNumber = false;
/**
* Determines if we must order fixtures by its dependencies
*
* @var bool
*/
private $orderFixturesByDependencies = false;
/**
* The file extension of fixture files.
*
* @var string
*/
private $fileExtension = '.php';
/**
* Find fixtures classes in a given directory and load them.
*
* @param string $dir Directory to find fixture classes in.
*
* @return array $fixtures Array of loaded fixture object instances.
*/
public function loadFromDirectory($dir)
{
if (! is_dir($dir)) {
throw new InvalidArgumentException(sprintf('"%s" does not exist', $dir));
}
$iterator = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator($dir),
RecursiveIteratorIterator::LEAVES_ONLY
);
return $this->loadFromIterator($iterator);
}
/**
* Find fixtures classes in a given file and load them.
*
* @param string $fileName File to find fixture classes in.
*
* @return array $fixtures Array of loaded fixture object instances.
*/
public function loadFromFile($fileName)
{
if (! is_readable($fileName)) {
throw new InvalidArgumentException(sprintf('"%s" does not exist or is not readable', $fileName));
}
$iterator = new ArrayIterator([new SplFileInfo($fileName)]);
return $this->loadFromIterator($iterator);
}
/**
* Has fixture?
*
* @param FixtureInterface $fixture
*
* @return bool
*/
public function hasFixture($fixture)
{
return isset($this->fixtures[get_class($fixture)]);
}
/**
* Get a specific fixture instance
*
* @param string $className
*
* @return FixtureInterface
*/
public function getFixture($className)
{
if (! isset($this->fixtures[$className])) {
throw new InvalidArgumentException(sprintf(
'"%s" is not a registered fixture',
$className
));
}
return $this->fixtures[$className];
}
/**
* Add a fixture object instance to the loader.
*/
public function addFixture(FixtureInterface $fixture)
{
$fixtureClass = get_class($fixture);
if (isset($this->fixtures[$fixtureClass])) {
return;
}
if ($fixture instanceof OrderedFixtureInterface && $fixture instanceof DependentFixtureInterface) {
throw new InvalidArgumentException(sprintf(
'Class "%s" can\'t implement "%s" and "%s" at the same time.',
get_class($fixture),
'OrderedFixtureInterface',
'DependentFixtureInterface'
));
}
$this->fixtures[$fixtureClass] = $fixture;
if ($fixture instanceof OrderedFixtureInterface) {
$this->orderFixturesByNumber = true;
} elseif ($fixture instanceof DependentFixtureInterface) {
$this->orderFixturesByDependencies = true;
foreach ($fixture->getDependencies() as $class) {
if (! class_exists($class)) {
continue;
}
$this->addFixture($this->createFixture($class));
}
}
}
/**
* Returns the array of data fixtures to execute.
*
* @psalm-return array<class-string<OrderedFixtureInterface>|int, OrderedFixtureInterface>
*/
public function getFixtures()
{
$this->orderedFixtures = [];
if ($this->orderFixturesByNumber) {
$this->orderFixturesByNumber();
}
if ($this->orderFixturesByDependencies) {
$this->orderFixturesByDependencies();
}
if (! $this->orderFixturesByNumber && ! $this->orderFixturesByDependencies) {
$this->orderedFixtures = $this->fixtures;
}
return $this->orderedFixtures;
}
/**
* Check if a given fixture is transient and should not be considered a data fixtures
* class.
*
* @psalm-param class-string<object> $className
*
* @return bool
*/
public function isTransient($className)
{
$rc = new ReflectionClass($className);
if ($rc->isAbstract()) {
return true;
}
$interfaces = class_implements($className);
return ! in_array(FixtureInterface::class, $interfaces);
}
/**
* Creates the fixture object from the class.
*
* @param string $class
*
* @return FixtureInterface
*/
protected function createFixture($class)
{
return new $class();
}
/**
* Orders fixtures by number
*
* @todo maybe there is a better way to handle reordering
*/
private function orderFixturesByNumber(): void
{
$this->orderedFixtures = $this->fixtures;
usort($this->orderedFixtures, static function (FixtureInterface $a, FixtureInterface $b): int {
if ($a instanceof OrderedFixtureInterface && $b instanceof OrderedFixtureInterface) {
if ($a->getOrder() === $b->getOrder()) {
return 0;
}
return $a->getOrder() < $b->getOrder() ? -1 : 1;
}
if ($a instanceof OrderedFixtureInterface) {
return $a->getOrder() === 0 ? 0 : 1;
}
if ($b instanceof OrderedFixtureInterface) {
return $b->getOrder() === 0 ? 0 : -1;
}
return 0;
});
}
/**
* Orders fixtures by dependencies
*
* @return void
*/
private function orderFixturesByDependencies()
{
/** @psalm-var array<class-string<DependentFixtureInterface>, int> */
$sequenceForClasses = [];
// If fixtures were already ordered by number then we need
// to remove classes which are not instances of OrderedFixtureInterface
// in case fixtures implementing DependentFixtureInterface exist.
// This is because, in that case, the method orderFixturesByDependencies
// will handle all fixtures which are not instances of
// OrderedFixtureInterface
if ($this->orderFixturesByNumber) {
$count = count($this->orderedFixtures);
for ($i = 0; $i < $count; ++$i) {
if ($this->orderedFixtures[$i] instanceof OrderedFixtureInterface) {
continue;
}
unset($this->orderedFixtures[$i]);
}
}
// First we determine which classes has dependencies and which don't
foreach ($this->fixtures as $fixture) {
$fixtureClass = get_class($fixture);
if ($fixture instanceof OrderedFixtureInterface) {
continue;
}
if ($fixture instanceof DependentFixtureInterface) {
$dependenciesClasses = $fixture->getDependencies();
$this->validateDependencies($dependenciesClasses);
if (! is_array($dependenciesClasses) || empty($dependenciesClasses)) {
throw new InvalidArgumentException(sprintf(
'Method "%s" in class "%s" must return an array of classes which are dependencies for the fixture, and it must be NOT empty.',
'getDependencies',
$fixtureClass
));
}
if (in_array($fixtureClass, $dependenciesClasses)) {
throw new InvalidArgumentException(sprintf(
'Class "%s" can\'t have itself as a dependency',
$fixtureClass
));
}
// We mark this class as unsequenced
$sequenceForClasses[$fixtureClass] = -1;
} else {
// This class has no dependencies, so we assign 0
$sequenceForClasses[$fixtureClass] = 0;
}
}
// Now we order fixtures by sequence
$sequence = 1;
$lastCount = -1;
while (($count = count($unsequencedClasses = $this->getUnsequencedClasses($sequenceForClasses))) > 0 && $count !== $lastCount) {
foreach ($unsequencedClasses as $key => $class) {
$fixture = $this->fixtures[$class];
$dependencies = $fixture->getDependencies();
$unsequencedDependencies = $this->getUnsequencedClasses($sequenceForClasses, $dependencies);
if (count($unsequencedDependencies) !== 0) {
continue;
}
$sequenceForClasses[$class] = $sequence++;
}
$lastCount = $count;
}
$orderedFixtures = [];
// If there're fixtures unsequenced left and they couldn't be sequenced,
// it means we have a circular reference
if ($count > 0) {
$msg = 'Classes "%s" have produced a CircularReferenceException. ';
$msg .= 'An example of this problem would be the following: Class C has class B as its dependency. ';
$msg .= 'Then, class B has class A has its dependency. Finally, class A has class C as its dependency. ';
$msg .= 'This case would produce a CircularReferenceException.';
throw new CircularReferenceException(sprintf($msg, implode(',', $unsequencedClasses)));
} else {
// We order the classes by sequence
asort($sequenceForClasses);
foreach ($sequenceForClasses as $class => $sequence) {
// If fixtures were ordered
$orderedFixtures[] = $this->fixtures[$class];
}
}
$this->orderedFixtures = array_merge($this->orderedFixtures, $orderedFixtures);
}
/** @psalm-param iterable<class-string> $dependenciesClasses */
private function validateDependencies(iterable $dependenciesClasses): bool
{
$loadedFixtureClasses = array_keys($this->fixtures);
foreach ($dependenciesClasses as $class) {
if (! in_array($class, $loadedFixtureClasses)) {
throw new RuntimeException(sprintf(
'Fixture "%s" was declared as a dependency, but it should be added in fixture loader first.',
$class
));
}
}
return true;
}
/**
* @psalm-param array<class-string<DependentFixtureInterface>, int> $sequences
* @psalm-param iterable<class-string<FixtureInterface>>|null $classes
*
* @psalm-return array<class-string<FixtureInterface>>
*/
private function getUnsequencedClasses(array $sequences, ?iterable $classes = null): array
{
$unsequencedClasses = [];
if ($classes === null) {
$classes = array_keys($sequences);
}
foreach ($classes as $class) {
if ($sequences[$class] !== -1) {
continue;
}
$unsequencedClasses[] = $class;
}
return $unsequencedClasses;
}
/**
* Load fixtures from files contained in iterator.
*
* @psalm-param Iterator<SplFileInfo> $iterator Iterator over files from
* which fixtures should be loaded.
*
* @psalm-return list<FixtureInterface> $fixtures Array of loaded fixture object instances.
*/
private function loadFromIterator(Iterator $iterator): array
{
$includedFiles = [];
foreach ($iterator as $file) {
$fileName = $file->getBasename($this->fileExtension);
if ($fileName === $file->getBasename()) {
continue;
}
$sourceFile = realpath($file->getPathName());
require_once $sourceFile;
$includedFiles[] = $sourceFile;
}
$fixtures = [];
$declared = get_declared_classes();
// Make the declared classes order deterministic
sort($declared);
foreach ($declared as $className) {
$reflClass = new ReflectionClass($className);
$sourceFile = $reflClass->getFileName();
if (! in_array($sourceFile, $includedFiles) || $this->isTransient($className)) {
continue;
}
$fixture = $this->createFixture($className);
$fixtures[] = $fixture;
$this->addFixture($fixture);
}
return $fixtures;
}
}

View File

@@ -0,0 +1,20 @@
<?php
declare(strict_types=1);
namespace Doctrine\Common\DataFixtures;
/**
* Ordered Fixture interface needs to be implemented
* by fixtures, which needs to have a specific order
* when being loaded by directory scan for example
*/
interface OrderedFixtureInterface
{
/**
* Get the order of this fixture
*
* @return int
*/
public function getOrder();
}

View File

@@ -0,0 +1,129 @@
<?php
declare(strict_types=1);
namespace Doctrine\Common\DataFixtures;
use function file_exists;
use function file_get_contents;
use function file_put_contents;
use function get_class;
use function serialize;
use function substr;
use function unserialize;
/**
* Proxy reference repository
*
* Allow data fixture references and identities to be persisted when cached data fixtures
* are pre-loaded, for example, by LiipFunctionalTestBundle\Test\WebTestCase loadFixtures().
*/
class ProxyReferenceRepository extends ReferenceRepository
{
/**
* Get real class name of a reference that could be a proxy
*
* @param string $className Class name of reference object
*
* @return string
*/
protected function getRealClass($className)
{
if (substr($className, -5) === 'Proxy') {
return substr($className, 0, -5);
}
return $className;
}
/**
* Serialize reference repository
*
* @return string
*/
public function serialize()
{
$unitOfWork = $this->getManager()->getUnitOfWork();
$simpleReferences = [];
foreach ($this->getReferences() as $name => $reference) {
$className = $this->getRealClass(get_class($reference));
$simpleReferences[$name] = [$className, $this->getIdentifier($reference, $unitOfWork)];
}
return serialize([
'references' => $simpleReferences,
'identities' => $this->getIdentities(),
]);
}
/**
* Unserialize reference repository
*
* @param string $serializedData Serialized data
*
* @return void
*/
public function unserialize($serializedData)
{
$repositoryData = unserialize($serializedData);
$references = $repositoryData['references'];
foreach ($references as $name => $proxyReference) {
$this->setReference(
$name,
$this->getManager()->getReference(
$proxyReference[0], // entity class name
$proxyReference[1] // identifiers
)
);
}
$identities = $repositoryData['identities'];
foreach ($identities as $name => $identity) {
$this->setReferenceIdentity($name, $identity);
}
}
/**
* Load data fixture reference repository
*
* @param string $baseCacheName Base cache name
*
* @return bool
*/
public function load($baseCacheName)
{
$filename = $baseCacheName . '.ser';
if (! file_exists($filename)) {
return false;
}
$serializedData = file_get_contents($filename);
if ($serializedData === false) {
return false;
}
$this->unserialize($serializedData);
return true;
}
/**
* Save data fixture reference repository
*
* @param string $baseCacheName Base cache name
*
* @return void
*/
public function save($baseCacheName)
{
$serializedData = $this->serialize();
file_put_contents($baseCacheName . '.ser', $serializedData);
}
}

View File

@@ -0,0 +1,61 @@
<?php
declare(strict_types=1);
namespace Doctrine\Common\DataFixtures\Purger;
use Doctrine\ODM\MongoDB\DocumentManager;
/**
* Class responsible for purging databases of data before reloading data fixtures.
*/
class MongoDBPurger implements PurgerInterface
{
/** @var DocumentManager|null */
private $dm;
/**
* Construct new purger instance.
*
* @param DocumentManager|null $dm DocumentManager instance used for persistence.
*/
public function __construct(?DocumentManager $dm = null)
{
$this->dm = $dm;
}
/**
* Set the DocumentManager instance this purger instance should use.
*
* @return void
*/
public function setDocumentManager(DocumentManager $dm)
{
$this->dm = $dm;
}
/**
* Retrieve the DocumentManager instance this purger instance is using.
*
* @return DocumentManager
*/
public function getObjectManager()
{
return $this->dm;
}
/** @inheritDoc */
public function purge()
{
$metadatas = $this->dm->getMetadataFactory()->getAllMetadata();
foreach ($metadatas as $metadata) {
if ($metadata->isMappedSuperclass) {
continue;
}
$this->dm->getDocumentCollection($metadata->name)->drop();
}
$this->dm->getSchemaManager()->ensureIndexes();
}
}

View File

@@ -0,0 +1,283 @@
<?php
declare(strict_types=1);
namespace Doctrine\Common\DataFixtures\Purger;
use Doctrine\Common\DataFixtures\Sorter\TopologicalSorter;
use Doctrine\DBAL\Platforms\AbstractPlatform;
use Doctrine\DBAL\Schema\Identifier;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\Mapping\ClassMetadata;
use function array_reverse;
use function array_search;
use function assert;
use function count;
use function is_callable;
use function method_exists;
use function preg_match;
/**
* Class responsible for purging databases of data before reloading data fixtures.
*/
class ORMPurger implements PurgerInterface, ORMPurgerInterface
{
public const PURGE_MODE_DELETE = 1;
public const PURGE_MODE_TRUNCATE = 2;
/** @var EntityManagerInterface|null */
private $em;
/**
* If the purge should be done through DELETE or TRUNCATE statements
*
* @var int
*/
private $purgeMode = self::PURGE_MODE_DELETE;
/**
* Table/view names to be excluded from purge
*
* @var string[]
*/
private $excluded;
/**
* Construct new purger instance.
*
* @param EntityManagerInterface|null $em EntityManagerInterface instance used for persistence.
* @param string[] $excluded array of table/view names to be excluded from purge
*/
public function __construct(?EntityManagerInterface $em = null, array $excluded = [])
{
$this->em = $em;
$this->excluded = $excluded;
}
/**
* Set the purge mode
*
* @param int $mode
*
* @return void
*/
public function setPurgeMode($mode)
{
$this->purgeMode = $mode;
}
/**
* Get the purge mode
*
* @return int
*/
public function getPurgeMode()
{
return $this->purgeMode;
}
/** @inheritDoc */
public function setEntityManager(EntityManagerInterface $em)
{
$this->em = $em;
}
/**
* Retrieve the EntityManagerInterface instance this purger instance is using.
*
* @return EntityManagerInterface
*/
public function getObjectManager()
{
return $this->em;
}
/** @inheritDoc */
public function purge()
{
$classes = [];
foreach ($this->em->getMetadataFactory()->getAllMetadata() as $metadata) {
if ($metadata->isMappedSuperclass || (isset($metadata->isEmbeddedClass) && $metadata->isEmbeddedClass)) {
continue;
}
$classes[] = $metadata;
}
$commitOrder = $this->getCommitOrder($this->em, $classes);
// Get platform parameters
$platform = $this->em->getConnection()->getDatabasePlatform();
// Drop association tables first
$orderedTables = $this->getAssociationTables($commitOrder, $platform);
// Drop tables in reverse commit order
for ($i = count($commitOrder) - 1; $i >= 0; --$i) {
$class = $commitOrder[$i];
if (
(isset($class->isEmbeddedClass) && $class->isEmbeddedClass) ||
$class->isMappedSuperclass ||
($class->isInheritanceTypeSingleTable() && $class->name !== $class->rootEntityName)
) {
continue;
}
$orderedTables[] = $this->getTableName($class, $platform);
}
$connection = $this->em->getConnection();
$filterExpr = method_exists(
$connection->getConfiguration(),
'getFilterSchemaAssetsExpression'
) ? $connection->getConfiguration()->getFilterSchemaAssetsExpression() : null;
$emptyFilterExpression = empty($filterExpr);
$schemaAssetsFilter = method_exists(
$connection->getConfiguration(),
'getSchemaAssetsFilter'
) ? $connection->getConfiguration()->getSchemaAssetsFilter() : null;
foreach ($orderedTables as $tbl) {
// If we have a filter expression, check it and skip if necessary
if (! $emptyFilterExpression && ! preg_match($filterExpr, $tbl)) {
continue;
}
// If the table is excluded, skip it as well
if (array_search($tbl, $this->excluded) !== false) {
continue;
}
// Support schema asset filters as presented in
if (is_callable($schemaAssetsFilter) && ! $schemaAssetsFilter($tbl)) {
continue;
}
if ($this->purgeMode === self::PURGE_MODE_DELETE) {
$connection->executeStatement($this->getDeleteFromTableSQL($tbl, $platform));
} else {
$connection->executeStatement($platform->getTruncateTableSQL($tbl, true));
}
}
}
/**
* @param ClassMetadata[] $classes
*
* @return ClassMetadata[]
*/
private function getCommitOrder(EntityManagerInterface $em, array $classes)
{
$sorter = new TopologicalSorter();
foreach ($classes as $class) {
if (! $sorter->hasNode($class->name)) {
$sorter->addNode($class->name, $class);
}
// $class before its parents
foreach ($class->parentClasses as $parentClass) {
$parentClass = $em->getClassMetadata($parentClass);
$parentClassName = $parentClass->getName();
if (! $sorter->hasNode($parentClassName)) {
$sorter->addNode($parentClassName, $parentClass);
}
$sorter->addDependency($class->name, $parentClassName);
}
foreach ($class->associationMappings as $assoc) {
if (! $assoc['isOwningSide']) {
continue;
}
$targetClass = $em->getClassMetadata($assoc['targetEntity']);
assert($targetClass instanceof ClassMetadata);
$targetClassName = $targetClass->getName();
if (! $sorter->hasNode($targetClassName)) {
$sorter->addNode($targetClassName, $targetClass);
}
// add dependency ($targetClass before $class)
$sorter->addDependency($targetClassName, $class->name);
// parents of $targetClass before $class, too
foreach ($targetClass->parentClasses as $parentClass) {
$parentClass = $em->getClassMetadata($parentClass);
$parentClassName = $parentClass->getName();
if (! $sorter->hasNode($parentClassName)) {
$sorter->addNode($parentClassName, $parentClass);
}
$sorter->addDependency($parentClassName, $class->name);
}
}
}
return array_reverse($sorter->sort());
}
/**
* @param ClassMetadata[] $classes
*
* @return string[]
*/
private function getAssociationTables(array $classes, AbstractPlatform $platform)
{
$associationTables = [];
foreach ($classes as $class) {
foreach ($class->associationMappings as $assoc) {
if (! $assoc['isOwningSide'] || $assoc['type'] !== ClassMetadata::MANY_TO_MANY) {
continue;
}
$associationTables[] = $this->getJoinTableName($assoc, $class, $platform);
}
}
return $associationTables;
}
private function getTableName(ClassMetadata $class, AbstractPlatform $platform): string
{
if (isset($class->table['schema']) && ! method_exists($class, 'getSchemaName')) {
return $class->table['schema'] . '.' .
$this->em->getConfiguration()
->getQuoteStrategy()
->getTableName($class, $platform);
}
return $this->em->getConfiguration()->getQuoteStrategy()->getTableName($class, $platform);
}
/** @param mixed[] $assoc */
private function getJoinTableName(
array $assoc,
ClassMetadata $class,
AbstractPlatform $platform
): string {
if (isset($assoc['joinTable']['schema']) && ! method_exists($class, 'getSchemaName')) {
return $assoc['joinTable']['schema'] . '.' .
$this->em->getConfiguration()
->getQuoteStrategy()
->getJoinTableName($assoc, $class, $platform);
}
return $this->em->getConfiguration()->getQuoteStrategy()->getJoinTableName($assoc, $class, $platform);
}
private function getDeleteFromTableSQL(string $tableName, AbstractPlatform $platform): string
{
$tableIdentifier = new Identifier($tableName);
return 'DELETE FROM ' . $tableIdentifier->getQuotedName($platform);
}
}

View File

@@ -0,0 +1,20 @@
<?php
declare(strict_types=1);
namespace Doctrine\Common\DataFixtures\Purger;
use Doctrine\ORM\EntityManagerInterface;
/**
* ORMPurgerInterface
*/
interface ORMPurgerInterface extends PurgerInterface
{
/**
* Set the EntityManagerInterface instance this purger instance should use.
*
* @return void
*/
public function setEntityManager(EntityManagerInterface $em);
}

View File

@@ -0,0 +1,42 @@
<?php
declare(strict_types=1);
namespace Doctrine\Common\DataFixtures\Purger;
use Doctrine\ODM\PHPCR\DocumentManager;
use Doctrine\ODM\PHPCR\DocumentManagerInterface;
use PHPCR\Util\NodeHelper;
/**
* Class responsible for purging databases of data before reloading data fixtures.
*/
class PHPCRPurger implements PurgerInterface
{
/** @var DocumentManagerInterface|null */
private $dm;
public function __construct(?DocumentManagerInterface $dm = null)
{
$this->dm = $dm;
}
public function setDocumentManager(DocumentManager $dm)
{
$this->dm = $dm;
}
/** @return DocumentManagerInterface|null */
public function getObjectManager()
{
return $this->dm;
}
/** @inheritDoc */
public function purge()
{
$session = $this->dm->getPhpcrSession();
NodeHelper::purgeWorkspace($session);
$session->save();
}
}

View File

@@ -0,0 +1,18 @@
<?php
declare(strict_types=1);
namespace Doctrine\Common\DataFixtures\Purger;
/**
* PurgerInterface
*/
interface PurgerInterface
{
/**
* Purge the data from the database for the given EntityManager.
*
* @return void
*/
public function purge();
}

View File

@@ -0,0 +1,265 @@
<?php
declare(strict_types=1);
namespace Doctrine\Common\DataFixtures;
use BadMethodCallException;
use Doctrine\ODM\PHPCR\DocumentManager as PhpcrDocumentManager;
use Doctrine\Persistence\ObjectManager;
use OutOfBoundsException;
use function array_key_exists;
use function array_keys;
use function get_class;
use function method_exists;
use function sprintf;
/**
* ReferenceRepository class manages references for
* fixtures in order to easily support the relations
* between fixtures
*/
class ReferenceRepository
{
/**
* List of named references to the fixture objects
* gathered during loads of fixtures
*
* @psalm-var array<string, object>
*/
private $references = [];
/**
* List of identifiers stored for references
* in case if reference gets unmanaged, it will
* use a proxy referenced by this identity
*
* @psalm-var array<string, mixed>
*/
private $identities = [];
/**
* Currently used object manager
*
* @var ObjectManager
*/
private $manager;
public function __construct(ObjectManager $manager)
{
$this->manager = $manager;
}
/**
* Get identifier for a unit of work
*
* @param object $reference Reference object
* @param object $uow Unit of work
*
* @return array
*/
protected function getIdentifier($reference, $uow)
{
// In case Reference is not yet managed in UnitOfWork
if (! $this->hasIdentifier($reference)) {
$class = $this->manager->getClassMetadata(get_class($reference));
return $class->getIdentifierValues($reference);
}
// Dealing with ORM UnitOfWork
if (method_exists($uow, 'getEntityIdentifier')) {
return $uow->getEntityIdentifier($reference);
}
// PHPCR ODM UnitOfWork
if ($this->manager instanceof PhpcrDocumentManager) {
return $uow->getDocumentId($reference);
}
// ODM UnitOfWork
return $uow->getDocumentIdentifier($reference);
}
/**
* Set the reference entry identified by $name
* and referenced to $reference. If $name
* already is set, it overrides it
*
* @param string $name
* @param object $reference
*
* @return void
*/
public function setReference($name, $reference)
{
$this->references[$name] = $reference;
if (! $this->hasIdentifier($reference)) {
return;
}
// in case if reference is set after flush, store its identity
$uow = $this->manager->getUnitOfWork();
$this->identities[$name] = $this->getIdentifier($reference, $uow);
}
/**
* Store the identifier of a reference
*
* @param string $name
* @param mixed $identity
*
* @return void
*/
public function setReferenceIdentity($name, $identity)
{
$this->identities[$name] = $identity;
}
/**
* Set the reference entry identified by $name
* and referenced to managed $object. $name must
* not be set yet
*
* Notice: in case if identifier is generated after
* the record is inserted, be sure tu use this method
* after $object is flushed
*
* @param string $name
* @param object $object - managed object
*
* @return void
*
* @throws BadMethodCallException - if repository already has a reference by $name.
*/
public function addReference($name, $object)
{
if (isset($this->references[$name])) {
throw new BadMethodCallException(sprintf(
'Reference to "%s" already exists, use method setReference in order to override it',
$name
));
}
$this->setReference($name, $object);
}
/**
* Loads an object using stored reference
* named by $name
*
* @param string $name
*
* @return object
*
* @throws OutOfBoundsException - if repository does not exist.
*/
public function getReference($name)
{
if (! $this->hasReference($name)) {
throw new OutOfBoundsException(sprintf('Reference to "%s" does not exist', $name));
}
$reference = $this->references[$name];
$meta = $this->manager->getClassMetadata(get_class($reference));
if (! $this->manager->contains($reference) && isset($this->identities[$name])) {
$reference = $this->manager->getReference(
$meta->name,
$this->identities[$name]
);
$this->references[$name] = $reference; // already in identity map
}
return $reference;
}
/**
* Check if an object is stored using reference
* named by $name
*
* @param string $name
*
* @return bool
*/
public function hasReference($name)
{
return isset($this->references[$name]);
}
/**
* Searches for reference names in the
* list of stored references
*
* @param object $reference
*
* @return array
*/
public function getReferenceNames($reference)
{
return array_keys($this->references, $reference, true);
}
/**
* Checks if reference has identity stored
*
* @param string $name
*
* @return bool
*/
public function hasIdentity($name)
{
return array_key_exists($name, $this->identities);
}
/**
* Get all stored identities
*
* @psalm-return array<string, object>
*/
public function getIdentities()
{
return $this->identities;
}
/**
* Get all stored references
*
* @psalm-return array<string, object>
*/
public function getReferences()
{
return $this->references;
}
/**
* Get object manager
*
* @return ObjectManager
*/
public function getManager()
{
return $this->manager;
}
/**
* Checks if object has identifier already in unit of work.
*
* @param string $reference
*
* @return bool
*/
private function hasIdentifier($reference)
{
// in case if reference is set after flush, store its identity
$uow = $this->manager->getUnitOfWork();
if ($this->manager instanceof PhpcrDocumentManager) {
return $uow->contains($reference);
}
return $uow->isInIdentityMap($reference);
}
}

View File

@@ -0,0 +1,16 @@
<?php
declare(strict_types=1);
namespace Doctrine\Common\DataFixtures;
/**
* Shared Fixture interface needs to be implemented
* by fixtures, which needs some references to be shared
* among other fixture classes in order to maintain
* relation mapping
*/
interface SharedFixtureInterface extends FixtureInterface
{
public function setReferenceRepository(ReferenceRepository $referenceRepository);
}

View File

@@ -0,0 +1,182 @@
<?php
declare(strict_types=1);
namespace Doctrine\Common\DataFixtures\Sorter;
use Doctrine\Common\DataFixtures\Exception\CircularReferenceException;
use Doctrine\ORM\Mapping\ClassMetadata;
use RuntimeException;
use function get_class;
use function sprintf;
/**
* TopologicalSorter is an ordering algorithm for directed graphs (DG) and/or
* directed acyclic graphs (DAG) by using a depth-first searching (DFS) to
* traverse the graph built in memory.
* This algorithm have a linear running time based on nodes (V) and dependency
* between the nodes (E), resulting in a computational complexity of O(V + E).
*
* @internal this class is to be used only by data-fixtures internals: do not
* rely on it in your own libraries/applications.
*/
class TopologicalSorter
{
/**
* Matrix of nodes (aka. vertex).
* Keys are provided hashes and values are the node definition objects.
*
* @var Vertex[]
*/
private $nodeList = [];
/**
* Volatile variable holding calculated nodes during sorting process.
*
* @var ClassMetadata[]
*/
private $sortedNodeList = [];
/**
* Allow or not cyclic dependencies
*
* @var bool
*/
private $allowCyclicDependencies;
/** @param bool $allowCyclicDependencies */
public function __construct($allowCyclicDependencies = true)
{
$this->allowCyclicDependencies = $allowCyclicDependencies;
}
/**
* Adds a new node (vertex) to the graph, assigning its hash and value.
*
* @param string $hash
*
* @return void
*/
public function addNode($hash, ClassMetadata $node)
{
$this->nodeList[$hash] = new Vertex($node);
}
/**
* Checks the existence of a node in the graph.
*
* @param string $hash
*
* @return bool
*/
public function hasNode($hash)
{
return isset($this->nodeList[$hash]);
}
/**
* Adds a new dependency (edge) to the graph using their hashes.
*
* @param string $fromHash
* @param string $toHash
*
* @return void
*/
public function addDependency($fromHash, $toHash)
{
$definition = $this->nodeList[$fromHash];
$definition->dependencyList[] = $toHash;
}
/**
* Return a valid order list of all current nodes.
* The desired topological sorting is the postorder of these searches.
*
* Note: Highly performance-sensitive method.
*
* @return ClassMetadata[]
*
* @throws RuntimeException
* @throws CircularReferenceException
*/
public function sort()
{
foreach ($this->nodeList as $definition) {
if ($definition->state !== Vertex::NOT_VISITED) {
continue;
}
$this->visit($definition);
}
$sortedList = $this->sortedNodeList;
$this->nodeList = [];
$this->sortedNodeList = [];
return $sortedList;
}
/**
* Visit a given node definition for reordering.
*
* Note: Highly performance-sensitive method.
*
* @return void
*
* @throws RuntimeException
* @throws CircularReferenceException
*/
private function visit(Vertex $definition)
{
$definition->state = Vertex::IN_PROGRESS;
foreach ($definition->dependencyList as $dependency) {
if (! isset($this->nodeList[$dependency])) {
throw new RuntimeException(sprintf(
'Fixture "%s" has a dependency of fixture "%s", but it not listed to be loaded.',
get_class($definition->value),
$dependency
));
}
$childDefinition = $this->nodeList[$dependency];
// allow self referencing classes
if ($definition === $childDefinition) {
continue;
}
switch ($childDefinition->state) {
case Vertex::VISITED:
break;
case Vertex::IN_PROGRESS:
if (! $this->allowCyclicDependencies) {
throw new CircularReferenceException(
sprintf(
<<<'EXCEPTION'
Graph contains cyclic dependency between the classes "%s" and
"%s". An example of this problem would be the following:
Class C has class B as its dependency. Then, class B has class A has its dependency.
Finally, class A has class C as its dependency.
EXCEPTION
,
$definition->value->getName(),
$childDefinition->value->getName()
)
);
}
break;
case Vertex::NOT_VISITED:
$this->visit($childDefinition);
}
}
$definition->state = Vertex::VISITED;
$this->sortedNodeList[] = $definition->value;
}
}

View File

@@ -0,0 +1,34 @@
<?php
declare(strict_types=1);
namespace Doctrine\Common\DataFixtures\Sorter;
use Doctrine\ORM\Mapping\ClassMetadata;
/**
* @internal this class is to be used only by data-fixtures internals: do not
* rely on it in your own libraries/applications. This class is
* designed to work with {@see \Doctrine\Common\DataFixtures\Sorter\TopologicalSorter}
* only.
*/
class Vertex
{
public const NOT_VISITED = 0;
public const IN_PROGRESS = 1;
public const VISITED = 2;
/** @var int one of either {@see self::NOT_VISITED}, {@see self::IN_PROGRESS} or {@see self::VISITED}. */
public $state = self::NOT_VISITED;
/** @var ClassMetadata Actual node value */
public $value;
/** @var string[] Map of node dependencies defined as hashes. */
public $dependencyList = [];
public function __construct(ClassMetadata $value)
{
$this->value = $value;
}
}