• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

codeigniter4 / CodeIgniter4 / 12282713649

11 Dec 2024 06:34PM UTC coverage: 84.432% (+0.007%) from 84.425%
12282713649

Pull #9302

github

web-flow
Merge c866e750c into 0b0126cf8
Pull Request #9302: refactor: fix `phpstan` only boolean allowed

108 of 115 new or added lines in 35 files covered. (93.91%)

1 existing line in 1 file now uncovered.

20420 of 24185 relevant lines covered (84.43%)

189.63 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

73.31
/system/Database/MigrationRunner.php
1
<?php
2

3
declare(strict_types=1);
4

5
/**
6
 * This file is part of CodeIgniter 4 framework.
7
 *
8
 * (c) CodeIgniter Foundation <admin@codeigniter.com>
9
 *
10
 * For the full copyright and license information, please view
11
 * the LICENSE file that was distributed with this source code.
12
 */
13

14
namespace CodeIgniter\Database;
15

16
use CodeIgniter\CLI\CLI;
17
use CodeIgniter\Events\Events;
18
use CodeIgniter\Exceptions\ConfigException;
19
use CodeIgniter\I18n\Time;
20
use Config\Database;
21
use Config\Migrations as MigrationsConfig;
22
use RuntimeException;
23
use stdClass;
24

25
/**
26
 * Class MigrationRunner
27
 */
28
class MigrationRunner
29
{
30
    /**
31
     * Whether or not migrations are allowed to run.
32
     *
33
     * @var bool
34
     */
35
    protected $enabled = false;
36

37
    /**
38
     * Name of table to store meta information
39
     *
40
     * @var string
41
     */
42
    protected $table;
43

44
    /**
45
     * The Namespace where migrations can be found.
46
     * `null` is all namespaces.
47
     *
48
     * @var string|null
49
     */
50
    protected $namespace;
51

52
    /**
53
     * The database Group to migrate.
54
     *
55
     * @var string
56
     */
57
    protected $group;
58

59
    /**
60
     * The migration name.
61
     *
62
     * @var string
63
     */
64
    protected $name;
65

66
    /**
67
     * The pattern used to locate migration file versions.
68
     *
69
     * @var string
70
     */
71
    protected $regex = '/\A(\d{4}[_-]?\d{2}[_-]?\d{2}[_-]?\d{6})_(\w+)\z/';
72

73
    /**
74
     * The main database connection. Used to store
75
     * migration information in.
76
     *
77
     * @var BaseConnection
78
     */
79
    protected $db;
80

81
    /**
82
     * If true, will continue instead of throwing
83
     * exceptions.
84
     *
85
     * @var bool
86
     */
87
    protected $silent = false;
88

89
    /**
90
     * used to return messages for CLI.
91
     *
92
     * @var array
93
     */
94
    protected $cliMessages = [];
95

96
    /**
97
     * Tracks whether we have already ensured
98
     * the table exists or not.
99
     *
100
     * @var bool
101
     */
102
    protected $tableChecked = false;
103

104
    /**
105
     * The full path to locate migration files.
106
     *
107
     * @var string
108
     */
109
    protected $path;
110

111
    /**
112
     * The database Group filter.
113
     *
114
     * @var string|null
115
     */
116
    protected $groupFilter;
117

118
    /**
119
     * Used to skip current migration.
120
     *
121
     * @var bool
122
     */
123
    protected $groupSkip = false;
124

125
    /**
126
     * The migration can manage multiple databases. So it should always use the
127
     * default DB group so that it creates the `migrations` table in the default
128
     * DB group. Therefore, passing $db is for testing purposes only.
129
     *
130
     * @param array|ConnectionInterface|string|null $db DB group. For testing purposes only.
131
     *
132
     * @throws ConfigException
133
     */
134
    public function __construct(MigrationsConfig $config, $db = null)
135
    {
136
        $this->enabled = $config->enabled ?? false;
692✔
137
        $this->table   = $config->table ?? 'migrations';
692✔
138

139
        $this->namespace = APP_NAMESPACE;
692✔
140

141
        // Even if a DB connection is passed, since it is a test,
142
        // it is assumed to use the default group name
143
        $this->group = is_string($db) ? $db : config(Database::class)->defaultGroup;
692✔
144

145
        $this->db = db_connect($db);
692✔
146
    }
147

148
    /**
149
     * Locate and run all new migrations
150
     *
151
     * @return bool
152
     *
153
     * @throws ConfigException
154
     * @throws RuntimeException
155
     */
156
    public function latest(?string $group = null)
157
    {
158
        if (! $this->enabled) {
631✔
159
            throw ConfigException::forDisabledMigrations();
1✔
160
        }
161

162
        $this->ensureTable();
630✔
163

164
        if ((string) $group !== '') {
630✔
165
            $this->groupFilter = $group;
613✔
166
            $this->setGroup($group);
613✔
167
        }
168

169
        $migrations = $this->findMigrations();
630✔
170

171
        if ($migrations === []) {
630✔
172
            return true;
×
173
        }
174

175
        foreach ($this->getHistory((string) $group) as $history) {
630✔
176
            unset($migrations[$this->getObjectUid($history)]);
12✔
177
        }
178

179
        $batch = $this->getLastBatch() + 1;
630✔
180

181
        foreach ($migrations as $migration) {
630✔
182
            if ($this->migrate('up', $migration)) {
619✔
183
                if ($this->groupSkip === true) {
619✔
184
                    $this->groupSkip = false;
×
185

186
                    continue;
×
187
                }
188

189
                $this->addHistory($migration, $batch);
619✔
190
            } else {
191
                $this->regress(-1);
×
192

193
                $message = lang('Migrations.generalFault');
×
194

195
                if ($this->silent) {
×
196
                    $this->cliMessages[] = "\t" . CLI::color($message, 'red');
×
197

198
                    return false;
×
199
                }
200

201
                throw new RuntimeException($message);
×
202
            }
203
        }
204

205
        $data           = get_object_vars($this);
630✔
206
        $data['method'] = 'latest';
630✔
207
        Events::trigger('migrate', $data);
630✔
208

209
        return true;
630✔
210
    }
211

212
    /**
213
     * Migrate down to a previous batch
214
     *
215
     * Calls each migration step required to get to the provided batch
216
     *
217
     * @param int         $targetBatch Target batch number, or negative for a relative batch, 0 for all
218
     * @param string|null $group       Deprecated. The designation has no effect.
219
     *
220
     * @return bool True on success, FALSE on failure or no migrations are found
221
     *
222
     * @throws ConfigException
223
     * @throws RuntimeException
224
     */
225
    public function regress(int $targetBatch = 0, ?string $group = null)
226
    {
227
        if (! $this->enabled) {
634✔
228
            throw ConfigException::forDisabledMigrations();
×
229
        }
230

231
        $this->ensureTable();
634✔
232

233
        $batches = $this->getBatches();
634✔
234

235
        if ($targetBatch < 0) {
634✔
236
            $targetBatch = $batches[count($batches) - 1 + $targetBatch] ?? 0;
1✔
237
        }
238

239
        if ($batches === [] && $targetBatch === 0) {
634✔
240
            return true;
28✔
241
        }
242

243
        if ($targetBatch !== 0 && ! in_array($targetBatch, $batches, true)) {
614✔
244
            $message = lang('Migrations.batchNotFound') . $targetBatch;
×
245

246
            if ($this->silent) {
×
247
                $this->cliMessages[] = "\t" . CLI::color($message, 'red');
×
248

249
                return false;
×
250
            }
251

252
            throw new RuntimeException($message);
×
253
        }
254

255
        $tmpNamespace = $this->namespace;
614✔
256

257
        $this->namespace = null;
614✔
258
        $allMigrations   = $this->findMigrations();
614✔
259

260
        $migrations = [];
614✔
261

262
        while ($batch = array_pop($batches)) {
614✔
263
            if ($batch <= $targetBatch) {
614✔
264
                break;
1✔
265
            }
266

267
            foreach ($this->getBatchHistory($batch, 'desc') as $history) {
614✔
268
                $uid = $this->getObjectUid($history);
614✔
269

270
                if (! isset($allMigrations[$uid])) {
614✔
271
                    $message = lang('Migrations.gap') . ' ' . $history->version;
×
272

273
                    if ($this->silent) {
×
274
                        $this->cliMessages[] = "\t" . CLI::color($message, 'red');
×
275

276
                        return false;
×
277
                    }
278

279
                    throw new RuntimeException($message);
×
280
                }
281

282
                $migration          = $allMigrations[$uid];
614✔
283
                $migration->history = $history;
614✔
284
                $migrations[]       = $migration;
614✔
285
            }
286
        }
287

288
        foreach ($migrations as $migration) {
614✔
289
            if ($this->migrate('down', $migration)) {
614✔
290
                $this->removeHistory($migration->history);
614✔
291
            } else {
292
                $message = lang('Migrations.generalFault');
×
293

294
                if ($this->silent) {
×
295
                    $this->cliMessages[] = "\t" . CLI::color($message, 'red');
×
296

297
                    return false;
×
298
                }
299

300
                throw new RuntimeException($message);
×
301
            }
302
        }
303

304
        $data           = get_object_vars($this);
614✔
305
        $data['method'] = 'regress';
614✔
306
        Events::trigger('migrate', $data);
614✔
307

308
        $this->namespace = $tmpNamespace;
614✔
309

310
        return true;
614✔
311
    }
312

313
    /**
314
     * Migrate a single file regardless of order or batches.
315
     * Method "up" or "down" determined by presence in history.
316
     * NOTE: This is not recommended and provided mostly for testing.
317
     *
318
     * @param string $path Full path to a valid migration file
319
     * @param string $path Namespace of the target migration
320
     */
321
    public function force(string $path, string $namespace, ?string $group = null)
322
    {
323
        if (! $this->enabled) {
×
324
            throw ConfigException::forDisabledMigrations();
×
325
        }
326

327
        $this->ensureTable();
×
328

NEW
329
        if ((string) $group !== '') {
×
330
            $this->groupFilter = $group;
×
331
            $this->setGroup($group);
×
332
        }
333

334
        $migration = $this->migrationFromFile($path, $namespace);
×
335
        if (empty($migration)) {
×
336
            $message = lang('Migrations.notFound');
×
337

338
            if ($this->silent) {
×
339
                $this->cliMessages[] = "\t" . CLI::color($message, 'red');
×
340

341
                return false;
×
342
            }
343

344
            throw new RuntimeException($message);
×
345
        }
346

347
        $method = 'up';
×
348
        $this->setNamespace($migration->namespace);
×
349

350
        foreach ($this->getHistory($this->group) as $history) {
×
351
            if ($this->getObjectUid($history) === $migration->uid) {
×
352
                $method             = 'down';
×
353
                $migration->history = $history;
×
354
                break;
×
355
            }
356
        }
357

358
        if ($method === 'up') {
×
359
            $batch = $this->getLastBatch() + 1;
×
360

361
            if ($this->migrate('up', $migration) && $this->groupSkip === false) {
×
362
                $this->addHistory($migration, $batch);
×
363

364
                return true;
×
365
            }
366

367
            $this->groupSkip = false;
×
368
        } elseif ($this->migrate('down', $migration)) {
×
369
            $this->removeHistory($migration->history);
×
370

371
            return true;
×
372
        }
373

374
        $message = lang('Migrations.generalFault');
×
375

376
        if ($this->silent) {
×
377
            $this->cliMessages[] = "\t" . CLI::color($message, 'red');
×
378

379
            return false;
×
380
        }
381

382
        throw new RuntimeException($message);
×
383
    }
384

385
    /**
386
     * Retrieves list of available migration scripts
387
     *
388
     * @return array List of all located migrations by their UID
389
     */
390
    public function findMigrations(): array
391
    {
392
        $namespaces = $this->namespace !== null ? [$this->namespace] : array_keys(service('autoloader')->getNamespace());
633✔
393
        $migrations = [];
633✔
394

395
        foreach ($namespaces as $namespace) {
633✔
396
            if (ENVIRONMENT !== 'testing' && $namespace === 'Tests\Support') {
633✔
397
                continue;
×
398
            }
399

400
            foreach ($this->findNamespaceMigrations($namespace) as $migration) {
633✔
401
                $migrations[$migration->uid] = $migration;
632✔
402
            }
403
        }
404

405
        // Sort migrations ascending by their UID (version)
406
        ksort($migrations);
633✔
407

408
        return $migrations;
633✔
409
    }
410

411
    /**
412
     * Retrieves a list of available migration scripts for one namespace
413
     */
414
    public function findNamespaceMigrations(string $namespace): array
415
    {
416
        $migrations = [];
633✔
417
        $locator    = service('locator', true);
633✔
418

419
        if (! empty($this->path)) {
633✔
420
            helper('filesystem');
×
421
            $dir   = rtrim($this->path, DIRECTORY_SEPARATOR) . '/';
×
422
            $files = get_filenames($dir, true, false, false);
×
423
        } else {
424
            $files = $locator->listNamespaceFiles($namespace, '/Database/Migrations/');
633✔
425
        }
426

427
        foreach ($files as $file) {
633✔
428
            $file = empty($this->path) ? $file : $this->path . str_replace($this->path, '', $file);
632✔
429

430
            if ($migration = $this->migrationFromFile($file, $namespace)) {
632✔
431
                $migrations[] = $migration;
632✔
432
            }
433
        }
434

435
        return $migrations;
633✔
436
    }
437

438
    /**
439
     * Create a migration object from a file path.
440
     *
441
     * @param string $path Full path to a valid migration file.
442
     *
443
     * @return false|object Returns the migration object, or false on failure
444
     */
445
    protected function migrationFromFile(string $path, string $namespace)
446
    {
447
        if (! str_ends_with($path, '.php')) {
632✔
448
            return false;
14✔
449
        }
450

451
        $filename = basename($path, '.php');
632✔
452

453
        if (! preg_match($this->regex, $filename)) {
632✔
454
            return false;
×
455
        }
456

457
        $locator = service('locator', true);
632✔
458

459
        $migration = new stdClass();
632✔
460

461
        $migration->version   = $this->getMigrationNumber($filename);
632✔
462
        $migration->name      = $this->getMigrationName($filename);
632✔
463
        $migration->path      = $path;
632✔
464
        $migration->class     = $locator->getClassname($path);
632✔
465
        $migration->namespace = $namespace;
632✔
466
        $migration->uid       = $this->getObjectUid($migration);
632✔
467

468
        return $migration;
632✔
469
    }
470

471
    /**
472
     * Allows other scripts to modify on the fly as needed.
473
     *
474
     * @return MigrationRunner
475
     */
476
    public function setNamespace(?string $namespace)
477
    {
478
        $this->namespace = $namespace;
647✔
479

480
        return $this;
647✔
481
    }
482

483
    /**
484
     * Allows other scripts to modify on the fly as needed.
485
     *
486
     * @return MigrationRunner
487
     */
488
    public function setGroup(string $group)
489
    {
490
        $this->group = $group;
614✔
491

492
        return $this;
614✔
493
    }
494

495
    /**
496
     * @return MigrationRunner
497
     */
498
    public function setName(string $name)
499
    {
500
        $this->name = $name;
621✔
501

502
        return $this;
621✔
503
    }
504

505
    /**
506
     * If $silent == true, then will not throw exceptions and will
507
     * attempt to continue gracefully.
508
     *
509
     * @return MigrationRunner
510
     */
511
    public function setSilent(bool $silent)
512
    {
513
        $this->silent = $silent;
688✔
514

515
        return $this;
688✔
516
    }
517

518
    /**
519
     * Extracts the migration number from a filename
520
     *
521
     * @param string $migration A migration filename w/o path.
522
     */
523
    protected function getMigrationNumber(string $migration): string
524
    {
525
        preg_match($this->regex, $migration, $matches);
636✔
526

527
        return $matches !== [] ? $matches[1] : '0';
636✔
528
    }
529

530
    /**
531
     * Extracts the migration name from a filename
532
     *
533
     * Note: The migration name should be the classname, but maybe they are
534
     *       different.
535
     *
536
     * @param string $migration A migration filename w/o path.
537
     */
538
    protected function getMigrationName(string $migration): string
539
    {
540
        preg_match($this->regex, $migration, $matches);
634✔
541

542
        return $matches !== [] ? $matches[2] : '';
634✔
543
    }
544

545
    /**
546
     * Uses the non-repeatable portions of a migration or history
547
     * to create a sortable unique key
548
     *
549
     * @param object $object migration or $history
550
     */
551
    public function getObjectUid($object): string
552
    {
553
        return preg_replace('/[^0-9]/', '', $object->version) . $object->class;
632✔
554
    }
555

556
    /**
557
     * Retrieves messages formatted for CLI output
558
     */
559
    public function getCliMessages(): array
560
    {
561
        return $this->cliMessages;
11✔
562
    }
563

564
    /**
565
     * Clears any CLI messages.
566
     *
567
     * @return MigrationRunner
568
     */
569
    public function clearCliMessages()
570
    {
571
        $this->cliMessages = [];
11✔
572

573
        return $this;
11✔
574
    }
575

576
    /**
577
     * Truncates the history table.
578
     */
579
    public function clearHistory()
580
    {
581
        if ($this->db->tableExists($this->table)) {
6✔
582
            $this->db->table($this->table)->truncate();
5✔
583
        }
584
    }
585

586
    /**
587
     * Add a history to the table.
588
     *
589
     * @param object $migration
590
     */
591
    protected function addHistory($migration, int $batch)
592
    {
593
        $this->db->table($this->table)->insert([
619✔
594
            'version'   => $migration->version,
619✔
595
            'class'     => $migration->class,
619✔
596
            'group'     => $this->group,
619✔
597
            'namespace' => $migration->namespace,
619✔
598
            'time'      => Time::now()->getTimestamp(),
619✔
599
            'batch'     => $batch,
619✔
600
        ]);
619✔
601

602
        if (is_cli()) {
619✔
603
            $this->cliMessages[] = sprintf(
619✔
604
                "\t%s(%s) %s_%s",
619✔
605
                CLI::color(lang('Migrations.added'), 'yellow'),
619✔
606
                $migration->namespace,
619✔
607
                $migration->version,
619✔
608
                $migration->class
619✔
609
            );
619✔
610
        }
611
    }
612

613
    /**
614
     * Removes a single history
615
     *
616
     * @param object $history
617
     */
618
    protected function removeHistory($history)
619
    {
620
        $this->db->table($this->table)->where('id', $history->id)->delete();
614✔
621

622
        if (is_cli()) {
614✔
623
            $this->cliMessages[] = sprintf(
614✔
624
                "\t%s(%s) %s_%s",
614✔
625
                CLI::color(lang('Migrations.removed'), 'yellow'),
614✔
626
                $history->namespace,
614✔
627
                $history->version,
614✔
628
                $history->class
614✔
629
            );
614✔
630
        }
631
    }
632

633
    /**
634
     * Grabs the full migration history from the database for a group
635
     */
636
    public function getHistory(string $group = 'default'): array
637
    {
638
        $this->ensureTable();
632✔
639

640
        $builder = $this->db->table($this->table);
632✔
641

642
        // If group was specified then use it
643
        if ($group !== '') {
632✔
644
            $builder->where('group', $group);
618✔
645
        }
646

647
        // If a namespace was specified then use it
648
        if ($this->namespace !== null) {
632✔
649
            $builder->where('namespace', $this->namespace);
627✔
650
        }
651

652
        $query = $builder->orderBy('id', 'ASC')->get();
632✔
653

654
        return ! empty($query) ? $query->getResultObject() : [];
632✔
655
    }
656

657
    /**
658
     * Returns the migration history for a single batch.
659
     *
660
     * @param string $order
661
     */
662
    public function getBatchHistory(int $batch, $order = 'asc'): array
663
    {
664
        $this->ensureTable();
614✔
665

666
        $query = $this->db->table($this->table)
614✔
667
            ->where('batch', $batch)
614✔
668
            ->orderBy('id', $order)
614✔
669
            ->get();
614✔
670

671
        return ! empty($query) ? $query->getResultObject() : [];
614✔
672
    }
673

674
    /**
675
     * Returns all the batches from the database history in order
676
     */
677
    public function getBatches(): array
678
    {
679
        $this->ensureTable();
634✔
680

681
        $batches = $this->db->table($this->table)
634✔
682
            ->select('batch')
634✔
683
            ->distinct()
634✔
684
            ->orderBy('batch', 'asc')
634✔
685
            ->get()
634✔
686
            ->getResultArray();
634✔
687

688
        return array_map(intval(...), array_column($batches, 'batch'));
634✔
689
    }
690

691
    /**
692
     * Returns the value of the last batch in the database.
693
     */
694
    public function getLastBatch(): int
695
    {
696
        $this->ensureTable();
630✔
697

698
        $batch = $this->db->table($this->table)
630✔
699
            ->selectMax('batch')
630✔
700
            ->get()
630✔
701
            ->getResultObject();
630✔
702

703
        $batch = is_array($batch) && $batch !== []
630✔
704
            ? end($batch)->batch
630✔
705
            : 0;
×
706

707
        return (int) $batch;
630✔
708
    }
709

710
    /**
711
     * Returns the version number of the first migration for a batch.
712
     * Mostly just for tests.
713
     */
714
    public function getBatchStart(int $batch): string
715
    {
716
        if ($batch < 0) {
1✔
717
            $batches = $this->getBatches();
×
718
            $batch   = $batches[count($batches) - 1] ?? 0;
×
719
        }
720

721
        $migration = $this->db->table($this->table)
1✔
722
            ->where('batch', $batch)
1✔
723
            ->orderBy('id', 'asc')
1✔
724
            ->limit(1)
1✔
725
            ->get()
1✔
726
            ->getResultObject();
1✔
727

728
        return $migration !== [] ? $migration[0]->version : '0';
1✔
729
    }
730

731
    /**
732
     * Returns the version number of the last migration for a batch.
733
     * Mostly just for tests.
734
     */
735
    public function getBatchEnd(int $batch): string
736
    {
737
        if ($batch < 0) {
5✔
738
            $batches = $this->getBatches();
×
739
            $batch   = $batches[count($batches) - 1] ?? 0;
×
740
        }
741

742
        $migration = $this->db->table($this->table)
5✔
743
            ->where('batch', $batch)
5✔
744
            ->orderBy('id', 'desc')
5✔
745
            ->limit(1)
5✔
746
            ->get()
5✔
747
            ->getResultObject();
5✔
748

749
        return $migration === [] ? '0' : $migration[0]->version;
5✔
750
    }
751

752
    /**
753
     * Ensures that we have created our migrations table
754
     * in the database.
755
     */
756
    public function ensureTable()
757
    {
758
        if ($this->tableChecked || $this->db->tableExists($this->table)) {
647✔
759
            return;
647✔
760
        }
761

762
        $forge = Database::forge($this->db);
4✔
763

764
        $forge->addField([
4✔
765
            'id' => [
4✔
766
                'type'           => 'BIGINT',
4✔
767
                'constraint'     => 20,
4✔
768
                'unsigned'       => true,
4✔
769
                'auto_increment' => true,
4✔
770
            ],
4✔
771
            'version' => [
4✔
772
                'type'       => 'VARCHAR',
4✔
773
                'constraint' => 255,
4✔
774
                'null'       => false,
4✔
775
            ],
4✔
776
            'class' => [
4✔
777
                'type'       => 'VARCHAR',
4✔
778
                'constraint' => 255,
4✔
779
                'null'       => false,
4✔
780
            ],
4✔
781
            'group' => [
4✔
782
                'type'       => 'VARCHAR',
4✔
783
                'constraint' => 255,
4✔
784
                'null'       => false,
4✔
785
            ],
4✔
786
            'namespace' => [
4✔
787
                'type'       => 'VARCHAR',
4✔
788
                'constraint' => 255,
4✔
789
                'null'       => false,
4✔
790
            ],
4✔
791
            'time' => [
4✔
792
                'type'       => 'INT',
4✔
793
                'constraint' => 11,
4✔
794
                'null'       => false,
4✔
795
            ],
4✔
796
            'batch' => [
4✔
797
                'type'       => 'INT',
4✔
798
                'constraint' => 11,
4✔
799
                'unsigned'   => true,
4✔
800
                'null'       => false,
4✔
801
            ],
4✔
802
        ]);
4✔
803

804
        $forge->addPrimaryKey('id');
4✔
805
        $forge->createTable($this->table, true);
4✔
806

807
        $this->tableChecked = true;
4✔
808
    }
809

810
    /**
811
     * Handles the actual running of a migration.
812
     *
813
     * @param string $direction "up" or "down"
814
     * @param object $migration The migration to run
815
     */
816
    protected function migrate($direction, $migration): bool
817
    {
818
        include_once $migration->path;
620✔
819

820
        $class = $migration->class;
620✔
821
        $this->setName($migration->name);
620✔
822

823
        // Validate the migration file structure
824
        if (! class_exists($class, false)) {
620✔
825
            $message = sprintf(lang('Migrations.classNotFound'), $class);
×
826

827
            if ($this->silent) {
×
828
                $this->cliMessages[] = "\t" . CLI::color($message, 'red');
×
829

830
                return false;
×
831
            }
832

833
            throw new RuntimeException($message);
×
834
        }
835

836
        /** @var Migration $instance */
837
        $instance = new $class(Database::forge($this->db));
620✔
838
        $group    = $instance->getDBGroup() ?? $this->group;
620✔
839

840
        if (ENVIRONMENT !== 'testing' && $group === 'tests' && $this->groupFilter !== 'tests') {
620✔
841
            // @codeCoverageIgnoreStart
842
            $this->groupSkip = true;
×
843

844
            return true;
×
845
            // @codeCoverageIgnoreEnd
846
        }
847

848
        if ($direction === 'up' && $this->groupFilter !== null && $this->groupFilter !== $group) {
620✔
849
            $this->groupSkip = true;
×
850

851
            return true;
×
852
        }
853

854
        if (! is_callable([$instance, $direction])) {
620✔
855
            $message = sprintf(lang('Migrations.missingMethod'), $direction);
×
856

857
            if ($this->silent) {
×
858
                $this->cliMessages[] = "\t" . CLI::color($message, 'red');
×
859

860
                return false;
×
861
            }
862

863
            throw new RuntimeException($message);
×
864
        }
865

866
        $instance->{$direction}();
620✔
867

868
        return true;
620✔
869
    }
870
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc