Passed
Push — master ( cdf2f8...25e095 )
by Sylvain
13:19
created

AbstractDatabase::getMariadbArgs()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 21
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 12

Importance

Changes 0
Metric Value
eloc 12
c 0
b 0
f 0
dl 0
loc 21
ccs 0
cts 12
cp 0
rs 9.8666
cc 3
nc 4
nop 0
crap 12
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Ecodev\Felix\Service;
6
7
use Exception;
8
9
/**
10
 * Tool to reload the entire local database from remote database for a given site.
11
 *
12
 * Requirements:
13
 *
14
 * - ssh access to remote server (via ~/.ssh/config)
15
 * - both local and remote sites must be accessible via: /sites/MY_SITE
16
 */
17
abstract class AbstractDatabase
18
{
19
    /**
20
     * This is lazy architecture, and we should instead convert the whole class
21
     * into instantiable service with configuration in constructor, and default
22
     * factory that get the PHP version from config.
23
     */
24
    protected static function getPhp(): string
25
    {
26
        return 'php8.2';
27
    }
28
29
    /**
30
     * Dump data from database on $remote server.
31
     */
32
    private static function dumpDataRemotely(string $remote, string $dumpFile): void
33
    {
34
        $php = static::getPhp();
35
        $sshCmd = <<<STRING
36
            ssh $remote "cd /sites/$remote/ && $php bin/dump-data.php $dumpFile"
37
            STRING;
38
39
        echo "dumping data $dumpFile on $remote...\n";
40
        self::executeLocalCommand($sshCmd);
41
    }
42
43
    /**
44
     * Dump data from database.
45
     */
46
    final public static function dumpData(string $dumpFile): void
47
    {
48
        $mariadbArgs = self::getMariadbArgs();
49
50
        echo "dumping $dumpFile...\n";
51
        $dumpCmd = "mariadb-dump -v $mariadbArgs | LC_CTYPE=C LANG=C sed 's/DEFINER=[^*]*\\*/\\*/g' | gzip > $dumpFile";
52
        self::executeLocalCommand($dumpCmd);
53
    }
54
55
    /**
56
     * Copy a file from $remote.
57
     */
58
    private static function copyFile(string $remote, string $dumpFile): void
59
    {
60
        $copyCmd = <<<STRING
61
            rsync -avz --progress $remote:$dumpFile $dumpFile
62
            STRING;
63
64
        echo "copying dump to $dumpFile ...\n";
65
        self::executeLocalCommand($copyCmd);
66
    }
67
68
    /**
69
     * Load SQL dump in local database.
70
     */
71
    final public static function loadData(string $dumpFile): void
72
    {
73
        $mariadbArgs = self::getMariadbArgs();
74
        $dumpFile = self::absolutePath($dumpFile);
75
76
        echo "loading dump $dumpFile...\n";
77
        $database = self::getDatabaseName();
78
79
        // We close the connection to DB here to avoid a timeout when loading the backup
80
        // It will be re-opened automatically
81
        echo "closing connection to DB\n";
82
        _em()->getConnection()->close();
83
84
        self::executeLocalCommand(PHP_BINARY . ' ./bin/doctrine orm:schema-tool:drop --ansi --full-database --force');
85
        self::executeLocalCommand("gunzip -c \"$dumpFile\" | LC_CTYPE=C LANG=C sed 's/ALTER DATABASE `[^`]*`/ALTER DATABASE `$database`/g' | mariadb $mariadbArgs");
86
        self::executeLocalCommand(PHP_BINARY . ' ./bin/doctrine migrations:migrate --ansi --no-interaction');
87
        static::loadTriggers();
88
        static::loadTestUsers();
89
    }
90
91
    private static function getDatabaseName(): string
92
    {
93
        /** @var array<string,string> $dbConfig */
94
        $dbConfig = _em()->getConnection()->getParams();
95
96
        return $dbConfig['dbname'];
97
    }
98
99
    private static function getMariadbArgs(): string
100
    {
101
        /** @var array<string,int|string> $dbConfig */
102
        $dbConfig = _em()->getConnection()->getParams();
103
104
        $host = $dbConfig['host'] ?? 'localhost';
105
        $username = $dbConfig['user'];
106
        $database = $dbConfig['dbname'];
107
        $password = $dbConfig['password'];
108
        $port = $dbConfig['port'] ?? null;
109
110
        if ($port) {
111
            $port = "--protocol tcp --port=$port";
112
        } else {
113
            $port = '--protocol socket';
114
        }
115
116
        // It's possible to have no password at all
117
        $password = $password ? '-p' . $password : '';
118
119
        return "--user=$username $password --host=$host $port $database";
120
    }
121
122
    final public static function loadRemoteData(string $remote): void
123
    {
124
        $dumpFile = "/tmp/$remote." . exec('whoami') . '.backup.sql.gz';
125
        self::dumpDataRemotely($remote, $dumpFile);
126
        self::copyFile($remote, $dumpFile);
127
        self::loadData($dumpFile);
128
129
        echo "database updated\n";
130
    }
131
132
    /**
133
     * Execute a shell command and throw exception if fails.
134
     */
135
    final public static function executeLocalCommand(string $command): void
136
    {
137
        // This allows to specify an application environnement even for commands that are not ours, such as Doctrine one.
138
        // Thus, this allows us to correctly load test data in a separate test database for OKpilot.
139
        if (defined('APPLICATION_ENV')) {
140
            $env = 'APPLICATION_ENV=' . APPLICATION_ENV;
0 ignored issues
show
Bug introduced by
The constant Ecodev\Felix\Service\APPLICATION_ENV was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
141
        } else {
142
            $env = '';
143
        }
144
145
        $return_var = null;
146
        $fullCommand = "$env $command 2>&1";
147
        passthru($fullCommand, $return_var);
148
        if ($return_var) {
149
            throw new Exception('FAILED executing: ' . $command);
150
        }
151
    }
152
153
    /**
154
     * Load test data.
155
     */
156
    public static function loadTestData(): void
157
    {
158
        self::executeLocalCommand(PHP_BINARY . ' ./bin/doctrine orm:schema-tool:drop --ansi --full-database --force');
159
        self::executeLocalCommand(PHP_BINARY . ' ./bin/doctrine migrations:migrate --ansi --no-interaction');
160
        static::loadTriggers();
161
        static::loadTestUsers();
162
        self::importFile('tests/data/fixture.sql');
163
    }
164
165
    /**
166
     * Load triggers.
167
     */
168
    public static function loadTriggers(): void
169
    {
170
        self::importFile('data/triggers.sql');
171
    }
172
173
    /**
174
     * Load test users.
175
     */
176
    protected static function loadTestUsers(): void
177
    {
178
        self::importFile('tests/data/users.sql');
179
    }
180
181
    /**
182
     * Import a SQL file into DB.
183
     *
184
     * This use mariadb command, instead of DBAL methods, to allow to see errors if any, and
185
     * also because it seems trigger creation do not work with DBAL for some unclear reasons.
186
     */
187
    final public static function importFile(string $file): void
188
    {
189
        $file = self::absolutePath($file);
190
        $mariadbArgs = self::getMariadbArgs();
191
192
        echo 'importing ' . $file . "\n";
193
194
        $importCommand = "echo 'SET NAMES utf8mb4;' | cat - $file | mariadb $mariadbArgs";
195
196
        self::executeLocalCommand($importCommand);
197
    }
198
199
    private static function absolutePath(string $file): string
200
    {
201
        $absolutePath = realpath($file);
202
        if ($absolutePath === false) {
203
            throw new Exception('Cannot find absolute path for file: ' . $file);
204
        }
205
206
        if (!is_readable($absolutePath)) {
207
            throw new Exception("Cannot read dump file \"$absolutePath\"");
208
        }
209
210
        return $absolutePath;
211
    }
212
}
213