Enfoque de componentes. Componente SQL de migraciones a PHP

Todavía no he escrito sobre Habré sobre cómo se me ocurrió la idea de formar componentes para mis proyectos futuros o el actual en lugar de escribir código directamente. Para decirlo muy brevemente, fue así ... escribí muchos proyectos diferentes, inventé pseudo componentes y cada vez me encontré con el hecho de que en un proyecto es muy conveniente usarlo, y en otro es terriblemente inconveniente. Traté de transferir componentes "convenientes" al proyecto y se volvió aún más inconveniente ... En resumen, mis manos no están en el lugar correcto, mi cabeza es demasiado ambiciosa ... Con el tiempo, llegué a otro pensamiento: " Necesitamos hacer repositorios en GitHub con componentes separados, que no dependerán de otros componentes "... Todo iba bien, pero llegué al mismo componente que quiere trabajar con otro componente ... Como resultado, interfaces con Los métodos vinieron al rescate.Y ahora hablemos deel componente SQL de las migraciones en la forma en que lo veo.





Entonces, la mayoría de las personas, así como mis colegas, confían en que las migraciones sirven no solo para actualizar la base de datos entre desarrolladores, sino también para operaciones con archivos, carpetas, etc. Por ejemplo, cree un directorio para todos los desarrolladores o algo más para algo allí ...





Quizás podría estar equivocado, pero personalmente estoy seguro de que las migraciones son necesarias exclusivamente para las operaciones de la base de datos SQL. Para actualizar archivos, puede usar el mismo git o archivo de inicio central, como está en Yii2.





Ocurrencia

El componente de migraciones, al ser exclusivamente para operaciones SQL, se basará en 2 archivos SQL. Sí, aquí ahora habrá una oleada de críticas sobre el umbral de entrada y otras cosas, pero diré de inmediato que durante el tiempo que trabajamos en la empresa, pasamos de SQLBuilder a SQL puro, ya que es más rápido. Además, la mayoría de los IDE modernos pueden generar DDL para operaciones de bases de datos. E imagínese, necesita crear una tabla, llenarla con datos y también cambiar algo en otra tabla. Por un lado, obtienes un código largo con un constructor, por otro lado, puedes usar SQL puro en el mismo constructor, o tal vez esta situación sea mixta ... En resumen, luego me di cuenta y decidí que en mi componente y el enfoque de la programación en general habrá la menor dualidad posible. Debido a esto, decidí usar solo código SQL.





: , 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