SchemaManager   B
last analyzed

Complexity

Total Complexity 49

Size/Duplication

Total Lines 308
Duplicated Lines 0 %

Importance

Changes 2
Bugs 1 Features 0
Metric Value
wmc 49
eloc 140
c 2
b 1
f 0
dl 0
loc 308
rs 8.48

12 Methods

Rating   Name   Duplication   Size   Complexity  
A updateAuditSchema() 0 18 5
A __construct() 0 3 1
A collectAuditableEntities() 0 22 4
A getAuditableTableNames() 0 18 4
A getUpdateAuditSchemaSql() 0 42 5
A processIndices() 0 16 5
A computeAuditTablename() 0 10 1
B processColumns() 0 28 7
B createAuditTable() 0 48 11
A updateAuditTable() 0 25 2
A resolveAuditTableName() 0 7 1
A resolveTableName() 0 11 3

How to fix   Complexity   

Complex Class

Complex classes like SchemaManager often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use SchemaManager, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
declare(strict_types=1);
4
5
namespace DH\Auditor\Provider\Doctrine\Persistence\Schema;
6
7
use DH\Auditor\Exception\InvalidArgumentException;
8
use DH\Auditor\Provider\Doctrine\Configuration;
9
use DH\Auditor\Provider\Doctrine\DoctrineProvider;
10
use DH\Auditor\Provider\Doctrine\Persistence\Helper\DoctrineHelper;
11
use DH\Auditor\Provider\Doctrine\Persistence\Helper\PlatformHelper;
12
use DH\Auditor\Provider\Doctrine\Persistence\Helper\SchemaHelper;
13
use DH\Auditor\Provider\Doctrine\Service\AuditingService;
14
use DH\Auditor\Provider\Doctrine\Service\StorageService;
15
use Doctrine\DBAL\Connection;
16
use Doctrine\DBAL\Platforms\AbstractPlatform;
17
use Doctrine\DBAL\Schema\Schema;
18
use Doctrine\DBAL\Schema\SchemaException;
19
use Doctrine\DBAL\Schema\Table;
20
use Doctrine\DBAL\Types\Types;
21
use Doctrine\ORM\EntityManagerInterface;
22
23
/**
24
 * @see \DH\Auditor\Tests\Provider\Doctrine\Persistence\Schema\SchemaManagerTest
25
 */
26
final class SchemaManager
27
{
28
    private DoctrineProvider $provider;
29
30
    public function __construct(DoctrineProvider $provider)
31
    {
32
        $this->provider = $provider;
33
    }
34
35
    public function updateAuditSchema(?array $sqls = null, ?callable $callback = null): void
36
    {
37
        if (null === $sqls) {
38
            $sqls = $this->getUpdateAuditSchemaSql();
39
        }
40
41
        /** @var StorageService[] $storageServices */
42
        $storageServices = $this->provider->getStorageServices();
43
        foreach ($sqls as $name => $queries) {
44
            $connection = $storageServices[$name]->getEntityManager()->getConnection();
45
            foreach ($queries as $index => $sql) {
46
                $statement = $connection->prepare($sql);
47
                $statement->executeStatement();
48
49
                if (null !== $callback) {
50
                    $callback([
51
                        'total' => \count($sqls),
52
                        'current' => $index,
53
                    ]);
54
                }
55
            }
56
        }
57
    }
58
59
    /**
60
     * Returns an array of audit table names indexed by entity FQN.
61
     * Only auditable entities are considered.
62
     */
63
    public function getAuditableTableNames(EntityManagerInterface $entityManager): array
64
    {
65
        $metadataDriver = $entityManager->getConfiguration()->getMetadataDriverImpl();
66
        $entities = [];
67
        if ($metadataDriver instanceof \Doctrine\Persistence\Mapping\Driver\MappingDriver) {
68
            $entities = $metadataDriver->getAllClassNames();
69
        }
70
71
        $audited = [];
72
        foreach ($entities as $entity) {
73
            if ($this->provider->isAuditable($entity)) {
74
                $audited[$entity] = $entityManager->getClassMetadata($entity)->getTableName();
75
            }
76
        }
77
78
        ksort($audited);
79
80
        return $audited;
81
    }
82
83
    public function collectAuditableEntities(): array
84
    {
85
        // auditable entities by storage entity manager
86
        $repository = [];
87
88
        /** @var AuditingService[] $auditingServices */
89
        $auditingServices = $this->provider->getAuditingServices();
90
        foreach ($auditingServices as $auditingService) {
91
            $classes = $this->getAuditableTableNames($auditingService->getEntityManager());
92
            // Populate the auditable entities repository
93
            foreach ($classes as $entity => $tableName) {
94
                $storageService = $this->provider->getStorageServiceForEntity($entity);
95
                $key = array_search($storageService, $this->provider->getStorageServices(), true);
96
                if (!isset($repository[$key])) {
97
                    $repository[$key] = [];
98
                }
99
100
                $repository[$key][$entity] = $tableName;
101
            }
102
        }
103
104
        return $repository;
105
    }
106
107
    public function getUpdateAuditSchemaSql(): array
108
    {
109
        /** @var Configuration $configuration */
110
        $configuration = $this->provider->getConfiguration();
111
112
        /** @var StorageService[] $storageServices */
113
        $storageServices = $this->provider->getStorageServices();
114
115
        // Collect auditable entities from auditing entity managers
116
        $repository = $this->collectAuditableEntities();
117
118
        // Compute and collect SQL queries
119
        $sqls = [];
120
        foreach ($repository as $name => $classes) {
121
            $storageConnection = $storageServices[$name]->getEntityManager()->getConnection();
122
            $storageSchemaManager = $storageConnection->createSchemaManager();
123
124
            $storageSchema = $storageSchemaManager->introspectSchema();
125
            $fromSchema = clone $storageSchema;
126
127
            $processed = [];
128
            foreach ($classes as $entityFQCN => $tableName) {
129
                if (!\in_array($entityFQCN, $processed, true)) {
130
                    /** @var string $auditTablename */
131
                    $auditTablename = $this->resolveAuditTableName($entityFQCN, $configuration, $storageConnection->getDatabasePlatform());
132
133
                    if ($storageSchema->hasTable($auditTablename)) {
134
                        // Audit table exists, let's update it if needed
135
                        $this->updateAuditTable($entityFQCN, $storageSchema);
136
                    } else {
137
                        // Audit table does not exists, let's create it
138
                        $this->createAuditTable($entityFQCN, $storageSchema);
139
                    }
140
141
                    $processed[] = $entityFQCN;
142
                }
143
            }
144
145
            $sqls[$name] = DoctrineHelper::getMigrateToSql($storageConnection, $fromSchema, $storageSchema);
146
        }
147
148
        return $sqls;
149
    }
150
151
    /**
152
     * Creates an audit table.
153
     *
154
     * @throws \Doctrine\DBAL\Exception
155
     */
156
    public function createAuditTable(string $entity, ?Schema $schema = null): Schema
157
    {
158
        /** @var StorageService $storageService */
159
        $storageService = $this->provider->getStorageServiceForEntity($entity);
160
        $connection = $storageService->getEntityManager()->getConnection();
161
162
        if (!$schema instanceof \Doctrine\DBAL\Schema\Schema) {
163
            $schemaManager = DoctrineHelper::createSchemaManager($connection);
164
            $schema = DoctrineHelper::introspectSchema($schemaManager);
165
        }
166
167
        /** @var Configuration $configuration */
168
        $configuration = $this->provider->getConfiguration();
169
        $auditTablename = $this->resolveAuditTableName($entity, $configuration, $connection->getDatabasePlatform());
170
171
        if (null !== $auditTablename && !$schema->hasTable($auditTablename)) {
172
            $auditTable = $schema->createTable($auditTablename);
173
174
            // Add columns to audit table
175
            $isJsonSupported = PlatformHelper::isJsonSupported($connection);
176
            foreach (SchemaHelper::getAuditTableColumns() as $columnName => $struct) {
177
                if (Types::JSON === $struct['type'] && !$isJsonSupported) {
178
                    $type = Types::TEXT;
179
                } else {
180
                    $type = $struct['type'];
181
                }
182
183
                $auditTable->addColumn($columnName, $type, $struct['options']);
184
            }
185
186
            // Add indices to audit table
187
            foreach (SchemaHelper::getAuditTableIndices($auditTablename) as $columnName => $struct) {
188
                if ('primary' === $struct['type']) {
189
                    $auditTable->setPrimaryKey([$columnName]);
190
                } elseif (isset($struct['name'])) {
191
                    $auditTable->addIndex(
192
                        [$columnName],
193
                        $struct['name'],
194
                        [],
195
                        PlatformHelper::isIndexLengthLimited($columnName, $connection) ? ['lengths' => [191]] : []
196
                    );
197
                } else {
198
                    throw new InvalidArgumentException(sprintf("Missing key 'name' for column '%s'", $columnName));
199
                }
200
            }
201
        }
202
203
        return $schema;
204
    }
205
206
    /**
207
     * Ensures an audit table's structure is valid.
208
     *
209
     * @throws SchemaException
210
     * @throws \Doctrine\DBAL\Exception
211
     */
212
    public function updateAuditTable(string $entity, ?Schema $schema = null): Schema
213
    {
214
        /** @var StorageService $storageService */
215
        $storageService = $this->provider->getStorageServiceForEntity($entity);
216
        $connection = $storageService->getEntityManager()->getConnection();
217
218
        $schemaManager = DoctrineHelper::createSchemaManager($connection);
219
        if (!$schema instanceof \Doctrine\DBAL\Schema\Schema) {
220
            $schema = DoctrineHelper::introspectSchema($schemaManager);
221
        }
222
223
        /** @var Configuration $configuration */
224
        $configuration = $this->provider->getConfiguration();
225
226
        $auditTablename = $this->resolveAuditTableName($entity, $configuration, $connection->getDatabasePlatform());
227
        \assert(\is_string($auditTablename));
228
        $table = $schema->getTable($auditTablename);
229
230
        // process columns
231
        $this->processColumns($table, $table->getColumns(), SchemaHelper::getAuditTableColumns(), $connection);
232
233
        // process indices
234
        $this->processIndices($table, SchemaHelper::getAuditTableIndices($auditTablename), $connection);
235
236
        return $schema;
237
    }
238
239
    /**
240
     * Resolves table name, including namespace/schema.
241
     */
242
    public function resolveTableName(string $tableName, string $namespaceName, AbstractPlatform $platform): string
243
    {
244
        if (empty($namespaceName)) {
245
            $prefix = '';
246
        } elseif (!$platform->supportsSchemas()) {
247
            $prefix = $namespaceName.'__';
248
        } else {
249
            $prefix = $namespaceName.'.';
250
        }
251
252
        return $prefix.$tableName;
253
    }
254
255
    /**
256
     * Resolves audit table name, including namespace/schema.
257
     */
258
    public function resolveAuditTableName(string $entity, Configuration $configuration, AbstractPlatform $platform): ?string
259
    {
260
        $entities = $configuration->getEntities();
261
        $entityOptions = $entities[$entity];
262
        $tablename = $this->resolveTableName($entityOptions['table_name'], $entityOptions['audit_table_schema'], $platform);
263
264
        return $this->computeAuditTablename($tablename, $configuration);
265
    }
266
267
    /**
268
     * Computes audit table name **without** namespace/schema.
269
     */
270
    public function computeAuditTablename(string $entityTableName, Configuration $configuration): ?string
271
    {
272
        return preg_replace(
273
            '#^([^.]+\.)?(.+)$#',
274
            sprintf(
275
                '$1%s$2%s',
276
                preg_quote($configuration->getTablePrefix(), '#'),
277
                preg_quote($configuration->getTableSuffix(), '#')
278
            ),
279
            $entityTableName
280
        );
281
    }
282
283
    private function processColumns(Table $table, array $columns, array $expectedColumns, Connection $connection): void
284
    {
285
        $processed = [];
286
287
        $isJsonSupported = PlatformHelper::isJsonSupported($connection);
288
        foreach ($columns as $column) {
289
            if (\array_key_exists($column->getName(), $expectedColumns)) {
290
                // column is part of expected columns
291
                $table->dropColumn($column->getName());
292
293
                if (Types::JSON === $expectedColumns[$column->getName()]['type'] && !$isJsonSupported) {
294
                    $type = Types::TEXT;
295
                } else {
296
                    $type = $expectedColumns[$column->getName()]['type'];
297
                }
298
299
                $table->addColumn($column->getName(), $type, $expectedColumns[$column->getName()]['options']);
300
            } else {
301
                // column is not part of expected columns so it has to be removed
302
                $table->dropColumn($column->getName());
303
            }
304
305
            $processed[] = $column->getName();
306
        }
307
308
        foreach ($expectedColumns as $columnName => $options) {
309
            if (!\in_array($columnName, $processed, true)) {
310
                $table->addColumn($columnName, $options['type'], $options['options']);
311
            }
312
        }
313
    }
314
315
    /**
316
     * @throws SchemaException
317
     */
318
    private function processIndices(Table $table, array $expectedIndices, Connection $connection): void
319
    {
320
        foreach ($expectedIndices as $columnName => $options) {
321
            if ('primary' === $options['type']) {
322
                $table->dropPrimaryKey();
323
                $table->setPrimaryKey([$columnName]);
324
            } else {
325
                if ($table->hasIndex($options['name'])) {
326
                    $table->dropIndex($options['name']);
327
                }
328
329
                $table->addIndex(
330
                    [$columnName],
331
                    $options['name'],
332
                    [],
333
                    PlatformHelper::isIndexLengthLimited($columnName, $connection) ? ['lengths' => [191]] : []
334
                );
335
            }
336
        }
337
    }
338
}
339