Completed
Branch feature/pre-split (ce4b6b)
by Anton
03:56
created

SchemaBuilder::getSchemas()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 2
nc 1
nop 0
dl 0
loc 4
rs 10
c 0
b 0
f 0
1
<?php
2
/**
3
 * components
4
 *
5
 * @author    Wolfy-J
6
 */
7
namespace Spiral\ORM\Schemas;
8
9
use Psr\Log\LoggerInterface;
10
use Spiral\Database\DatabaseManager;
11
use Spiral\Database\Exceptions\DBALException;
12
use Spiral\Database\Exceptions\DriverException;
13
use Spiral\Database\Exceptions\QueryException;
14
use Spiral\Database\Helpers\SynchronizationPool;
15
use Spiral\Database\Schemas\Prototypes\AbstractTable;
16
use Spiral\ORM\Exceptions\DefinitionException;
17
use Spiral\ORM\Exceptions\DoubleReferenceException;
18
use Spiral\ORM\Exceptions\SchemaException;
19
use Spiral\ORM\ORMInterface;
20
use Spiral\ORM\Schemas\Definitions\RelationContext;
21
22
class SchemaBuilder
23
{
24
    /**
25
     * @var DatabaseManager
26
     */
27
    private $manager;
28
29
    /**
30
     * @var RelationManager
31
     */
32
    private $relations;
33
34
    /**
35
     * @var AbstractTable[]
36
     */
37
    private $tables = [];
38
39
    /**
40
     * @var SchemaInterface[]
41
     */
42
    private $schemas = [];
43
44
    /**
45
     * Class names of sources associated with specific class.
46
     *
47
     * @var array
48
     */
49
    private $sources = [];
50
51
    /**
52
     * @param DatabaseManager $manager
53
     * @param RelationManager $relations
54
     */
55
    public function __construct(DatabaseManager $manager, RelationManager $relations)
56
    {
57
        $this->manager = $manager;
58
        $this->relations = $relations;
59
    }
60
61
    /**
62
     * Add new model schema into pool.
63
     *
64
     * @param SchemaInterface $schema
65
     *
66
     * @return self|$this
67
     */
68
    public function addSchema(SchemaInterface $schema): SchemaBuilder
69
    {
70
        $this->schemas[$schema->getClass()] = $schema;
71
72
        return $this;
73
    }
74
75
    /**
76
     * @param string $class
77
     *
78
     * @return bool
79
     */
80
    public function hasSchema(string $class): bool
81
    {
82
        return isset($this->schemas[$class]);
83
    }
84
85
    /**
86
     * @param string $class
87
     *
88
     * @return SchemaInterface
89
     *
90
     * @throws SchemaException
91
     */
92
    public function getSchema(string $class): SchemaInterface
93
    {
94
        if (!$this->hasSchema($class)) {
95
            throw new SchemaException("Unable to find schema for class '{$class}'");
96
        }
97
98
        return $this->schemas[$class];
99
    }
100
101
    /**
102
     * All available document schemas.
103
     *
104
     * @return SchemaInterface[]
105
     */
106
    public function getSchemas(): array
107
    {
108
        return $this->schemas;
109
    }
110
111
    /**
112
     * Associate source class with entity class. Source will be automatically associated with given
113
     * class and all classes from the same collection which extends it.
114
     *
115
     * @param string $class
116
     * @param string $source
117
     *
118
     * @return SchemaBuilder
119
     *
120
     * @throws SchemaException
121
     */
122
    public function addSource(string $class, string $source): SchemaBuilder
123
    {
124
        if (!$this->hasSchema($class)) {
125
            throw new SchemaException("Unable to add source to '{$class}', class is unknown to ORM");
126
        }
127
128
        $this->sources[$class] = $source;
129
130
        return $this;
131
    }
132
133
    /**
134
     * Check if given entity has associated source.
135
     *
136
     * @param string $class
137
     *
138
     * @return bool
139
     */
140
    public function hasSource(string $class): bool
141
    {
142
        return array_key_exists($class, $this->sources);
143
    }
144
145
    /**
146
     * Get source associated with specific class, if any.
147
     *
148
     * @param string $class
149
     *
150
     * @return string|null
151
     */
152
    public function getSource(string $class)
153
    {
154
        if (!$this->hasSource($class)) {
155
            return null;
156
        }
157
158
        return $this->sources[$class];
159
    }
160
161
    /**
162
     * Process all added schemas and relations in order to created needed tables, indexes and etc.
163
     * Attention, this method will return new instance of SchemaBuilder without affecting original
164
     * object. You MUST call this method before calling packSchema() method.
165
     *
166
     * Attention, this methods DOES NOT write anything into database, use pushSchema() to push
167
     * changes into database using automatic diff generation. You can also access list of
168
     * generated/changed tables via getTables() to create your own migrations.
169
     *
170
     * @see packSchema()
171
     * @see pushSchema()
172
     * @see getTables()
173
     *
174
     * @return SchemaBuilder
175
     *
176
     * @throws SchemaException
177
     */
178
    public function renderSchema(): SchemaBuilder
179
    {
180
        //Declaring tables associated with records
181
        $this->renderModels();
182
183
        //Defining all relations declared by our schemas
184
        $this->renderRelations();
185
186
        //Inverse relations (if requested)
187
        $this->relations->inverseRelations();
188
189
        //Rendering needed columns, FKs and indexes needed for our relations (if relation is ORM specific)
190
        foreach ($this->relations->declareTables($this) as $table) {
191
            $this->setTable($table);
192
        }
193
194
        return $this;
195
    }
196
197
    /**
198
     * Request table schema by name/database combination. Attention, you can only save table by
199
     * pushing it back using pushTable() method.
200
     *
201
     * @see setTable()
202
     *
203
     * @param string      $table
204
     * @param string|null $database
205
     * @param bool        $resetState When set to true current table state will be reset in order
206
     *                                to allow model to redefine it's schema.
207
     * @param bool        $unique     Set to true (default), to throw an exception when table
208
     *                                already referenced by another model.
209
     *
210
     * @return AbstractTable          Unlinked.
211
     *
212
     * @throws DoubleReferenceException When two records refers to same table and unique option
213
     *                                  set.
214
     */
215
    public function requestTable(
216
        string $table,
217
        string $database = null,
218
        bool $unique = false,
219
        bool $resetState = false
220
    ): AbstractTable {
221
        if (isset($this->tables[$database . '.' . $table])) {
222
            $schema = $this->tables[$database . '.' . $table];
223
224
            if ($unique) {
225
                throw new DoubleReferenceException(
226
                    "Table '{$table}' of '{$database} 'been requested by multiple models"
227
                );
228
            }
229
        } else {
230
            //Requesting thought DatabaseManager
231
            $schema = $this->manager->database($database)->table($table)->getSchema();
232
            $this->tables[$database . '.' . $table] = $schema;
233
        }
234
235
        $schema = clone $schema;
236
237
        if ($resetState) {
238
            //Emptying our current state (initial not affected)
239
            $schema->setState(null);
240
        }
241
242
        return $schema;
243
    }
244
245
    /**
246
     * Get all defined tables, make sure to call renderSchema() first. Attention, all given tables
247
     * will be returned in detached state.
248
     *
249
     * @return AbstractTable[]
250
     *
251
     * @throws SchemaException
252
     */
253
    public function getTables(): array
254
    {
255
        if (empty($this->tables) && !empty($this->schemas)) {
256
            throw new SchemaException(
257
                "Unable to get tables, no tables are were found, call renderSchema() first"
258
            );
259
        }
260
261
        $result = [];
262
        foreach ($this->tables as $table) {
263
            //Detaching
264
            $result[] = clone $table;
265
        }
266
267
        return $result;
268
    }
269
270
    /**
271
     * Indication that tables in database require syncing before being matched with ORM models.
272
     *
273
     * @return bool
274
     */
275
    public function hasChanges(): bool
276
    {
277
        foreach ($this->getTables() as $table) {
278
            if ($table->getComparator()->hasChanges()) {
279
                return true;
280
            }
281
        }
282
283
        return false;
284
    }
285
286
    /**
287
     * Save every change made to generated tables. Method utilizes default DBAL diff mechanism,
288
     * use getTables() method in order to generate your own migrations.
289
     *
290
     * @param LoggerInterface|null $logger
291
     *
292
     * @throws SchemaException
293
     * @throws DBALException
294
     * @throws QueryException
295
     * @throws DriverException
296
     */
297
    public function pushSchema(LoggerInterface $logger = null)
298
    {
299
        $bus = new SynchronizationPool($this->getTables());
300
        $bus->run($logger);
301
    }
302
303
    /**
304
     * Pack declared schemas in a normalized form, make sure to call renderSchema() first.
305
     *
306
     * @return array
307
     *
308
     * @throws SchemaException
309
     */
310
    public function packSchema(): array
311
    {
312
        if (empty($this->tables) && !empty($this->schemas)) {
313
            throw new SchemaException(
314
                "Unable to pack schema, no defined tables were found, call renderSchema() first"
315
            );
316
        }
317
318
        $result = [];
319
        foreach ($this->schemas as $class => $schema) {
320
            //Table which is being related to model schema
321
            $table = $this->requestTable($schema->getTable(), $schema->getDatabase(), false);
322
323
            $result[$class] = [
324
                ORMInterface::R_INSTANTIATOR => $schema->getInstantiator(),
325
326
                ORMInterface::R_ROLE_NAME => $schema->getRole(),
327
328
                //Schema includes list of fields, default values and nullable fields
329
                ORMInterface::R_SCHEMA    => $schema->packSchema($this, clone $table),
330
331
                ORMInterface::R_SOURCE_CLASS => $this->getSource($class),
332
333
                //Data location
334
                ORMInterface::R_DATABASE     => $schema->getDatabase(),
335
                ORMInterface::R_TABLE        => $schema->getTable(),
336
337
                //Pack model specific relations
338
                ORMInterface::R_RELATIONS    => $this->relations->packRelations($schema->getClass())
339
            ];
340
        }
341
342
        return $result;
343
    }
344
345
    /**
346
     * Walk thought schemas and define structure of associated tables.
347
     *
348
     * @throws SchemaException
349
     * @throws DBALException
350
     */
351
    protected function renderModels()
352
    {
353
        foreach ($this->schemas as $schema) {
354
            /**
355
             * Attention, this method will request table schema from DBAL and will empty it's state
356
             * so schema can define it from ground zero!
357
             */
358
            $table = $this->requestTable(
359
                $schema->getTable(),
360
                $schema->getDatabase(),
361
                true,
362
                true
363
            );
364
365
            //Render table schema
366
            $table = $schema->declareTable($table);
367
368
            //Working with indexes
369
            foreach ($schema->getIndexes() as $index) {
370
                $table->index($index->getColumns())->unique($index->isUnique());
371
                $table->index($index->getColumns())->setName($index->getName());
372
            }
373
374
            //And put it back :)
375
            $this->setTable($table, $schema->getDatabase());
376
        }
377
    }
378
379
    /**
380
     * Walk thought all record schemas, fetch and declare needed relations using relation manager.
381
     *
382
     * @throws SchemaException
383
     * @throws DefinitionException
384
     */
385
    protected function renderRelations()
386
    {
387
        foreach ($this->schemas as $schema) {
388
            foreach ($schema->getRelations() as $name => $relation) {
389
390
                //Source context defines where relation comes from
391
                $sourceContext = RelationContext::createContent(
392
                    $schema,
393
                    $this->requestTable($schema->getTable(), $schema->getDatabase())
394
                );
395
396
                //Target context might only exist if relation points to another record in ORM,
397
                //in some cases it might point outside of ORM scope
398
                $targetContext = null;
399
400
                if ($this->hasSchema($relation->getTarget())) {
401
                    $target = $this->getSchema($relation->getTarget());
402
403
                    $targetContext = RelationContext::createContent(
404
                        $target,
405
                        $this->requestTable($target->getTable(), $target->getDatabase())
406
                    );
407
                }
408
409
                $this->relations->registerRelation(
410
                    $relation->withContext($sourceContext, $targetContext)
411
                );
412
            }
413
        }
414
    }
415
416
    /**
417
     * Update table state.
418
     *
419
     * @param AbstractTable $schema
420
     * @param string|null   $database
421
     *
422
     * @throws SchemaException
423
     */
424
    protected function setTable(AbstractTable $schema, string $database = null)
425
    {
426
        //We have to make sure that local table name is used
427
        $table = substr($schema->getName(), strlen($schema->getPrefix()));
428
429
        if (empty($this->tables[$database . '.' . $table])) {
430
            throw new SchemaException("AbstractTable must be requested before pushing back");
431
        }
432
433
        $this->tables[$database . '.' . $table] = $schema;
434
    }
435
}