ResetAutoIncrementORMPurger::setPurgeMode()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 1
c 0
b 0
f 0
nc 1
nop 1
dl 0
loc 3
rs 10
1
<?php
2
/*
3
 * This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
4
 *
5
 *  Copyright (C) 2019 - 2022 Jan Böhmer (https://github.com/jbtronics)
6
 *
7
 *  This program is free software: you can redistribute it and/or modify
8
 *  it under the terms of the GNU Affero General Public License as published
9
 *  by the Free Software Foundation, either version 3 of the License, or
10
 *  (at your option) any later version.
11
 *
12
 *  This program is distributed in the hope that it will be useful,
13
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
14
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15
 *  GNU Affero General Public License for more details.
16
 *
17
 *  You should have received a copy of the GNU Affero General Public License
18
 *  along with this program.  If not, see <https://www.gnu.org/licenses/>.
19
 */
20
21
declare(strict_types=1);
22
23
namespace App\Doctrine\Purger;
24
25
use Doctrine\Common\DataFixtures\Purger\ORMPurgerInterface;
26
use Doctrine\Common\DataFixtures\Purger\PurgerInterface;
27
use Doctrine\Common\DataFixtures\Sorter\TopologicalSorter;
28
use Doctrine\DBAL\Platforms\AbstractMySQLPlatform;
29
use Doctrine\DBAL\Platforms\AbstractPlatform;
30
use Doctrine\DBAL\Platforms\MySQLPlatform;
31
use Doctrine\DBAL\Schema\Identifier;
32
use Doctrine\ORM\EntityManagerInterface;
33
use Doctrine\ORM\Mapping\ClassMetadata;
34
35
use Doctrine\ORM\Mapping\ClassMetadataInfo;
36
37
use function array_reverse;
38
use function array_search;
39
use function assert;
40
use function count;
41
use function is_callable;
42
use function method_exists;
43
use function preg_match;
44
45
/**
46
 * Class responsible for purging databases of data before reloading data fixtures.
47
 *
48
 * Based on Doctrine\Common\DataFixtures\Purger\ORMPurger
49
 */
50
class ResetAutoIncrementORMPurger implements PurgerInterface, ORMPurgerInterface
51
{
52
    public const PURGE_MODE_DELETE   = 1;
53
    public const PURGE_MODE_TRUNCATE = 2;
54
55
    /** @var EntityManagerInterface|null */
56
    private $em;
57
58
    /**
59
     * If the purge should be done through DELETE or TRUNCATE statements
60
     *
61
     * @var int
62
     */
63
    private $purgeMode = self::PURGE_MODE_DELETE;
64
65
    /**
66
     * Table/view names to be excluded from purge
67
     *
68
     * @var string[]
69
     */
70
    private $excluded;
71
72
    /**
73
     * Construct new purger instance.
74
     *
75
     * @param  EntityManagerInterface|null  $em  EntityManagerInterface instance used for persistence.
76
     * @param  string[]  $excluded  array of table/view names to be excluded from purge
77
     */
78
    public function __construct(?EntityManagerInterface $em = null, array $excluded = [])
79
    {
80
        $this->em       = $em;
81
        $this->excluded = $excluded;
82
    }
83
84
    /**
85
     * Set the purge mode
86
     *
87
     * @param  int  $mode
88
     *
89
     * @return void
90
     */
91
    public function setPurgeMode(int $mode): void
92
    {
93
        $this->purgeMode = $mode;
94
    }
95
96
    /**
97
     * Get the purge mode
98
     *
99
     * @return int
100
     */
101
    public function getPurgeMode(): int
102
    {
103
        return $this->purgeMode;
104
    }
105
106
    /** @inheritDoc */
107
    public function setEntityManager(EntityManagerInterface $em): void
108
    {
109
        $this->em = $em;
110
    }
111
112
    /**
113
     * Retrieve the EntityManagerInterface instance this purger instance is using.
114
     *
115
     * @return EntityManagerInterface
116
     */
117
    public function getObjectManager(): ?EntityManagerInterface
118
    {
119
        return $this->em;
120
    }
121
122
    /** @inheritDoc */
123
    public function purge(): void
124
    {
125
        $classes = [];
126
127
        foreach ($this->em->getMetadataFactory()->getAllMetadata() as $metadata) {
0 ignored issues
show
Bug introduced by
The method getMetadataFactory() does not exist on null. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

127
        foreach ($this->em->/** @scrutinizer ignore-call */ getMetadataFactory()->getAllMetadata() as $metadata) {

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
128
            if ($metadata->isMappedSuperclass || (isset($metadata->isEmbeddedClass) && $metadata->isEmbeddedClass)) {
129
                continue;
130
            }
131
132
            $classes[] = $metadata;
133
        }
134
135
        $commitOrder = $this->getCommitOrder($this->em, $classes);
0 ignored issues
show
Bug introduced by
It seems like $this->em can also be of type null; however, parameter $em of App\Doctrine\Purger\Rese...urger::getCommitOrder() does only seem to accept Doctrine\ORM\EntityManagerInterface, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

135
        $commitOrder = $this->getCommitOrder(/** @scrutinizer ignore-type */ $this->em, $classes);
Loading history...
136
137
        // Get platform parameters
138
        $platform = $this->em->getConnection()->getDatabasePlatform();
139
140
        // Drop association tables first
141
        $orderedTables = $this->getAssociationTables($commitOrder, $platform);
142
143
        // Drop tables in reverse commit order
144
        for ($i = count($commitOrder) - 1; $i >= 0; --$i) {
145
            $class = $commitOrder[$i];
146
147
            if (
148
                (isset($class->isEmbeddedClass) && $class->isEmbeddedClass) ||
149
                $class->isMappedSuperclass ||
150
                ($class->isInheritanceTypeSingleTable() && $class->name !== $class->rootEntityName)
151
            ) {
152
                continue;
153
            }
154
155
            $orderedTables[] = $this->getTableName($class, $platform);
156
        }
157
158
        $connection            = $this->em->getConnection();
159
        $filterExpr            = method_exists(
160
            $connection->getConfiguration(),
161
            'getFilterSchemaAssetsExpression'
162
        ) ? $connection->getConfiguration()->getFilterSchemaAssetsExpression() : null;
163
        $emptyFilterExpression = empty($filterExpr);
164
165
        $schemaAssetsFilter = method_exists(
166
            $connection->getConfiguration(),
167
            'getSchemaAssetsFilter'
168
        ) ? $connection->getConfiguration()->getSchemaAssetsFilter() : null;
169
170
        //Disable foreign key checks
171
        if($platform instanceof AbstractMySQLPlatform) {
172
            $connection->executeQuery('SET foreign_key_checks = 0;');
173
        }
174
175
        foreach ($orderedTables as $tbl) {
176
            // If we have a filter expression, check it and skip if necessary
177
            if (! $emptyFilterExpression && ! preg_match($filterExpr, $tbl)) {
178
                continue;
179
            }
180
181
            // If the table is excluded, skip it as well
182
            if (array_search($tbl, $this->excluded) !== false) {
183
                continue;
184
            }
185
186
            // Support schema asset filters as presented in
187
            if (is_callable($schemaAssetsFilter) && ! $schemaAssetsFilter($tbl)) {
188
                continue;
189
            }
190
191
            if ($this->purgeMode === self::PURGE_MODE_DELETE) {
192
                $connection->executeStatement($this->getDeleteFromTableSQL($tbl, $platform));
193
            } else {
194
                $connection->executeStatement($platform->getTruncateTableSQL($tbl, true));
195
            }
196
197
            //Reseting autoincrement is only supported on MySQL platforms
198
            if ($platform instanceof AbstractMySQLPlatform) {
199
                $connection->beginTransaction();
200
                $connection->executeQuery($this->getResetAutoIncrementSQL($tbl, $platform));
201
            }
202
        }
203
204
        //Reenable foreign key checks
205
        if($platform instanceof AbstractMySQLPlatform) {
206
            $connection->executeQuery('SET foreign_key_checks = 1;');
207
        }
208
    }
209
210
    private function getResetAutoIncrementSQL(string $tableName, AbstractPlatform $platform): string
211
    {
212
        $tableIdentifier = new Identifier($tableName);
213
214
        return 'ALTER TABLE '. $tableIdentifier->getQuotedName($platform) .' AUTO_INCREMENT = 1;';
215
    }
216
217
    /**
218
     * @param ClassMetadata[] $classes
219
     *
220
     * @return ClassMetadata[]
221
     */
222
    private function getCommitOrder(EntityManagerInterface $em, array $classes): array
223
    {
224
        $sorter = new TopologicalSorter();
225
226
        foreach ($classes as $class) {
227
            if (! $sorter->hasNode($class->name)) {
228
                $sorter->addNode($class->name, $class);
229
            }
230
231
            // $class before its parents
232
            foreach ($class->parentClasses as $parentClass) {
233
                $parentClass     = $em->getClassMetadata($parentClass);
234
                $parentClassName = $parentClass->getName();
235
236
                if (! $sorter->hasNode($parentClassName)) {
237
                    $sorter->addNode($parentClassName, $parentClass);
238
                }
239
240
                $sorter->addDependency($class->name, $parentClassName);
241
            }
242
243
            foreach ($class->associationMappings as $assoc) {
244
                if (! $assoc['isOwningSide']) {
245
                    continue;
246
                }
247
248
                $targetClass = $em->getClassMetadata($assoc['targetEntity']);
249
                assert($targetClass instanceof ClassMetadata);
250
                $targetClassName = $targetClass->getName();
251
252
                if (! $sorter->hasNode($targetClassName)) {
253
                    $sorter->addNode($targetClassName, $targetClass);
254
                }
255
256
                // add dependency ($targetClass before $class)
257
                $sorter->addDependency($targetClassName, $class->name);
258
259
                // parents of $targetClass before $class, too
260
                foreach ($targetClass->parentClasses as $parentClass) {
261
                    $parentClass     = $em->getClassMetadata($parentClass);
262
                    $parentClassName = $parentClass->getName();
263
264
                    if (! $sorter->hasNode($parentClassName)) {
265
                        $sorter->addNode($parentClassName, $parentClass);
266
                    }
267
268
                    $sorter->addDependency($parentClassName, $class->name);
269
                }
270
            }
271
        }
272
273
        return array_reverse($sorter->sort());
274
    }
275
276
    /**
277
     * @param array $classes
278
     *
279
     * @return array
280
     */
281
    private function getAssociationTables(array $classes, AbstractPlatform $platform): array
282
    {
283
        $associationTables = [];
284
285
        foreach ($classes as $class) {
286
            foreach ($class->associationMappings as $assoc) {
287
                if (! $assoc['isOwningSide'] || $assoc['type'] !== ClassMetadataInfo::MANY_TO_MANY) {
288
                    continue;
289
                }
290
291
                $associationTables[] = $this->getJoinTableName($assoc, $class, $platform);
292
            }
293
        }
294
295
        return $associationTables;
296
    }
297
298
    private function getTableName(ClassMetadata $class, AbstractPlatform $platform): string
299
    {
300
        if (isset($class->table['schema']) && ! method_exists($class, 'getSchemaName')) {
301
            return $class->table['schema'] . '.' .
302
                $this->em->getConfiguration()
303
                    ->getQuoteStrategy()
304
                    ->getTableName($class, $platform);
305
        }
306
307
        return $this->em->getConfiguration()->getQuoteStrategy()->getTableName($class, $platform);
308
    }
309
310
    /**
311
     * @param  array  $assoc
312
     */
313
    private function getJoinTableName(
314
        array $assoc,
315
        ClassMetadata $class,
316
        AbstractPlatform $platform
317
    ): string {
318
        if (isset($assoc['joinTable']['schema']) && ! method_exists($class, 'getSchemaName')) {
319
            return $assoc['joinTable']['schema'] . '.' .
320
                $this->em->getConfiguration()
321
                    ->getQuoteStrategy()
322
                    ->getJoinTableName($assoc, $class, $platform);
323
        }
324
325
        return $this->em->getConfiguration()->getQuoteStrategy()->getJoinTableName($assoc, $class, $platform);
326
    }
327
328
    private function getDeleteFromTableSQL(string $tableName, AbstractPlatform $platform): string
329
    {
330
        $tableIdentifier = new Identifier($tableName);
331
332
        return 'DELETE FROM ' . $tableIdentifier->getQuotedName($platform);
333
    }
334
}
335