Passed
Push — main ( 9c95cc...5952b6 )
by Thomas
01:49
created

Environment::getMigrations()   A

Complexity

Conditions 6
Paths 3

Size

Total Lines 30
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 19
CRAP Score 6

Importance

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