AlterPrimaryKeyScript::syncRelatedFields()   A
last analyzed

Complexity

Conditions 5
Paths 10

Size

Total Lines 63
Code Lines 38

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 5
eloc 38
nc 10
nop 4
dl 0
loc 63
rs 9.0008
c 0
b 0
f 0

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
namespace Charcoal\Admin\Script\Object\Table;
4
5
use PDO;
6
use PDOStatement;
7
8
use Countable;
9
use Traversable;
10
use Exception;
11
use RuntimeException;
12
use UnexpectedValueException;
13
use InvalidArgumentException;
14
15
// From PSR-7
16
use Psr\Http\Message\RequestInterface;
17
use Psr\Http\Message\ResponseInterface;
18
19
// From 'charcoal-core'
20
use Charcoal\Model\ModelInterface;
21
22
// From 'charcoal-property'
23
use Charcoal\Property\IdProperty;
24
use Charcoal\Property\PropertyField;
25
use Charcoal\Property\PropertyInterface;
26
27
// From 'charcoal-app'
28
use Charcoal\App\Script\ArgScriptTrait;
29
30
// From 'charcoal-admin'
31
use Charcoal\Admin\AdminScript;
32
33
/**
34
 * Alter an object's primary key (SQL source).
35
 */
36
class AlterPrimaryKeyScript extends AdminScript
37
{
38
    use ArgScriptTrait;
39
40
    /**
41
     * The model to alter.
42
     *
43
     * @var ModelInterface|null
44
     */
45
    protected $targetModel;
46
47
    /**
48
     * The related models to update.
49
     *
50
     * @var ModelInterface[]|null
51
     */
52
    protected $relatedModels;
53
54
    /**
55
     * The related model properties to update.
56
     *
57
     * @var PropertyInterface[]|null
58
     */
59
    protected $relatedProperties;
60
61
    /**
62
     * @return void
63
     */
64
    protected function init()
65
    {
66
        parent::init();
67
68
        $this->setDescription(
69
            'The <underline>object/table/alter-primary-key</underline> script replaces '.
70
            'the existing primary key with the new definition from the given model\'s metadata.'
71
        );
72
    }
73
74
    /**
75
     * Run the script.
76
     *
77
     * @param  RequestInterface  $request  A PSR-7 compatible Request instance.
78
     * @param  ResponseInterface $response A PSR-7 compatible Response instance.
79
     * @return ResponseInterface
80
     */
81
    public function run(RequestInterface $request, ResponseInterface $response)
82
    {
83
        unset($request);
84
85
        try {
86
            $this->start();
87
        } catch (Exception $e) {
88
            $this->climate()->error($e->getMessage());
89
        }
90
91
        return $response;
92
    }
93
94
    /**
95
     * Execute the prime directive.
96
     *
97
     * @return self
98
     */
99
    public function start()
100
    {
101
        $cli = $this->climate();
102
103
        $cli->br();
104
        $cli->bold()->underline()->out('Alter Model\'s Primary Key');
105
        $cli->br();
106
107
        $objType = $this->argOrInput('target_model');
108
        $this->setTargetModel($objType);
0 ignored issues
show
Bug introduced by
It seems like $objType can also be of type true; however, parameter $model of Charcoal\Admin\Script\Ob...cript::setTargetModel() does only seem to accept Charcoal\Model\ModelInterface|string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

108
        $this->setTargetModel(/** @scrutinizer ignore-type */ $objType);
Loading history...
109
110
        $model  = $this->targetModel();
111
        $source = $model->source();
112
        $table  = $source->table();
0 ignored issues
show
Bug introduced by
The method table() does not exist on Charcoal\Source\SourceInterface. It seems like you code against a sub-type of Charcoal\Source\SourceInterface such as Charcoal\Source\DatabaseSource. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

112
        /** @scrutinizer ignore-call */ 
113
        $table  = $source->table();
Loading history...
113
114
        $cli->comment(sprintf('The "%s" table will be altered.', $table));
115
        $cli->br();
116
        $cli->shout('This process is destructive. A backup should be made before proceeding.');
117
        $cli->br();
118
        $cli->red()->flank('Analyse your tables before proceeding. Not all fields can or might be affected.', '!');
119
        $cli->br();
120
121
        $input = $cli->confirm('Continue?');
122
        if ($input->confirmed()) {
123
            $cli->info('Starting Conversion');
124
        } else {
125
            $cli->info('Canceled Conversion');
126
127
            return $this;
128
        }
129
130
        $cli->br();
131
132
        $db = $source->db();
0 ignored issues
show
Bug introduced by
The method db() does not exist on Charcoal\Source\SourceInterface. It seems like you code against a sub-type of Charcoal\Source\SourceInterface such as Charcoal\Source\DatabaseSource. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

132
        /** @scrutinizer ignore-call */ 
133
        $db = $source->db();
Loading history...
133
        if (!$db) {
134
            $cli->error(
135
                'Could not instantiate a database connection.'
136
            );
137
138
            return $this;
139
        }
140
141
        if (!$source->tableExists()) {
0 ignored issues
show
Bug introduced by
The method tableExists() does not exist on Charcoal\Source\SourceInterface. It seems like you code against a sub-type of Charcoal\Source\SourceInterface such as Charcoal\Source\DatabaseSource. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

141
        if (!$source->/** @scrutinizer ignore-call */ tableExists()) {
Loading history...
142
            $cli->error(
143
                sprintf(
144
                    'The table "%s" does not exist. This script can only alter existing tables.',
145
                    $table
146
                )
147
            );
148
149
            return $this;
150
        }
151
152
        $oldKey = $model->key();
153
        $newKey = sprintf('%s_new', $model->key());
154
155
        $db->query(
156
            strtr('LOCK TABLES `%table` WRITE;', ['%table' => $table])
157
        );
158
159
        $this->prepareProperties($oldKey, $newKey, $oldProp, $newProp);
160
161
        if ($newProp->mode() === $oldProp->mode()) {
162
            $cli->error(
163
                sprintf(
164
                    'The ID is already %s. Canceling conversion.',
165
                    $this->labelFromMode($newProp)
166
                )
167
            );
168
            $db->query('UNLOCK TABLES;');
169
170
            return $this;
171
        }
172
173
        $newField = $this->propertyField($newProp);
174
        $oldField = $this->propertyField($oldProp);
175
        $oldField->setExtra('');
176
177
        if (!$this->quiet()) {
178
            $this->describeConversion($newProp);
179
        }
180
181
        $this->convertIdField($newProp, $newField, $oldProp, $oldField);
182
183
        $db->query('UNLOCK TABLES;');
184
185
        if (!$this->quiet()) {
186
            $cli->br();
187
            $cli->info('Success!');
188
        }
189
190
        return $this;
191
    }
192
193
194
195
    // Alter Table
196
    // =========================================================================
197
198
    /**
199
     * Retrieve the old and new ID properties.
200
     *
201
     * @param  string          $oldKey  The previous key.
202
     * @param  string          $newKey  The new key.
203
     * @param  IdProperty|null $oldProp If provided, then it is filled with an instance of IdProperty.
204
     * @param  IdProperty|null $newProp If provided, then it is filled with an instance of IdProperty.
205
     * @return IdProperty[]
206
     */
207
    protected function prepareProperties($oldKey, $newKey, &$oldProp = null, &$newProp = null)
208
    {
209
        $model  = $this->targetModel();
210
        $source = $model->source();
211
212
        $oldProp = $model->property($oldKey)->setAllowNull(false);
213
        $newProp = clone $oldProp;
214
        $newProp->setIdent($newKey);
215
216
        $sql  = strtr('SHOW COLUMNS FROM `%table`;', ['%table' => $source->table()]);
217
        $cols = $source->db()->query($sql, PDO::FETCH_ASSOC);
218
        foreach ($cols as $col) {
219
            if ($col['Field'] !== $oldKey) {
220
                continue;
221
            }
222
223
            if (!$this->quiet()) {
224
                $this->climate()->comment(
225
                    sprintf('Evaluating the current `%s` column.', $oldKey)
226
                );
227
            }
228
229
            if (preg_match('~\bINT\(?(?:$|\b)~i', $col['Type'])) {
230
                $oldProp->setMode(IdProperty::MODE_AUTO_INCREMENT);
231
            } elseif (preg_match('~(?:^|\b)(?:VAR)?CHAR\(13\)(?:$|\b)~i', $col['Type'])) {
232
                $oldProp->setMode(IdProperty::MODE_UNIQID);
233
            } elseif (preg_match('~(?:^|\b)(?:VAR)?CHAR\(36\)(?:$|\b)~i', $col['Type'])) {
234
                $oldProp->setMode(IdProperty::MODE_UUID);
235
            }
236
        }
237
238
        return [
239
            'old' => $oldProp,
240
            'new' => $newProp,
241
        ];
242
    }
243
244
    /**
245
     * Retrieve a label for the ID's mode.
246
     *
247
     * @param  string|IdProperty $mode The mode or property to resolve.
248
     * @throws UnexpectedValueException If the ID mode is invalid.
249
     * @return string
250
     */
251
    protected function labelFromMode($mode)
252
    {
253
        if ($mode instanceof IdProperty) {
254
            $mode = $mode->mode();
255
        }
256
257
        switch ($mode) {
258
            case IdProperty::MODE_AUTO_INCREMENT:
259
                return 'auto-increment';
260
261
            case IdProperty::MODE_UNIQID:
262
                return 'uniqid()';
263
264
            case IdProperty::MODE_UUID:
265
                return 'RFC-4122 UUID';
266
        }
267
268
        throw new UnexpectedValueException(sprintf(
269
            'The ID mode was not recognized: %s',
270
            is_object($mode) ? get_class($mode) : gettype($mode)
0 ignored issues
show
introduced by
The condition is_object($mode) is always false.
Loading history...
271
        ));
272
    }
273
274
    /**
275
     * Describe what we are converting to.
276
     *
277
     * @param  IdProperty $prop The property to analyse.
278
     * @return self
279
     */
280
    protected function describeConversion(IdProperty $prop)
281
    {
282
        $cli  = $this->climate();
283
        $mode = $prop->mode();
284
        if ($mode === IdProperty::MODE_AUTO_INCREMENT) {
285
            $cli->comment('Converting to auto-increment ID.');
286
        } else {
287
            $label = $this->labelFromMode($mode);
288
            if ($label) {
289
                $cli->comment(
290
                    sprintf('Converting to auto-generated ID (%s).', $label)
291
                );
292
            } else {
293
                $cli->comment('Converting to auto-generated ID.');
294
            }
295
        }
296
297
        return $this;
298
    }
299
300
    /**
301
     * Retrieve the given property's field.
302
     *
303
     * @param  IdProperty $prop The property to retrieve the field from.
304
     * @return PropertyField
305
     */
306
    protected function propertyField(IdProperty $prop)
307
    {
308
        $fields = $prop->fields('');
309
310
        return reset($fields);
311
    }
312
313
    /**
314
     * Retrieve the target model's rows.
315
     *
316
     * @return array|Traversable
317
     */
318
    private function fetchTargetRows()
319
    {
320
        $model  = $this->targetModel();
321
        $source = $model->source();
322
323
        $sql = strtr('SELECT %key FROM `%table`', [
324
            '%table' => $source->table(),
325
            '%key'   => $model->key(),
326
        ]);
327
328
        return $source->db()->query($sql, PDO::FETCH_ASSOC);
329
    }
330
331
    /**
332
     * Describe the given count.
333
     *
334
     * @param  array|Traversable $rows The target model's existing rows.
335
     * @throws InvalidArgumentException If the given argument is not iterable.
336
     * @return boolean
337
     */
338
    private function describeCount($rows = null)
339
    {
340
        if ($rows === null) {
341
            $rows = $this->fetchTargetRows();
342
        }
343
344
        if (!is_array($rows) && !($rows instanceof Traversable)) {
0 ignored issues
show
introduced by
$rows is always a sub-type of Traversable.
Loading history...
345
            throw new InvalidArgumentException(
346
                sprintf(
347
                    'The rows must be iterable; received %s',
348
                    is_object($rows) ? get_class($rows) : gettype($rows)
349
                )
350
            );
351
        }
352
353
        $cli   = $this->climate();
354
        $model = $this->targetModel();
355
356
        if (is_array($rows) || $rows instanceof Countable) {
357
            $count = count($rows);
358
        } elseif ($rows instanceof PDOStatement) {
359
            $count = $rows->rowCount();
360
        } else {
361
            $count = iterator_count($rows);
362
        }
363
364
        if ($count === 0) {
365
            $cli->comment('The object table is empty.');
366
            $cli->comment(
367
                sprintf('Only changing `%s` column.', $model->key())
368
            );
369
370
            return false;
371
        } elseif ($count === 1) {
372
            if (!$this->quiet()) {
373
                $cli->comment('The object table has 1 row.');
374
            }
375
        } else {
376
            if (!$this->quiet()) {
377
                $cli->comment(
378
                    sprintf('The object table has %s rows.', $count)
379
                );
380
            }
381
        }
382
383
        return true;
384
    }
385
386
    /**
387
     * Insert the given field.
388
     *
389
     * @param  PropertyField $field The new ID field.
390
     * @param  IdProperty    $prop  The new ID property.
391
     * @return self
392
     */
393
    private function insertNewField(PropertyField $field, IdProperty $prop)
394
    {
395
        unset($prop);
396
397
        $model  = $this->targetModel();
398
        $source = $model->source();
399
400
        $extra = $field->extra();
401
        $field->setExtra('');
402
403
        // Don't alter table if column name already exists.
404
        $sql = strtr('SHOW COLUMNS FROM `%table` LIKE "%key"', [
405
            '%table' => $source->table(),
406
            '%key'   => $field->ident(),
407
        ]);
408
409
        $res = $source->db()->query($sql);
410
411
        if ($res->fetch(1)) {
412
            // Column name already exists.
413
            return $this;
414
        }
415
416
        $sql = strtr('ALTER TABLE `%table` ADD COLUMN %field FIRST;', [
417
            '%table' => $source->table(),
418
            '%field' => $field->sql(),
419
        ]);
420
        $field->setExtra($extra);
421
422
        $source->db()->query($sql);
423
424
        return $this;
425
    }
426
427
    /**
428
     * Drop the primary key from the given field.
429
     *
430
     * @param  PropertyField $field The previous ID field.
431
     * @param  IdProperty    $prop  The previous ID property.
432
     * @return self
433
     */
434
    private function dropPrimaryKey(PropertyField $field, IdProperty $prop)
435
    {
436
        unset($prop);
437
438
        $keepId = $this->climate()->arguments->defined('keep_id');
439
        $model  = $this->targetModel();
440
        $source = $model->source();
441
        $db     = $source->db();
442
        $key    = $model->key();
443
444
        if ($keepId) {
445
            $field->setIdent(sprintf('%1$s_%2$s', $key, date('Ymd_His')));
446
            $sql = strtr(
447
                'ALTER TABLE `%table`
448
                    CHANGE COLUMN `%key` %field,
449
                    DROP PRIMARY KEY;',
450
                [
451
                    '%table' => $source->table(),
452
                    '%field' => $field->sql(),
453
                    '%key'   => $key,
454
                ]
455
            );
456
        } else {
457
            $sql = strtr(
458
                'ALTER TABLE `%table`
459
                    MODIFY COLUMN %field,
460
                    DROP PRIMARY KEY;',
461
                [
462
                    '%table' => $source->table(),
463
                    '%field' => $field->sql(),
464
                ]
465
            );
466
        }
467
468
        $db->query($sql);
469
470
        return $this;
471
    }
472
473
    /**
474
     * Set the given field as the primary key.
475
     *
476
     * @param  PropertyField $field The new ID field.
477
     * @param  IdProperty    $prop  The new ID property.
478
     * @return self
479
     */
480
    private function applyPrimaryKey(PropertyField $field, IdProperty $prop)
481
    {
482
        unset($prop);
483
484
        $model  = $this->targetModel();
485
        $source = $model->source();
486
487
        $sql = strtr(
488
            'ALTER TABLE `%table` ADD PRIMARY KEY (`%key`);',
489
            [
490
                '%table' => $source->table(),
491
                '%key'   => $field->ident(),
492
            ]
493
        );
494
        $source->db()->query($sql);
495
496
        return $this;
497
    }
498
499
    /**
500
     * Rename the given field.
501
     *
502
     * @param  PropertyField $field The field to rename.
503
     * @param  string        $from  The original field key.
504
     * @param  string        $to    The new field key.
505
     * @return self
506
     */
507
    private function renameColumn(PropertyField $field, $from, $to)
508
    {
509
        $model  = $this->targetModel();
510
        $source = $model->source();
511
        $key    = $model->key();
0 ignored issues
show
Unused Code introduced by
The assignment to $key is dead and can be removed.
Loading history...
512
513
        $field->setIdent($to);
514
        $sql = strtr(
515
            'ALTER TABLE `%table` CHANGE COLUMN `%from` %field;',
516
            [
517
                '%table' => $source->table(),
518
                '%field' => $field->sql(),
519
                '%from'  => $from,
520
                '%to'    => $to,
521
            ]
522
        );
523
        $source->db()->query($sql);
524
525
        return $this;
526
    }
527
528
    /**
529
     * Remove the given field.
530
     *
531
     * @param  PropertyField $field The field to remove.
532
     * @return self
533
     */
534
    private function removeColumn(PropertyField $field)
535
    {
536
        $source = $this->targetModel()->source();
537
538
        $sql = strtr(
539
            'ALTER TABLE `%table` DROP COLUMN `%key`;',
540
            [
541
                '%table' => $source->table(),
542
                '%key'   => $field->ident(),
543
            ]
544
        );
545
        $source->db()->query($sql);
546
547
        return $this;
548
    }
549
550
    /**
551
     * Convert a primary key column from one format to another.
552
     *
553
     * @param  IdProperty    $newProp  The new ID property.
554
     * @param  PropertyField $newField The new ID field.
555
     * @param  IdProperty    $oldProp  The previous ID property.
556
     * @param  PropertyField $oldField The previous ID field.
557
     * @throws InvalidArgumentException If the new property does not implement the proper mode.
558
     * @return self
559
     */
560
    protected function convertIdField(
561
        IdProperty $newProp,
562
        PropertyField $newField,
563
        IdProperty $oldProp,
564
        PropertyField $oldField
565
    ) {
566
        $cli = $this->climate();
567
568
        $keepId = $cli->arguments->defined('keep_id');
569
        $model  = $this->targetModel();
570
        $source = $model->source();
571
        $table  = $source->table();
572
        $db     = $source->db();
0 ignored issues
show
Unused Code introduced by
The assignment to $db is dead and can be removed.
Loading history...
573
574
        $newKey = $newProp->ident();
575
        $oldKey = $oldProp->ident();
576
577
        $this->insertNewField($newField, $newProp);
578
579
        $rows = $this->fetchTargetRows();
580
        if ($this->describeCount($rows)) {
581
            if (!$this->quiet()) {
582
                $cli->br();
583
                $progress = $cli->progress($rows->rowCount());
0 ignored issues
show
Bug introduced by
The method rowCount() does not exist on Traversable. It seems like you code against a sub-type of Traversable such as PDOStatement. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

583
                $progress = $cli->progress($rows->/** @scrutinizer ignore-call */ rowCount());
Loading history...
584
            }
585
586
            if ($newProp->mode() === IdProperty::MODE_AUTO_INCREMENT) {
587
                $pool = 0;
588
                $ids  = function () use (&$pool) {
589
                    return ++$pool;
590
                };
591
            } else {
592
                $pool = [];
593
                $ids  = function () use (&$pool, $newProp) {
594
                    $id = $newProp->autoGenerate();
595
                    while (in_array($id, $pool)) {
596
                        $id = $newProp->autoGenerate();
597
                    }
598
599
                    $pool[] = $id;
600
601
                    return $id;
602
                };
603
            }
604
605
            foreach ($rows as $row) {
606
                $id  = $ids();
607
                $sql = strtr('UPDATE `%table` SET `%newKey` = :new WHERE `%oldKey` = :old;', [
608
                    '%table'  => $table,
609
                    '%newKey' => $newKey,
610
                    '%oldKey' => $oldKey,
611
                ]);
612
                $source->dbQuery(
0 ignored issues
show
Bug introduced by
The method dbQuery() does not exist on Charcoal\Source\SourceInterface. It seems like you code against a sub-type of Charcoal\Source\SourceInterface such as Charcoal\Source\DatabaseSource. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

612
                $source->/** @scrutinizer ignore-call */ 
613
                         dbQuery(
Loading history...
613
                    $sql,
614
                    [
615
                        'new' => $id,
616
                        'old' => $row[$oldKey],
617
                    ],
618
                    [
619
                        'new' => $newField->sqlPdoType(),
620
                        'old' => $oldField->sqlPdoType(),
621
                    ]
622
                );
623
624
                if (!$this->quiet()) {
625
                    $progress->advance();
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $progress does not seem to be defined for all execution paths leading up to this point.
Loading history...
626
                }
627
            }
628
        }
629
630
        $this->dropPrimaryKey($oldField, $oldProp);
631
        $this->applyPrimaryKey($newField, $newProp);
632
633
        /** @todo Alter related tables */
634
        $this->syncRelatedFields($newProp, $newField, $oldProp, $oldField);
635
636
        if (!$keepId) {
637
            $this->removeColumn($oldField);
638
        }
639
640
        $this->renameColumn($newField, $newKey, $oldKey);
641
642
        return $this;
643
    }
644
645
    /**
646
     * Sync the new primary keys to related models.
647
     *
648
     * @param  IdProperty    $newProp  The new ID property.
649
     * @param  PropertyField $newField The new ID field.
650
     * @param  IdProperty    $oldProp  The previous ID property.
651
     * @param  PropertyField $oldField The previous ID field.
652
     * @throws InvalidArgumentException If the new property does not implement the proper mode.
653
     * @return self
654
     */
655
    protected function syncRelatedFields(
656
        IdProperty $newProp,
657
        PropertyField $newField,
658
        IdProperty $oldProp,
659
        PropertyField $oldField
660
    ) {
661
        unset($newProp, $oldProp, $oldField);
662
663
        $cli = $this->climate();
664
        if (!$this->quiet()) {
665
            $cli->br();
666
            $cli->comment('Syncing new IDs to related tables.');
667
        }
668
669
        $related = $cli->arguments->get('related_model');
670
        if (!$related) {
671
            $cli->br();
672
            $input = $cli->confirm('Are there any model(s) related to the target?');
673
            if (!$input->confirmed()) {
674
                return $this;
675
            }
676
677
            $related = $this->argOrInput('related_model');
678
        }
679
        $this->setRelatedModels($related);
680
681
        $target = $this->targetModel();
682
        $table  = $target->source()->table();
683
        foreach ($this->relatedModels() as $model) {
684
            $src = $model->source();
685
            $tbl = $src->table();
686
            $db  = $src->db();
687
688
            $db->query(
689
                strtr(
690
                    'LOCK TABLES
691
                        `%relatedTable` AS a WRITE,
692
                        `%sourceTable` AS b WRITE;',
693
                    [
694
                        '%relatedTable' => $tbl,
695
                        '%sourceTable'  => $table,
696
                    ]
697
                )
698
            );
699
700
            $sql = strtr(
701
                'UPDATE `%relatedTable` AS a
702
                    JOIN `%sourceTable` AS b ON a.`%prop` = b.`%oldKey`
703
                    SET a.`%prop` = b.`%newKey`;',
704
                [
705
                    '%relatedTable' => $tbl,
706
                    '%prop'         => $this->relatedProperties[$model->objType()],
0 ignored issues
show
Bug introduced by
The method objType() does not exist on Charcoal\Model\ModelInterface. It seems like you code against a sub-type of said class. However, the method does not exist in Charcoal\Queue\QueueItemInterface or Charcoal\Object\ContentInterface or Charcoal\Object\UserDataInterface or Charcoal\User\UserInterface. Are you sure you never get one of those? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

706
                    '%prop'         => $this->relatedProperties[$model->/** @scrutinizer ignore-call */ objType()],
Loading history...
707
                    '%sourceTable'  => $table,
708
                    '%newKey'       => $newField->ident(),
709
                    '%oldKey'       => $target->key(),
710
                ]
711
            );
712
            $db->query($sql);
713
714
            $db->query('UNLOCK TABLES;');
715
        }
716
717
        return $this;
718
    }
719
720
721
722
    // CLI Arguments
723
    // =========================================================================
724
725
    /**
726
     * Retrieve the script's supported arguments.
727
     *
728
     * @return array
729
     */
730
    public function defaultArguments()
731
    {
732
        static $arguments;
733
734
        if ($arguments === null) {
735
            $validateModel = function ($response) {
736
                if (strlen($response) === 0) {
737
                    return false;
738
                }
739
740
                try {
741
                    $this->modelFactory()->get($response);
742
                } catch (Exception $e) {
743
                    unset($e);
744
745
                    return false;
746
                }
747
748
                return true;
749
            };
750
751
            $validateModels = function ($response) {
752
                if (strlen($response) === 0) {
753
                    return false;
754
                }
755
756
                try {
757
                    $arr = $this->parseAsArray($response);
758
                    foreach ($arr as $model) {
759
                        $this->resolveRelatedModel($model);
760
                    }
761
                } catch (Exception $e) {
762
                    unset($e);
763
764
                    return false;
765
                }
766
767
                return true;
768
            };
769
770
            $arguments = [
771
                'keep_id'       => [
772
                    'longPrefix'  => 'keep-id',
773
                    'noValue'     => true,
774
                    'description' => 'Skip the deletion of the ID field to be replaced.',
775
                ],
776
                'target_model'  => [
777
                    'prefix'      => 'o',
778
                    'longPrefix'  => 'obj-type',
779
                    'required'    => true,
780
                    'description' => 'The object type to alter.',
781
                    'prompt'      => 'What model must be altered?',
782
                    'acceptValue' => $validateModel->bindTo($this)
783
                ],
784
                'related_model' => [
785
                    'prefix'      => 'r',
786
                    'longPrefix'  => 'related-obj-type',
787
                    'description' => 'Properties of related object types to synchronize (ObjType:propertyIdent,…).',
788
                    'prompt'      => 'List related models and properties (ObjType:propertyIdent,…):',
789
                    'acceptValue' => $validateModels->bindTo($this)
790
                ]
791
            ];
792
793
            $arguments = array_merge(parent::defaultArguments(), $arguments);
794
        }
795
796
        return $arguments;
797
    }
798
799
    /**
800
     * Retrieve the script's parent arguments.
801
     *
802
     * Useful for specialty classes extending this one that might not want
803
     * options for selecting specific objects.
804
     *
805
     * @return array
806
     */
807
    public function parentArguments()
808
    {
809
        return parent::defaultArguments();
810
    }
811
812
    /**
813
     * Set the model to alter.
814
     *
815
     * @param  string|ModelInterface $model An object model.
816
     * @throws InvalidArgumentException If the given argument is not a model.
817
     * @return self
818
     */
819
    public function setTargetModel($model)
820
    {
821
        if (is_string($model)) {
822
            $model = $this->modelFactory()->get($model);
823
        }
824
825
        if (!$model instanceof ModelInterface) {
826
            throw new InvalidArgumentException(
827
                sprintf(
828
                    'The model must be an instance of "%s"',
829
                    ModelInterface::class
830
                )
831
            );
832
        }
833
834
        $this->targetModel = $model;
835
836
        return $this;
837
    }
838
839
    /**
840
     * Retrieve the model to alter.
841
     *
842
     * @throws RuntimeException If a target model has not been defined.
843
     * @return ModelInterface
844
     */
845
    public function targetModel()
846
    {
847
        if (!isset($this->targetModel)) {
848
            throw new RuntimeException('A model must be targeted.');
849
        }
850
851
        return $this->targetModel;
852
    }
853
854
    /**
855
     * Set the related models to update.
856
     *
857
     * @param  string|array $models One or more object models.
858
     * @throws InvalidArgumentException If the given argument is not a model.
859
     * @return self
860
     */
861
    public function setRelatedModels($models)
862
    {
863
        $models = $this->parseAsArray($models);
864
        foreach ($models as $i => $model) {
865
            if (is_string($model)) {
866
                list($model, $prop) = $this->resolveRelatedModel($model);
867
                $models[$i]                                 = $model;
868
                $this->relatedProperties[$model->objType()] = $prop;
869
            } elseif ($model instanceof ModelInterface) {
870
                if (!isset($this->relatedProperties[$model->objType()])) {
871
                    throw new InvalidArgumentException(
872
                        sprintf(
873
                            'The related model [%s] requires a target property',
874
                            get_class($model)
875
                        )
876
                    );
877
                }
878
                $models[$i] = $model;
879
            } else {
880
                throw new InvalidArgumentException(
881
                    sprintf(
882
                        'A related model must be defined as "%s"',
883
                        'ObjType:propertyIdent'
884
                    )
885
                );
886
            }
887
        }
888
889
        $this->relatedModels = $models;
890
891
        return $this;
892
    }
893
894
    /**
895
     * Resolve the given related model.
896
     *
897
     * @param  string $pattern A 'model:property' identifier.
898
     * @throws InvalidArgumentException If the identifier is invalid.
899
     * @return array Returns an array containing a ModelInterface and a property identifier.
900
     */
901
    protected function resolveRelatedModel($pattern)
902
    {
903
        list($class, $prop) = array_pad($this->parseAsArray($pattern, ':'), 2, null);
904
        $model = $this->modelFactory()->get($class);
905
906
        if (!$prop) {
907
            throw new InvalidArgumentException(
908
                sprintf(
909
                    'The related model [%s] requires a target property',
910
                    get_class($model)
911
                )
912
            );
913
        }
914
915
        $metadata = $model->metadata();
916
        if (!$metadata->property($prop)) {
917
            throw new InvalidArgumentException(
918
                sprintf(
919
                    'The related model [%1$s] does not have the target property [%2$s]',
920
                    $class,
921
                    (is_string($prop) ? $prop : gettype($prop))
922
                )
923
            );
924
        }
925
926
        return [$model, $prop];
927
    }
928
929
    /**
930
     * Retrieve the related models to update.
931
     *
932
     * @return ModelInterface[]|null
933
     */
934
    public function relatedModels()
935
    {
936
        return $this->relatedModels;
937
    }
938
}
939