Completed
Push — master ( a8519f...34c0d8 )
by Alexander
02:54
created

MigrationManager::executeSQLMigration()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 15
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 10
CRAP Score 3

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 15
ccs 10
cts 10
cp 1
rs 9.4285
cc 3
eloc 8
nc 3
nop 1
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;
12
13
14
use Pimple\Container;
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 Container
30
	 */
31
	private $_container;
32
33
	/**
34
	 * Migration manager context.
35
	 *
36
	 * @var MigrationManagerContext
37
	 */
38
	private $_context;
39
40
	/**
41
	 * Creates migration manager instance.
42
	 *
43
	 * @param string    $migrations_directory Migrations directory.
44
	 * @param Container $container            Container.
45
	 *
46
	 * @throws \InvalidArgumentException When migrations directory does not exist.
47
	 */
48 12
	public function __construct($migrations_directory, Container $container)
49
	{
50 12
		if ( !file_exists($migrations_directory) || !is_dir($migrations_directory) ) {
51 2
			throw new \InvalidArgumentException(
52 2
				'The "' . $migrations_directory . '" does not exist or not a directory.'
53 2
			);
54
		}
55
56 10
		$this->_migrationsDirectory = $migrations_directory;
57 10
		$this->_container = $container;
58 10
	}
59
60
	/**
61
	 * Executes outstanding migrations.
62
	 *
63
	 * @param MigrationManagerContext $context Context.
64
	 *
65
	 * @return void
66
	 */
67 9
	public function run(MigrationManagerContext $context)
68
	{
69 9
		$this->setContext($context);
70 9
		$this->createMigrationsTable();
71
72 9
		$all_migrations = $this->getAllMigrations();
73 9
		$executed_migrations = $this->getExecutedMigrations();
74
75 9
		$migrations_to_execute = array_diff($all_migrations, $executed_migrations);
76 9
		$this->executeMigrations($migrations_to_execute);
77
78 7
		$migrations_to_delete = array_diff($executed_migrations, $all_migrations);
79 7
		$this->deleteMigrations($migrations_to_delete);
80 7
	}
81
82
	/**
83
	 * Sets current context.
84
	 *
85
	 * @param MigrationManagerContext $context Context.
86
	 *
87
	 * @return void
88
	 */
89 9
	protected function setContext(MigrationManagerContext $context)
90
	{
91 9
		$this->_context = $context;
92 9
		$this->_context->setContainer($this->_container);
93 9
	}
94
95
	/**
96
	 * Creates migration table, when missing.
97
	 *
98
	 * @return void
99
	 */
100 9
	protected function createMigrationsTable()
101
	{
102 9
		$db = $this->_context->getDatabase();
103
104
		$sql = "SELECT name
105
				FROM sqlite_master
106 9
				WHERE type = 'table' AND name = :table_name";
107 9
		$migrations_table = $db->fetchValue($sql, array('table_name' => 'Migrations'));
108
109 9
		if ( $migrations_table !== false ) {
110 3
			return;
111
		}
112
113
		$sql = 'CREATE TABLE "Migrations" (
114
					"Name" TEXT(255,0) NOT NULL,
115
					"ExecutedOn" INTEGER NOT NULL,
116
					PRIMARY KEY("Name")
117 9
				)';
118 9
		$db->perform($sql);
119 9
	}
120
121
	/**
122
	 * Returns all migrations.
123
	 *
124
	 * @return array
125
	 */
126 9
	protected function getAllMigrations()
127
	{
128 9
		$migrations = glob($this->_migrationsDirectory . '/*.{sql,php}', GLOB_BRACE | GLOB_NOSORT);
129 9
		$migrations = array_map('basename', $migrations);
130 9
		sort($migrations);
131
132 9
		return $migrations;
133
	}
134
135
	/**
136
	 * Returns executed migrations.
137
	 *
138
	 * @return array
139
	 */
140 9
	protected function getExecutedMigrations()
141
	{
142
		$sql = 'SELECT Name
143 9
				FROM Migrations';
144
145 9
		return $this->_context->getDatabase()->fetchCol($sql);
146
	}
147
148
	/**
149
	 * Executes migrations.
150
	 *
151
	 * @param array $migrations Migrations.
152
	 *
153
	 * @return void
154
	 */
155 9
	protected function executeMigrations(array $migrations)
156
	{
157 9
		if ( !$migrations ) {
158 4
			return;
159
		}
160
161 7
		$db = $this->_context->getDatabase();
162 7
		$profiler = $db->getProfiler();
163
164
		// Allow duplicate statements during migration execution.
165 7
		if ( $profiler instanceof StatementProfiler ) {
166 1
			$profiler->trackDuplicates(false);
167 1
		}
168
169 7
		foreach ( $migrations as $migration ) {
170 7
			$db->beginTransaction();
171 7
			$migration_type = pathinfo($migration, PATHINFO_EXTENSION);
172
173 7
			if ( $migration_type === 'sql' ) {
174 6
				$this->executeSQLMigration($migration);
175 5
			}
176 2
			elseif ( $migration_type === 'php' ) {
177 2
				$this->executePHPMigration($migration);
178 1
			}
179
180
			$sql = 'INSERT INTO Migrations (Name, ExecutedOn)
181 5
					VALUES (:name, :executed_on)';
182 5
			$db->perform($sql, array('name' => $migration, 'executed_on' => time()));
183 5
			$db->commit();
184 5
		}
185
186
		// Doesn't allow duplicate statements after migration execution.
187 5
		if ( $profiler instanceof StatementProfiler ) {
188 1
			$profiler->trackDuplicates(true);
189 1
		}
190
191 5
		if ( is_object($profiler) ) {
192 2
			$profiler->resetProfiles();
193 2
		}
194 5
	}
195
196
	/**
197
	 * Executes SQL migration.
198
	 *
199
	 * @param string $migration Migration.
200
	 *
201
	 * @return void
202
	 * @throws \LogicException When an empty migration is discovered.
203
	 */
204 6
	protected function executeSQLMigration($migration)
205
	{
206 6
		$sqls = file_get_contents($this->_migrationsDirectory . '/' . $migration);
207 6
		$sqls = array_filter(preg_split('/;\s+/', $sqls));
208
209 6
		if ( !$sqls ) {
210 1
			throw new \LogicException('The "' . $migration . '" migration contains no SQL statements.');
211
		}
212
213 5
		$db = $this->_context->getDatabase();
214
215 5
		foreach ( $sqls as $sql ) {
216 5
			$db->perform($sql);
217 5
		}
218 5
	}
219
220
	/**
221
	 * Executes PHP migration.
222
	 *
223
	 * @param string $migration Migration.
224
	 *
225
	 * @return void
226
	 * @throws \LogicException When migration doesn't contain a closure.
227
	 */
228 2
	protected function executePHPMigration($migration)
229
	{
230 2
		$closure = require $this->_migrationsDirectory . '/' . $migration;
231
232 2
		if ( !is_callable($closure) ) {
233 1
			throw new \LogicException('The "' . $migration . '" migration doesn\'t return a closure.');
234
		}
235
236 1
		call_user_func($closure, $this->_context);
237 1
	}
238
239
	/**
240
	 * Deletes migrations.
241
	 *
242
	 * @param array $migrations Migrations.
243
	 *
244
	 * @return void
245
	 */
246 7
	protected function deleteMigrations(array $migrations)
247
	{
248 7
		if ( !$migrations ) {
249 7
			return;
250
		}
251
252
		$sql = 'DELETE FROM Migrations
253 1
				WHERE Name IN (:names)';
254 1
		$this->_context->getDatabase()->perform($sql, array('names' => $migrations));
255 1
	}
256
257
}
258