2021-10-02 20:14:48 +02:00
|
|
|
<?php
|
2022-11-29 21:21:26 +01:00
|
|
|
/*
|
|
|
|
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
|
|
|
|
*
|
|
|
|
* Copyright (C) 2019 - 2022 Jan Böhmer (https://github.com/jbtronics)
|
|
|
|
*
|
|
|
|
* This program is free software: you can redistribute it and/or modify
|
|
|
|
* it under the terms of the GNU Affero General Public License as published
|
|
|
|
* by the Free Software Foundation, either version 3 of the License, or
|
|
|
|
* (at your option) any later version.
|
|
|
|
*
|
|
|
|
* This program is distributed in the hope that it will be useful,
|
|
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
|
* GNU Affero General Public License for more details.
|
|
|
|
*
|
|
|
|
* You should have received a copy of the GNU Affero General Public License
|
|
|
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
|
|
*/
|
2021-10-02 20:14:48 +02:00
|
|
|
|
|
|
|
declare(strict_types=1);
|
|
|
|
|
|
|
|
namespace App\Doctrine\Purger;
|
|
|
|
|
|
|
|
use Doctrine\Common\DataFixtures\Purger\ORMPurgerInterface;
|
|
|
|
use Doctrine\Common\DataFixtures\Purger\PurgerInterface;
|
|
|
|
use Doctrine\Common\DataFixtures\Sorter\TopologicalSorter;
|
2022-03-04 13:32:53 +01:00
|
|
|
use Doctrine\DBAL\Platforms\AbstractMySQLPlatform;
|
2021-10-02 20:14:48 +02:00
|
|
|
use Doctrine\DBAL\Platforms\AbstractPlatform;
|
2022-03-04 13:32:53 +01:00
|
|
|
use Doctrine\DBAL\Platforms\MySQLPlatform;
|
2021-10-02 20:14:48 +02:00
|
|
|
use Doctrine\DBAL\Schema\Identifier;
|
|
|
|
use Doctrine\ORM\EntityManagerInterface;
|
|
|
|
use Doctrine\ORM\Mapping\ClassMetadata;
|
|
|
|
|
2023-02-05 03:01:25 +01:00
|
|
|
use Doctrine\ORM\Mapping\ClassMetadataInfo;
|
|
|
|
|
2021-10-02 20:14:48 +02:00
|
|
|
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.
|
|
|
|
*
|
|
|
|
* Based on Doctrine\Common\DataFixtures\Purger\ORMPurger
|
|
|
|
*/
|
|
|
|
class ResetAutoIncrementORMPurger implements PurgerInterface, ORMPurgerInterface
|
|
|
|
{
|
|
|
|
public const PURGE_MODE_DELETE = 1;
|
|
|
|
public const PURGE_MODE_TRUNCATE = 2;
|
|
|
|
|
|
|
|
/** @var EntityManagerInterface|null */
|
2023-04-15 22:05:29 +02:00
|
|
|
private ?EntityManagerInterface $em;
|
2021-10-02 20:14:48 +02:00
|
|
|
|
|
|
|
/**
|
|
|
|
* If the purge should be done through DELETE or TRUNCATE statements
|
|
|
|
*
|
|
|
|
* @var int
|
|
|
|
*/
|
2023-04-15 22:05:29 +02:00
|
|
|
private int $purgeMode = self::PURGE_MODE_DELETE;
|
2021-10-02 20:14:48 +02:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Table/view names to be excluded from purge
|
|
|
|
*
|
|
|
|
* @var string[]
|
|
|
|
*/
|
2023-04-15 22:05:29 +02:00
|
|
|
private array $excluded;
|
2021-10-02 20:14:48 +02:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Construct new purger instance.
|
|
|
|
*
|
2022-08-14 19:39:07 +02:00
|
|
|
* @param EntityManagerInterface|null $em EntityManagerInterface instance used for persistence.
|
|
|
|
* @param string[] $excluded array of table/view names to be excluded from purge
|
2021-10-02 20:14:48 +02:00
|
|
|
*/
|
|
|
|
public function __construct(?EntityManagerInterface $em = null, array $excluded = [])
|
|
|
|
{
|
|
|
|
$this->em = $em;
|
|
|
|
$this->excluded = $excluded;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Set the purge mode
|
|
|
|
*
|
2022-08-14 19:09:07 +02:00
|
|
|
* @param int $mode
|
2021-10-02 20:14:48 +02:00
|
|
|
*
|
|
|
|
* @return void
|
|
|
|
*/
|
2022-08-14 19:09:07 +02:00
|
|
|
public function setPurgeMode(int $mode): void
|
2021-10-02 20:14:48 +02:00
|
|
|
{
|
|
|
|
$this->purgeMode = $mode;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get the purge mode
|
|
|
|
*
|
|
|
|
* @return int
|
|
|
|
*/
|
2022-08-14 19:09:07 +02:00
|
|
|
public function getPurgeMode(): int
|
2021-10-02 20:14:48 +02:00
|
|
|
{
|
|
|
|
return $this->purgeMode;
|
|
|
|
}
|
|
|
|
|
|
|
|
/** @inheritDoc */
|
2022-08-14 19:09:07 +02:00
|
|
|
public function setEntityManager(EntityManagerInterface $em): void
|
2021-10-02 20:14:48 +02:00
|
|
|
{
|
|
|
|
$this->em = $em;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Retrieve the EntityManagerInterface instance this purger instance is using.
|
|
|
|
*
|
|
|
|
* @return EntityManagerInterface
|
|
|
|
*/
|
2022-08-14 19:09:07 +02:00
|
|
|
public function getObjectManager(): ?EntityManagerInterface
|
2021-10-02 20:14:48 +02:00
|
|
|
{
|
|
|
|
return $this->em;
|
|
|
|
}
|
|
|
|
|
|
|
|
/** @inheritDoc */
|
2022-08-14 19:09:07 +02:00
|
|
|
public function purge(): void
|
2021-10-02 20:14:48 +02:00
|
|
|
{
|
|
|
|
$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();
|
2022-03-04 13:32:53 +01:00
|
|
|
$filterExpr = method_exists(
|
|
|
|
$connection->getConfiguration(),
|
|
|
|
'getFilterSchemaAssetsExpression'
|
|
|
|
) ? $connection->getConfiguration()->getFilterSchemaAssetsExpression() : null;
|
2021-10-02 20:14:48 +02:00
|
|
|
$emptyFilterExpression = empty($filterExpr);
|
|
|
|
|
2022-03-04 13:32:53 +01:00
|
|
|
$schemaAssetsFilter = method_exists(
|
|
|
|
$connection->getConfiguration(),
|
|
|
|
'getSchemaAssetsFilter'
|
|
|
|
) ? $connection->getConfiguration()->getSchemaAssetsFilter() : null;
|
2021-10-02 20:14:48 +02:00
|
|
|
|
|
|
|
//Disable foreign key checks
|
2022-03-04 13:32:53 +01:00
|
|
|
if($platform instanceof AbstractMySQLPlatform) {
|
2021-10-02 20:14:48 +02:00
|
|
|
$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
|
2023-03-23 01:16:12 +01:00
|
|
|
if (in_array($tbl, $this->excluded, true)) {
|
2021-10-02 20:14:48 +02:00
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Support schema asset filters as presented in
|
|
|
|
if (is_callable($schemaAssetsFilter) && ! $schemaAssetsFilter($tbl)) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
if ($this->purgeMode === self::PURGE_MODE_DELETE) {
|
2022-03-04 13:32:53 +01:00
|
|
|
$connection->executeStatement($this->getDeleteFromTableSQL($tbl, $platform));
|
2021-10-02 20:14:48 +02:00
|
|
|
} else {
|
2022-03-04 13:32:53 +01:00
|
|
|
$connection->executeStatement($platform->getTruncateTableSQL($tbl, true));
|
2021-10-02 20:14:48 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
//Reseting autoincrement is only supported on MySQL platforms
|
2022-03-04 13:32:53 +01:00
|
|
|
if ($platform instanceof AbstractMySQLPlatform) {
|
2021-10-02 20:14:48 +02:00
|
|
|
$connection->beginTransaction();
|
|
|
|
$connection->executeQuery($this->getResetAutoIncrementSQL($tbl, $platform));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
//Reenable foreign key checks
|
2022-03-04 13:32:53 +01:00
|
|
|
if($platform instanceof AbstractMySQLPlatform) {
|
2021-10-02 20:14:48 +02:00
|
|
|
$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[]
|
|
|
|
*/
|
2022-08-14 19:09:07 +02:00
|
|
|
private function getCommitOrder(EntityManagerInterface $em, array $classes): array
|
2021-10-02 20:14:48 +02:00
|
|
|
{
|
|
|
|
$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
|
|
|
|
*/
|
2022-08-14 19:09:07 +02:00
|
|
|
private function getAssociationTables(array $classes, AbstractPlatform $platform): array
|
2021-10-02 20:14:48 +02:00
|
|
|
{
|
|
|
|
$associationTables = [];
|
|
|
|
|
|
|
|
foreach ($classes as $class) {
|
|
|
|
foreach ($class->associationMappings as $assoc) {
|
2023-02-05 03:01:25 +01:00
|
|
|
if (! $assoc['isOwningSide'] || $assoc['type'] !== ClassMetadataInfo::MANY_TO_MANY) {
|
2021-10-02 20:14:48 +02:00
|
|
|
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')) {
|
2022-03-04 13:32:53 +01:00
|
|
|
return $class->table['schema'] . '.' .
|
|
|
|
$this->em->getConfiguration()
|
|
|
|
->getQuoteStrategy()
|
|
|
|
->getTableName($class, $platform);
|
2021-10-02 20:14:48 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
return $this->em->getConfiguration()->getQuoteStrategy()->getTableName($class, $platform);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2022-08-14 19:32:53 +02:00
|
|
|
* @param array $assoc
|
2021-10-02 20:14:48 +02:00
|
|
|
*/
|
|
|
|
private function getJoinTableName(
|
|
|
|
array $assoc,
|
|
|
|
ClassMetadata $class,
|
|
|
|
AbstractPlatform $platform
|
|
|
|
): string {
|
|
|
|
if (isset($assoc['joinTable']['schema']) && ! method_exists($class, 'getSchemaName')) {
|
2022-03-04 13:32:53 +01:00
|
|
|
return $assoc['joinTable']['schema'] . '.' .
|
|
|
|
$this->em->getConfiguration()
|
|
|
|
->getQuoteStrategy()
|
|
|
|
->getJoinTableName($assoc, $class, $platform);
|
2021-10-02 20:14:48 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
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);
|
|
|
|
}
|
|
|
|
}
|