Completed
Branch feature/pre-split (5afa53)
by Anton
13:51
created

AbstractTable::createIndex()

Size

Total Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
nc 1
dl 0
loc 1
c 0
b 0
f 0
1
<?php
2
/**
3
 * components
4
 *
5
 * @author    Wolfy-J
6
 */
7
namespace Spiral\Database\Schemas\Prototypes;
8
9
use Interop\Container\ContainerInterface;
10
use Psr\Log\LoggerAwareInterface;
11
use Spiral\Core\Component;
12
use Spiral\Database\Entities\Driver;
13
use Spiral\Database\Exceptions\SchemaException;
14
use Spiral\Database\Schemas\ColumnInterface;
15
use Spiral\Database\Schemas\IndexInterface;
16
use Spiral\Database\Schemas\ReferenceInterface;
17
use Spiral\Database\Schemas\StateComparator;
18
use Spiral\Database\Schemas\TableInterface;
19
use Spiral\Database\Schemas\TableState;
20
use Spiral\Debug\Traits\LoggerTrait;
21
22
/**
23
 * AbstractTable class used to describe and manage state of specified table. It provides ability to
24
 * get table introspection, update table schema and automatically generate set of diff operations.
25
 *
26
 * Most of table operation like column, index or foreign key creation/altering will be applied when
27
 * save() method will be called.
28
 *
29
 * Column configuration shortcuts:
30
 *
31
 * @method AbstractColumn primary($column)
32
 * @method AbstractColumn bigPrimary($column)
33
 * @method AbstractColumn enum($column, array $values)
34
 * @method AbstractColumn string($column, $length = 255)
35
 * @method AbstractColumn decimal($column, $precision, $scale)
36
 * @method AbstractColumn boolean($column)
37
 * @method AbstractColumn integer($column)
38
 * @method AbstractColumn tinyInteger($column)
39
 * @method AbstractColumn bigInteger($column)
40
 * @method AbstractColumn text($column)
41
 * @method AbstractColumn tinyText($column)
42
 * @method AbstractColumn longText($column)
43
 * @method AbstractColumn double($column)
44
 * @method AbstractColumn float($column)
45
 * @method AbstractColumn datetime($column)
46
 * @method AbstractColumn date($column)
47
 * @method AbstractColumn time($column)
48
 * @method AbstractColumn timestamp($column)
49
 * @method AbstractColumn binary($column)
50
 * @method AbstractColumn tinyBinary($column)
51
 * @method AbstractColumn longBinary($column)
52
 */
53
abstract class AbstractTable extends Component implements TableInterface, LoggerAwareInterface
54
{
55
    use LoggerTrait;
56
57
    /**
58
     * Indication that table is exists and current schema is fetched from database.
59
     *
60
     * @var bool
61
     */
62
    private $exists = false;
63
64
    /**
65
     * Database specific tablePrefix. Required for table renames.
66
     *
67
     * @var string
68
     */
69
    private $prefix = '';
70
71
    /**
72
     * @invisible
73
     *
74
     * @var Driver
75
     */
76
    protected $driver = null;
77
78
    /**
79
     * Initial table state.
80
     *
81
     * @invisible
82
     * @var TableState
83
     */
84
    protected $initialState = null;
85
86
    /**
87
     * Currently defined table state.
88
     *
89
     * @invisible
90
     * @var TableState
91
     */
92
    protected $currentState = null;
93
94
    /**
95
     * @param Driver $driver Parent driver.
96
     * @param string $name   Table name, must include table prefix.
97
     * @param string $prefix Database specific table prefix.
98
     */
99
    public function __construct(Driver $driver, string $name, string $prefix)
100
    {
101
        $this->driver = $driver;
102
        $this->prefix = $prefix;
103
104
        //Initializing states
105
        $this->initialState = new TableState($this->prefix . $name);
106
        $this->currentState = new TableState($this->prefix . $name);
107
108
        $this->exists = $this->driver->hasTable($this->getName());
109
110
        if ($this->exists) {
111
            //Initiating table schema
112
            $this->initSchema($this->initialState);
113
        }
114
115
        $this->setState($this->initialState);
116
    }
117
118
    /**
119
     * Get instance of associated driver.
120
     *
121
     * @return Driver
122
     */
123
    public function getDriver(): Driver
124
    {
125
        return $this->driver;
126
    }
127
128
    /**
129
     * @return StateComparator
130
     */
131
    public function getComparator(): StateComparator
132
    {
133
        return new StateComparator($this->initialState, $this->currentState);
134
    }
135
136
    /**
137
     * Check if table schema has been modified since synchronization.
138
     *
139
     * @return bool
140
     */
141
    protected function hasChanges(): bool
142
    {
143
        return $this->getComparator()->hasChanges();
144
    }
145
146
    /**
147
     * {@inheritdoc}
148
     */
149
    public function exists(): bool
150
    {
151
        return $this->exists;
152
    }
153
154
    /**
155
     * Return database specific table prefix.
156
     *
157
     * @return string
158
     */
159
    public function getPrefix(): string
160
    {
161
        return $this->prefix;
162
    }
163
164
    /**
165
     * Sets table name. Use this function in combination with save to rename table.
166
     *
167
     * @param string $name
168
     *
169
     * @return string Prefixed table name.
170
     */
171
    public function setName(string $name): string
172
    {
173
        $this->currentState->setName($this->prefix . $name);
174
175
        return $this->getName();
176
    }
177
178
    /**
179
     * {@inheritdoc}
180
     */
181
    public function getName(): string
182
    {
183
        return $this->currentState->getName();
184
    }
185
186
    /**
187
     * Set table primary keys. Operation can only be applied for newly created tables. Now every
188
     * database might support compound indexes.
189
     *
190
     * @param array $columns
191
     *
192
     * @return self
193
     */
194
    public function setPrimaryKeys(array $columns): AbstractTable
195
    {
196
        //Originally we were forcing an exception when primary key were changed, now we should
197
        //force it when table will be synced
198
199
        //Updating primary keys in current state
200
        $this->currentState->setPrimaryKeys($columns);
201
202
        return $this;
203
    }
204
205
    /**
206
     * {@inheritdoc}
207
     */
208
    public function getPrimaryKeys(): array
209
    {
210
        return $this->currentState->getPrimaryKeys();
211
    }
212
213
    /**
214
     * {@inheritdoc}
215
     */
216
    public function hasColumn(string $name): bool
217
    {
218
        return $this->currentState->hasColumn($name);
219
    }
220
221
    /**
222
     * {@inheritdoc}
223
     *
224
     * @return ColumnInterface[]|AbstractColumn[]
225
     */
226
    public function getColumns(): array
227
    {
228
        return $this->currentState->getColumns();
229
    }
230
231
    /**
232
     * {@inheritdoc}
233
     */
234
    public function hasIndex(array $columns = []): bool
235
    {
236
        return $this->currentState->hasIndex($columns);
237
    }
238
239
    /**
240
     * {@inheritdoc}
241
     *
242
     * @return IndexInterface[]|AbstractIndex[]
243
     */
244
    public function getIndexes(): array
245
    {
246
        return $this->currentState->getIndexes();
247
    }
248
249
    /**
250
     * {@inheritdoc}
251
     */
252
    public function hasForeign(string $column): bool
253
    {
254
        return $this->currentState->hasForeign($column);
255
    }
256
257
    /**
258
     * {@inheritdoc}
259
     *
260
     * @return ReferenceInterface[]|AbstractReference[]
261
     */
262
    public function getForeigns(): array
263
    {
264
        return $this->currentState->getForeigns();
265
    }
266
267
    /**
268
     * {@inheritdoc}
269
     */
270
    public function getDependencies(): array
271
    {
272
        $tables = [];
273
        foreach ($this->currentState->getForeigns() as $foreign) {
274
            $tables[] = $foreign->getForeignTable();
275
        }
276
277
        return $tables;
278
    }
279
280
    /**
281
     * Get/create instance of AbstractColumn associated with current table.
282
     *
283
     * Examples:
284
     * $table->column('name')->string();
285
     *
286
     * @param string $name
287
     *
288
     * @return AbstractColumn
289
     */
290
    public function column(string $name): AbstractColumn
291
    {
292
        if ($this->currentState->hasColumn($name)) {
293
            //Column already exists
294
            return $this->currentState->findColumn($name);
295
        }
296
297
        $column = $this->createColumn($name);
298
        $this->currentState->registerColumn($column);
299
300
        return $column;
301
    }
302
303
    /**
304
     * Shortcut for column() method.
305
     *
306
     * @param string $column
307
     *
308
     * @return AbstractColumn
309
     */
310
    public function __get(string $column)
311
    {
312
        return $this->column($column);
313
    }
314
315
    /**
316
     * Get/create instance of AbstractIndex associated with current table based on list of forming
317
     * column names.
318
     *
319
     * Example:
320
     * $table->index('key');
321
     * $table->index('key', 'key2');
322
     * $table->index(['key', 'key2']);
323
     *
324
     * @param mixed $columns Column name, or array of columns.
325
     *
326
     * @return AbstractIndex
327
     *
328
     * @throws SchemaException
329
     */
330
    public function index($columns): AbstractIndex
331
    {
332
        $columns = is_array($columns) ? $columns : func_get_args();
333
334
        foreach ($columns as $column) {
335
            if (!$this->currentState->hasColumn($column)) {
336
                throw new SchemaException("Undefined column '{$column}' of '{$this->getName()}'");
337
            }
338
        }
339
340
        if ($this->currentState->hasIndex($columns)) {
341
            return $this->currentState->findIndex($columns);
342
        }
343
344
        $index = $this->createIndex($this->createIdentifier('index', $columns));
345
        $index->columns($columns);
346
        $this->currentState->registerIndex($index);
347
348
        return $index;
349
    }
350
351
    /**
352
     * Get/create instance of AbstractReference associated with current table based on local column
353
     * name.
354
     *
355
     * @param string $column
356
     *
357
     * @return AbstractReference
358
     */
359
    public function references(string $column): AbstractReference
360
    {
361
        if (!$this->currentState->hasColumn($column)) {
362
            throw new SchemaException("Undefined column '{$column}' of '{$this->getName()}'");
363
        }
364
365
        if ($this->currentState->hasForeign($column)) {
366
            return $this->currentState->findForeign($column);
367
        }
368
369
        $foreign = $this->createReference($this->createIdentifier('foreign', [$column]));
370
        $foreign->column($column);
371
        $this->currentState->registerReference($foreign);
372
373
        return $foreign;
374
    }
375
376
    //------
377
    //Altering operations
378
    //------
379
380
381
    /**
382
     * Column creation/altering shortcut, call chain is identical to:
383
     * AbstractTable->column($name)->$type($arguments).
384
     *
385
     * Example:
386
     * $table->string("name");
387
     * $table->text("some_column");
388
     *
389
     * @param string $type
390
     * @param array  $arguments Type specific parameters.
391
     *
392
     * @return AbstractColumn
393
     */
394
    public function __call(string $type, array $arguments)
395
    {
396
        return call_user_func_array(
397
            [$this->column($arguments[0]), $type],
398
            array_slice($arguments, 1)
399
        );
400
    }
401
402
    /**
403
     * Reset table state to new form.
404
     *
405
     * @param TableState $state Use null to flush table schema.
406
     *
407
     * @return self|$this
408
     */
409
    public function setState(TableState $state = null): AbstractTable
410
    {
411
        $this->currentState = new TableState($this->initialState->getName());
412
413
        if (!empty($state)) {
414
            $this->currentState->setName($state->getName());
415
            $this->currentState->syncState($state);
0 ignored issues
show
Documentation introduced by
$state is of type object<Spiral\Database\Schemas\TableState>, but the function expects a object<self>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
416
        }
417
418
        return $this;
419
    }
420
421
    /**
422
     * Reset table state to it initial form.
423
     *
424
     * @return self|$this
425
     */
426
    public function resetState(): AbstractTable
427
    {
428
        $this->setState($this->initialState);
429
430
        return $this;
431
    }
432
433
    /**
434
     * @return AbstractColumn|string
435
     */
436
    public function __toString(): string
437
    {
438
        return $this->getName();
439
    }
440
441
    /**
442
     * @return array
443
     */
444
    public function __debugInfo()
445
    {
446
        return [
447
            'name'        => $this->getName(),
448
            'primaryKeys' => $this->getPrimaryKeys(),
449
            'columns'     => array_values($this->getColumns()),
450
            'indexes'     => array_values($this->getIndexes()),
451
            'references'  => array_values($this->getForeigns()),
452
        ];
453
    }
454
455
    /**
456
     * Populate table schema with values from database.
457
     *
458
     * @param TableState $state
459
     */
460
    protected function initSchema(TableState $state)
461
    {
462
        foreach ($this->fetchColumns() as $column) {
463
            $state->registerColumn($column);
0 ignored issues
show
Compatibility introduced by
$column of type object<Spiral\Database\Schemas\ColumnInterface> is not a sub-type of object<Spiral\Database\S...totypes\AbstractColumn>. It seems like you assume a concrete implementation of the interface Spiral\Database\Schemas\ColumnInterface to be always present.

This check looks for parameters that are defined as one type in their type hint or doc comment but seem to be used as a narrower type, i.e an implementation of an interface or a subclass.

Consider changing the type of the parameter or doing an instanceof check before assuming your parameter is of the expected type.

Loading history...
464
        }
465
466
        foreach ($this->fetchIndexes() as $index) {
467
            $state->registerIndex($index);
0 ignored issues
show
Compatibility introduced by
$index of type object<Spiral\Database\Schemas\IndexInterface> is not a sub-type of object<Spiral\Database\S...ototypes\AbstractIndex>. It seems like you assume a concrete implementation of the interface Spiral\Database\Schemas\IndexInterface to be always present.

This check looks for parameters that are defined as one type in their type hint or doc comment but seem to be used as a narrower type, i.e an implementation of an interface or a subclass.

Consider changing the type of the parameter or doing an instanceof check before assuming your parameter is of the expected type.

Loading history...
468
        }
469
470
        foreach ($this->fetchReferences() as $foreign) {
471
            $state->registerReference($foreign);
0 ignored issues
show
Compatibility introduced by
$foreign of type object<Spiral\Database\S...mas\ReferenceInterface> is not a sub-type of object<Spiral\Database\S...ypes\AbstractReference>. It seems like you assume a concrete implementation of the interface Spiral\Database\Schemas\ReferenceInterface to be always present.

This check looks for parameters that are defined as one type in their type hint or doc comment but seem to be used as a narrower type, i.e an implementation of an interface or a subclass.

Consider changing the type of the parameter or doing an instanceof check before assuming your parameter is of the expected type.

Loading history...
472
        }
473
474
        $state->setPrimaryKeys($this->fetchPrimaryKeys());
475
476
        //DBMS specific initialization can be placed here
477
    }
478
479
    /**
480
     * Fetch index declarations from database.
481
     *
482
     * @return ColumnInterface[]
483
     */
484
    abstract protected function fetchColumns(): array;
485
486
    /**
487
     * Fetch index declarations from database.
488
     *
489
     * @return IndexInterface[]
490
     */
491
    abstract protected function fetchIndexes(): array;
492
493
    /**
494
     * Fetch references declaration from database.
495
     *
496
     * @return ReferenceInterface[]
497
     */
498
    abstract protected function fetchReferences(): array;
499
500
    /**
501
     * Fetch names of primary keys from table.
502
     *
503
     * @return array
504
     */
505
    abstract protected function fetchPrimaryKeys(): array;
506
507
    /**
508
     * Create column with a given name.
509
     *
510
     * @param string $name
511
     *
512
     * @return AbstractColumn
513
     */
514
    abstract protected function createColumn(string $name): AbstractColumn;
515
516
    /**
517
     * Create index for a given set of columns.
518
     *
519
     * @param string $name
520
     *
521
     * @return AbstractIndex
522
     */
523
    abstract protected function createIndex(string $name): AbstractIndex;
524
525
    /**
526
     * Create reference on a given column set.
527
     *
528
     * @param string $name
529
     *
530
     * @return AbstractReference
531
     */
532
    abstract protected function createReference(string $name): AbstractReference;
533
534
    /**
535
     * Generate unique name for indexes and foreign keys.
536
     *
537
     * @param string $type
538
     * @param array  $columns
539
     *
540
     * @return string
541
     */
542
    protected function createIdentifier(string $type, array $columns): string
543
    {
544
        $name = $this->getName() . '_' . $type . '_' . join('_', $columns) . '_' . uniqid();
545
546
        if (strlen($name) > 64) {
547
            //Many DBMS has limitations on identifier length
548
            $name = md5($name);
549
        }
550
551
        return $name;
552
    }
553
554
    /**
555
     * @return ContainerInterface
556
     */
557
    protected function iocContainer()
558
    {
559
        //Falling back to driver specific container
560
        return $this->driver->iocContainer();
0 ignored issues
show
Bug introduced by
The method iocContainer() cannot be called from this context as it is declared protected in class Spiral\Core\Component.

This check looks for access to methods that are not accessible from the current context.

If you need to make a method accessible to another context you can raise its visibility level in the defining class.

Loading history...
561
    }
562
}