Approche par composants. Composant SQL des migrations vers PHP

Je n'ai pas encore écrit sur Habré sur la façon dont je suis venu à l'idée de former des composants pour mes futurs projets ou celui en cours au lieu d'écrire directement du code. Pour le dire très brièvement, c'était comme ça ... J'ai écrit beaucoup de projets différents, inventé des pseudo composants et chaque fois que je suis tombé sur le fait que dans un projet, il est extrêmement pratique de l'utiliser, et dans un autre c'est terriblement incommode. J'ai essayé de transférer des composants "pratiques" au projet et c'est devenu encore plus gênant ... Bref, mes mains ne sont pas au bon endroit, ma tête est trop ambitieuse ... Avec le temps, je suis venu à une autre pensée: " Nous devons créer des référentiels sur GitHub avec des composants séparés, qui ne dépendront pas des autres composants "... Tout allait bien, mais je suis arrivé au composant même qui veut travailler avec un autre composant ... En conséquence, les interfaces avec les méthodes sont venues à la rescousse.Et maintenant parlons dele composant SQL des migrations tel que je le vois.





Ainsi, la plupart des gens, ainsi que mes collègues, sont convaincus que les migrations servent non seulement à mettre à jour la base de données entre les développeurs, mais également aux opérations avec des fichiers, des dossiers, etc. Par exemple, créez un répertoire pour tous les développeurs ou autre chose pour quelque chose là-bas ...





Peut-être que je pourrais me tromper, mais personnellement je suis sûr que les migrations sont nécessaires exclusivement pour les opérations de base de données SQL. Pour mettre à jour les fichiers, vous pouvez utiliser le même fichier d'initialisation git ou central, comme dans Yii2.





Idée

Le composant migrations, puisqu'il est exclusivement destiné aux opérations SQL, sera basé sur 2 fichiers SQL. Oui, ici maintenant il y aura une rafale de critiques sur le seuil d'entrée et d'autres choses, mais je dirai tout de suite qu'au fil du temps que nous avons travaillé dans l'entreprise, nous sommes passés de SQLBuilder à SQL pur, car c'est plus rapide. En outre, la plupart des IDE modernes peuvent générer du DDL pour les opérations de base de données. Et imaginez, vous devez créer une table, la remplir de données et également modifier quelque chose dans une autre table. D'une part, vous obtenez un code long avec un constructeur, d'autre part, vous pouvez utiliser du SQL pur dans le même constructeur, ou peut-être que cette situation est mitigée ... Bref, alors j'ai réalisé et décidé que dans mon composant et l'approche de la programmation en général, il y aura le moins de dualité possible. Pour cette raison, j'ai décidé de n'utiliser que du code 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