Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
53 / 53
100.00% covered (success)
100.00%
5 / 5
CRAP
100.00% covered (success)
100.00%
1 / 1
DatabaseIntegrityChecker
100.00% covered (success)
100.00%
53 / 53
100.00% covered (success)
100.00%
5 / 5
22
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 check
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
4
 getAppliedMigrations
100.00% covered (success)
100.00%
16 / 16
100.00% covered (success)
100.00%
1 / 1
8
 getMigrationFiles
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
5
 generateSchemaFingerprint
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
4
1<?php
2
3declare(strict_types=1);
4
5namespace App\Shared\Diagnostics;
6
7use Yiisoft\Db\Connection\ConnectionInterface;
8
9 class DatabaseIntegrityChecker
10{
11    public function __construct(
12        private ConnectionInterface $db,
13        private string $migrationsDirectory
14    ) {}
15
16    /**
17     * @return array{status: string, pending_migrations: list<string>, applied_migrations_count: int, schema_fingerprint: string}
18     */
19    public function check(): array
20    {
21        $appliedMigrations = $this->getAppliedMigrations();
22        $allMigrationFiles = $this->getMigrationFiles();
23
24        $pending = [];
25        foreach ($allMigrationFiles as $file) {
26            if (!in_array($file, $appliedMigrations, true)) {
27                $pending[] = $file;
28            }
29        }
30
31        $fingerprint = $this->generateSchemaFingerprint();
32
33        return [
34            'status' => empty($pending) ? 'OK' : 'MIGRATIONS_PENDING',
35            'pending_migrations' => array_values($pending),
36            'applied_migrations_count' => count($appliedMigrations),
37            'schema_fingerprint' => $fingerprint,
38        ];
39    }
40
41    /**
42     * @return list<string>
43     */
44    private function getAppliedMigrations(): array
45    {
46        $result = [];
47        try {
48            $table = $this->db->getTablePrefix() . 'migration';
49            $schema = $this->db->getSchema()->getTableSchema($table);
50
51            if ($schema !== null) {
52                $column = null;
53                if ($schema->getColumn('name') !== null) {
54                    $column = 'name';
55                } elseif ($schema->getColumn('version') !== null) {
56                    $column = 'version';
57                }
58
59                if ($column !== null) {
60                    /** @var list<mixed> $rows */
61                    $rows = $this->db->createCommand('SELECT ' . $this->db->getQuoter()->quoteColumnName($column) . ' FROM {{%migration}} ORDER BY apply_time ASC')->queryColumn();
62
63                    foreach ($rows as $row) {
64                        if (is_string($row)) {
65                            $result[] = $row;
66                        }
67                    }
68                }
69            }
70        } catch (\Throwable) {
71            // Keep empty result
72        }
73
74        return $result;
75    }
76
77    /**
78     * @return list<string>
79     */
80    private function getMigrationFiles(): array
81    {
82        if (!is_dir($this->migrationsDirectory)) {
83            return [];
84        }
85
86        $files = scandir($this->migrationsDirectory);
87        if ($files === false) {
88            return [];
89        }
90
91        $migrations = [];
92        foreach ($files as $file) {
93            if (str_ends_with($file, '.php')) {
94                $migrations[] = pathinfo($file, PATHINFO_FILENAME);
95            }
96        }
97
98        sort($migrations);
99        return $migrations;
100    }
101
102    private function generateSchemaFingerprint(): string
103    {
104        try {
105            $tables = $this->db->getSchema()->getTableNames();
106            sort($tables);
107
108            $schemaData = [];
109            foreach ($tables as $table) {
110                $tableSchema = $this->db->getSchema()->getTableSchema($table);
111                if ($tableSchema !== null) {
112                    $cols = array_keys($tableSchema->getColumns());
113                    sort($cols);
114                    $schemaData[$table] = $cols;
115                }
116            }
117
118            return hash('sha256', (string) json_encode($schemaData));
119        } catch (\Throwable) {
120            return 'ERROR';
121        }
122    }
123}