Completed
Push — master ( 8db2ab...90c078 )
by Alexander
04:06
created

MigrationManager::__construct()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 11
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 3

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 11
ccs 8
cts 8
cp 1
rs 9.4285
nc 2
cc 3
eloc 6
nop 2
crap 3
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 14
	}
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 13
	}
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
				'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 6
	}
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 6
	}
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
		$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
		$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 6
	}
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 4
			}
205 6
		}
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
		$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
			$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 4
		}
254 4
	}
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
		$sql = 'DELETE FROM Migrations
270 1
				WHERE Name IN (:names)';
271 1
		$this->_context->getDatabase()->perform($sql, array('names' => $migrations));
272 1
	}
273
274
}
275