Completed
Push — master ( 47b2ba...97e8f3 )
by Thomas
27s
created

MigrationService::ensureMigrationsAreLoaded()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 3
nc 2
nop 0
dl 0
loc 5
rs 9.4285
c 0
b 0
f 0
1
<?php
2
/**
3
 * @author Thomas Müller <[email protected]>
4
 *
5
 * @copyright Copyright (c) 2016, ownCloud GmbH.
6
 * @license AGPL-3.0
7
 *
8
 * This code is free software: you can redistribute it and/or modify
9
 * it under the terms of the GNU Affero General Public License, version 3,
10
 * as published by the Free Software Foundation.
11
 *
12
 * This program is distributed in the hope that it will be useful,
13
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
14
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15
 * GNU Affero General Public License for more details.
16
 *
17
 * You should have received a copy of the GNU Affero General Public License, version 3,
18
 * along with this program.  If not, see <http://www.gnu.org/licenses/>
19
 *
20
 */
21
22
namespace OC\DB;
23
24
use OC\IntegrityCheck\Helpers\AppLocator;
25
use OC\Migration\SimpleOutput;
26
use OCP\AppFramework\QueryException;
27
use OCP\IDBConnection;
28
use OCP\Migration\IOutput;
29
use OCP\Migration\ISchemaMigration;
30
use OCP\Migration\ISimpleMigration;
31
use OCP\Migration\ISqlMigration;
32
use Doctrine\DBAL\Schema\Column;
33
use Doctrine\DBAL\Schema\Table;
34
use Doctrine\DBAL\Types\Type;
35
36
class MigrationService {
37
38
	/** @var boolean */
39
	private $migrationTableCreated;
40
	/** @var array */
41
	private $migrations;
42
	/** @var IOutput */
43
	private $output;
44
	/** @var Connection */
45
	private $connection;
46
	/** @var string */
47
	private $appName;
48
49
	/**
50
	 * MigrationService constructor.
51
	 *
52
	 * @param $appName
53
	 * @param IDBConnection $connection
54
	 * @param AppLocator $appLocator
55
	 * @param IOutput|null $output
56
	 * @throws \Exception
57
	 */
58
	function __construct($appName, IDBConnection $connection, IOutput $output = null, AppLocator $appLocator = null) {
59
		$this->appName = $appName;
60
		$this->connection = $connection;
0 ignored issues
show
Documentation Bug introduced by
$connection is of type object<OCP\IDBConnection>, but the property $connection was declared to be of type object<OC\DB\Connection>. Are you sure that you always receive this specific sub-class here, or does it make sense to add an instanceof check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a given class or a super-class is assigned to a property that is type hinted more strictly.

Either this assignment is in error or an instanceof check should be added for that assignment.

class Alien {}

class Dalek extends Alien {}

class Plot
{
    /** @var  Dalek */
    public $villain;
}

$alien = new Alien();
$plot = new Plot();
if ($alien instanceof Dalek) {
    $plot->villain = $alien;
}
Loading history...
61
		$this->output = $output;
62
		if (is_null($this->output)) {
63
			$this->output = new SimpleOutput(\OC::$server->getLogger(), $appName);
64
		}
65
66
		if ($appName === 'core') {
67
			$this->migrationsPath = \OC::$SERVERROOT . '/core/Migrations';
0 ignored issues
show
Bug introduced by
The property migrationsPath does not seem to exist. Did you mean migrations?

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
68
			$this->migrationsNamespace = 'OC\\Migrations';
0 ignored issues
show
Bug introduced by
The property migrationsNamespace does not seem to exist. Did you mean migrations?

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
69
		} else {
70
			if (is_null($appLocator)) {
71
				$appLocator = new AppLocator();
72
			}
73
			$appPath = $appLocator->getAppPath($appName);
74
			$this->migrationsPath = "$appPath/appinfo/Migrations";
0 ignored issues
show
Bug introduced by
The property migrationsPath does not seem to exist. Did you mean migrations?

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
75
			$this->migrationsNamespace = "OCA\\$appName\\Migrations";
0 ignored issues
show
Bug introduced by
The property migrationsNamespace does not seem to exist. Did you mean migrations?

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
76
		}
77
78
		if (!is_dir($this->migrationsPath)) {
0 ignored issues
show
Bug introduced by
The property migrationsPath does not seem to exist. Did you mean migrations?

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
79
			if (!mkdir($this->migrationsPath)) {
0 ignored issues
show
Bug introduced by
The property migrationsPath does not seem to exist. Did you mean migrations?

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
80
				throw new \Exception("Could not create migration folder \"{$this->migrationsPath}\"");
0 ignored issues
show
Bug introduced by
The property migrationsPath does not seem to exist. Did you mean migrations?

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
81
			};
82
		}
83
	}
84
85
	private static function requireOnce($file) {
86
		require_once $file;
87
	}
88
89
	/**
90
	 * Returns the name of the app for which this migration is executed
91
	 *
92
	 * @return string
93
	 */
94
	public function getApp() {
95
		return $this->appName;
96
	}
97
98
	/**
99
	 * @return bool
100
	 * @codeCoverageIgnore - this will implicitly tested on installation
101
	 */
102
	private function createMigrationTable() {
103
		if ($this->migrationTableCreated) {
104
			return false;
105
		}
106
107
		if ($this->connection->tableExists('migrations')) {
108
			$this->migrationTableCreated = true;
109
			return false;
110
		}
111
112
		$tableName = $this->connection->getPrefix() . 'migrations';
113
		$tableName = $this->connection->getDatabasePlatform()->quoteIdentifier($tableName);
114
115
		$columns = [
116
			'app' => new Column($this->connection->getDatabasePlatform()->quoteIdentifier('app'), Type::getType('string'), ['length' => 255]),
117
			'version' => new Column($this->connection->getDatabasePlatform()->quoteIdentifier('version'), Type::getType('string'), ['length' => 255]),
118
		];
119
		$table = new Table($tableName, $columns);
120
		$table->setPrimaryKey([
121
			$this->connection->getDatabasePlatform()->quoteIdentifier('app'),
122
			$this->connection->getDatabasePlatform()->quoteIdentifier('version')]);
123
		$this->connection->getSchemaManager()->createTable($table);
124
125
		$this->migrationTableCreated = true;
126
127
		return true;
128
	}
129
130
	/**
131
	 * Returns all versions which have already been applied
132
	 *
133
	 * @return string[]
134
	 * @codeCoverageIgnore - no need to test this
135
	 */
136 View Code Duplication
	public function getMigratedVersions() {
137
		$this->createMigrationTable();
138
		$qb = $this->connection->getQueryBuilder();
139
140
		$qb->select('version')
141
			->from('migrations')
142
			->where($qb->expr()->eq('app', $qb->createNamedParameter($this->getApp())))
143
			->orderBy('version');
144
145
		$result = $qb->execute();
146
		$rows = $result->fetchAll(\PDO::FETCH_COLUMN);
147
		$result->closeCursor();
148
149
		return $rows;
150
	}
151
152
	/**
153
	 * Returns all versions which are available in the migration folder
154
	 *
155
	 * @return array
156
	 */
157
	public function getAvailableVersions() {
158
		$this->ensureMigrationsAreLoaded();
159
		return array_keys($this->migrations);
160
	}
161
162
	protected function findMigrations() {
163
		$directory = realpath($this->migrationsPath);
0 ignored issues
show
Bug introduced by
The property migrationsPath does not seem to exist. Did you mean migrations?

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
164
		$iterator = new \RegexIterator(
165
			new \RecursiveIteratorIterator(
166
				new \RecursiveDirectoryIterator($directory, \FilesystemIterator::SKIP_DOTS),
167
				\RecursiveIteratorIterator::LEAVES_ONLY
168
			),
169
			'#^.+\\/Version[^\\/]{1,255}\\.php$#i',
170
			\RegexIterator::GET_MATCH);
171
172
		$files = array_keys(iterator_to_array($iterator));
173
		uasort($files, function ($a, $b) {
174
			return (basename($a) < basename($b)) ? -1 : 1;
175
		});
176
177
		$migrations = [];
178
179
		foreach ($files as $file) {
180
			static::requireOnce($file);
0 ignored issues
show
Bug introduced by
Since requireOnce() is declared private, calling it with static will lead to errors in possible sub-classes. You can either use self, or increase the visibility of requireOnce() to at least protected.

Let’s assume you have a class which uses late-static binding:

class YourClass
{
    private static function getTemperature() {
        return "3422 °C";
}

public static function getSomeVariable()
{
    return static::getTemperature();
}

}

The code above will run fine in your PHP runtime. However, if you now create a sub-class and call the getSomeVariable() on that sub-class, you will receive a runtime error:

class YourSubClass extends YourClass {
      private static function getTemperature() {
        return "-182 °C";
    }
}

print YourSubClass::getSomeVariable(); // Will cause an access error.

In the case above, it makes sense to update SomeClass to use self instead:

class YourClass
{
    private static function getTemperature() {
        return "3422 °C";
    }

    public static function getSomeVariable()
    {
        return self::getTemperature();
    }
}
Loading history...
181
			$className = basename($file, '.php');
182
			$version = (string) substr($className, 7);
183
			if ($version === '0') {
184
				throw new \InvalidArgumentException(
185
					"Cannot load a migrations with the name '$version' because it is a reserved number"
186
				);
187
			}
188
			$migrations[$version] = sprintf('%s\\%s', $this->migrationsNamespace, $className);
0 ignored issues
show
Bug introduced by
The property migrationsNamespace does not seem to exist. Did you mean migrations?

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
189
		}
190
191
		return $migrations;
192
	}
193
194
	/**
195
	 * @param string $to
196
	 */
197
	private function getMigrationsToExecute($to) {
198
		$knownMigrations = $this->getMigratedVersions();
199
		$availableMigrations = $this->getAvailableVersions();
200
201
		$toBeExecuted = [];
202
		foreach ($availableMigrations as $v) {
203
			if ($to !== 'latest' && $v > $to) {
204
				continue;
205
			}
206
			if ($this->shallBeExecuted($v, $knownMigrations)) {
207
				$toBeExecuted[] = $v;
208
			}
209
		}
210
211
		return $toBeExecuted;
212
	}
213
214
	/**
215
	 * @param string[] $knownMigrations
216
	 */
217
	private function shallBeExecuted($m, $knownMigrations) {
218
		if (in_array($m, $knownMigrations)) {
219
			return false;
220
		}
221
222
		return true;
223
	}
224
225
	/**
226
	 * @param string $version
227
	 */
228
	private function markAsExecuted($version) {
229
		$this->connection->insertIfNotExist('*PREFIX*migrations', [
230
			'app' => $this->appName,
231
			'version' => $version
232
		]);
233
	}
234
235
	/**
236
	 * Returns the name of the table which holds the already applied versions
237
	 *
238
	 * @return string
239
	 */
240
	public function getMigrationsTableName() {
241
		return $this->connection->getPrefix() . 'migrations';
242
	}
243
244
	/**
245
	 * Returns the namespace of the version classes
246
	 *
247
	 * @return string
248
	 */
249
	public function getMigrationsNamespace() {
250
		return $this->migrationsNamespace;
0 ignored issues
show
Bug introduced by
The property migrationsNamespace does not seem to exist. Did you mean migrations?

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
251
	}
252
253
	/**
254
	 * Returns the directory which holds the versions
255
	 *
256
	 * @return string
257
	 */
258
	public function getMigrationsDirectory() {
259
		return $this->migrationsPath;
0 ignored issues
show
Bug introduced by
The property migrationsPath does not seem to exist. Did you mean migrations?

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
260
	}
261
262
	/**
263
	 * Return the explicit version for the aliases; current, next, prev, latest
264
	 *
265
	 * @param string $alias
266
	 * @return mixed|null|string
267
	 */
268
	public function getMigration($alias) {
269
		switch($alias) {
270
			case 'current':
271
				return $this->getCurrentVersion();
272
			case 'next':
273
				return $this->getRelativeVersion($this->getCurrentVersion(), 1);
274
			case 'prev':
275
				return $this->getRelativeVersion($this->getCurrentVersion(), -1);
276
			case 'latest':
277
				$this->ensureMigrationsAreLoaded();
278
279
				return @end($this->getAvailableVersions());
0 ignored issues
show
Bug introduced by
$this->getAvailableVersions() cannot be passed to end() as the parameter $array expects a reference.
Loading history...
280
		}
281
		return '0';
282
	}
283
284
	/**
285
	 * @param string $version
286
	 * @param int $delta
287
	 * @return null|string
288
	 */
289
	private function getRelativeVersion($version, $delta) {
290
		$this->ensureMigrationsAreLoaded();
291
292
		$versions = $this->getAvailableVersions();
293
		array_unshift($versions, 0);
294
		$offset = array_search($version, $versions);
295
		if ($offset === false || !isset($versions[$offset + $delta])) {
296
			// Unknown version or delta out of bounds.
297
			return null;
298
		}
299
300
		return (string) $versions[$offset + $delta];
301
	}
302
303
	/**
304
	 * @return string
305
	 */
306
	private function getCurrentVersion() {
307
		$m = $this->getMigratedVersions();
308
		if (count($m) === 0) {
309
			return '0';
310
		}
311
		return @end(array_values($m));
0 ignored issues
show
Bug introduced by
array_values($m) cannot be passed to end() as the parameter $array expects a reference.
Loading history...
312
	}
313
314
	/**
315
	 * @return string
316
	 */
317
	private function getClass($version) {
318
		$this->ensureMigrationsAreLoaded();
319
320
		if (isset($this->migrations[$version])) {
321
			return $this->migrations[$version];
322
		}
323
324
		throw new \InvalidArgumentException("Version $version is unknown.");
325
	}
326
327
	/**
328
	 * Allows to set an IOutput implementation which is used for logging progress and messages
329
	 *
330
	 * @param IOutput $output
331
	 */
332
	public function setOutput(IOutput $output) {
333
		$this->output = $output;
334
	}
335
336
	/**
337
	 * Applies all not yet applied versions up to $to
338
	 *
339
	 * @param string $to
340
	 */
341
	public function migrate($to = 'latest') {
342
		// read known migrations
343
		$toBeExecuted = $this->getMigrationsToExecute($to);
344
		foreach ($toBeExecuted as $version) {
345
			$this->executeStep($version);
346
		}
347
	}
348
349
	/**
350
	 * @param string $version
351
	 */
352 View Code Duplication
	protected function createInstance($version) {
353
		$class = $this->getClass($version);
354
		try {
355
			$s = \OC::$server->query($class);
356
		} catch (QueryException $e) {
357
			if (class_exists($class)) {
358
				$s = new $class();
359
			} else {
360
				throw new \Exception("Migration step '$class' is unknown");
361
			}
362
		}
363
364
		return $s;
365
	}
366
367
	/**
368
	 * Executes one explicit version
369
	 *
370
	 * @param string $version
371
	 */
372
	public function executeStep($version) {
373
374
		$instance = $this->createInstance($version);
375
		if ($instance instanceof ISimpleMigration) {
376
			$instance->run($this->output);
377
		}
378
		if ($instance instanceof ISqlMigration) {
379
			$sqls = $instance->sql($this->connection);
380
			foreach ($sqls as $s) {
381
				$this->connection->executeQuery($s);
382
			}
383
		}
384
		if ($instance instanceof ISchemaMigration) {
385
			$toSchema = $this->connection->createSchema();
386
			$instance->changeSchema($toSchema, ['tablePrefix' => $this->connection->getPrefix()]);
387
			$this->connection->migrateToSchema($toSchema);
388
		}
389
		$this->markAsExecuted($version);
390
	}
391
392
	private function ensureMigrationsAreLoaded() {
393
		if (empty($this->migrations)) {
394
			$this->migrations = $this->findMigrations();
395
		}
396
	}
397
}
398