Failed Conditions
Push — master ( 196d3a...e5e4f4 )
by Adrien
12:14
created

AbstractDatabase::getPhp()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 1
c 1
b 0
f 0
nc 1
nop 0
dl 0
loc 3
ccs 0
cts 3
cp 0
crap 2
rs 10
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 'php7.4';
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
        $mysqlArgs = self::getMysqlArgs();
49
50
        echo "dumping $dumpFile...\n";
51
        $dumpCmd = "mysqldump -v $mysqlArgs | 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
        $mysqlArgs = self::getMysqlArgs();
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 . ' ./vendor/bin/doctrine orm:schema-tool:drop --ansi --full-database --force');
85
        self::executeLocalCommand("gunzip -c \"$dumpFile\" | sed  's/ALTER DATABASE `[^`]*`/ALTER DATABASE `$database`/g' | mysql $mysqlArgs");
86
        self::executeLocalCommand(PHP_BINARY . ' ./vendor/bin/doctrine-migrations migrations:migrate --ansi --no-interaction');
87
        static::loadTriggers();
88
        static::loadTestUsers();
89
    }
90
91
    private static function getDatabaseName(): string
92
    {
93
        $dbConfig = _em()->getConnection()->getParams();
94
95
        return $dbConfig['dbname'];
96
    }
97
98
    private static function getMysqlArgs(): string
99
    {
100
        $dbConfig = _em()->getConnection()->getParams();
101
102
        $host = $dbConfig['host'] ?? 'localhost';
103
        $username = $dbConfig['user'];
104
        $database = $dbConfig['dbname'];
105
        $password = $dbConfig['password'];
106
        $port = $dbConfig['port'] ?? 3306;
107
108
        // It's possible to have no password at all
109
        $password = $password ? '-p' . $password : '';
110
111
        return "--user=$username $password --host=$host --port=$port $database";
112
    }
113
114
    final public static function loadRemoteData(string $remote): void
115
    {
116
        $dumpFile = "/tmp/$remote." . exec('whoami') . '.backup.sql.gz';
117
        self::dumpDataRemotely($remote, $dumpFile);
118
        self::copyFile($remote, $dumpFile);
119
        self::loadData($dumpFile);
120
121
        echo "database updated\n";
122
    }
123
124
    /**
125
     * Execute a shell command and throw exception if fails
126
     */
127
    final public static function executeLocalCommand(string $command): void
128
    {
129
        // This allow to specify an application environnement even for commands that are not ours, such as Doctrine one.
130
        // Thus this allow us to correctly load test data in a separate test database for OKpilot.
131
        if (defined('APPLICATION_ENV')) {
132
            $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...
133
        } else {
134
            $env = '';
135
        }
136
137
        $return_var = null;
138
        $fullCommand = "$env $command 2>&1";
139
        passthru($fullCommand, $return_var);
140
        if ($return_var) {
141
            throw new Exception('FAILED executing: ' . $command);
142
        }
143
    }
144
145
    /**
146
     * Load test data
147
     */
148
    public static function loadTestData(): void
149
    {
150
        self::executeLocalCommand(PHP_BINARY . ' ./vendor/bin/doctrine orm:schema-tool:drop --ansi --full-database --force');
151
        self::executeLocalCommand(PHP_BINARY . ' ./vendor/bin/doctrine-migrations migrations:migrate --ansi --no-interaction');
152
        static::loadTriggers();
153
        static::loadTestUsers();
154
        self::importFile('tests/data/fixture.sql');
155
    }
156
157
    /**
158
     * Load triggers
159
     */
160
    public static function loadTriggers(): void
161
    {
162
        self::importFile('data/triggers.sql');
163
    }
164
165
    /**
166
     * Load test users
167
     */
168
    protected static function loadTestUsers(): void
169
    {
170
        self::importFile('tests/data/users.sql');
171
    }
172
173
    /**
174
     * Import a SQL file into DB
175
     *
176
     * This use mysql command, instead of DBAL methods, to allow to see errors if any, and
177
     * also because it seems trigger creation do not work with DBAL for some unclear reasons.
178
     */
179
    final public static function importFile(string $file): void
180
    {
181
        $file = self::absolutePath($file);
182
        $mysqlArgs = self::getMysqlArgs();
183
184
        echo 'importing ' . $file . "\n";
185
186
        $importCommand = "echo 'SET NAMES utf8mb4;' | cat - $file | mysql $mysqlArgs";
187
188
        self::executeLocalCommand($importCommand);
189
    }
190
191
    private static function absolutePath(string $file): string
192
    {
193
        $absolutePath = realpath($file);
194
        if ($absolutePath === false) {
195
            throw new Exception('Cannot find absolute path for file: ' . $file);
196
        }
197
198
        if (!is_readable($absolutePath)) {
199
            throw new Exception("Cannot read dump file \"$absolutePath\"");
200
        }
201
202
        return $absolutePath;
203
    }
204
}
205