Ainda não escrevi sobre Habré sobre como tive a ideia de formar componentes para meus projetos futuros ou o atual, em vez de escrever código diretamente. Resumindo, era assim ... Eu escrevi muitos projetos diferentes, inventei pseudo componentes e toda vez que me deparei com o fato de que em um projeto é terrivelmente conveniente usá-lo e em outro é terrivelmente inconveniente. Tentei transferir componentes "convenientes" para o projeto e ficou ainda mais inconveniente ... Resumindo, minhas mãos não estão no lugar certo, minha cabeça é muito ambiciosa ... Com o tempo, tive outro pensamento: " Precisamos fazer repositórios no GitHub com componentes separados, que não dependerão de outros componentes "... Tudo estava indo bem, mas cheguei ao próprio componente que quer trabalhar com outro componente ... Como resultado, interfaces com métodos vieram em seu socorro.E agora vamos falar sobreo componente SQL de migrações na minha opinião
Portanto, a maioria das pessoas, assim como meus colegas, está confiante de que as migrações servem não apenas para atualizar o banco de dados entre os desenvolvedores, mas também para operações com arquivos, pastas e assim por diante. Por exemplo, crie um diretório para todos os desenvolvedores ou algo mais para algo lá ...
Talvez eu possa estar errado, mas pessoalmente tenho certeza de que as migrações são necessárias exclusivamente para operações de banco de dados SQL. Para atualizar os arquivos, você pode usar o mesmo arquivo git ou init central, como no Yii2.
Ideia
O componente de migrações, por ser exclusivamente para operações SQL, será baseado em 2 arquivos SQL. Sim, aqui agora haverá uma enxurrada de críticas sobre o limiar de entrada e outras coisas, mas direi desde já que ao longo do tempo que trabalhamos na empresa, mudamos do SQLBuilder para o SQL puro, por ser mais rápido. Além disso, a maioria dos IDEs modernos pode gerar DDL para operações de banco de dados. E imagine só, você precisa criar uma tabela, preenchê-la com dados e também alterar algo em outra tabela. Por um lado, você obtém um código longo com um construtor, por outro lado, você pode usar SQL puro no mesmo construtor, ou talvez essa situação seja mista ... Resumindo, então percebi e decidi que no meu componente e abordagem à programação em geral, haverá o mínimo de dualidade possível. Devido a isso, decidi usar apenas código SQL.
: , UP DOWN, . . .
SqlMigration
, . . .
ConsoleSqlMigration
, SqlMigration
. parent::
().
DatabaseInterface
. :
schema -
table -
path -
() , (). .
SqlMigration
. , , - . :
public function up(int $count = 0): array;
public function down(int $count = 0): array;
public function history(int $limit = 0): array;
public function create(string $name): bool;
. , PHPDoc:
/**
*
*
* @param int $count (0 - )
*
* @return array . :
* 1. , ,
* 2. :
* [
* 'success' => [...],
* 'error' => [...]
* ]
* error .
*
* @throws SqlMigrationException
*/
public function up(int $count = 0): array;
/**
*
*
* @param int $count (0 - )
*
* @return array . :
* 1. , ,
* 2. :
* [
* 'success' => [...],
* 'error' => [...]
* ]
* error .
*
* @throws SqlMigrationException
*/
public function down(int $count = 0): array;
/**
*
*
* @param int $limit (null - )
*
* @return array
*/
public function history(int $limit = 0): array;
/**
*
*
* @param string $name
*
* @return bool true, .
*
* @throws RuntimeException|SqlMigrationException
*/
public function create(string $name): bool;
SqlMigration
. . , :
/**
*
*/
public const UP = 'up';
public const DOWN = 'down';
/**
* SqlMigration constructor.
*
* @param DatabaseInterface $database
* @param array $settings
*
* @throws SqlMigrationException
*/
public function __construct(DatabaseInterface $database, array $settings) {
$this->database = $database;
$this->settings = $settings;
foreach (['schema', 'table', 'path'] as $settingsKey) {
if (!array_key_exists($settingsKey, $settings)) {
throw new SqlMigrationException(" {$settingsKey} .");
}
}
}
, . bool
:
/**
*
*
* @return bool true, .
*
*
* @throws SqlMigrationException
*/
public function initSchemaAndTable(): bool {
$schemaSql = <<<SQL
CREATE SCHEMA IF NOT EXISTS {$this->settings['schema']};
SQL;
if (!$this->database->execute($schemaSql)) {
throw new SqlMigrationException(' ');
}
$tableSql = <<<SQL
CREATE TABLE IF NOT EXISTS {$this->settings['schema']}.{$this->settings['table']} (
"name" varchar(180) COLLATE "default" NOT NULL,
apply_time int4,
CONSTRAINT {$this->settings['table']}_pk PRIMARY KEY ("name")
) WITH (OIDS=FALSE)
SQL;
if (!$this->database->execute($tableSql)) {
throw new SqlMigrationException(' ');
}
return true;
}
. ( ):
/**
*
*
* @param string $name
*
* @throws SqlMigrationException
*/
protected function validateName(string $name): void {
if (!preg_match('/^[\w]+$/', $name)) {
throw new SqlMigrationException(' , .');
}
}
/**
* : m{ Ymd_His}_name
*
* @param string $name
*
* @return string
*/
protected function generateName(string $name): string {
return 'm' . gmdate('Ymd_His') . "_{$name}";
}
, . : m___ - , :
/**
* @inheritDoc
*
* @throws RuntimeException|SqlMigrationException
*/
public function create(string $name): bool {
$this->validateName($name);
$migrationMame = $this->generateName($name);
$path = "{$this->settings['path']}/{$migrationMame}";
if (!mkdir($path, 0775, true) && !is_dir($path)) {
throw new RuntimeException(" . {$path} ");
}
if (file_put_contents($path . '/up.sql', '') === false) {
throw new RuntimeException(" {$path}/up.sql");
}
if (!file_put_contents($path . '/down.sql', '') === false) {
throw new RuntimeException(" {$path}/down.sql");
}
return true;
}
, , . :
/**
*
*
* @param int $limit (null - )
*
* @return array
*/
protected function getHistoryList(int $limit = 0): array {
$limitSql = $limit === 0 ? '' : "LIMIT {$limit}";
$historySql = <<<SQL
SELECT "name", apply_time
FROM {$this->settings['schema']}.{$this->settings['table']}
ORDER BY apply_time DESC, "name" DESC {$limitSql}
SQL;
return $this->database->queryAll($historySql);
}
, :
/**
* @inheritDoc
*/
public function history(int $limit = 0): array {
$historyList = $this->getHistoryList($limit);
if (empty($historyList)) {
return [' '];
}
$messages = [];
foreach ($historyList as $historyRow) {
$messages[] = " {$historyRow['name']} " . date('Y-m-d H:i:s', $historyRow['apply_time']);
}
return $messages;
}
, , , . , .
/**
*
*
* @param string $name
*
* @return bool true, ( ).
* .
*
* @throws SqlMigrationException
*/
protected function addHistory(string $name): bool {
$sql = <<<SQL
INSERT INTO {$this->settings['schema']}.{$this->settings['table']} ("name", apply_time) VALUES(:name, :apply_time);
SQL;
if (!$this->database->execute($sql, ['name' => $name, 'apply_time' => time()])) {
throw new SqlMigrationException(" {$name}");
}
return true;
}
/**
*
*
* @param string $name
*
* @return bool true, ( ).
* .
*
* @throws SqlMigrationException
*/
protected function removeHistory(string $name): bool {
$sql = <<<SQL
DELETE FROM {$this->settings['schema']}.{$this->settings['table']} WHERE "name" = :name;
SQL;
if (!$this->database->execute($sql, ['name' => $name])) {
throw new SqlMigrationException(" {$name}");
}
return true;
}
, . , .
/**
*
*
* @return array
*/
protected function getNotAppliedList(): array {
$historyList = $this->getHistoryList();
$historyMap = [];
foreach ($historyList as $item) {
$historyMap[$item['name']] = true;
}
$notApplied = [];
$directoryList = glob("{$this->settings['path']}/m*_*_*");
foreach ($directoryList as $directory) {
if (!is_dir($directory)) {
continue;
}
$directoryParts = explode('/', $directory);
preg_match('/^(m(\d{8}_?\d{6})\D.*?)$/is', end($directoryParts), $matches);
$migrationName = $matches[1];
if (!isset($historyMap[$migrationName])) {
$migrationDateTime = DateTime::createFromFormat('Ymd_His', $matches[2])->format('Y-m-d H:i:s');
$notApplied[] = [
'path' => $directory,
'name' => $migrationName,
'date_time' => $migrationDateTime
];
}
}
ksort($notApplied);
return $notApplied;
}
: up down. , up down . , , . , ( ) (up/down - , ).
/**
*
*
* @param array $list
* @param int $count
* @param string $type (up/down)
*
* @return array
*
* @throws RuntimeException
*/
protected function execute(array $list, int $count, string $type): array {
$migrationInfo = [];
for ($index = 0; $index < $count; $index++) {
$migration = $list[$index];
$migration['path'] = array_key_exists('path', $migration) ? $migration['path'] :
"{$this->settings['path']}/{$migration['name']}";
$migrationContent = file_get_contents("{$migration['path']}/{$type}.sql");
if ($migrationContent === false) {
throw new RuntimeException(' / ');
}
try {
if (!empty($migrationContent)) {
$this->database->beginTransaction();
$this->database->execute($migrationContent);
$this->database->commit();
}
if ($type === self::UP) {
$this->addHistory($migration['name']);
} else {
$this->removeHistory($migration['name']);
}
$migrationInfo['success'][] = $migration;
} catch (SqlMigrationException | PDOException $exception) {
$migrationInfo['error'][] = array_merge($migration, ['errorMessage' => $exception->getMessage()]);
break;
}
}
return $migrationInfo;
}
:
$migration['path'] =
array_key_exists('path', $migration) ? $migration['path'] : "{$this->settings['path']}/{$migration['name']}";
( ):
$migrationContent = file_get_contents("{$migration['path']}/{$type}.sql");
. UP - , .
( , ).
, . () up down:
/**
* @inheritDoc
*/
public function up(int $count = 0): array {
$executeList = $this->getNotAppliedList();
if (empty($executeList)) {
return [];
}
$executeListCount = count($executeList);
$executeCount = $count === 0 ? $executeListCount : min($count, $executeListCount);
return $this->execute($executeList, $executeCount, self::UP);
}
/**
* @inheritDoc
*/
public function down(int $count = 0): array {
$executeList = $this->getHistoryList();
if (empty($executeList)) {
return [];
}
$executeListCount = count($executeList);
$executeCount = $count === 0 ? $executeListCount : min($count, $executeListCount);
return $this->execute($executeList, $executeCount, self::DOWN);
}
. , . , , . - API . , , , :
<?php
declare(strict_types = 1);
namespace mepihindeveloper\components;
use mepihindeveloper\components\exceptions\SqlMigrationException;
use mepihindeveloper\components\interfaces\DatabaseInterface;
use RuntimeException;
/**
* Class ConsoleSqlMigration
*
* SQL ()
*
* @package mepihindeveloper\components
*/
class ConsoleSqlMigration extends SqlMigration {
public function __construct(DatabaseInterface $database, array $settings) {
parent::__construct($database, $settings);
try {
$this->initSchemaAndTable();
Console::writeLine(' ', Console::FG_GREEN);
} catch (SqlMigrationException $exception) {
Console::writeLine($exception->getMessage(), Console::FG_RED);
exit;
}
}
public function up(int $count = 0): array {
$migrations = parent::up($count);
if (empty($migrations)) {
Console::writeLine(" ");
exit;
}
foreach ($migrations['success'] as $successMigration) {
Console::writeLine(" {$successMigration['name']} ", Console::FG_GREEN);
}
if (array_key_exists('error', $migrations)) {
foreach ($migrations['error'] as $errorMigration) {
Console::writeLine(" {$errorMigration['name']}", Console::FG_RED);
}
exit;
}
return $migrations;
}
public function down(int $count = 0): array {
$migrations = parent::down($count);
if (empty($migrations)) {
Console::writeLine(" ");
exit;
}
if (array_key_exists('error', $migrations)) {
foreach ($migrations['error'] as $errorMigration) {
Console::writeLine(" {$errorMigration['name']} : " .
PHP_EOL .
$errorMigration['errorMessage'],
Console::FG_RED);
}
exit;
}
foreach ($migrations['success'] as $successMigration) {
Console::writeLine(" {$successMigration['name']} ", Console::FG_GREEN);
}
return $migrations;
}
public function create(string $name): bool {
try {
parent::create($name);
Console::writeLine(" {$name} ");
} catch (RuntimeException | SqlMigrationException $exception) {
Console::writeLine($exception->getMessage(), Console::FG_RED);
return false;
}
return true;
}
public function history(int $limit = 0): array {
$historyList = parent::history($limit);
foreach ($historyList as $historyRow) {
Console::writeLine($historyRow);
}
return $historyList;
}
}