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

codeigniter4 / CodeIgniter4 / 14569795065

21 Apr 2025 07:55AM UTC coverage: 84.402% (+0.007%) from 84.395%
14569795065

Pull #9528

github

web-flow
Merge 4ad1f19d8 into 3d3ba0512
Pull Request #9528: feat: add Time::addCalendarMonths() function

11 of 11 new or added lines in 1 file covered. (100.0%)

136 existing lines in 21 files now uncovered.

20827 of 24676 relevant lines covered (84.4%)

191.03 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\Exceptions\RuntimeException;
20
use CodeIgniter\I18n\Time;
21
use Config\Database;
22
use Config\Migrations as MigrationsConfig;
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;
714✔
137
        $this->table   = $config->table ?? 'migrations';
714✔
138

139
        $this->namespace = APP_NAMESPACE;
714✔
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;
714✔
144

145
        $this->db = db_connect($db);
714✔
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) {
653✔
159
            throw ConfigException::forDisabledMigrations();
1✔
160
        }
161

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

164
        if ($group !== null) {
652✔
165
            $this->groupFilter = $group;
635✔
166
            $this->setGroup($group);
635✔
167
        }
168

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

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

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

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

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

186
                    continue;
×
187
                }
188

189
                $this->addHistory($migration, $batch);
641✔
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);
652✔
206
        $data['method'] = 'latest';
652✔
207
        Events::trigger('migrate', $data);
652✔
208

209
        return true;
652✔
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) {
656✔
228
            throw ConfigException::forDisabledMigrations();
×
229
        }
230

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

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

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

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

243
        if ($targetBatch !== 0 && ! in_array($targetBatch, $batches, true)) {
636✔
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;
636✔
256

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

260
        $migrations = [];
636✔
261

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

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

270
                if (! isset($allMigrations[$uid])) {
636✔
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];
636✔
283
                $migration->history = $history;
636✔
284
                $migrations[]       = $migration;
636✔
285
            }
286
        }
287

288
        foreach ($migrations as $migration) {
636✔
289
            if ($this->migrate('down', $migration)) {
636✔
290
                $this->removeHistory($migration->history);
636✔
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);
636✔
305
        $data['method'] = 'regress';
636✔
306
        Events::trigger('migrate', $data);
636✔
307

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

310
        return true;
636✔
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
     * @return bool
322
     */
323
    public function force(string $path, string $namespace, ?string $group = null)
324
    {
UNCOV
325
        if (! $this->enabled) {
×
UNCOV
326
            throw ConfigException::forDisabledMigrations();
×
327
        }
328

329
        $this->ensureTable();
×
330

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

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

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

UNCOV
343
                return false;
×
344
            }
345

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

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

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

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

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

UNCOV
366
                return true;
×
367
            }
368

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

UNCOV
373
            return true;
×
374
        }
375

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

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

UNCOV
381
            return false;
×
382
        }
383

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

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

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

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

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

410
        return $migrations;
655✔
411
    }
412

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

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

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

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

437
        return $migrations;
655✔
438
    }
439

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

453
        $filename = basename($path, '.php');
654✔
454

455
        if (preg_match($this->regex, $filename) !== 1) {
654✔
UNCOV
456
            return false;
×
457
        }
458

459
        $locator = service('locator', true);
654✔
460

461
        $migration = new stdClass();
654✔
462

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

470
        return $migration;
654✔
471
    }
472

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

482
        return $this;
669✔
483
    }
484

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

494
        return $this;
636✔
495
    }
496

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

504
        return $this;
643✔
505
    }
506

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

517
        return $this;
710✔
518
    }
519

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

529
        return $matches !== [] ? $matches[1] : '0';
658✔
530
    }
531

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

544
        return $matches !== [] ? $matches[2] : '';
656✔
545
    }
546

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

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

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

575
        return $this;
11✔
576
    }
577

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

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

608
        if (is_cli()) {
641✔
609
            $this->cliMessages[] = sprintf(
641✔
610
                "\t%s(%s) %s_%s",
641✔
611
                CLI::color(lang('Migrations.added'), 'yellow'),
641✔
612
                $migration->namespace,
641✔
613
                $migration->version,
641✔
614
                $migration->class,
641✔
615
            );
641✔
616
        }
617
    }
618

619
    /**
620
     * Removes a single history
621
     *
622
     * @param object $history
623
     *
624
     * @return void
625
     */
626
    protected function removeHistory($history)
627
    {
628
        $this->db->table($this->table)->where('id', $history->id)->delete();
636✔
629

630
        if (is_cli()) {
636✔
631
            $this->cliMessages[] = sprintf(
636✔
632
                "\t%s(%s) %s_%s",
636✔
633
                CLI::color(lang('Migrations.removed'), 'yellow'),
636✔
634
                $history->namespace,
636✔
635
                $history->version,
636✔
636
                $history->class,
636✔
637
            );
636✔
638
        }
639
    }
640

641
    /**
642
     * Grabs the full migration history from the database for a group
643
     */
644
    public function getHistory(string $group = 'default'): array
645
    {
646
        $this->ensureTable();
654✔
647

648
        $builder = $this->db->table($this->table);
654✔
649

650
        // If group was specified then use it
651
        if ($group !== '') {
654✔
652
            $builder->where('group', $group);
640✔
653
        }
654

655
        // If a namespace was specified then use it
656
        if ($this->namespace !== null) {
654✔
657
            $builder->where('namespace', $this->namespace);
649✔
658
        }
659

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

662
        return ! empty($query) ? $query->getResultObject() : [];
654✔
663
    }
664

665
    /**
666
     * Returns the migration history for a single batch.
667
     *
668
     * @param string $order
669
     */
670
    public function getBatchHistory(int $batch, $order = 'asc'): array
671
    {
672
        $this->ensureTable();
636✔
673

674
        $query = $this->db->table($this->table)
636✔
675
            ->where('batch', $batch)
636✔
676
            ->orderBy('id', $order)
636✔
677
            ->get();
636✔
678

679
        return ! empty($query) ? $query->getResultObject() : [];
636✔
680
    }
681

682
    /**
683
     * Returns all the batches from the database history in order
684
     */
685
    public function getBatches(): array
686
    {
687
        $this->ensureTable();
656✔
688

689
        $batches = $this->db->table($this->table)
656✔
690
            ->select('batch')
656✔
691
            ->distinct()
656✔
692
            ->orderBy('batch', 'asc')
656✔
693
            ->get()
656✔
694
            ->getResultArray();
656✔
695

696
        return array_map(intval(...), array_column($batches, 'batch'));
656✔
697
    }
698

699
    /**
700
     * Returns the value of the last batch in the database.
701
     */
702
    public function getLastBatch(): int
703
    {
704
        $this->ensureTable();
652✔
705

706
        $batch = $this->db->table($this->table)
652✔
707
            ->selectMax('batch')
652✔
708
            ->get()
652✔
709
            ->getResultObject();
652✔
710

711
        $batch = is_array($batch) && $batch !== []
652✔
712
            ? end($batch)->batch
652✔
UNCOV
713
            : 0;
×
714

715
        return (int) $batch;
652✔
716
    }
717

718
    /**
719
     * Returns the version number of the first migration for a batch.
720
     * Mostly just for tests.
721
     */
722
    public function getBatchStart(int $batch): string
723
    {
724
        if ($batch < 0) {
1✔
UNCOV
725
            $batches = $this->getBatches();
×
UNCOV
726
            $batch   = $batches[count($batches) - 1] ?? 0;
×
727
        }
728

729
        $migration = $this->db->table($this->table)
1✔
730
            ->where('batch', $batch)
1✔
731
            ->orderBy('id', 'asc')
1✔
732
            ->limit(1)
1✔
733
            ->get()
1✔
734
            ->getResultObject();
1✔
735

736
        return $migration !== [] ? $migration[0]->version : '0';
1✔
737
    }
738

739
    /**
740
     * Returns the version number of the last migration for a batch.
741
     * Mostly just for tests.
742
     */
743
    public function getBatchEnd(int $batch): string
744
    {
745
        if ($batch < 0) {
5✔
UNCOV
746
            $batches = $this->getBatches();
×
UNCOV
747
            $batch   = $batches[count($batches) - 1] ?? 0;
×
748
        }
749

750
        $migration = $this->db->table($this->table)
5✔
751
            ->where('batch', $batch)
5✔
752
            ->orderBy('id', 'desc')
5✔
753
            ->limit(1)
5✔
754
            ->get()
5✔
755
            ->getResultObject();
5✔
756

757
        return $migration === [] ? '0' : $migration[0]->version;
5✔
758
    }
759

760
    /**
761
     * Ensures that we have created our migrations table
762
     * in the database.
763
     *
764
     * @return void
765
     */
766
    public function ensureTable()
767
    {
768
        if ($this->tableChecked || $this->db->tableExists($this->table)) {
669✔
769
            return;
669✔
770
        }
771

772
        $forge = Database::forge($this->db);
4✔
773

774
        $forge->addField([
4✔
775
            'id' => [
4✔
776
                'type'           => 'BIGINT',
4✔
777
                'constraint'     => 20,
4✔
778
                'unsigned'       => true,
4✔
779
                'auto_increment' => true,
4✔
780
            ],
4✔
781
            'version' => [
4✔
782
                'type'       => 'VARCHAR',
4✔
783
                'constraint' => 255,
4✔
784
                'null'       => false,
4✔
785
            ],
4✔
786
            'class' => [
4✔
787
                'type'       => 'VARCHAR',
4✔
788
                'constraint' => 255,
4✔
789
                'null'       => false,
4✔
790
            ],
4✔
791
            'group' => [
4✔
792
                'type'       => 'VARCHAR',
4✔
793
                'constraint' => 255,
4✔
794
                'null'       => false,
4✔
795
            ],
4✔
796
            'namespace' => [
4✔
797
                'type'       => 'VARCHAR',
4✔
798
                'constraint' => 255,
4✔
799
                'null'       => false,
4✔
800
            ],
4✔
801
            'time' => [
4✔
802
                'type'       => 'INT',
4✔
803
                'constraint' => 11,
4✔
804
                'null'       => false,
4✔
805
            ],
4✔
806
            'batch' => [
4✔
807
                'type'       => 'INT',
4✔
808
                'constraint' => 11,
4✔
809
                'unsigned'   => true,
4✔
810
                'null'       => false,
4✔
811
            ],
4✔
812
        ]);
4✔
813

814
        $forge->addPrimaryKey('id');
4✔
815
        $forge->createTable($this->table, true);
4✔
816

817
        $this->tableChecked = true;
4✔
818
    }
819

820
    /**
821
     * Handles the actual running of a migration.
822
     *
823
     * @param string $direction "up" or "down"
824
     * @param object $migration The migration to run
825
     */
826
    protected function migrate($direction, $migration): bool
827
    {
828
        include_once $migration->path;
642✔
829

830
        $class = $migration->class;
642✔
831
        $this->setName($migration->name);
642✔
832

833
        // Validate the migration file structure
834
        if (! class_exists($class, false)) {
642✔
UNCOV
835
            $message = sprintf(lang('Migrations.classNotFound'), $class);
×
836

UNCOV
837
            if ($this->silent) {
×
UNCOV
838
                $this->cliMessages[] = "\t" . CLI::color($message, 'red');
×
839

UNCOV
840
                return false;
×
841
            }
842

UNCOV
843
            throw new RuntimeException($message);
×
844
        }
845

846
        /** @var Migration $instance */
847
        $instance = new $class(Database::forge($this->db));
642✔
848
        $group    = $instance->getDBGroup() ?? $this->group;
642✔
849

850
        if (ENVIRONMENT !== 'testing' && $group === 'tests' && $this->groupFilter !== 'tests') {
642✔
851
            // @codeCoverageIgnoreStart
UNCOV
852
            $this->groupSkip = true;
×
853

UNCOV
854
            return true;
×
855
            // @codeCoverageIgnoreEnd
856
        }
857

858
        if ($direction === 'up' && $this->groupFilter !== null && $this->groupFilter !== $group) {
642✔
UNCOV
859
            $this->groupSkip = true;
×
860

UNCOV
861
            return true;
×
862
        }
863

864
        if (! is_callable([$instance, $direction])) {
642✔
UNCOV
865
            $message = sprintf(lang('Migrations.missingMethod'), $direction);
×
866

UNCOV
867
            if ($this->silent) {
×
UNCOV
868
                $this->cliMessages[] = "\t" . CLI::color($message, 'red');
×
869

UNCOV
870
                return false;
×
871
            }
872

UNCOV
873
            throw new RuntimeException($message);
×
874
        }
875

876
        $instance->{$direction}();
642✔
877

878
        return true;
642✔
879
    }
880
}
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