Failed Conditions
Push — master ( 34c0d8...598f4b )
by Alexander
03:28
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
cc 3
eloc 6
nc 2
nop 2
crap 3
1
<?php
2
/**
3
 * This file is part of the SVN-Buddy 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/svn-buddy
9
 */
10
11
namespace ConsoleHelpers\SVNBuddy\Database\Migration;
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 19
	public function __construct($migrations_directory, \ArrayAccess $container)
56
	{
57 19
		if ( !file_exists($migrations_directory) || !is_dir($migrations_directory) ) {
58 2
			throw new \InvalidArgumentException(
59 2
				'The "' . $migrations_directory . '" does not exist or not a directory.'
60 2
			);
61
		}
62
63 17
		$this->_migrationsDirectory = $migrations_directory;
64 17
		$this->_container = $container;
65 17
	}
66
67
	/**
68
	 * Registers a migration runner.
69
	 *
70
	 * @param AbstractMigrationRunner $migration_runner Migration runner.
71
	 *
72
	 * @return void
73
	 */
74 16
	public function registerMigrationRunner(AbstractMigrationRunner $migration_runner)
75
	{
76 16
		$this->_migrationRunners[$migration_runner->getFileExtension()] = $migration_runner;
77 16
	}
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 6
	public function createMigration($name, $file_extension)
90
	{
91 6
		if ( preg_replace('/[a-z\d\._]/', '', $name) ) {
92 3
			throw new \InvalidArgumentException(
93
				'The migration name can consist only from alpha-numeric characters, as well as dots and underscores.'
94 3
			);
95
		}
96
97 3
		if ( !in_array($file_extension, $this->getMigrationFileExtensions()) ) {
98 1
			throw new \InvalidArgumentException(
99 1
				'The migration runner for "' . $file_extension . '" file extension is not registered.'
100 1
			);
101
		}
102
103 2
		$migration_file = $this->_migrationsDirectory . '/' . date('Ymd_Hi') . '_' . $name . '.' . $file_extension;
104
105 2
		if ( file_exists($migration_file) ) {
106 1
			throw new \LogicException('The migration file "' . basename($migration_file) . '" already exists.');
107
		}
108
109 2
		file_put_contents($migration_file, $this->_migrationRunners[$file_extension]->getTemplate());
110
111 2
		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 13
	public function getMigrationFileExtensions()
121
	{
122 13
		if ( !$this->_migrationRunners ) {
123 1
			throw new \LogicException('No migrations runners registered.');
124
		}
125
126 12
		return array_keys($this->_migrationRunners);
127
	}
128
129
	/**
130
	 * Executes outstanding migrations.
131
	 *
132
	 * @param MigrationContext $context Context.
133
	 *
134
	 * @return void
135
	 */
136 8
	public function run(MigrationContext $context)
137
	{
138 8
		$this->setContext($context);
139 8
		$this->createMigrationsTable();
140
141 8
		$all_migrations = $this->getAllMigrations();
142 8
		$executed_migrations = $this->getExecutedMigrations();
143
144 8
		$migrations_to_execute = array_diff($all_migrations, $executed_migrations);
145 8
		$this->executeMigrations($migrations_to_execute);
146
147 8
		$migrations_to_delete = array_diff($executed_migrations, $all_migrations);
148 8
		$this->deleteMigrations($migrations_to_delete);
149 8
	}
150
151
	/**
152
	 * Sets current context.
153
	 *
154
	 * @param MigrationContext $context Context.
155
	 *
156
	 * @return void
157
	 */
158 8
	protected function setContext(MigrationContext $context)
159
	{
160 8
		$this->_context = $context;
161 8
		$this->_context->setContainer($this->_container);
162 8
	}
163
164
	/**
165
	 * Creates migration table, when missing.
166
	 *
167
	 * @return void
168
	 */
169 8
	protected function createMigrationsTable()
170
	{
171 8
		$db = $this->_context->getDatabase();
172
173
		$sql = "SELECT name
174
				FROM sqlite_master
175 8
				WHERE type = 'table' AND name = :table_name";
176 8
		$migrations_table = $db->fetchValue($sql, array('table_name' => 'Migrations'));
177
178 8
		if ( $migrations_table !== false ) {
179 3
			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 8
				)';
187 8
		$db->perform($sql);
188 8
	}
189
190
	/**
191
	 * Returns all migrations.
192
	 *
193
	 * @return array
194
	 */
195 8
	protected function getAllMigrations()
196
	{
197 8
		$file_extensions = implode(',', $this->getMigrationFileExtensions());
198 8
		$migrations = glob($this->_migrationsDirectory . '/*.{' . $file_extensions . '}', GLOB_BRACE | GLOB_NOSORT);
199 8
		$migrations = array_map('basename', $migrations);
200 8
		sort($migrations); // This sorting is better, then one offered by "glob" function.
201
202 8
		return $migrations;
203
	}
204
205
	/**
206
	 * Returns executed migrations.
207
	 *
208
	 * @return array
209
	 */
210 8
	protected function getExecutedMigrations()
211
	{
212
		$sql = 'SELECT Name
213 8
				FROM Migrations';
214
215 8
		return $this->_context->getDatabase()->fetchCol($sql);
216
	}
217
218
	/**
219
	 * Executes migrations.
220
	 *
221
	 * @param array $migrations Migrations.
222
	 *
223
	 * @return void
224
	 */
225 8
	protected function executeMigrations(array $migrations)
226
	{
227 8
		if ( !$migrations ) {
228 4
			return;
229
		}
230
231 6
		$db = $this->_context->getDatabase();
232 6
		$profiler = $db->getProfiler();
233
234
		// Allow duplicate statements during migration execution.
235 6
		if ( $profiler instanceof StatementProfiler ) {
236 1
			$profiler->trackDuplicates(false);
237 1
		}
238
239 6
		foreach ( $migrations as $migration ) {
240 6
			$db->beginTransaction();
241 6
			$migration_type = pathinfo($migration, PATHINFO_EXTENSION);
242
243 6
			$this->_migrationRunners[$migration_type]->run(
244 6
				$this->_migrationsDirectory . '/' . $migration,
245 6
				$this->_context
246 6
			);
247
248
			$sql = 'INSERT INTO Migrations (Name, ExecutedOn)
249 6
					VALUES (:name, :executed_on)';
250 6
			$db->perform($sql, array('name' => $migration, 'executed_on' => time()));
251 6
			$db->commit();
252 6
		}
253
254
		// Doesn't allow duplicate statements after migration execution.
255 6
		if ( $profiler instanceof StatementProfiler ) {
256 1
			$profiler->trackDuplicates(true);
257 1
		}
258
259 6
		if ( is_object($profiler) ) {
260 2
			$profiler->resetProfiles();
261 2
		}
262 6
	}
263
264
	/**
265
	 * Deletes migrations.
266
	 *
267
	 * @param array $migrations Migrations.
268
	 *
269
	 * @return void
270
	 */
271 8
	protected function deleteMigrations(array $migrations)
272
	{
273 8
		if ( !$migrations ) {
274 8
			return;
275
		}
276
277
		$sql = 'DELETE FROM Migrations
278 1
				WHERE Name IN (:names)';
279 1
		$this->_context->getDatabase()->perform($sql, array('names' => $migrations));
280 1
	}
281
282
}
283