1
|
|
|
<?php |
2
|
|
|
/** |
3
|
|
|
* This file is part of the DB-Migration library. |
4
|
|
|
* For the full copyright and license information, please view |
5
|
|
|
* the LICENSE file that was distributed with this source code. |
6
|
|
|
* |
7
|
|
|
* @copyright Alexander Obuhovich <[email protected]> |
8
|
|
|
* @link https://github.com/console-helpers/db-migration |
9
|
|
|
*/ |
10
|
|
|
|
11
|
|
|
namespace ConsoleHelpers\DatabaseMigration; |
12
|
|
|
|
13
|
|
|
|
14
|
|
|
class MigrationManager |
15
|
|
|
{ |
16
|
|
|
|
17
|
|
|
/** |
18
|
|
|
* Migrations directory. |
19
|
|
|
* |
20
|
|
|
* @var string |
21
|
|
|
*/ |
22
|
|
|
private $_migrationsDirectory; |
23
|
|
|
|
24
|
|
|
/** |
25
|
|
|
* Container. |
26
|
|
|
* |
27
|
|
|
* @var \ArrayAccess |
28
|
|
|
*/ |
29
|
|
|
private $_container; |
30
|
|
|
|
31
|
|
|
/** |
32
|
|
|
* Migration manager context. |
33
|
|
|
* |
34
|
|
|
* @var MigrationContext |
35
|
|
|
*/ |
36
|
|
|
private $_context; |
37
|
|
|
|
38
|
|
|
/** |
39
|
|
|
* Migration runners. |
40
|
|
|
* |
41
|
|
|
* @var AbstractMigrationRunner[] |
42
|
|
|
*/ |
43
|
|
|
private $_migrationRunners = array(); |
44
|
|
|
|
45
|
|
|
/** |
46
|
|
|
* Creates migration manager instance. |
47
|
|
|
* |
48
|
|
|
* @param string $migrations_directory Migrations directory. |
49
|
|
|
* @param \ArrayAccess $container Container. |
50
|
|
|
* |
51
|
|
|
* @throws \InvalidArgumentException When migrations directory does not exist. |
52
|
|
|
*/ |
53
|
16 |
|
public function __construct($migrations_directory, \ArrayAccess $container) |
54
|
|
|
{ |
55
|
16 |
|
if ( !file_exists($migrations_directory) || !is_dir($migrations_directory) ) { |
56
|
2 |
|
throw new \InvalidArgumentException( |
57
|
2 |
|
'The "' . $migrations_directory . '" does not exist or not a directory.' |
58
|
2 |
|
); |
59
|
|
|
} |
60
|
|
|
|
61
|
14 |
|
$this->_migrationsDirectory = $migrations_directory; |
62
|
14 |
|
$this->_container = $container; |
63
|
|
|
} |
64
|
|
|
|
65
|
|
|
/** |
66
|
|
|
* Registers a migration runner. |
67
|
|
|
* |
68
|
|
|
* @param AbstractMigrationRunner $migration_runner Migration runner. |
69
|
|
|
* |
70
|
|
|
* @return void |
71
|
|
|
*/ |
72
|
13 |
|
public function registerMigrationRunner(AbstractMigrationRunner $migration_runner) |
73
|
|
|
{ |
74
|
13 |
|
$this->_migrationRunners[$migration_runner->getFileExtension()] = $migration_runner; |
75
|
|
|
} |
76
|
|
|
|
77
|
|
|
/** |
78
|
|
|
* Creates new migration. |
79
|
|
|
* |
80
|
|
|
* @param string $name Migration name. |
81
|
|
|
* @param string $file_extension Migration file extension. |
82
|
|
|
* |
83
|
|
|
* @return string |
84
|
|
|
* @throws \InvalidArgumentException When migration name/file extension is invalid. |
85
|
|
|
* @throws \LogicException When new migration already exists. |
86
|
|
|
*/ |
87
|
6 |
|
public function createMigration($name, $file_extension) |
88
|
|
|
{ |
89
|
6 |
|
if ( preg_replace('/[a-z\d\._]/', '', $name) ) { |
90
|
3 |
|
throw new \InvalidArgumentException( |
91
|
3 |
|
'The migration name can consist only from alpha-numeric characters, as well as dots and underscores.' |
92
|
3 |
|
); |
93
|
|
|
} |
94
|
|
|
|
95
|
3 |
|
if ( !in_array($file_extension, $this->getMigrationFileExtensions()) ) { |
96
|
1 |
|
throw new \InvalidArgumentException( |
97
|
1 |
|
'The migration runner for "' . $file_extension . '" file extension is not registered.' |
98
|
1 |
|
); |
99
|
|
|
} |
100
|
|
|
|
101
|
2 |
|
$migration_file = $this->_migrationsDirectory . '/' . date('Ymd_Hi') . '_' . $name . '.' . $file_extension; |
102
|
|
|
|
103
|
2 |
|
if ( file_exists($migration_file) ) { |
104
|
1 |
|
throw new \LogicException('The migration file "' . basename($migration_file) . '" already exists.'); |
105
|
|
|
} |
106
|
|
|
|
107
|
2 |
|
file_put_contents($migration_file, $this->_migrationRunners[$file_extension]->getTemplate()); |
108
|
|
|
|
109
|
2 |
|
return basename($migration_file); |
110
|
|
|
} |
111
|
|
|
|
112
|
|
|
/** |
113
|
|
|
* Returns supported migration file extensions. |
114
|
|
|
* |
115
|
|
|
* @return array |
116
|
|
|
* @throws \LogicException When no migration runners added. |
117
|
|
|
*/ |
118
|
11 |
|
public function getMigrationFileExtensions() |
119
|
|
|
{ |
120
|
11 |
|
if ( !$this->_migrationRunners ) { |
|
|
|
|
121
|
1 |
|
throw new \LogicException('No migrations runners registered.'); |
122
|
|
|
} |
123
|
|
|
|
124
|
10 |
|
return array_keys($this->_migrationRunners); |
125
|
|
|
} |
126
|
|
|
|
127
|
|
|
/** |
128
|
|
|
* Executes outstanding migrations. |
129
|
|
|
* |
130
|
|
|
* @param MigrationContext $context Context. |
131
|
|
|
* |
132
|
|
|
* @return void |
133
|
|
|
*/ |
134
|
6 |
|
public function run(MigrationContext $context) |
135
|
|
|
{ |
136
|
6 |
|
$this->setContext($context); |
137
|
6 |
|
$this->createMigrationsTable(); |
138
|
|
|
|
139
|
6 |
|
$all_migrations = $this->getAllMigrations(); |
140
|
6 |
|
$executed_migrations = $this->getExecutedMigrations(); |
141
|
|
|
|
142
|
6 |
|
$migrations_to_execute = array_diff($all_migrations, $executed_migrations); |
143
|
6 |
|
$this->executeMigrations($migrations_to_execute); |
144
|
|
|
|
145
|
6 |
|
$migrations_to_delete = array_diff($executed_migrations, $all_migrations); |
146
|
6 |
|
$this->deleteMigrations($migrations_to_delete); |
147
|
|
|
} |
148
|
|
|
|
149
|
|
|
/** |
150
|
|
|
* Sets current context. |
151
|
|
|
* |
152
|
|
|
* @param MigrationContext $context Context. |
153
|
|
|
* |
154
|
|
|
* @return void |
155
|
|
|
*/ |
156
|
6 |
|
protected function setContext(MigrationContext $context) |
157
|
|
|
{ |
158
|
6 |
|
$this->_context = $context; |
159
|
6 |
|
$this->_context->setContainer($this->_container); |
160
|
|
|
} |
161
|
|
|
|
162
|
|
|
/** |
163
|
|
|
* Creates migration table, when missing. |
164
|
|
|
* |
165
|
|
|
* @return void |
166
|
|
|
*/ |
167
|
6 |
|
protected function createMigrationsTable() |
168
|
|
|
{ |
169
|
6 |
|
$db = $this->_context->getDatabase(); |
170
|
|
|
|
171
|
6 |
|
$sql = "SELECT name |
172
|
|
|
FROM sqlite_master |
173
|
6 |
|
WHERE type = 'table' AND name = :table_name"; |
174
|
6 |
|
$migrations_table = $db->fetchValue($sql, array('table_name' => 'Migrations')); |
175
|
|
|
|
176
|
6 |
|
if ( $migrations_table !== false ) { |
177
|
3 |
|
return; |
178
|
|
|
} |
179
|
|
|
|
180
|
6 |
|
$sql = 'CREATE TABLE "Migrations" ( |
181
|
|
|
"Name" TEXT(255,0) NOT NULL, |
182
|
|
|
"ExecutedOn" INTEGER NOT NULL, |
183
|
|
|
PRIMARY KEY("Name") |
184
|
6 |
|
)'; |
185
|
6 |
|
$db->perform($sql); |
186
|
|
|
} |
187
|
|
|
|
188
|
|
|
/** |
189
|
|
|
* Returns all migrations. |
190
|
|
|
* |
191
|
|
|
* @return array |
192
|
|
|
*/ |
193
|
6 |
|
protected function getAllMigrations() |
194
|
|
|
{ |
195
|
6 |
|
$migrations = array(); |
196
|
6 |
|
$file_extensions = $this->getMigrationFileExtensions(); |
197
|
|
|
|
198
|
|
|
// Use "DirectoryIterator" instead of "glob", because it works within PHAR files as well. |
199
|
6 |
|
$directory_iterator = new \DirectoryIterator($this->_migrationsDirectory); |
200
|
|
|
|
201
|
6 |
|
foreach ( $directory_iterator as $file ) { |
202
|
6 |
|
if ( $file->isFile() && in_array($file->getExtension(), $file_extensions) ) { |
203
|
4 |
|
$migrations[] = $file->getBasename(); |
204
|
|
|
} |
205
|
|
|
} |
206
|
|
|
|
207
|
6 |
|
sort($migrations); |
208
|
|
|
|
209
|
6 |
|
return $migrations; |
210
|
|
|
} |
211
|
|
|
|
212
|
|
|
/** |
213
|
|
|
* Returns executed migrations. |
214
|
|
|
* |
215
|
|
|
* @return array |
216
|
|
|
*/ |
217
|
6 |
|
protected function getExecutedMigrations() |
218
|
|
|
{ |
219
|
6 |
|
$sql = 'SELECT Name |
220
|
6 |
|
FROM Migrations'; |
221
|
|
|
|
222
|
6 |
|
return $this->_context->getDatabase()->fetchCol($sql); |
223
|
|
|
} |
224
|
|
|
|
225
|
|
|
/** |
226
|
|
|
* Executes migrations. |
227
|
|
|
* |
228
|
|
|
* @param array $migrations Migrations. |
229
|
|
|
* |
230
|
|
|
* @return void |
231
|
|
|
*/ |
232
|
6 |
|
protected function executeMigrations(array $migrations) |
233
|
|
|
{ |
234
|
6 |
|
if ( !$migrations ) { |
|
|
|
|
235
|
4 |
|
return; |
236
|
|
|
} |
237
|
|
|
|
238
|
4 |
|
$db = $this->_context->getDatabase(); |
239
|
|
|
|
240
|
4 |
|
foreach ( $migrations as $migration ) { |
241
|
4 |
|
$db->beginTransaction(); |
242
|
4 |
|
$migration_type = pathinfo($migration, PATHINFO_EXTENSION); |
243
|
|
|
|
244
|
4 |
|
$this->_migrationRunners[$migration_type]->run( |
245
|
4 |
|
$this->_migrationsDirectory . '/' . $migration, |
246
|
4 |
|
$this->_context |
247
|
4 |
|
); |
248
|
|
|
|
249
|
4 |
|
$sql = 'INSERT INTO Migrations (Name, ExecutedOn) |
250
|
4 |
|
VALUES (:name, :executed_on)'; |
251
|
4 |
|
$db->perform($sql, array('name' => $migration, 'executed_on' => time())); |
252
|
4 |
|
$db->commit(); |
253
|
|
|
} |
254
|
|
|
} |
255
|
|
|
|
256
|
|
|
/** |
257
|
|
|
* Deletes migrations. |
258
|
|
|
* |
259
|
|
|
* @param array $migrations Migrations. |
260
|
|
|
* |
261
|
|
|
* @return void |
262
|
|
|
*/ |
263
|
6 |
|
protected function deleteMigrations(array $migrations) |
264
|
|
|
{ |
265
|
6 |
|
if ( !$migrations ) { |
|
|
|
|
266
|
6 |
|
return; |
267
|
|
|
} |
268
|
|
|
|
269
|
1 |
|
$sql = 'DELETE FROM Migrations |
270
|
1 |
|
WHERE Name IN (:names)'; |
271
|
1 |
|
$this->_context->getDatabase()->perform($sql, array('names' => $migrations)); |
272
|
|
|
} |
273
|
|
|
|
274
|
|
|
} |
275
|
|
|
|
This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.
Consider making the comparison explicit by using
empty(..)
or! empty(...)
instead.