mirror of
https://github.com/jbcr/ezmigrationbundle.git
synced 2026-03-25 17:32:05 +01:00
332 lines
11 KiB
PHP
332 lines
11 KiB
PHP
<?php
|
|
|
|
namespace Kaliop\eZMigrationBundle\Core\StorageHandler;
|
|
|
|
use Kaliop\eZMigrationBundle\API\StorageHandlerInterface;
|
|
use Kaliop\eZMigrationBundle\API\Collection\MigrationCollection;
|
|
use eZ\Publish\Core\Persistence\Database\DatabaseHandler;
|
|
use Doctrine\DBAL\Schema\Schema;
|
|
use eZ\Publish\Core\Persistence\Database\SelectQuery;
|
|
use Kaliop\eZMigrationBundle\API\Value\Migration;
|
|
use Kaliop\eZMigrationBundle\API\Value\MigrationDefinition;
|
|
|
|
/**
|
|
* Database-backed storage for info on executed migrations
|
|
*
|
|
* @todo replace all usage of the ezcdb api with the doctrine dbal one, so that we only depend on one
|
|
*/
|
|
class Database implements StorageHandlerInterface
|
|
{
|
|
/**
|
|
* Flag to indicate that the migration version table has been created
|
|
*
|
|
* @var boolean
|
|
*/
|
|
private $migrationsTableExists = false;
|
|
|
|
/**
|
|
* Name of the database table where installed migration versions are tracked.
|
|
* @var string
|
|
*
|
|
* @todo add setter/getter, as we need to clear versionTableExists when switching this
|
|
*/
|
|
private $migrationsTableName;
|
|
|
|
/**
|
|
* @var DatabaseHandler $connection
|
|
*/
|
|
protected $dbHandler;
|
|
|
|
/**
|
|
* @param DatabaseHandler $dbHandler
|
|
* @param string $migrationsTableName
|
|
*/
|
|
public function __construct(DatabaseHandler $dbHandler, $migrationsTableName = 'kaliop_migrations')
|
|
{
|
|
$this->dbHandler = $dbHandler;
|
|
$this->migrationsTableName = $migrationsTableName;
|
|
}
|
|
|
|
/**
|
|
* @return MigrationCollection
|
|
* @todo add support offset, limit
|
|
*/
|
|
public function loadMigrations()
|
|
{
|
|
$this->createMigrationsTableIfNeeded();
|
|
|
|
/** @var \eZ\Publish\Core\Persistence\Database\SelectQuery $q */
|
|
$q = $this->dbHandler->createSelectQuery();
|
|
$q->select('migration, md5, path, execution_date, status, execution_error')
|
|
->from($this->migrationsTableName)
|
|
->orderBy('migration', SelectQuery::ASC);
|
|
$stmt = $q->prepare();
|
|
$stmt->execute();
|
|
$results = $stmt->fetchAll();
|
|
|
|
$migrations = array();
|
|
foreach ($results as $result) {
|
|
$migrations[$result['migration']] = $this->arrayToMigration($result);
|
|
}
|
|
|
|
return new MigrationCollection($migrations);
|
|
}
|
|
|
|
/**
|
|
* @param string $migrationName
|
|
* @return Migration|null
|
|
*/
|
|
public function loadMigration($migrationName)
|
|
{
|
|
/** @var \eZ\Publish\Core\Persistence\Database\SelectQuery $q */
|
|
$q = $this->dbHandler->createSelectQuery();
|
|
$q->select('migration, md5, path, execution_date, status, execution_error')
|
|
->from($this->migrationsTableName)
|
|
->where($q->expr->eq('migration', $q->bindValue($migrationName)));
|
|
$stmt = $q->prepare();
|
|
$stmt->execute();
|
|
$result = $stmt->fetch(\PDO::FETCH_ASSOC);
|
|
|
|
if (is_array($result) && !empty($result)) {
|
|
return $this->arrayToMigration($result);
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Starts a migration, given its definition: stores its status in the db, returns the Migration object
|
|
*
|
|
* @param MigrationDefinition $migrationDefinition
|
|
* @return Migration
|
|
* @throws \Exception if migration was already executing or already done
|
|
* @todo add a parameter to allow re-execution of already-done migrations
|
|
*/
|
|
public function startMigration(MigrationDefinition $migrationDefinition)
|
|
{
|
|
$this->createMigrationsTableIfNeeded();
|
|
|
|
// select for update
|
|
|
|
// annoyingly enough, neither Doctrine nor EZP provide built in support for 'FOR UPDATE' in their query builders...
|
|
// at least the doctrine one allows us to still use parameter binding when we add our sql pqrticle
|
|
$conn = $this->dbHandler->getConnection();
|
|
|
|
$qb = $conn->createQueryBuilder();
|
|
$qb->select('*')
|
|
->from($this->migrationsTableName)
|
|
->where('migration = ?');
|
|
$sql = $qb->getSQL() . ' FOR UPDATE';
|
|
|
|
$conn->beginTransaction();
|
|
|
|
$stmt = $conn->executeQuery($sql, array($migrationDefinition->name));
|
|
$existingMigrationData = $stmt->fetch(\PDO::FETCH_ASSOC);
|
|
|
|
if (is_array($existingMigrationData)) {
|
|
// migration exists
|
|
|
|
// fail if it was already executing or already done
|
|
if ($existingMigrationData['status'] == Migration::STATUS_STARTED) {
|
|
// commit to release the lock
|
|
$conn->commit();
|
|
throw new \Exception("Migration '{$migrationDefinition->name}' can not be started as it is already executing");
|
|
}
|
|
if ($existingMigrationData['status'] == Migration::STATUS_DONE) {
|
|
// commit to release the lock
|
|
$conn->commit();
|
|
throw new \Exception("Migration '{$migrationDefinition->name}' can not be started as it was already executed");
|
|
}
|
|
|
|
$migration = new Migration(
|
|
$migrationDefinition->name,
|
|
md5($migrationDefinition->rawDefinition),
|
|
$migrationDefinition->path,
|
|
time(),
|
|
Migration::STATUS_STARTED
|
|
);
|
|
$conn->update(
|
|
$this->migrationsTableName,
|
|
array(
|
|
'execution_date' => $migration->executionDate,
|
|
'status' => Migration::STATUS_STARTED,
|
|
'execution_error' => null,
|
|
),
|
|
array('migration' => $migrationDefinition->name)
|
|
);
|
|
} else {
|
|
// migration did not exist. Create it!
|
|
|
|
$migration = new Migration(
|
|
$migrationDefinition->name,
|
|
md5($migrationDefinition->rawDefinition),
|
|
$migrationDefinition->path,
|
|
time(),
|
|
Migration::STATUS_STARTED
|
|
);
|
|
$conn->insert($this->migrationsTableName, $this->migrationToArray($migration));
|
|
}
|
|
|
|
$conn->commit();
|
|
return $migration;
|
|
}
|
|
|
|
/**
|
|
* Stops a migration by storing it in the db. Migration status can not be 'started'
|
|
*
|
|
* @param Migration $migration
|
|
* @throws \Exception
|
|
*/
|
|
public function endMigration(Migration $migration)
|
|
{
|
|
if ($migration->status == Migration::STATUS_STARTED) {
|
|
throw new \Exception("Migration '{$migration->name}' can not be ended as its status is 'started'...");
|
|
}
|
|
|
|
$this->createMigrationsTableIfNeeded();
|
|
|
|
// select for update
|
|
|
|
// annoyingly enough, neither Doctrine nor EZP provide built in support for 'FOR UPDATE' in their query builders...
|
|
// at least the doctrine one allows us to still use parameter binding when we add our sql pqrticle
|
|
$conn = $this->dbHandler->getConnection();
|
|
|
|
$qb = $conn->createQueryBuilder();
|
|
$qb->select('*')
|
|
->from($this->migrationsTableName)
|
|
->where('migration = ?');
|
|
$sql = $qb->getSQL() . ' FOR UPDATE';
|
|
|
|
$conn->beginTransaction();
|
|
|
|
$stmt = $conn->executeQuery($sql, array($migration->name));
|
|
$existingMigrationData = $stmt->fetch(\PDO::FETCH_ASSOC);
|
|
|
|
// fail if it was not executing
|
|
|
|
if (!is_array($existingMigrationData)) {
|
|
// commit to release the lock
|
|
$conn->commit();
|
|
throw new \Exception("Migration '{$migration->name}' can not be ended as it is not found");
|
|
}
|
|
|
|
if ($existingMigrationData['status'] != Migration::STATUS_STARTED) {
|
|
// commit to release the lock
|
|
$conn->commit();
|
|
throw new \Exception("Migration '{$migration->name}' can not be ended as it is not executing");
|
|
}
|
|
|
|
$conn->update(
|
|
$this->migrationsTableName,
|
|
array(
|
|
'status' => $migration->status,
|
|
'execution_error' => $migration->executionError,
|
|
'execution_date' => $migration->executionDate
|
|
),
|
|
array('migration' => $migration->name)
|
|
);
|
|
|
|
$conn->commit();
|
|
}
|
|
|
|
/**
|
|
* Removes a Migration from the table
|
|
* @param Migration $migration
|
|
*/
|
|
public function deleteMigration(Migration $migration)
|
|
{
|
|
$this->createMigrationsTableIfNeeded();
|
|
$conn = $this->dbHandler->getConnection();
|
|
$conn->delete($this->migrationsTableName, array('migration' => $migration->name));
|
|
}
|
|
|
|
/**
|
|
* Check if the version db table exists and create it if not.
|
|
*
|
|
* @return bool true if table has been created, false if it was already there
|
|
*
|
|
* @todo add a 'force' flag to force table re-creation
|
|
* @todo manage changes to table definition
|
|
*/
|
|
public function createMigrationsTableIfNeeded()
|
|
{
|
|
if ($this->migrationsTableExists) {
|
|
return false;
|
|
}
|
|
|
|
if ($this->tableExist($this->migrationsTableName)) {
|
|
$this->migrationsTableExists = true;
|
|
return false;
|
|
}
|
|
|
|
$this->createMigrationsTable();
|
|
|
|
$this->migrationsTableExists = true;
|
|
return true;
|
|
}
|
|
|
|
public function createMigrationsTable()
|
|
{
|
|
/** @var \Doctrine\DBAL\Schema\AbstractSchemaManager $sm */
|
|
$sm = $this->dbHandler->getConnection()->getSchemaManager();
|
|
$dbPlatform = $sm->getDatabasePlatform();
|
|
|
|
$schema = new Schema();
|
|
|
|
$t = $schema->createTable($this->migrationsTableName);
|
|
$t->addColumn('migration', 'string', array('length' => 255));
|
|
$t->addColumn('path', 'string', array('length' => 4000));
|
|
$t->addColumn('md5', 'string', array('length' => 32));
|
|
$t->addColumn('execution_date', 'integer', array('notnull' => false));
|
|
$t->addColumn('status', 'integer', array('default ' => Migration::STATUS_TODO));
|
|
$t->addColumn('execution_error', 'string', array('length' => 4000, 'notnull' => false));
|
|
$t->setPrimaryKey(array('migration'));
|
|
|
|
foreach($schema->toSql($dbPlatform) as $sql) {
|
|
$this->dbHandler->exec($sql);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check if a table exists in the database
|
|
*
|
|
* @param string $tableName
|
|
* @return bool
|
|
*/
|
|
protected function tableExist($tableName)
|
|
{
|
|
/** @var \Doctrine\DBAL\Schema\AbstractSchemaManager $sm */
|
|
$sm = $this->dbHandler->getConnection()->getSchemaManager();
|
|
foreach($sm->listTables() as $table) {
|
|
if ($table->getName() == $tableName) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
protected function migrationToArray(Migration $migration)
|
|
{
|
|
return array(
|
|
'migration' => $migration->name,
|
|
'md5' => $migration->md5,
|
|
'path' => $migration->path,
|
|
'execution_date' => $migration->executionDate,
|
|
'status' => $migration->status,
|
|
'execution_error' => $migration->executionError
|
|
);
|
|
}
|
|
|
|
protected function arrayToMigration(array $data)
|
|
{
|
|
return new Migration(
|
|
$data['migration'],
|
|
$data['md5'],
|
|
$data['path'],
|
|
$data['execution_date'],
|
|
$data['status'],
|
|
$data['execution_error']
|
|
);
|
|
}
|
|
} |