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

codeigniter4 / CodeIgniter4 / 8677009716

13 Apr 2024 11:45PM UTC coverage: 84.44% (-2.2%) from 86.607%
8677009716

push

github

web-flow
Merge pull request #8776 from kenjis/fix-findQualifiedNameFromPath-Cannot-declare-class-v3

fix: Cannot declare class CodeIgniter\Config\Services, because the name is already in use

0 of 3 new or added lines in 1 file covered. (0.0%)

478 existing lines in 72 files now uncovered.

20318 of 24062 relevant lines covered (84.44%)

188.23 hits per line

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

72.99
/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 Config\Services;
23
use RuntimeException;
24
use stdClass;
25

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

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

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

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

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

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

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

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

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

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

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

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

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

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

140
        $this->namespace = APP_NAMESPACE;
680✔
141

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

146
        $this->db = db_connect($db);
680✔
147
    }
148

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

163
        $this->ensureTable();
621✔
164

165
        if ($group !== null) {
621✔
166
            $this->groupFilter = $group;
606✔
167
            $this->setGroup($group);
606✔
168
        }
169

170
        $migrations = $this->findMigrations();
621✔
171

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

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

180
        $batch = $this->getLastBatch() + 1;
621✔
181

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

187
                    continue;
×
188
                }
189

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

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

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

199
                    return false;
×
200
                }
201

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

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

210
        return true;
621✔
211
    }
212

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

232
        $this->ensureTable();
625✔
233

234
        $batches = $this->getBatches();
625✔
235

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

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

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

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

250
                return false;
×
251
            }
252

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

256
        $tmpNamespace = $this->namespace;
605✔
257

258
        $this->namespace = null;
605✔
259
        $allMigrations   = $this->findMigrations();
605✔
260

261
        $migrations = [];
605✔
262

263
        while ($batch = array_pop($batches)) {
605✔
264
            if ($batch <= $targetBatch) {
605✔
265
                break;
×
266
            }
267

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

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

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

277
                        return false;
×
278
                    }
279

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

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

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

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

298
                    return false;
×
299
                }
300

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

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

309
        $this->namespace = $tmpNamespace;
605✔
310

311
        return true;
605✔
312
    }
313

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

328
        $this->ensureTable();
×
329

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

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

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

342
                return false;
×
343
            }
344

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

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

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

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

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

365
                return true;
×
366
            }
367

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

372
            return true;
×
373
        }
374

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

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

380
            return false;
×
381
        }
382

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

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

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

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

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

409
        return $migrations;
624✔
410
    }
411

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

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

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

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

436
        return $migrations;
624✔
437
    }
438

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

452
        $filename = basename($path, '.php');
623✔
453

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

458
        $locator = Services::locator(true);
623✔
459

460
        $migration = new stdClass();
623✔
461

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

469
        return $migration;
623✔
470
    }
471

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

481
        return $this;
638✔
482
    }
483

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

493
        return $this;
607✔
494
    }
495

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

503
        return $this;
612✔
504
    }
505

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

516
        return $this;
676✔
517
    }
518

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

528
        return count($matches) ? $matches[1] : '0';
627✔
529
    }
530

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

543
        return count($matches) ? $matches[2] : '';
625✔
544
    }
545

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

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

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

574
        return $this;
9✔
575
    }
576

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

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

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

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

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

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

641
        $builder = $this->db->table($this->table);
623✔
642

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

648
        // If a namespace was specified then use it
649
        if ($this->namespace) {
623✔
650
            $builder->where('namespace', $this->namespace);
620✔
651
        }
652

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

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

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

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

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

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

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

689
        return array_map('intval', array_column($batches, 'batch'));
625✔
690
    }
691

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

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

704
        $batch = is_array($batch) && count($batch)
621✔
705
            ? end($batch)->batch
621✔
706
            : 0;
×
707

708
        return (int) $batch;
621✔
709
    }
710

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

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

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

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

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

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

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

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

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

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

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

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

821
        $class = $migration->class;
611✔
822
        $this->setName($migration->name);
611✔
823

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

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

831
                return false;
×
832
            }
833

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

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

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

UNCOV
845
            return true;
×
846
            // @codeCoverageIgnoreEnd
847
        }
848

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

852
            return true;
×
853
        }
854

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

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

861
                return false;
×
862
            }
863

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

867
        $instance->{$direction}();
611✔
868

869
        return true;
611✔
870
    }
871
}
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