Completed
Push — master ( 3d89d6...b1b27b )
by Alexander
03:40
created

MigrationManager::getMigrationFileExtensions()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 8
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 2.0625

Importance

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