Passed
Push — main ( 5326f7...3bbf58 )
by Thomas
12:47
created

Environment   A

Complexity

Total Complexity 19

Size/Duplication

Total Lines 146
Duplicated Lines 0 %

Test Coverage

Coverage 100%

Importance

Changes 0
Metric Value
eloc 84
dl 0
loc 146
ccs 79
cts 79
cp 1
rs 10
c 0
b 0
f 0
wmc 19

4 Methods

Rating   Name   Duplication   Size   Complexity  
A checkIfMigrationsTableExists() 0 35 5
A getMigrations() 0 30 6
B getMigrationsTableDDL() 0 40 6
A __construct() 0 23 2
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Conia\Quma;
6
7
use Conia\Cli\Opts;
8
use Conia\Quma\Connection;
9
use Conia\Quma\Database;
10
use PDO;
11
use RuntimeException;
12
use Throwable;
13
14
/**
15
 * @psalm-api
16
 *
17
 * @psalm-import-type MigrationDirs from \Conia\Quma\Connection
18
 */
19
class Environment
20
{
21
    public readonly Connection $conn;
22
    public readonly string $driver;
23
    public readonly bool $showStacktrace;
24
    public readonly bool $convenience;
25
    public readonly string $table;
26
    public readonly string $columnMigration;
27
    public readonly string $columnApplied;
28
    public readonly Database $db;
29
30
    /** @psalm-param array<non-empty-string, Connection> $connections */
31 39
    public function __construct(
32
        array $connections,
33
        public readonly array $options,
34
    ) {
35 39
        $opts = new Opts();
36
37
        try {
38 39
            $key = $opts->get('--conn', 'default');
39
            assert(isset($connections[$key]));
40 39
            $this->conn = $connections[$key];
41 1
        } catch (Throwable) {
42 1
            $key = $key ?? '<undefied>';
43
44 1
            throw new RuntimeException("Connection '{$key}' does not exist");
45
        }
46
47 38
        $this->showStacktrace = $opts->has('--stacktrace');
48 38
        $this->db = new Database($this->conn);
49 38
        $this->driver = $this->conn->driver;
50 38
        $this->convenience = in_array($this->driver, ['sqlite', 'mysql', 'pgsql']);
51 38
        $this->table = $this->conn->migrationsTable();
52 38
        $this->columnMigration = $this->conn->migrationsColumnMigration();
53 38
        $this->columnApplied = $this->conn->migrationsColumnApplied();
54
    }
55
56 22
    public function getMigrations(): array|false
57
    {
58
        /** @psalm-var MigrationDirs */
59 22
        $migrations = [];
60 22
        $migrationDirs = $this->conn->migrations();
61
62 22
        if (count($migrationDirs) === 0) {
63 1
            echo "\033[1;31mNotice\033[0m: No migration directories defined in configuration\033[0m\n";
64
65 1
            return false;
66
        }
67
68 21
        foreach ($migrationDirs as $path) {
69 21
            $migrations = array_merge(
70 21
                $migrations,
71 21
                array_filter(glob("{$path}/*.php"), 'is_file'),
72 21
                array_filter(glob("{$path}/*.sql"), 'is_file'),
73 21
                array_filter(glob("{$path}/*.tpql"), 'is_file'),
74 21
            );
75
        }
76
77
        // Sort by file name instead of full path
78 21
        uasort($migrations, function ($a, $b) {
79 21
            $a = is_string($a) ? $a : '';
80 21
            $b = is_string($b) ? $b : '';
81
82 21
            return (basename($a) < basename($b)) ? -1 : 1;
83 21
        });
84
85 21
        return $migrations;
86
    }
87
88 33
    public function checkIfMigrationsTableExists(Database $db): bool
89
    {
90 33
        $driver = $db->getPdoDriver();
91 33
        $table = $this->table;
92
93 33
        if ($driver === 'pgsql' && strpos($table, '.') !== false) {
94 9
            [$schema, $table] = explode('.', $table);
95
        } else {
96 24
            $schema = 'public';
97
        }
98
99 33
        $query = match ($driver) {
100 33
            'sqlite' => "
101
                SELECT count(*) AS available
102
                FROM sqlite_master
103
                WHERE type='table'
104 33
                AND name='{$table}';",
105
106 33
            'mysql' => "
107
                SELECT count(*) AS available
108
                FROM information_schema.tables
109 33
                WHERE table_name='{$table}';",
110
111 33
            'pgsql' => "
112
                SELECT count(*) AS available
113
                FROM pg_tables
114 33
                WHERE schemaname = '{$schema}'
115 33
                AND tablename = '{$table}';",
116 33
        };
117
118 33
        if ($query && ($db->execute($query)->one(PDO::FETCH_ASSOC)['available'] ?? 0) === 1) {
119 28
            return true;
120
        }
121
122 5
        return false;
123
    }
124
125 5
    public function getMigrationsTableDDL(): string|false
126
    {
127 5
        if ($this->driver === 'pgsql' && strpos($this->table, '.') !== false) {
128 1
            [$schema, $table] = explode('.', $this->table);
129
        } else {
130 4
            $schema = 'public';
131 4
            $table = $this->table;
132
        }
133 5
        $columnMigration = $this->columnMigration;
134 5
        $columnApplied = $this->columnApplied;
135
136 5
        switch ($this->driver) {
137 5
            case 'sqlite':
138 3
                return "CREATE TABLE {$table} (
139 3
    {$columnMigration} text NOT NULL,
140 3
    {$columnApplied} text DEFAULT CURRENT_TIMESTAMP,
141 3
    PRIMARY KEY ({$columnMigration}),
142 3
    CHECK(typeof(\"{$columnMigration}\") = \"text\" AND length(\"{$columnMigration}\") <= 256),
143 3
    CHECK(typeof(\"{$columnApplied}\") = \"text\" AND length(\"{$columnApplied}\") = 19)
144 3
);";
145
146 2
            case 'pgsql':
147 1
                return "CREATE TABLE {$schema}.{$table} (
148 1
    {$columnMigration} text NOT NULL CHECK (char_length({$columnMigration}) <= 256),
149 1
    {$columnApplied} timestamp with time zone DEFAULT now() NOT NULL,
150 1
    CONSTRAINT pk_{$table} PRIMARY KEY ({$columnMigration})
151 1
);";
152
153 1
            case 'mysql':
154 1
                return "CREATE TABLE {$table} (
155 1
    {$columnMigration} varchar(256) NOT NULL,
156 1
    {$columnApplied} timestamp DEFAULT CURRENT_TIMESTAMP,
157 1
    PRIMARY KEY ({$columnMigration})
158 1
);";
159
160
            default:
161
                // Cannot be reliably tested.
162
                // Would require an unsupported driver to be installed.
163
                // @codeCoverageIgnoreStart
164
                return false;
165
                // @codeCoverageIgnoreEnd
166
        }
167
    }
168
}
169