From 63065a8b58711c624a59b7e3c4c714f0b16e41a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20B=C3=B6hmer?= Date: Sat, 2 Oct 2021 20:14:48 +0200 Subject: [PATCH] Reset autoincrement in a custom purger not in DataFixtures. This makes things a lot prettier in the DataFixtures. --- .github/workflows/tests.yml | 2 +- config/services.yaml | 5 + src/DataFixtures/DataStructureFixtures.php | 3 - src/DataFixtures/LabelProfileFixtures.php | 1 - src/DataFixtures/PartFixtures.php | 2 - src/DataFixtures/UserFixtures.php | 3 - .../Purger/ResetAutoIncrementORMPurger.php | 300 ++++++++++++++++++ .../ResetAutoIncrementPurgerFactory.php | 20 ++ 8 files changed, 326 insertions(+), 10 deletions(-) create mode 100644 src/Doctrine/Purger/ResetAutoIncrementORMPurger.php create mode 100644 src/Doctrine/Purger/ResetAutoIncrementPurgerFactory.php diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index b7b74624..83b07c59 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -92,7 +92,7 @@ jobs: run: php bin/console --env test doctrine:migrations:migrate -n - name: Load fixtures - run: php bin/console --env test doctrine:fixtures:load -n + run: php bin/console --env test doctrine:fixtures:load -n --purger reset_autoincrement_purger - name: Run PHPunit and generate coverage run: ./bin/phpunit --coverage-clover=coverage.xml diff --git a/config/services.yaml b/config/services.yaml index 9a08ff86..3e997774 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -42,6 +42,7 @@ services: Doctrine\Migrations\DependencyFactory: alias: 'doctrine.migrations.dependency_factory' + #################################################################################################################### # Email #################################################################################################################### @@ -210,3 +211,7 @@ services: arguments: $default_locale: '%partdb.locale%' $enforce_index_php: '%env(bool:NO_URL_REWRITE_AVAILABLE)%' + + App\Doctrine\Purger\ResetAutoIncrementPurgerFactory: + tags: + - { name: 'doctrine.fixtures.purger_factory', alias: 'reset_autoincrement_purger' } diff --git a/src/DataFixtures/DataStructureFixtures.php b/src/DataFixtures/DataStructureFixtures.php index a9aad137..3e25e57a 100644 --- a/src/DataFixtures/DataStructureFixtures.php +++ b/src/DataFixtures/DataStructureFixtures.php @@ -93,9 +93,6 @@ class DataStructureFixtures extends Fixture throw new InvalidArgumentException('$class must be a StructuralDBElement!'); } - $table_name = $this->em->getClassMetadata($class)->getTableName(); - $this->em->getConnection()->exec("ALTER TABLE `${table_name}` AUTO_INCREMENT = 1;"); - /** @var AbstractStructuralDBElement $node1 */ $node1 = new $class(); $node1->setName('Node 1'); diff --git a/src/DataFixtures/LabelProfileFixtures.php b/src/DataFixtures/LabelProfileFixtures.php index 24a08f3d..6d725fe8 100644 --- a/src/DataFixtures/LabelProfileFixtures.php +++ b/src/DataFixtures/LabelProfileFixtures.php @@ -40,7 +40,6 @@ class LabelProfileFixtures extends Fixture public function load(ObjectManager $manager): void { - $this->em->getConnection()->exec('ALTER TABLE `label_profiles` AUTO_INCREMENT = 1;'); $profile1 = new LabelProfile(); $profile1->setName('Profile 1'); diff --git a/src/DataFixtures/PartFixtures.php b/src/DataFixtures/PartFixtures.php index a6377b8f..17b0d617 100644 --- a/src/DataFixtures/PartFixtures.php +++ b/src/DataFixtures/PartFixtures.php @@ -50,8 +50,6 @@ class PartFixtures extends Fixture public function load(ObjectManager $manager): void { - $table_name = $this->em->getClassMetadata(Part::class)->getTableName(); - $this->em->getConnection()->exec("ALTER TABLE `${table_name}` AUTO_INCREMENT = 1;"); /** Simple part */ $part = new Part(); diff --git a/src/DataFixtures/UserFixtures.php b/src/DataFixtures/UserFixtures.php index 825701b3..a91ca3c1 100644 --- a/src/DataFixtures/UserFixtures.php +++ b/src/DataFixtures/UserFixtures.php @@ -61,9 +61,6 @@ class UserFixtures extends Fixture public function load(ObjectManager $manager): void { - //Reset autoincrement - $this->em->getConnection()->exec('ALTER TABLE `users` AUTO_INCREMENT = 1;'); - $anonymous = new User(); $anonymous->setName('anonymous'); $anonymous->setGroup($this->getReference(GroupFixtures::READONLY)); diff --git a/src/Doctrine/Purger/ResetAutoIncrementORMPurger.php b/src/Doctrine/Purger/ResetAutoIncrementORMPurger.php new file mode 100644 index 00000000..8469a7c7 --- /dev/null +++ b/src/Doctrine/Purger/ResetAutoIncrementORMPurger.php @@ -0,0 +1,300 @@ +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 = $connection->getConfiguration()->getFilterSchemaAssetsExpression(); + $emptyFilterExpression = empty($filterExpr); + + $schemaAssetsFilter = method_exists($connection->getConfiguration(), 'getSchemaAssetsFilter') ? $connection->getConfiguration()->getSchemaAssetsFilter() : null; + + //Disable foreign key checks + if($platform->getName() === 'mysql') { + $connection->executeQuery('SET foreign_key_checks = 0;'); + } + + 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->executeUpdate($this->getDeleteFromTableSQL($tbl, $platform)); + } else { + $connection->executeUpdate($platform->getTruncateTableSQL($tbl, true)); + } + + //Reseting autoincrement is only supported on MySQL platforms + if ($platform->getName() === 'mysql') { + $connection->beginTransaction(); + $connection->executeQuery($this->getResetAutoIncrementSQL($tbl, $platform)); + } + } + + //Reenable foreign key checks + if($platform->getName() === 'mysql') { + $connection->executeQuery('SET foreign_key_checks = 1;'); + } + } + + private function getResetAutoIncrementSQL(string $tableName, AbstractPlatform $platform): string + { + $tableIdentifier = new Identifier($tableName); + + return 'ALTER TABLE '. $tableIdentifier->getQuotedName($platform) .' AUTO_INCREMENT = 1;'; + } + + /** + * @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 array $classes + * + * @return array + */ + 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); + } +} diff --git a/src/Doctrine/Purger/ResetAutoIncrementPurgerFactory.php b/src/Doctrine/Purger/ResetAutoIncrementPurgerFactory.php new file mode 100644 index 00000000..fc795574 --- /dev/null +++ b/src/Doctrine/Purger/ResetAutoIncrementPurgerFactory.php @@ -0,0 +1,20 @@ +setPurgeMode($purgeWithTruncate ? ORMPurger::PURGE_MODE_TRUNCATE : ORMPurger::PURGE_MODE_DELETE); + + return $purger; + } +} \ No newline at end of file