Passed
Push — master ( 9f4ef0...3dc1a3 )
by Damien
04:17
created

SchemaManager   B

Complexity

Total Complexity 46

Size/Duplication

Total Lines 280
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
wmc 46
eloc 135
dl 0
loc 280
rs 8.72
c 0
b 0
f 0

8 Methods

Rating   Name   Duplication   Size   Complexity  
B getUpdateAuditSchemaSql() 0 73 10
A getAuditableTableNames() 0 16 4
A updateAuditSchema() 0 18 5
A __construct() 0 3 1
A processIndices() 0 15 5
B processColumns() 0 35 9
B createAuditTable() 0 55 10
A updateAuditTable() 0 21 2

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
namespace DH\Auditor\Provider\Doctrine\Persistence\Schema;
4
5
use DH\Auditor\Provider\Doctrine\Configuration;
6
use DH\Auditor\Provider\Doctrine\DoctrineProvider;
7
use DH\Auditor\Provider\Doctrine\Persistence\Helper\DoctrineHelper;
8
use DH\Auditor\Provider\Doctrine\Persistence\Helper\PlatformHelper;
9
use DH\Auditor\Provider\Doctrine\Persistence\Helper\SchemaHelper;
10
use DH\Auditor\Provider\Doctrine\Service\AuditingService;
11
use DH\Auditor\Provider\Doctrine\Service\StorageService;
12
use Doctrine\DBAL\Connection;
13
use Doctrine\DBAL\Schema\Schema;
14
use Doctrine\DBAL\Schema\SchemaException;
15
use Doctrine\DBAL\Schema\Table;
16
use Doctrine\ORM\EntityManagerInterface;
17
18
class SchemaManager
19
{
20
    /**
21
     * @var DoctrineProvider
22
     */
23
    private $provider;
24
25
    public function __construct(DoctrineProvider $provider)
26
    {
27
        $this->provider = $provider;
28
    }
29
30
    public function updateAuditSchema(?array $sqls = null, ?callable $callback = null): void
31
    {
32
        // TODO: FIXME will create the same schema on all connections
33
        if (null === $sqls) {
34
            $sqls = $this->getUpdateAuditSchemaSql();
35
        }
36
37
        /** @var StorageService[] $storageServices */
38
        $storageServices = $this->provider->getStorageServices();
39
        foreach ($sqls as $name => $queries) {
40
            foreach ($queries as $index => $sql) {
41
                $statement = $storageServices[$name]->getEntityManager()->getConnection()->prepare($sql);
42
                $statement->execute();
43
44
                if (null !== $callback) {
45
                    $callback([
46
                        'total' => \count($sqls),
47
                        'current' => $index,
48
                    ]);
49
                }
50
            }
51
        }
52
    }
53
54
    /**
55
     * Returns an array of audit table names indexed by entity FQN.
56
     * Only auditable entities are considered.
57
     *
58
     * @throws \Doctrine\ORM\ORMException
59
     */
60
    public function getAuditableTableNames(EntityManagerInterface $entityManager): array
61
    {
62
        $metadataDriver = $entityManager->getConfiguration()->getMetadataDriverImpl();
63
        $entities = [];
64
        if (null !== $metadataDriver) {
65
            $entities = $metadataDriver->getAllClassNames();
66
        }
67
        $audited = [];
68
        foreach ($entities as $entity) {
69
            if ($this->provider->isAuditable($entity)) {
70
                $audited[$entity] = $entityManager->getClassMetadata($entity)->getTableName();
71
            }
72
        }
73
        ksort($audited);
74
75
        return $audited;
76
    }
77
78
    public function getUpdateAuditSchemaSql(): array
79
    {
80
        /** @var Configuration $configuration */
81
        $configuration = $this->provider->getConfiguration();
82
83
        /** @var StorageService[] $storageServices */
84
        $storageServices = $this->provider->getStorageServices();
85
86
        // auditable entities by storage entity manager
87
        $repository = [];
88
89
        // Collect auditable entities from auditing storage managers
90
        /** @var AuditingService[] $auditingServices */
91
        $auditingServices = $this->provider->getAuditingServices();
92
        foreach ($auditingServices as $name => $auditingService) {
93
            $classes = $this->getAuditableTableNames($auditingService->getEntityManager());
94
            // Populate the auditable entities repository
95
            foreach ($classes as $entity => $tableName) {
96
                $storageService = $this->provider->getStorageServiceForEntity($entity);
97
                $key = array_search($storageService, $this->provider->getStorageServices(), true);
98
                if (!isset($repository[$key])) {
99
                    $repository[$key] = [];
100
                }
101
                $repository[$key][$entity] = $tableName;
102
            }
103
        }
104
        $findEntityByTablename = static function (string $tableName) use ($repository): ?string {
105
            foreach ($repository as $emName => $map) {
106
                $result = array_search($tableName, $map, true);
107
                if (false !== $result) {
108
                    return (string) $result;
109
                }
110
            }
111
112
            return null;
113
        };
114
115
        // Compute and collect SQL queries
116
        $sqls = [];
117
        foreach ($repository as $name => $classes) {
118
            $storageSchemaManager = $storageServices[$name]->getEntityManager()->getConnection()->getSchemaManager();
119
120
            $storageSchema = $storageSchemaManager->createSchema();
121
            $fromSchema = clone $storageSchema;
122
            $tables = $storageSchema->getTables();
123
            foreach ($tables as $table) {
124
                if (\in_array($table->getName(), array_values($repository[$name]), true)) {
125
                    // table is the one of an auditable entity
126
127
                    /** @var string $auditTablename */
128
                    $auditTablename = preg_replace(
129
                        sprintf('#^([^\.]+\.)?(%s)$#', preg_quote($table->getName(), '#')),
130
                        sprintf(
131
                            '$1%s$2%s',
132
                            preg_quote($configuration->getTablePrefix(), '#'),
133
                            preg_quote($configuration->getTableSuffix(), '#')
134
                        ),
135
                        $table->getName()
136
                    );
137
                    if ($storageSchema->hasTable($auditTablename)) {
138
                        // Audit table exists, let's update it if needed
139
                        $this->updateAuditTable($findEntityByTablename($table->getName()), $storageSchema->getTable($auditTablename), $storageSchema);
140
                    } else {
141
                        // Audit table does not exists, let's create it
142
                        $this->createAuditTable($findEntityByTablename($table->getName()), $table, $storageSchema);
143
                    }
144
                }
145
            }
146
147
            $sqls[$name] = $fromSchema->getMigrateToSql($storageSchema, $storageSchemaManager->getDatabasePlatform());
148
        }
149
150
        return $sqls;
151
    }
152
153
    /**
154
     * Creates an audit table.
155
     */
156
    public function createAuditTable(string $entity, Table $table, ?Schema $schema = null): Schema
157
    {
158
        /** @var StorageService $storageService */
159
        $storageService = $this->provider->getStorageServiceForEntity($entity);
160
        $connection = $storageService->getEntityManager()->getConnection();
161
162
        if (null === $schema) {
163
            $schemaManager = $connection->getSchemaManager();
164
            $schema = $schemaManager->createSchema();
165
        }
166
167
        /** @var Configuration $configuration */
168
        $configuration = $this->provider->getConfiguration();
169
170
        $auditTablename = preg_replace(
171
            sprintf('#^([^\.]+\.)?(%s)$#', preg_quote($table->getName(), '#')),
172
            sprintf(
173
                '$1%s$2%s',
174
                preg_quote($configuration->getTablePrefix(), '#'),
175
                preg_quote($configuration->getTableSuffix(), '#')
176
            ),
177
            $table->getName()
178
        );
179
180
        if (null !== $auditTablename && !$schema->hasTable($auditTablename)) {
181
            $auditTable = $schema->createTable($auditTablename);
182
183
            // Add columns to audit table
184
            $isJsonSupported = PlatformHelper::isJsonSupported($connection);
185
            foreach (SchemaHelper::getAuditTableColumns() as $columnName => $struct) {
186
                if (DoctrineHelper::getDoctrineType('JSON') === $struct['type'] && $isJsonSupported) {
187
                    $type = DoctrineHelper::getDoctrineType('TEXT');
188
                } else {
189
                    $type = $struct['type'];
190
                }
191
192
                $auditTable->addColumn($columnName, $type, $struct['options']);
193
            }
194
195
            // Add indices to audit table
196
            foreach (SchemaHelper::getAuditTableIndices($auditTablename) as $columnName => $struct) {
197
                if ('primary' === $struct['type']) {
198
                    $auditTable->setPrimaryKey([$columnName]);
199
                } else {
200
                    $auditTable->addIndex(
201
                        [$columnName],
202
                        $struct['name'],
203
                        [],
204
                        PlatformHelper::isIndexLengthLimited($columnName, $connection) ? ['lengths' => [191]] : []
205
                    );
206
                }
207
            }
208
        }
209
210
        return $schema;
211
    }
212
213
    /**
214
     * Ensures an audit table's structure is valid.
215
     *
216
     * @throws SchemaException
217
     */
218
    public function updateAuditTable(string $entity, Table $table, ?Schema $schema = null): Schema
219
    {
220
        /** @var StorageService $storageService */
221
        $storageService = $this->provider->getStorageServiceForEntity($entity);
222
        $connection = $storageService->getEntityManager()->getConnection();
223
224
        $schemaManager = $connection->getSchemaManager();
225
        if (null === $schema) {
226
            $schema = $schemaManager->createSchema();
227
        }
228
229
        $table = $schema->getTable($table->getName());
230
        $columns = $schemaManager->listTableColumns($table->getName());
231
232
        // process columns
233
        $this->processColumns($table, $columns, SchemaHelper::getAuditTableColumns(), $connection);
234
235
        // process indices
236
        $this->processIndices($table, SchemaHelper::getAuditTableIndices($table->getName()), $connection);
237
238
        return $schema;
239
    }
240
241
    private function processColumns(Table $table, array $columns, array $expectedColumns, Connection $connection): void
242
    {
243
        $processed = [];
244
245
        $isJsonSupported = PlatformHelper::isJsonSupported($connection);
246
        foreach ($columns as $column) {
247
            if (\array_key_exists($column->getName(), $expectedColumns)) {
248
                // column is part of expected columns
249
                $table->dropColumn($column->getName());
250
251
                if (DoctrineHelper::getDoctrineType('JSON') === $expectedColumns[$column->getName()]['type'] && $isJsonSupported) {
252
                    $type = DoctrineHelper::getDoctrineType('TEXT');
253
                } else {
254
                    $type = $expectedColumns[$column->getName()]['type'];
255
                }
256
257
                $table->addColumn($column->getName(), $type, $expectedColumns[$column->getName()]['options']);
258
            } else {
259
                // column is not part of expected columns so it has to be removed
260
                $table->dropColumn($column->getName());
261
            }
262
263
            $processed[] = $column->getName();
264
        }
265
266
        foreach ($expectedColumns as $columnName => $options) {
267
            if (!\in_array($columnName, $processed, true)) {
268
                // expected column in not part of concrete ones so it's a new column, we need to add it
269
                if (DoctrineHelper::getDoctrineType('JSON') === $options['type'] && $isJsonSupported) {
270
                    $type = DoctrineHelper::getDoctrineType('TEXT');
0 ignored issues
show
Unused Code introduced by
The assignment to $type is dead and can be removed.
Loading history...
271
                } else {
272
                    $type = $options['type'];
273
                }
274
275
                $table->addColumn($columnName, $options['type'], $options['options']);
276
            }
277
        }
278
    }
279
280
    /**
281
     * @throws SchemaException
282
     */
283
    private function processIndices(Table $table, array $expectedIndices, Connection $connection): void
284
    {
285
        foreach ($expectedIndices as $columnName => $options) {
286
            if ('primary' === $options['type']) {
287
                $table->dropPrimaryKey();
288
                $table->setPrimaryKey([$columnName]);
289
            } else {
290
                if ($table->hasIndex($options['name'])) {
291
                    $table->dropIndex($options['name']);
292
                }
293
                $table->addIndex(
294
                    [$columnName],
295
                    $options['name'],
296
                    [],
297
                    PlatformHelper::isIndexLengthLimited($columnName, $connection) ? ['lengths' => [191]] : []
298
                );
299
            }
300
        }
301
    }
302
}
303