Completed
Branch develop (c2aa4c)
by Anton
05:17
created

SchemaBuilder   B

Complexity

Total Complexity 49

Size/Duplication

Total Lines 422
Duplicated Lines 4.03 %

Coupling/Cohesion

Components 2
Dependencies 11

Importance

Changes 6
Bugs 0 Features 0
Metric Value
wmc 49
c 6
b 0
f 0
lcom 2
cbo 11
dl 17
loc 422
rs 8.5455

18 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 11 1
A hasRecord() 0 4 1
A record() 0 13 4
A getRecords() 0 4 1
A hasTable() 0 4 1
A declareTable() 0 12 2
A synchronizeSchema() 0 5 1
A resolveDatabase() 0 4 1
A getMutators() 0 4 1
A mutatorAlias() 0 4 1
B normalizeSchema() 0 30 3
A relationSchema() 0 22 3
B getTables() 0 24 6
C castRelations() 0 32 7
A findRecord() 0 11 3
A packRelations() 0 9 2
B locateRecords() 0 33 6
B locateSources() 17 17 5

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like SchemaBuilder 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. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

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 SchemaBuilder, and based on these observations, apply Extract Interface, too.

1
<?php
2
/**
3
 * Spiral Framework.
4
 *
5
 * @license   MIT
6
 * @author    Anton Titov (Wolfy-J)
7
 */
8
namespace Spiral\ORM\Entities;
9
10
use Spiral\Core\Component;
11
use Spiral\Database\Entities\Schemas\AbstractTable;
12
use Spiral\Database\Entities\SynchronizationBus;
13
use Spiral\ORM\Configs\ORMConfig;
14
use Spiral\ORM\Entities\Schemas\RecordSchema;
15
use Spiral\ORM\Exceptions\RecordSchemaException;
16
use Spiral\ORM\Exceptions\RelationSchemaException;
17
use Spiral\ORM\Exceptions\SchemaException;
18
use Spiral\ORM\ORM;
19
use Spiral\ORM\Record;
20
use Spiral\ORM\RecordEntity;
21
use Spiral\ORM\Schemas\RelationInterface;
22
use Spiral\Tokenizer\ClassLocatorInterface;
23
24
/**
25
 * Schema builder responsible for static analysis of existed ORM Records, their schemas,
26
 * validations, related tables, requested indexes and etc.
27
 */
28
class SchemaBuilder extends Component
29
{
30
    /**
31
     * @var RecordSchema[]
32
     */
33
    private $records = [];
34
35
    /**
36
     * @var AbstractTable[]
37
     */
38
    private $tables = [];
39
40
    /**
41
     * @var ORMConfig
42
     */
43
    protected $config = null;
44
45
    /**
46
     * @invisible
47
     * @var ORM
48
     */
49
    protected $orm = null;
50
51
    /**
52
     * @param ORMConfig             $config
53
     * @param ORM                   $orm
54
     * @param ClassLocatorInterface $locator
55
     */
56
    public function __construct(ORMConfig $config, ORM $orm, ClassLocatorInterface $locator)
57
    {
58
        $this->config = $config;
59
        $this->orm = $orm;
60
61
        //Locating all models and sources
62
        $this->locateRecords($locator)->locateSources($locator);
63
64
        //Casting relations
65
        $this->castRelations();
66
    }
67
68
    /**
69
     * Check if Record class known to schema builder.
70
     *
71
     * @param string $class
72
     * @return bool
73
     */
74
    public function hasRecord($class)
75
    {
76
        return isset($this->records[$class]);
77
    }
78
79
    /**
80
     * Instance of RecordSchema associated with given class name.
81
     *
82
     * @param string $class
83
     * @return RecordSchema
84
     * @throws SchemaException
85
     * @throws RecordSchemaException
86
     */
87
    public function record($class)
88
    {
89
        if ($class == RecordEntity::class || $class == Record::class) {
90
            //No need to remember schema for abstract Document
91
            return new RecordSchema($this, RecordEntity::class);
92
        }
93
94
        if (!isset($this->records[$class])) {
95
            throw new SchemaException("Unknown record class '{$class}'.");
96
        }
97
98
        return $this->records[$class];
99
    }
100
101
    /**
102
     * @return RecordSchema[]
103
     */
104
    public function getRecords()
105
    {
106
        return $this->records;
107
    }
108
109
    /**
110
     * Check if given table was declared by one of record or relation.
111
     *
112
     * @param string $database Table database.
113
     * @param string $table    Table name without prefix.
114
     * @return bool
115
     */
116
    public function hasTable($database, $table)
117
    {
118
        return isset($this->tables[$database . '/' . $table]);
119
    }
120
121
    /**
122
     * Request table schema. Every non empty table schema will be synchronized with it's databases
123
     * when executeSchema() method will be called.
124
     *
125
     * Attention, every declared table will be synced with database if their initiator allows such
126
     * operation.
127
     *
128
     * @param string $database Table database.
129
     * @param string $table    Table name without prefix.
130
     * @return AbstractTable
131
     */
132
    public function declareTable($database, $table)
133
    {
134
        $database = $this->resolveDatabase($database);
135
136
        if (isset($this->tables[$database . '/' . $table])) {
137
            return $this->tables[$database . '/' . $table];
138
        }
139
140
        $schema = $this->orm->database($database)->table($table)->schema();
141
142
        return $this->tables[$database . '/' . $table] = $schema;
143
    }
144
145
    /**
146
     * Perform schema reflection to database(s). All declared tables will created or altered. Only
147
     * tables linked to non abstract records and record with active schema parameter will be
148
     * executed.
149
     *
150
     * SchemaBuilder will not allow (SchemaException) to create or alter tables columns declared
151
     * by abstract or records with ACTIVE_SCHEMA constant set to false. ActiveSchema still can
152
     * declare foreign keys and indexes (most of relations automatically request index or foreign
153
     * key), but they are going to be ignored.
154
     *
155
     * Due principals of database schemas and ORM component logic no data or columns will ever be
156
     * removed from database. In addition column renaming will cause creation of another column.
157
     *
158
     * Use database migrations to solve more complex database questions. Or disable ACTIVE_SCHEMA
159
     * and live like normal people.
160
     *
161
     * @throws SchemaException
162
     * @throws \Spiral\Database\Exceptions\SchemaException
163
     * @throws \Spiral\Database\Exceptions\QueryException
164
     * @throws \Spiral\Database\Exceptions\DriverException
165
     */
166
    public function synchronizeSchema()
167
    {
168
        $bus = new SynchronizationBus($this->getTables());
169
        $bus->syncronize();
170
    }
171
172
    /**
173
     * Resolve real database name using it's alias.
174
     *
175
     * @see DatabaseProvider
176
     * @param string|null $alias
177
     * @return string
178
     */
179
    public function resolveDatabase($alias)
180
    {
181
        return $this->orm->database($alias)->getName();
182
    }
183
184
    /**
185
     * Get all mutators associated with field type.
186
     *
187
     * @param string $type Field type.
188
     * @return array
189
     */
190
    public function getMutators($type)
191
    {
192
        return $this->config->getMutators($type);
193
    }
194
195
    /**
196
     * Get mutator alias if presented. Aliases used to simplify schema (accessors) definition.
197
     *
198
     * @param string $alias
199
     * @return string|array
200
     */
201
    public function mutatorAlias($alias)
202
    {
203
        return $this->config->mutatorAlias($alias);
204
    }
205
206
    /**
207
     * Normalize record schema in lighter structure to be saved in ORM component memory.
208
     *
209
     * @return array
210
     * @throws SchemaException
211
     */
212
    public function normalizeSchema()
213
    {
214
        $result = [];
215
        foreach ($this->records as $record) {
216
            if ($record->isAbstract()) {
217
                continue;
218
            }
219
220
            $schema = [
221
                ORM::M_ROLE_NAME   => $record->getRole(),
222
                ORM::M_SOURCE      => $record->getSource(),
223
                ORM::M_TABLE       => $record->getTable(),
224
                ORM::M_DB          => $record->getDatabase(),
225
                ORM::M_PRIMARY_KEY => $record->getPrimaryKey(),
226
                ORM::M_HIDDEN      => $record->getHidden(),
227
                ORM::M_SECURED     => $record->getSecured(),
228
                ORM::M_FILLABLE    => $record->getFillable(),
229
                ORM::M_COLUMNS     => $record->getDefaults(),
230
                ORM::M_NULLABLE    => $record->getNullable(),
231
                ORM::M_MUTATORS    => $record->getMutators(),
232
                ORM::M_VALIDATES   => $record->getValidates(),
233
                ORM::M_RELATIONS   => $this->packRelations($record)
234
            ];
235
236
            ksort($schema);
237
            $result[$record->getName()] = $schema;
238
        }
239
240
        return $result;
241
    }
242
243
    /**
244
     * Create appropriate instance of RelationSchema based on it's definition provided by ORM Record
245
     * or manually. Due internal format first definition key will be stated as definition type and
246
     * key value as record/entity definition relates too.
247
     *
248
     * @param RecordSchema $record
249
     * @param string       $name
250
     * @param array        $definition
251
     * @return RelationInterface
252
     * @throws SchemaException
253
     */
254
    public function relationSchema(RecordSchema $record, $name, array $definition)
255
    {
256
        if (empty($definition)) {
257
            throw new SchemaException("Relation definition can not be empty.");
258
        }
259
260
        reset($definition);
261
262
        //Relation type must be provided as first in definition
263
        $type = key($definition);
264
265
        //We are letting ORM to resolve relation schema using container
266
        $relation = $this->orm->relationSchema($type, $this, $record, $name, $definition);
267
268
        if ($relation->hasEquivalent()) {
269
            //Some relations may declare equivalent relation to be used instead,
270
            //used for Morphed relations
271
            return $relation->createEquivalent();
272
        }
273
274
        return $relation;
275
    }
276
277
    /**
278
     * Get list of tables to be updated, method must automatically check if table actually allowed
279
     * to be updated.
280
     *
281
     * @return AbstractTable[]
282
     */
283
    public function getTables()
284
    {
285
        $tables = [];
286
        foreach ($this->tables as $table) {
287
            //We can only alter table columns if record allows us
288
            $record = $this->findRecord($table);
289
290
            if (empty($record)) {
291
                $tables[] = $table;
292
293
                //Potentially pivot table, no related records
294
                continue;
295
            }
296
297
            if ($record->isAbstract() || !$record->isActive() || empty($table->getColumns())) {
298
                //Abstract tables might declare table schema, but we are going to ignore it
299
                continue;
300
            }
301
302
            $tables[] = $table;
303
        }
304
305
        return $tables;
306
    }
307
308
    /**
309
     * Locate every available Record class.
310
     *
311
     * @param ClassLocatorInterface $locator
312
     * @return $this
313
     * @throws SchemaException
314
     */
315
    protected function locateRecords(ClassLocatorInterface $locator)
316
    {
317
        //Table names associated with records
318
        $tables = [];
319
        foreach ($locator->getClasses(RecordEntity::class) as $class => $definition) {
320
            if ($class == RecordEntity::class || $class == Record::class) {
321
                continue;
322
            }
323
324
            $this->records[$class] = $record = new RecordSchema($this, $class);
325
326
            if (!$record->isAbstract()) {
327
                //See comment near exception
328
                continue;
329
            }
330
331
            //Record associated tableID (includes resolved database name)
332
            $tableID = $record->getTableID();
333
334
            if (isset($tables[$tableID])) {
335
                //We are not allowing multiple records talk to same database, unless they one of them
336
                //is abstract
337
                throw new SchemaException(
338
                    "Record '{$record}' associated with "
339
                    . "same source table '{$tableID}' as '{$tables[$tableID]}'."
340
                );
341
            }
342
343
            $tables[$tableID] = $record;
344
        }
345
346
        return $this;
347
    }
348
349
    /**
350
     * Locate ORM entities sources.
351
     *
352
     * @param ClassLocatorInterface $locator
353
     * @return $this
354
     */
355 View Code Duplication
    protected function locateSources(ClassLocatorInterface $locator)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
356
    {
357
        foreach ($locator->getClasses(RecordSource::class) as $class => $definition) {
358
            $reflection = new \ReflectionClass($class);
359
360
            if ($reflection->isAbstract() || empty($record = $reflection->getConstant('RECORD'))) {
361
                continue;
362
            }
363
364
            if ($this->hasRecord($record)) {
365
                //Associating source with record
366
                $this->record($record)->setSource($class);
367
            }
368
        }
369
370
        return $this;
371
    }
372
373
    /**
374
     * SchemaBuilder will request every located RecordSchema to declare it's relations. In addition
375
     * this methods will create inversed set of relations.
376
     *
377
     * @throws SchemaException
378
     * @throws RelationSchemaException
379
     * @throws RecordSchemaException
380
     */
381
    protected function castRelations()
382
    {
383
        $inversedRelations = [];
384
        foreach ($this->records as $record) {
385
            if ($record->isAbstract()) {
386
                //Abstract records can not declare relations or tables
387
                continue;
388
            }
389
390
            $record->castRelations();
391
392
            foreach ($record->getRelations() as $relation) {
393
                if ($relation->isInversable()) {
394
                    //Relation can be automatically inversed
395
                    $inversedRelations[] = $relation;
396
                }
397
            }
398
        }
399
400
        /**
401
         * We have to perform inversion after every generic relation was defined. Sometimes records
402
         * can define inversed relation by themselves.
403
         *
404
         * @var RelationInterface $relation
405
         */
406
        foreach ($inversedRelations as $relation) {
407
            if ($relation->isInversable()) {
408
                //We have to check inversion again in case if relation name already taken
409
                $relation->inverseRelation();
410
            }
411
        }
412
    }
413
414
    /**
415
     * Find record related to given table. This operation is required to catch if some
416
     * relation/schema declared values in passive (no altering) table. Might return if no records
417
     * find (pivot or user specified tables).
418
     *
419
     * @param AbstractTable $table
420
     * @return RecordSchema|null
421
     */
422
    private function findRecord(AbstractTable $table)
423
    {
424
        foreach ($this->getRecords() as $record) {
425
            if ($record->tableSchema() === $table) {
426
                return $record;
427
            }
428
        }
429
430
        //No associated record were found
431
        return null;
432
    }
433
434
    /**
435
     * Normalize and pack every declared record relation schema.
436
     *
437
     * @param RecordSchema $record
438
     * @return array
439
     */
440
    private function packRelations(RecordSchema $record)
441
    {
442
        $result = [];
443
        foreach ($record->getRelations() as $name => $relation) {
444
            $result[$name] = $relation->normalizeSchema();
445
        }
446
447
        return $result;
448
    }
449
}