Completed
Push — master ( de4ae5...473414 )
by Anton
04:23
created

SchemaBuilder::databaseAlias()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 10
Code Lines 5

Duplication

Lines 10
Ratio 100 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 10
loc 10
rs 9.4285
cc 3
eloc 5
nc 3
nop 1
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
     * Associations between tables and their database and original table aliases.
42
     *
43
     * @var array
44
     */
45
    private $aliases = [];
46
47
    /**
48
     * @var ORMConfig
49
     */
50
    protected $config = null;
51
52
    /**
53
     * @invisible
54
     * @var ORM
55
     */
56
    protected $orm = null;
57
58
    /**
59
     * @param ORMConfig             $config
60
     * @param ORM                   $orm
61
     * @param ClassLocatorInterface $locator
62
     */
63
    public function __construct(ORMConfig $config, ORM $orm, ClassLocatorInterface $locator)
64
    {
65
        $this->config = $config;
66
        $this->orm = $orm;
67
68
        //Locating all models and sources
69
        $this->locateRecords($locator)->locateSources($locator);
70
71
        //Casting relations
72
        $this->castSchemas();
73
    }
74
75
    /**
76
     * Check if Record class known to schema builder.
77
     *
78
     * @param string $class
79
     * @return bool
80
     */
81
    public function hasRecord($class)
82
    {
83
        return isset($this->records[$class]);
84
    }
85
86
    /**
87
     * Instance of RecordSchema associated with given class name.
88
     *
89
     * @param string $class
90
     * @return RecordSchema
91
     * @throws SchemaException
92
     * @throws RecordSchemaException
93
     */
94
    public function record($class)
95
    {
96
        if ($class == RecordEntity::class || $class == Record::class) {
97
            //No need to remember schema for abstract Document
98
            return new RecordSchema($this, RecordEntity::class);
99
        }
100
101
        if (!isset($this->records[$class])) {
102
            throw new SchemaException("Unknown record class '{$class}'.");
103
        }
104
105
        return $this->records[$class];
106
    }
107
108
    /**
109
     * @return RecordSchema[]
110
     */
111
    public function getRecords()
112
    {
113
        return $this->records;
114
    }
115
116
    /**
117
     * Check if given table was declared by one of record or relation.
118
     *
119
     * @param string $database Table database.
120
     * @param string $table    Table name without prefix.
121
     * @return bool
122
     */
123
    public function hasTable($database, $table)
124
    {
125
        return isset($this->tables[$database . '/' . $table]);
126
    }
127
128
    /**
129
     * Request table schema. Every non empty table schema will be synchronized with it's databases
130
     * when executeSchema() method will be called.
131
     *
132
     * Attention, every declared table will be synced with database if their initiator allows such
133
     * operation.
134
     *
135
     * @param string $database Table database.
136
     * @param string $table    Table name without prefix.
137
     * @return AbstractTable
138
     */
139
    public function declareTable($database, $table)
140
    {
141
        $normalizedDatabase = $this->resolveDatabase($database);
142
143
        if (isset($this->tables[$normalizedDatabase . '/' . $table])) {
144
            return $this->tables[$normalizedDatabase . '/' . $table];
145
        }
146
147
        $schema = $this->orm->database($normalizedDatabase)->table($table)->schema();
148
149
        $this->aliases[] = [
150
            'schema'   => $schema,
151
            'database' => $database,
152
            'table'    => $table
153
        ];
154
155
        return $this->tables[$normalizedDatabase . '/' . $table] = $schema;
156
    }
157
158
    /**
159
     * Database aliases associated with given table schema.
160
     *
161
     * @param AbstractTable $table
162
     * @return string
163
     * @throws SchemaException
164
     */
165 View Code Duplication
    public function databaseAlias(AbstractTable $table)
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...
166
    {
167
        foreach ($this->aliases as $item) {
168
            if ($item['schema'] === $table) {
169
                return $item['database'];
170
            }
171
        }
172
173
        throw new SchemaException("Unable to resolve database alias for table '{$table->getName()}'");
174
    }
175
176
    /**
177
     * Table aliases associated with given table schema.
178
     *
179
     * @param AbstractTable $table
180
     * @return string
181
     * @throws SchemaException
182
     */
183 View Code Duplication
    public function tableAlias(AbstractTable $table)
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...
184
    {
185
        foreach ($this->aliases as $item) {
186
            if ($item['schema'] === $table) {
187
                return $item['table'];
188
            }
189
        }
190
191
        throw new SchemaException("Unable to resolve table alias for table '{$table->getName()}'");
192
    }
193
194
    /**
195
     * Perform schema reflection to database(s). All declared tables will created or altered. Only
196
     * tables linked to non abstract records and record with active schema parameter will be
197
     * executed.
198
     *
199
     * SchemaBuilder will not allow (SchemaException) to create or alter tables columns declared
200
     * by abstract or records with ACTIVE_SCHEMA constant set to false. ActiveSchema still can
201
     * declare foreign keys and indexes (most of relations automatically request index or foreign
202
     * key), but they are going to be ignored.
203
     *
204
     * Due principals of database schemas and ORM component logic no data or columns will ever be
205
     * removed from database. In addition column renaming will cause creation of another column.
206
     *
207
     * Use database migrations to solve more complex database questions. Or disable ACTIVE_SCHEMA
208
     * and live like normal people.
209
     *
210
     * @throws SchemaException
211
     * @throws \Spiral\Database\Exceptions\SchemaException
212
     * @throws \Spiral\Database\Exceptions\QueryException
213
     * @throws \Spiral\Database\Exceptions\DriverException
214
     */
215
    public function synchronizeSchema()
216
    {
217
        //As aternative you can get access to TableSchemas and generate needed migrations
218
        //@todo Phinx exporter module is needed
219
        $bus = new SynchronizationBus($this->getTables());
220
        $bus->syncronize();
221
    }
222
223
    /**
224
     * Resolve real database name using it's alias.
225
     *
226
     * @see DatabaseProvider
227
     * @param string|null $alias
228
     * @return string
229
     */
230
    public function resolveDatabase($alias)
231
    {
232
        return $this->orm->database($alias)->getName();
233
    }
234
235
    /**
236
     * Get all mutators associated with field type.
237
     *
238
     * @param string $type Field type.
239
     * @return array
240
     */
241
    public function getMutators($type)
242
    {
243
        return $this->config->getMutators($type);
244
    }
245
246
    /**
247
     * Get mutator alias if presented. Aliases used to simplify schema (accessors) definition.
248
     *
249
     * @param string $alias
250
     * @return string|array
251
     */
252
    public function mutatorAlias($alias)
253
    {
254
        return $this->config->mutatorAlias($alias);
255
    }
256
257
    /**
258
     * Normalize record schema in lighter structure to be saved in ORM component memory.
259
     *
260
     * @return array
261
     * @throws SchemaException
262
     */
263
    public function normalizeSchema()
264
    {
265
        $result = [];
266
        foreach ($this->records as $record) {
267
            if ($record->isAbstract()) {
268
                continue;
269
            }
270
271
            $schema = [
272
                ORM::M_ROLE_NAME   => $record->getRole(),
273
                ORM::M_SOURCE      => $record->getSource(),
274
                ORM::M_TABLE       => $record->getTable(),
275
                ORM::M_DB          => $record->getDatabase(),
276
                ORM::M_PRIMARY_KEY => $record->getPrimaryKey(),
277
                ORM::M_HIDDEN      => $record->getHidden(),
278
                ORM::M_SECURED     => $record->getSecured(),
279
                ORM::M_FILLABLE    => $record->getFillable(),
280
                ORM::M_COLUMNS     => $record->getDefaults(),
281
                ORM::M_NULLABLE    => $record->getNullable(),
282
                ORM::M_MUTATORS    => $record->getMutators(),
283
                ORM::M_VALIDATES   => $record->getValidates(),
284
                ORM::M_RELATIONS   => $this->packRelations($record)
285
            ];
286
287
            ksort($schema);
288
            $result[$record->getName()] = $schema;
289
        }
290
291
        return $result;
292
    }
293
294
    /**
295
     * Create appropriate instance of RelationSchema based on it's definition provided by ORM Record
296
     * or manually. Due internal format first definition key will be stated as definition type and
297
     * key value as record/entity definition relates too.
298
     *
299
     * @param RecordSchema $record
300
     * @param string       $name
301
     * @param array        $definition
302
     * @return RelationInterface
303
     * @throws SchemaException
304
     */
305
    public function relationSchema(RecordSchema $record, $name, array $definition)
306
    {
307
        if (empty($definition)) {
308
            throw new SchemaException("Relation definition can not be empty.");
309
        }
310
311
        reset($definition);
312
313
        //Relation type must be provided as first in definition
314
        $type = key($definition);
315
316
        //We are letting ORM to resolve relation schema using container
317
        $relation = $this->orm->relationSchema($type, $this, $record, $name, $definition);
318
319
        if ($relation->hasEquivalent()) {
320
            //Some relations may declare equivalent relation to be used instead,
321
            //used for Morphed relations
322
            return $relation->createEquivalent();
323
        }
324
325
        return $relation;
326
    }
327
328
    /**
329
     * Get list of tables to be updated, method must automatically check if table actually allowed
330
     * to be updated.
331
     *
332
     * @return AbstractTable[]
333
     */
334
    public function getTables()
335
    {
336
        $tables = [];
337
        foreach ($this->tables as $table) {
338
            //We can only alter table columns if record allows us
339
            $record = $this->findRecord($table);
340
341
            if (empty($record)) {
342
                $tables[] = $table;
343
344
                //Potentially pivot table, no related records
345
                continue;
346
            }
347
348
            if ($record->isAbstract() || !$record->isActive() || empty($table->getColumns())) {
349
                //Abstract tables might declare table schema, but we are going to ignore it
350
                continue;
351
            }
352
353
            $tables[] = $table;
354
        }
355
356
        return $tables;
357
    }
358
359
    /**
360
     * Locate every available Record class.
361
     *
362
     * @param ClassLocatorInterface $locator
363
     * @return $this
364
     * @throws SchemaException
365
     */
366
    protected function locateRecords(ClassLocatorInterface $locator)
367
    {
368
        //Table names associated with records
369
        $tables = [];
370
        foreach ($locator->getClasses(RecordEntity::class) as $class => $definition) {
371
            if ($class == RecordEntity::class || $class == Record::class) {
372
                continue;
373
            }
374
375
            $this->records[$class] = $record = new RecordSchema($this, $class);
376
377
            if (!$record->isAbstract()) {
378
                //See comment near exception
379
                continue;
380
            }
381
382
            //Record associated tableID (includes resolved database name)
383
            $tableID = $record->getTableID();
384
385
            if (isset($tables[$tableID])) {
386
                //We are not allowing multiple records talk to same database, unless they one of them
387
                //is abstract
388
                throw new SchemaException(
389
                    "Record '{$record}' associated with "
390
                    . "same source table '{$tableID}' as '{$tables[$tableID]}'."
391
                );
392
            }
393
394
            $tables[$tableID] = $record;
395
        }
396
397
        return $this;
398
    }
399
400
    /**
401
     * Locate ORM entities sources.
402
     *
403
     * @param ClassLocatorInterface $locator
404
     * @return $this
405
     */
406 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...
407
    {
408
        foreach ($locator->getClasses(RecordSource::class) as $class => $definition) {
409
            $reflection = new \ReflectionClass($class);
410
411
            if ($reflection->isAbstract() || empty($record = $reflection->getConstant('RECORD'))) {
412
                continue;
413
            }
414
415
            if ($this->hasRecord($record)) {
416
                //Associating source with record
417
                $this->record($record)->setSource($class);
418
            }
419
        }
420
421
        return $this;
422
    }
423
424
    /**
425
     * SchemaBuilder will request every located RecordSchema to declare it's schemas and relations.
426
     * In addition this methods will create inversed set of relations.
427
     *
428
     * @throws SchemaException
429
     * @throws RelationSchemaException
430
     * @throws RecordSchemaException
431
     */
432
    protected function castSchemas()
433
    {
434
        foreach ($this->records as $record) {
435
            $record->castSchema();
436
        }
437
438
        $inversedRelations = [];
439
        foreach ($this->records as $record) {
440
            if ($record->isAbstract()) {
441
                //Abstract records can not declare relations or tables
442
                continue;
443
            }
444
445
            $record->castRelations();
446
447
            foreach ($record->getRelations() as $relation) {
448
                if ($relation->isInversable()) {
449
                    //Relation can be automatically inversed
450
                    $inversedRelations[] = $relation;
451
                }
452
            }
453
        }
454
455
        /**
456
         * We have to perform inversion after every generic relation was defined. Sometimes records
457
         * can define inversed relation by themselves.
458
         *
459
         * @var RelationInterface $relation
460
         */
461
        foreach ($inversedRelations as $relation) {
462
            if ($relation->isInversable()) {
463
                //We have to check inversion again in case if relation name already taken
464
                $relation->inverseRelation();
465
            }
466
        }
467
    }
468
469
    /**
470
     * Find record related to given table. This operation is required to catch if some
471
     * relation/schema declared values in passive (no altering) table. Might return if no records
472
     * find (pivot or user specified tables).
473
     *
474
     * @param AbstractTable $table
475
     * @return RecordSchema|null
476
     */
477
    private function findRecord(AbstractTable $table)
478
    {
479
        foreach ($this->getRecords() as $record) {
480
            if ($record->tableSchema() === $table) {
481
                return $record;
482
            }
483
        }
484
485
        //No associated record were found
486
        return null;
487
    }
488
489
    /**
490
     * Normalize and pack every declared record relation schema.
491
     *
492
     * @param RecordSchema $record
493
     * @return array
494
     */
495
    private function packRelations(RecordSchema $record)
496
    {
497
        $result = [];
498
        foreach ($record->getRelations() as $name => $relation) {
499
            $result[$name] = $relation->normalizeSchema();
500
        }
501
502
        return $result;
503
    }
504
}
505