Komponentenansatz. SQL-Komponente von Migrationen zu PHP

Ich habe noch nicht über Habré geschrieben, wie ich auf die Idee gekommen bin, Komponenten für meine zukünftigen oder die aktuellen Projekte zu formen, anstatt direkt Code zu schreiben. Um es ganz kurz auszudrücken, es war so ... Ich habe viele verschiedene Projekte geschrieben, Pseudokomponenten erfunden und jedes Mal, wenn ich auf die Tatsache stieß, dass es in einem Projekt schrecklich bequem ist, es zu verwenden, und in einem anderen ist es schrecklich ungünstig. Ich habe versucht, "bequeme" Komponenten in das Projekt zu übertragen, und es wurde noch unpraktischer ... Kurz gesagt, meine Hände sind nicht an der richtigen Stelle, mein Kopf ist zu ehrgeizig ... Mit der Zeit kam ich zu einem anderen Gedanken: " Wir müssen Repositorys auf GitHub mit separaten Komponenten erstellen, die nicht von anderen Komponenten abhängen. "... Alles lief gut, aber ich kam zu genau der Komponente, die mit einer anderen Komponente arbeiten möchte Methoden kamen zur Rettung.Und jetzt reden wir darüberdie SQL-Komponente von Migrationen, wie ich es sehe.





Daher sind die meisten Menschen sowie meine Kollegen zuversichtlich, dass Migrationen nicht nur dazu dienen, die Datenbank zwischen Entwicklern zu aktualisieren, sondern auch für Vorgänge mit Dateien, Ordnern usw. Erstellen Sie beispielsweise ein Verzeichnis für alle Entwickler oder etwas anderes für etwas dort ...





Vielleicht könnte ich mich irren, aber ich persönlich bin mir sicher, dass Migrationen ausschließlich für SQL-Datenbankoperationen erforderlich sind. Zum Aktualisieren von Dateien können Sie dieselbe Git- oder zentrale Init-Datei wie in Yii2 verwenden.





Idee

Die Migrationskomponente basiert, da sie ausschließlich für SQL-Vorgänge vorgesehen ist, auf zwei SQL-Dateien. Ja, hier wird es jetzt eine Menge Kritik an der Eintrittsschwelle und anderen Dingen geben, aber ich werde gleich sagen, dass wir im Laufe der Zeit, in der wir in der Firma gearbeitet haben, von SQLBuilder zu reinem SQL gewechselt sind, da es schneller ist. Darüber hinaus können die meisten modernen IDEs DDL für Datenbankoperationen generieren. Und stellen Sie sich vor, Sie müssen eine Tabelle erstellen, sie mit Daten füllen und auch etwas in einer anderen Tabelle ändern. Einerseits erhalten Sie einen langen Code mit einem Builder, andererseits können Sie reines SQL im selben Builder verwenden, oder diese Situation ist gemischt ... Kurz gesagt, dann habe ich das in meiner Komponente erkannt und entschieden und Ansatz zur Programmierung im Allgemeinen wird es so wenig Dualität wie möglich geben. Aus diesem Grund habe ich mich entschieden, nur SQL-Code zu verwenden.





: , UP DOWN, . . .





 SqlMigration



, . . .





 ConsoleSqlMigration



,  SqlMigration



  .  parent::



  ().





 DatabaseInterface



  . :





  • schema -





  • table -





  • path -





() , (). .





SqlMigration



. , , - . :





  1. public function up(int $count = 0): array;







  2. public function down(int $count = 0): array;







  3. public function history(int $limit = 0): array;







  4. 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';
      
      



. DatabaseInterface



. (DI) :





/**
 * 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;
}
      
      



:









  1. $migration['path'] = array_key_exists('path', $migration) ? $migration['path'] : "{$this->settings['path']}/{$migration['name']}";







  2. ( ): $migrationContent = file_get_contents("{$migration['path']}/{$type}.sql");







  3. . UP - , .





  4. ( , ).





, . () 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;
	}
}
      
      



, DI , . GitHub Composer.








All Articles