Completed
Push — master ( cb0132...57743c )
by Alexander
02:47
created

MigrationManager::executePHPMigration()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 10
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 2

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 10
ccs 6
cts 6
cp 1
rs 9.4285
cc 2
eloc 5
nc 2
nop 1
crap 2
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 8
	public function __construct($migrations_directory, Container $container)
49
	{
50 8
		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 6
		$this->_migrationsDirectory = $migrations_directory;
57 6
		$this->_container = $container;
58 6
	}
59
60
	/**
61
	 * Executes outstanding migrations.
62
	 *
63
	 * @param MigrationManagerContext $context Context.
64
	 *
65
	 * @return void
66
	 */
67 6
	public function run(MigrationManagerContext $context)
68
	{
69 6
		$this->setContext($context);
70 6
		$this->createMigrationsTable();
71
72 6
		$all_migrations = $this->getAllMigrations();
73 6
		$executed_migrations = $this->getExecutedMigrations();
74
75 6
		$migrations_to_execute = array_diff($all_migrations, $executed_migrations);
76 6
		$this->executeMigrations($migrations_to_execute);
77
78 4
		$migrations_to_delete = array_diff($executed_migrations, $all_migrations);
79 4
		$this->deleteMigrations($migrations_to_delete);
80 4
	}
81
82
	/**
83
	 * Sets current context.
84
	 *
85
	 * @param MigrationManagerContext $context Context.
86
	 *
87
	 * @return void
88
	 */
89 6
	protected function setContext(MigrationManagerContext $context)
90
	{
91 6
		$this->_context = $context;
92 6
		$this->_context->setContainer($this->_container);
93 6
	}
94
95
	/**
96
	 * Creates migration table, when missing.
97
	 *
98
	 * @return void
99
	 */
100 6
	protected function createMigrationsTable()
101
	{
102 6
		$db = $this->_context->getDatabase();
103
104
		$sql = "SELECT name
105
				FROM sqlite_master
106 6
				WHERE type = 'table' AND name = :table_name";
107 6
		$migrations_table = $db->fetchValue($sql, array('table_name' => 'Migrations'));
108
109 6
		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 6
				)';
118 6
		$db->perform($sql);
119 6
	}
120
121
	/**
122
	 * Returns all migrations.
123
	 *
124
	 * @return array
125
	 */
126 6
	protected function getAllMigrations()
127
	{
128 6
		$migrations = glob($this->_migrationsDirectory . '/*.{sql,php}', GLOB_BRACE | GLOB_NOSORT);
129 6
		$migrations = array_map('basename', $migrations);
130 6
		sort($migrations);
131
132 6
		return $migrations;
133
	}
134
135
	/**
136
	 * Returns executed migrations.
137
	 *
138
	 * @return array
139
	 */
140 6
	protected function getExecutedMigrations()
141
	{
142
		$sql = 'SELECT Name
143 6
				FROM Migrations';
144
145 6
		return $this->_context->getDatabase()->fetchCol($sql);
146
	}
147
148
	/**
149
	 * Executes migrations.
150
	 *
151
	 * @param array $migrations Migrations.
152
	 *
153
	 * @return void
154
	 */
155 6
	protected function executeMigrations(array $migrations)
156
	{
157 6
		if ( !$migrations ) {
158 3
			return;
159
		}
160
161 5
		$db = $this->_context->getDatabase();
162
163 5
		foreach ( $migrations as $migration ) {
164 5
			$db->beginTransaction();
165 5
			$migration_type = pathinfo($migration, PATHINFO_EXTENSION);
166
167 5
			if ( $migration_type === 'sql' ) {
168 4
				$this->executeSQLMigration($migration);
169 3
			}
170 2
			elseif ( $migration_type === 'php' ) {
171 2
				$this->executePHPMigration($migration);
172 1
			}
173
174
			$sql = 'INSERT INTO Migrations (Name, ExecutedOn)
175 3
					VALUES (:name, :executed_on)';
176 3
			$db->perform($sql, array('name' => $migration, 'executed_on' => time()));
177 3
			$db->commit();
178 3
		}
179 3
	}
180
181
	/**
182
	 * Executes SQL migration.
183
	 *
184
	 * @param string $migration Migration.
185
	 *
186
	 * @return void
187
	 * @throws \LogicException When an empty migration is discovered.
188
	 */
189 4
	protected function executeSQLMigration($migration)
190
	{
191 4
		$sqls = file_get_contents($this->_migrationsDirectory . '/' . $migration);
192 4
		$sqls = array_filter(preg_split('/;\s+/', $sqls));
193
194 4
		if ( !$sqls ) {
195 1
			throw new \LogicException('The "' . $migration . '" migration contains no SQL statements.');
196
		}
197
198 3
		$db = $this->_context->getDatabase();
199
200 3
		foreach ( $sqls as $sql ) {
201 3
			$db->perform($sql);
202 3
		}
203 3
	}
204
205
	/**
206
	 * Executes PHP migration.
207
	 *
208
	 * @param string $migration Migration.
209
	 *
210
	 * @return void
211
	 * @throws \LogicException When migration doesn't contain a closure.
212
	 */
213 2
	protected function executePHPMigration($migration)
214
	{
215 2
		$closure = require $this->_migrationsDirectory . '/' . $migration;
216
217 2
		if ( !is_callable($closure) ) {
218 1
			throw new \LogicException('The "' . $migration . '" migration doesn\'t return a closure.');
219
		}
220
221 1
		call_user_func($closure, $this->_context);
222 1
	}
223
224
	/**
225
	 * Deletes migrations.
226
	 *
227
	 * @param array $migrations Migrations.
228
	 *
229
	 * @return void
230
	 */
231 4
	protected function deleteMigrations(array $migrations)
232
	{
233 4
		if ( !$migrations ) {
234 4
			return;
235
		}
236
237
		$sql = 'DELETE FROM Migrations
238 1
				WHERE Name IN (:names)';
239 1
		$this->_context->getDatabase()->perform($sql, array('names' => $migrations));
240 1
	}
241
242
}
243