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

codeigniter4 / CodeIgniter4 / 7191045469

13 Dec 2023 05:15AM UTC coverage: 85.233% (-0.004%) from 85.237%
7191045469

push

github

web-flow
fix: migrations not using custom DB connection of migration runner (#8221)

* Add failing test

* Add fix

* Fix setting of group

* Change priority order of migration's db group

7 of 8 new or added lines in 2 files covered. (87.5%)

1 existing line in 1 file now uncovered.

18597 of 21819 relevant lines covered (85.23%)

199.69 hits per line

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

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

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

12
namespace CodeIgniter\Database;
13

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

138
        $this->namespace = APP_NAMESPACE;
239✔
139

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

144
        $this->db = db_connect($db);
239✔
145
    }
146

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

161
        $this->ensureTable();
574✔
162

163
        if ($group !== null) {
574✔
164
            $this->groupFilter = $group;
559✔
165
            $this->setGroup($group);
559✔
166
        }
167

168
        $migrations = $this->findMigrations();
574✔
169

170
        if (empty($migrations)) {
574✔
171
            return true;
×
172
        }
173

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

178
        $batch = $this->getLastBatch() + 1;
574✔
179

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

185
                    continue;
×
186
                }
187

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

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

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

197
                    return false;
×
198
                }
199

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

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

208
        return true;
574✔
209
    }
210

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

230
        $this->ensureTable();
582✔
231

232
        $batches = $this->getBatches();
582✔
233

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

238
        if (empty($batches) && $targetBatch === 0) {
582✔
239
            return true;
28✔
240
        }
241

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

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

248
                return false;
×
249
            }
250

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

254
        $tmpNamespace = $this->namespace;
562✔
255

256
        $this->namespace = null;
562✔
257
        $allMigrations   = $this->findMigrations();
562✔
258

259
        $migrations = [];
562✔
260

261
        while ($batch = array_pop($batches)) {
562✔
262
            if ($batch <= $targetBatch) {
562✔
263
                break;
×
264
            }
265

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

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

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

275
                        return false;
×
276
                    }
277

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

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

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

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

296
                    return false;
×
297
                }
298

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

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

307
        $this->namespace = $tmpNamespace;
562✔
308

309
        return true;
562✔
310
    }
311

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

326
        $this->ensureTable();
×
327

328
        if ($group !== null) {
×
329
            $this->groupFilter = $group;
×
330
            $this->setGroup($group);
×
331
        }
332

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

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

340
                return false;
×
341
            }
342

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

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

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

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

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

363
                return true;
×
364
            }
365

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

370
            return true;
×
371
        }
372

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

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

378
            return false;
×
379
        }
380

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

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

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

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

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

407
        return $migrations;
577✔
408
    }
409

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

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

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

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

434
        return $migrations;
577✔
435
    }
436

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

450
        $filename = basename($path, '.php');
576✔
451

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

456
        $locator = Services::locator(true);
576✔
457

458
        $migration = new stdClass();
576✔
459

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

467
        return $migration;
576✔
468
    }
469

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

479
        return $this;
591✔
480
    }
481

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

491
        return $this;
560✔
492
    }
493

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

501
        return $this;
569✔
502
    }
503

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

514
        return $this;
606✔
515
    }
516

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

526
        return count($matches) ? $matches[1] : '0';
580✔
527
    }
528

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

541
        return count($matches) ? $matches[2] : '';
578✔
542
    }
543

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

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

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

572
        return $this;
9✔
573
    }
574

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

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

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

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

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

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

639
        $builder = $this->db->table($this->table);
576✔
640

641
        // If group was specified then use it
642
        if (! empty($group)) {
576✔
643
            $builder->where('group', $group);
564✔
644
        }
645

646
        // If a namespace was specified then use it
647
        if ($this->namespace) {
576✔
648
            $builder->where('namespace', $this->namespace);
573✔
649
        }
650

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

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

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

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

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

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

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

687
        return array_map('intval', array_column($batches, 'batch'));
582✔
688
    }
689

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

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

702
        $batch = is_array($batch) && count($batch)
574✔
703
            ? end($batch)->batch
574✔
704
            : 0;
×
705

706
        return (int) $batch;
574✔
707
    }
708

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

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

727
        return count($migration) ? $migration[0]->version : '0';
1✔
728
    }
729

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

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

748
        return count($migration) ? $migration[0]->version : 0;
5✔
749
    }
750

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

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

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

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

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

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

819
        $class = $migration->class;
568✔
820
        $this->setName($migration->name);
568✔
821

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

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

829
                return false;
×
830
            }
831

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

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

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

843
            return true;
844
            // @codeCoverageIgnoreEnd
845
        }
846

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

850
            return true;
×
851
        }
852

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

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

859
                return false;
×
860
            }
861

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

865
        $instance->{$direction}();
568✔
866

867
        return true;
568✔
868
    }
869
}
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