コンポーネントアプローチ。PHPへの移行のSQLコンポーネント

コードを直接書く代わりに、将来のプロジェクトまたは現在のプロジェクトのコンポーネントを形成するというアイデアにどのように到達したかについて、Habréについてはまだ書いていません。簡単に言えば、こんな感じでした…いろいろなプロジェクトを書いて、疑似コンポーネントを発明しました。あるプロジェクトではそれを使うのがとても便利で、別のプロジェクトではとても便利だという事実に出くわすたびに不便。 「便利な」コンポーネントをプロジェクトに転送しようとしましたが、さらに不便になりました...要するに、私の手が適切な場所になく、頭が野心的すぎます...時間が経つにつれて、私は別の考えに行き着きました。 GitHubで、他のコンポーネントに依存しない個別のコンポーネントを使用してリポジトリを作成する必要があります。 "...すべてが順調に進んでいましたが、別のコンポーネントと連携したいコンポーネントに到達しました...その結果、メソッドが助けになりました。そして今、について話しましょう私の見方では、移行のSQLコンポーネント





そのため、ほとんどの人と私の同僚は、移行が開発者間のデータベースの更新だけでなく、ファイルやフォルダーなどの操作にも役立つと確信しています。たとえば、すべての開発者用のディレクトリや、そこにある何かのための別のディレクトリを作成します...





おそらく私は間違っているかもしれませんが、個人的には、移行はSQLデータベースの操作にのみ必要であると確信しています。ファイルを更新するには、Yii2と同じgitまたはcentralinitファイルを使用できます。





考え

移行コンポーネントはSQL操作専用であるため、2つのSQLファイルに基づいています。はい、ここでは入場のしきい値などについての批判が殺到しますが、会社で働いていた時間の経過とともに、SQLBuilderから純粋なSQLに切り替えたのは高速であるためです。さらに、最新のIDEのほとんどは、データベース操作用のDDLを生成できます。そして想像してみてください。テーブルを作成し、データを入力し、別のテーブルで何かを変更する必要があります。一方では、ビルダーで長いコードを取得しますが、他方では、同じビルダーで純粋なSQLを使用できます。あるいは、この状況が混在している可能性があります...要するに、私は自分のコンポーネントでそれを認識し、決定しました一般的なプログラミングへのアプローチでは、二重性は可能な限り少なくなります。このため、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