<?php

use Behat\Behat\Context\ClosuredContextInterface,
    Behat\Behat\Context\TranslatedContextInterface,
    Behat\Behat\Context\BehatContext,
    Behat\Behat\Event\SuiteEvent;

use \WP_CLI\Process;
use \WP_CLI\Utils;

// Inside a community package
if ( file_exists( __DIR__ . '/utils.php' ) ) {
	require_once __DIR__ . '/utils.php';
	require_once __DIR__ . '/Process.php';
	$project_composer = dirname( dirname( dirname( __FILE__ ) ) ) . '/composer.json';
	if ( file_exists( $project_composer ) ) {
		$composer = json_decode( file_get_contents( $project_composer ) );
		if ( ! empty( $composer->autoload->files ) ) {
			$contents = 'require:' . PHP_EOL;
			foreach( $composer->autoload->files as $file ) {
				$contents .= '  - ' . dirname( dirname( dirname( __FILE__ ) ) ) . '/' . $file;
			}
			@mkdir( sys_get_temp_dir() . '/wp-cli-package-test/' );
			$project_config = sys_get_temp_dir() . '/wp-cli-package-test/config.yml';
			file_put_contents( $project_config, $contents );
			putenv( 'WP_CLI_CONFIG_PATH=' . $project_config );
		}
	}
// Inside WP-CLI
} else {
	require_once __DIR__ . '/../../php/utils.php';
	require_once __DIR__ . '/../../php/WP_CLI/Process.php';
	require_once __DIR__ . '/../../vendor/autoload.php';
}

/**
 * Features context.
 */
class FeatureContext extends BehatContext implements ClosuredContextInterface {

	private static $cache_dir, $suite_cache_dir;

	private static $db_settings = array(
		'dbname' => 'wp_cli_test',
		'dbuser' => 'wp_cli_test',
		'dbpass' => 'password1',
		'dbhost' => '127.0.0.1',
	);

	private $running_procs = array();

	public $variables = array();

	/**
	 * Get the environment variables required for launched `wp` processes
	 * @beforeSuite
	 */
	private static function get_process_env_variables() {
		// Ensure we're using the expected `wp` binary
		$bin_dir = getenv( 'WP_CLI_BIN_DIR' ) ?: realpath( __DIR__ . "/../../bin" );
		$env = array(
			'PATH' =>  $bin_dir . ':' . getenv( 'PATH' ),
			'BEHAT_RUN' => 1,
			'HOME' => '/tmp/wp-cli-home',
		);
		if ( $config_path = getenv( 'WP_CLI_CONFIG_PATH' ) ) {
			$env['WP_CLI_CONFIG_PATH'] = $config_path;
		}
		return $env;
	}

	// We cache the results of `wp core download` to improve test performance
	// Ideally, we'd cache at the HTTP layer for more reliable tests
	private static function cache_wp_files() {
		self::$cache_dir = sys_get_temp_dir() . '/wp-cli-test core-download-cache';

		if ( is_readable( self::$cache_dir . '/wp-config-sample.php' ) )
			return;

		$cmd = Utils\esc_cmd( 'wp core download --force --path=%s', self::$cache_dir );
		Process::create( $cmd, null, self::get_process_env_variables() )->run_check();
	}

	/**
	 * @BeforeSuite
	 */
	public static function prepare( SuiteEvent $event ) {
		$result = Process::create( 'wp cli info', null, self::get_process_env_variables() )->run_check();
		echo PHP_EOL;
		echo $result->stdout;
		echo PHP_EOL;
		self::cache_wp_files();
	}

	/**
	 * @AfterSuite
	 */
	public static function afterSuite( SuiteEvent $event ) {
		if ( self::$suite_cache_dir ) {
			Process::create( Utils\esc_cmd( 'rm -r %s', self::$suite_cache_dir ), null, self::get_process_env_variables() )->run();
		}
	}

	/**
	 * @BeforeScenario
	 */
	public function beforeScenario( $event ) {
		$this->variables['SRC_DIR'] = realpath( __DIR__ . '/../..' );
	}

	/**
	 * @AfterScenario
	 */
	public function afterScenario( $event ) {
		if ( isset( $this->variables['RUN_DIR'] ) ) {
			// remove altered WP install, unless there's an error
			if ( $event->getResult() < 4 ) {
				$this->proc( Utils\esc_cmd( 'rm -r %s', $this->variables['RUN_DIR'] ) )->run();
			}
		}

		foreach ( $this->running_procs as $proc ) {
			self::terminate_proc( $proc );
		}
	}

	/**
	 * Terminate a process and any of its children.
	 */
	private static function terminate_proc( $proc ) {
		$status = proc_get_status( $proc );

		$master_pid = $status['pid'];

		$output = `ps -o ppid,pid,command | grep $master_pid`;

		foreach ( explode( PHP_EOL, $output ) as $line ) {
			if ( preg_match( '/^\s*(\d+)\s+(\d+)/', $line, $matches ) ) {
				$parent = $matches[1];
				$child = $matches[2];

				if ( $parent == $master_pid ) {
					if ( ! posix_kill( (int) $child, 9 ) ) {
						throw new RuntimeException( posix_strerror( posix_get_last_error() ) );
					}
				}
			}
		}

		if ( ! posix_kill( (int) $master_pid, 9 ) ) {
			throw new RuntimeException( posix_strerror( posix_get_last_error() ) );
		}
	}

	public static function create_cache_dir() {
		self::$suite_cache_dir = sys_get_temp_dir() . '/' . uniqid( "wp-cli-test-suite-cache-", TRUE );
		mkdir( self::$suite_cache_dir );
		return self::$suite_cache_dir;
	}

	/**
	 * Initializes context.
	 * Every scenario gets it's own context object.
	 *
	 * @param array $parameters context parameters (set them up through behat.yml)
	 */
	public function __construct( array $parameters ) {
		$this->drop_db();
		$this->set_cache_dir();
		$this->variables['CORE_CONFIG_SETTINGS'] = Utils\assoc_args_to_str( self::$db_settings );
	}

	public function getStepDefinitionResources() {
		return glob( __DIR__ . '/../steps/*.php' );
	}

	public function getHookDefinitionResources() {
		return array();
	}

	public function replace_variables( $str ) {
		return preg_replace_callback( '/\{([A-Z_]+)\}/', array( $this, '_replace_var' ), $str );
	}

	private function _replace_var( $matches ) {
		$cmd = $matches[0];

		foreach ( array_slice( $matches, 1 ) as $key ) {
			$cmd = str_replace( '{' . $key . '}', $this->variables[ $key ], $cmd );
		}

		return $cmd;
	}

	public function create_run_dir() {
		if ( !isset( $this->variables['RUN_DIR'] ) ) {
			$this->variables['RUN_DIR'] = sys_get_temp_dir() . '/' . uniqid( "wp-cli-test-run-", TRUE );
			mkdir( $this->variables['RUN_DIR'] );
		}
	}

	public function build_phar( $version = 'same' ) {
		$this->variables['PHAR_PATH'] = $this->variables['RUN_DIR'] . '/' . uniqid( "wp-cli-build-", TRUE ) . '.phar';

		$this->proc( Utils\esc_cmd(
			'php -dphar.readonly=0 %1$s %2$s --version=%3$s && chmod +x %2$s',
			__DIR__ . '/../../utils/make-phar.php',
			$this->variables['PHAR_PATH'],
			$version
		) )->run_check();
	}

	private function set_cache_dir() {
		$path = sys_get_temp_dir() . '/wp-cli-test-cache';
		$this->proc( Utils\esc_cmd( 'mkdir -p %s', $path ) )->run_check();
		$this->variables['CACHE_DIR'] = $path;
	}

	private static function run_sql( $sql ) {
		Utils\run_mysql_command( 'mysql --no-defaults', array(
			'execute' => $sql,
			'host' => self::$db_settings['dbhost'],
			'user' => self::$db_settings['dbuser'],
			'pass' => self::$db_settings['dbpass'],
		) );
	}

	public function create_db() {
		$dbname = self::$db_settings['dbname'];
		self::run_sql( "CREATE DATABASE IF NOT EXISTS $dbname" );
	}

	public function drop_db() {
		$dbname = self::$db_settings['dbname'];
		self::run_sql( "DROP DATABASE IF EXISTS $dbname" );
	}

	public function proc( $command, $assoc_args = array(), $path = '' ) {
		if ( !empty( $assoc_args ) )
			$command .= Utils\assoc_args_to_str( $assoc_args );

		$env = self::get_process_env_variables();
		if ( isset( $this->variables['SUITE_CACHE_DIR'] ) ) {
			$env['WP_CLI_CACHE_DIR'] = $this->variables['SUITE_CACHE_DIR'];
		}

		if ( isset( $this->variables['RUN_DIR'] ) ) {
			$cwd = "{$this->variables['RUN_DIR']}/{$path}";
		} else {
			$cwd = null;
		}

		return Process::create( $command, $cwd, $env );
	}

	/**
	 * Start a background process. Will automatically be closed when the tests finish.
	 */
	public function background_proc( $cmd ) {
		$descriptors = array(
			0 => STDIN,
			1 => array( 'pipe', 'w' ),
			2 => array( 'pipe', 'w' ),
		);

		$proc = proc_open( $cmd, $descriptors, $pipes, $this->variables['RUN_DIR'], self::get_process_env_variables() );

		sleep(1);

		$status = proc_get_status( $proc );

		if ( !$status['running'] ) {
			throw new RuntimeException( stream_get_contents( $pipes[2] ) );
		} else {
			$this->running_procs[] = $proc;
		}
	}

	public function move_files( $src, $dest ) {
		rename( $this->variables['RUN_DIR'] . "/$src", $this->variables['RUN_DIR'] . "/$dest" );
	}

	public function add_line_to_wp_config( &$wp_config_code, $line ) {
		$token = "/* That's all, stop editing!";

		$wp_config_code = str_replace( $token, "$line\n\n$token", $wp_config_code );
	}

	public function download_wp( $subdir = '' ) {
		$dest_dir = $this->variables['RUN_DIR'] . "/$subdir";

		if ( $subdir ) {
			mkdir( $dest_dir );
		}

		$this->proc( Utils\esc_cmd( "cp -r %s/* %s", self::$cache_dir, $dest_dir ) )->run_check();

		// disable emailing
		mkdir( $dest_dir . '/wp-content/mu-plugins' );
		copy( __DIR__ . '/../extra/no-mail.php', $dest_dir . '/wp-content/mu-plugins/no-mail.php' );
	}

	public function create_config( $subdir = '' ) {
		$params = self::$db_settings;
		$params['dbprefix'] = $subdir ?: 'wp_';

		$params['skip-salts'] = true;
		$this->proc( 'wp core config', $params, $subdir )->run_check();
	}

	public function install_wp( $subdir = '' ) {
		$this->create_db();
		$this->create_run_dir();
		$this->download_wp( $subdir );

		$this->create_config( $subdir );

		$install_args = array(
			'url' => 'http://example.com',
			'title' => 'WP CLI Site',
			'admin_user' => 'admin',
			'admin_email' => 'admin@example.com',
			'admin_password' => 'password1'
		);

		$this->proc( 'wp core install', $install_args, $subdir )->run_check();
	}
}


<?php

namespace Pressbooks_CLI;

use Pressbooks\Book;
use Pressbooks\CustomCss;
use Pressbooks\Theme\Lock;
use WP_CLI;

class ThemeLockCommand extends PB_CLI_Command {

	/**
	 * Lock a book's theme.
	 *
	 * ## OPTIONS
	 *
	 * --url=<url>
	 * : The URL of the target book.
	 *
	 * @when after_wp_load
	 *
	 * @param array $args
	 * @param array $assoc_args
	 *
	 * @throws \WP_CLI\ExitException
	 */
	public function lock( $args, $assoc_args ) {
		if ( ! Book::isBook() ) {
			WP_CLI::warning( 'Not a book. Did you forget the --url parameter?' );
			return;
		}
		if ( CustomCss::isCustomCss() ) {
			WP_CLI::warning( "Deprecated! Can't lock a theme if it's Custom CSS" );
			return;
		}

		$lock = new Lock();
		if ( $lock->isLocked() ) {
			WP_CLI::warning( 'Theme already locked.' );
		} else {
			$data = $lock->lockTheme();
			if ( $data === false ) {
				WP_CLI::error( 'Theme could not be locked.' );
			} else {
				$this->updateThemeLockOption( true );
				WP_CLI::success( 'Theme was locked: ' . wp_json_encode( $data ) );
			}
		}
	}

	/**
	 * Unlock a book's theme.
	 *
	 * ## OPTIONS
	 *
	 * --url=<url>
	 * : The URL of the target book.
	 *
	 * @when after_wp_load
	 *
	 * @param array $args
	 * @param array $assoc_args
	 */
	public function unlock( $args, $assoc_args ) {
		if ( ! Book::isBook() ) {
			WP_CLI::warning( 'Not a book. Did you forget the --url parameter?' );
			return;
		}
		if ( CustomCss::isCustomCss() ) {
			WP_CLI::warning( "Deprecated! Can't unlock a theme if it's Custom CSS" );
			return;
		}

		$lock = new Lock();
		if ( ! $lock->isLocked() ) {
			WP_CLI::warning( 'Theme already unlocked.' );
		} else {
			$theme = $lock->unlockTheme();
			$this->updateThemeLockOption( false );
			WP_CLI::success( 'Theme was unlocked. Now using ' . $theme->get( 'Name' ) . ', version ' . $theme->get( 'Version' ) );
		}
	}

	/**
	 * @param bool $on
	 */
	private function updateThemeLockOption( $on ) {
		$option = get_option( 'pressbooks_export_options' );
		if ( $on ) {
			$option['theme_lock'] = 1;
		} else {
			unset( $option['theme_lock'] );
		}
		update_option( 'pressbooks_export_options', $option );
	}

}


<?php

namespace Pressbooks_CLI\I18n;

use WP_CLI;

class MakePotCommand extends WP_CLI\I18n\MakePotCommand {

	/**
	 * Same options as: https://github.com/wp-cli/i18n-command#wp-i18n-make-pot
	 *
	 * @when after_wp_load
	 */
	public function __invoke( $args, $assoc_args ) {
		parent::__invoke( $args, $assoc_args );
	}

	/**
	 * {@inheritdoc}
	 */
	protected function extract_strings() {

		$old_exclude = $this->exclude;
		$this->exclude[] = '*.blade.php';
		$translations = parent::extract_strings();

		WP_CLI::log( 'Extracting strings from .blade.php templates...' );

		try {
			// TODO: We're suppressing PHP Warnings until this PR is approved: https://github.com/wp-cli/i18n-command/pull/149
			@BladeCodeExtractor::fromDirectory( $this->source, $translations, [
				// Extract 'Template Name' headers in theme files.
				'wpExtractTemplates' => isset( $this->main_file_data['Theme Name'] ),
				'include'            => [ '*.blade.php' ],
				'exclude'            => $old_exclude,
				'extensions'         => [ 'php' ],
			] );
		} catch ( \Exception $e ) {
			WP_CLI::error( $e->getMessage() );
		}

		return $translations;
	}

}


<?php

namespace Pressbooks_CLI;

use WP_CLI;
use WP_CLI\Process;
use WP_CLI\Utils;

class ScaffoldBookThemeCommand {

	/**
	 * Generate the files needed for a Pressbooks book theme.
	 *
	 * Default behavior is to create the following files:
	 * - functions.php
	 * - .gitignore, .editorconfig, package.json, composer.json, composer.lock, yarn.lock
	 * - README.md
	 *
	 * Unless specified with `--dir=<dir>`, the theme is placed in the themes
	 * directory.
	 *
	 * ## OPTIONS
	 *
	 * <slug>
	 * : Slug for the new theme.
	 *
	 * <vendor>
	 * : Vendor for the new theme. Used in composer.json and package.json.
	 *
	 * [--theme_name=<title>]
	 * : What to put in the 'Theme Name:' header in 'style.css'. Defaults to <slug>.
	 *
	 * [--description=<description>]
	 * : Human-readable description for the theme.
	 *
	 * [--uri=<uri>]
	 * : What to put in the 'Theme URI:' header in 'style.css'.
	 *
	 * [--author=<author>]
	 * : What to put in the 'Author:' header in 'style.css'.
	 *
	 * [--author_uri=<author_uri>]
	 * : What to put in the 'Author URI:' header in 'style.css'.
	 *
	 * [--github_account=<github_account>]
	 * : The GitHub account that owns this project (e.g. 'pressbooks'). Defaults to vendor.
	 *
	 * [--github_repo=<github_repo>]
	 * : The GitHub repo name for this project (e.g. 'pressbooks-book'). Defaults to slug.
	 *
	 * [--license=<license>]
	 * : What to put in the 'License:' header in 'style.css'.
	 * ---
	 * default: GPL 2.0+
	 * ---
	 *
	 * [--textdomain=<textdomain>]
	 * : Text domain for the theme. Defaults to <slug>.
	 *
	 * [--version=<version>]
	 * : Version for the theme.
	 * ---
	 * default: 1.0
	 * ---
	 *
	 * [--dir=<dir>]
	 * : Specify a destination directory for the command. Defaults to the themes directory.
	 *
	 * [--activate]
	 * : Activate the newly created book theme.
	 *
	 * [--enable-network]
	 * : Enable the newly created book theme for the entire network.
	 *
	 * [--force]
	 * : Overwrite files that already exist.
	 *
	 * @when after_wp_load
	 */
	public function scaffold_book_theme( $args, $assoc_args ) {
		$theme_slug = $args[0];
		$theme_vendor = $args[1];
		$assoc_args = array_merge( [
			'description' => '',
			'uri' => '',
			'author' => '',
			'author_uri'  => '',
			'license' => 'GPL v3 or later',
			'spdx_license' => 'gpl-3.0-or-later',
			'version' => '1.0',
			'dir' => '',
			'github_account' => '',
			'github_repo' => '',
		], $assoc_args );

		$assoc_args['slug'] = $theme_slug;
		$assoc_args['vendor'] = $theme_vendor;
		$assoc_args['theme_function_safe'] = str_replace( '-', '_', $assoc_args['slug'] );

		if ( empty( $assoc_args['theme_name'] ) ) {
			$assoc_args['theme_name'] = ucfirst($theme_slug);
		}

		if ( empty( $assoc_args['textdomain'] ) ) {
			$assoc_args['textdomain'] = $theme_slug;
		}

		if ( empty( $assoc_args['github_account'] ) ) {
			$assoc_args['github_account'] = $theme_vendor;
		}

		if ( empty( $assoc_args['github_repo'] ) ) {
			$assoc_args['github_repo'] = $theme_slug;
		}

		if ( ! empty( $assoc_args['dir'] ) ) {
			$theme_dir = $assoc_args['dir'];
		} else {
			$theme_dir = WP_CONTENT_DIR . '/themes/' . $assoc_args['slug'];
		}

		foreach ( $assoc_args as $key => $value ) {
			$assoc_args[ 'has_' . $key ] = ! empty( $value );
		}

		$activate = Utils\get_flag_value( $assoc_args, 'activate' );
		$enable_network = Utils\get_flag_value( $assoc_args, 'enable-network' );
		$force = Utils\get_flag_value( $assoc_args, 'force' );
		$package_root = dirname( dirname( __FILE__ ) );
		$template_path = $package_root . '/templates/scaffold-book-theme';
		$files_written = $this->create_files( array(
			"{$theme_dir}/.github/ISSUE_TEMPLATE.md"  => Utils\mustache_render( "{$template_path}/.github/ISSUE_TEMPLATE.mustache", $assoc_args ),
			"{$theme_dir}/functions.php"  => Utils\mustache_render( "{$template_path}/functions.mustache", $assoc_args ),
			"{$theme_dir}/style.css"			=> Utils\mustache_render( "{$template_path}/style.mustache", $assoc_args ),
			"{$theme_dir}/assets/styles/epub/style.scss"		=> Utils\mustache_render( "{$template_path}/assets/styles/epub/style.mustache", $assoc_args ),
			"{$theme_dir}/assets/styles/prince/style.scss"  => Utils\mustache_render( "{$template_path}/assets/styles/prince/style.mustache", $assoc_args ),
			"{$theme_dir}/assets/styles/web/style.scss"			=> Utils\mustache_render( "{$template_path}/assets/styles/web/style.mustache", $assoc_args ),
			"{$theme_dir}/composer.json"										=> Utils\mustache_render( "{$template_path}/composer.mustache", $assoc_args ),
			"{$theme_dir}/package.json"											=> Utils\mustache_render( "{$template_path}/package.mustache", $assoc_args ),
			"{$theme_dir}/composer.lock"																=> file_get_contents( "{$template_path}/composer.lock" ),
			"{$theme_dir}/.gitignore"																		=> file_get_contents( "{$template_path}/.gitignore" ),
			"{$theme_dir}/.editorconfig"																=> file_get_contents( "{$template_path}/.editorconfig" ),
			"{$theme_dir}/yarn.lock"																		=> file_get_contents( "{$template_path}/yarn.lock" ),
			"{$theme_dir}/assets/styles/components/_colors.scss"				=> file_get_contents( "{$template_path}/assets/styles/components/_colors.scss" ),
			"{$theme_dir}/assets/styles/components/_elements.scss"			=> file_get_contents( "{$template_path}/assets/styles/components/_elements.scss" ),
			"{$theme_dir}/assets/styles/components/_accessibility.scss"	=> file_get_contents( "{$template_path}/assets/styles/components/_accessibility.scss" ),
			"{$theme_dir}/assets/styles/components/_colors.scss"				=> file_get_contents( "{$template_path}/assets/styles/components/_colors.scss" ),
			"{$theme_dir}/assets/styles/components/_elements.scss"			=> file_get_contents( "{$template_path}/assets/styles/components/_elements.scss" ),
			"{$theme_dir}/assets/styles/components/_media.scss"					=> file_get_contents( "{$template_path}/assets/styles/components/_media.scss" ),
			"{$theme_dir}/assets/styles/components/_pages.scss"					=> file_get_contents( "{$template_path}/assets/styles/components/_pages.scss" ),
			"{$theme_dir}/assets/styles/components/_section-titles.scss"				=> file_get_contents( "{$template_path}/assets/styles/components/_section-titles.scss" ),
			"{$theme_dir}/assets/styles/components/_specials.scss"			=> file_get_contents( "{$template_path}/assets/styles/components/_specials.scss" ),
			"{$theme_dir}/assets/styles/components/_structure.scss"			=> file_get_contents( "{$template_path}/assets/styles/components/_structure.scss" ),
			"{$theme_dir}/assets/styles/components/_toc.scss"						=> file_get_contents( "{$template_path}/assets/styles/components/_toc.scss" ),
			"{$theme_dir}/assets/styles/epub/_fonts.scss"								=> file_get_contents( "{$template_path}/assets/styles/epub/_fonts.scss" ),
			"{$theme_dir}/assets/styles/prince/_fonts.scss"							=> file_get_contents( "{$template_path}/assets/styles/prince/_fonts.scss" ),
			"{$theme_dir}/assets/styles/web/_fonts.scss"								=> file_get_contents( "{$template_path}/assets/styles/web/_fonts.scss" ),
		), $force );

		if ( empty( $files_written ) ) {
			WP_CLI::log( 'Theme files were skipped.' );
		} else {
			WP_CLI::success( 'Created theme ' . $assoc_args['theme_name'] . '.' );
		}

		if ( \WP_CLI\Utils\get_flag_value( $assoc_args, 'activate' ) ) {
			WP_CLI::run_command( array( 'theme', 'activate', $theme_slug ) );
		} else if ( \WP_CLI\Utils\get_flag_value( $assoc_args, 'enable-network' ) ) {
			WP_CLI::run_command( array( 'theme', 'enable', $theme_slug ), array( 'network' => true ) );
		}
	}

	private static function rewrap_param_desc( $matches ) {
		$param = $matches[1];
		$desc = self::indent( "\t\t", $matches[2] );
		return "\t$param\n$desc\n\n";
	}

	private static function indent( $whitespace, $text ) {
		$lines = explode( "\n", $text );
		foreach ( $lines as &$line ) {
			$line = $whitespace . $line;
		}
		return implode( $lines, "\n" );
	}

	private function prompt_if_files_will_be_overwritten( $filename, $force ) {
		$should_write_file = true;
		if ( ! file_exists( $filename ) ) {
			return true;
		}
		WP_CLI::warning( 'File already exists!' );
		WP_CLI::log( $filename );
		if ( ! $force ) {
			do {
				$answer = \cli\prompt(
					'Skip this file, or replace it with scaffolding?',
					$default = false,
					$marker = '[s/r]: '
				);
			} while ( ! in_array( $answer, array( 's', 'r' ) ) );
			$should_write_file = 'r' === $answer;
		}
		$outcome = $should_write_file ? 'Replacing.' : 'Skipping.';
		WP_CLI::log( $outcome . PHP_EOL );
		return $should_write_file;
	}

	private function create_files( $files_and_contents, $force ) {
		$wrote_files = array();
		foreach ( $files_and_contents as $filename => $contents ) {
			$should_write_file = $this->prompt_if_files_will_be_overwritten( $filename, $force );
			if ( ! $should_write_file ) {
				continue;
			}
			if ( ! is_dir( dirname( $filename ) ) ) {
				Process::create( Utils\esc_cmd( 'mkdir -p %s', dirname( $filename ) ) )->run();
			}
			if ( ! file_put_contents( $filename, $contents ) ) {
				WP_CLI::error( "Error creating file: $filename" );
			} elseif ( $should_write_file ) {
				$wrote_files[] = $filename;
			}
		}
		return $wrote_files;
	}
}


<?php

namespace Pressbooks_CLI;

use WP_CLI;
use WP_CLI\Process;
use WP_CLI\Utils;

class IssueTemplateCommand extends PB_CLI_Command {

	/**
	 * Generate an issue template for a Pressbooks theme or plugin, placing it in .github/ISSUE_TEMPLATE.md.
	 *
	 * ## OPTIONS
	 *
	 * <slug>
	 * : Slug for the theme or plugin (e.g. pressbooks, pressbooks-book).
	 *
	 * --type=<type>
	 * : The type of repo for which we're generating an issue template. Must be `theme` or `plugin`.
	 *
	 * --owner=<owner>
	 * : The GitHub username of this repo's owner (e.g. pressbooks).
	 *
	 * [--dir=<dir>]
	 * : Specify a destination directory for the command. Defaults to the theme or plugin's directory.
	 *
	 * [--force]
	 * : Overwrite files that already exist.
	 *
	 * @when after_wp_load
	 */
	public function generate_issue_template( $args, $assoc_args ) {
		$slug = $args[0];
		$assoc_args = array_merge( [], $assoc_args );

		$assoc_args['slug'] = $slug;

		if ( ! empty( $assoc_args['dir'] ) ) {
			$dir = $assoc_args['dir'];
		} else {
			$dir = WP_CONTENT_DIR . '/' . $assoc_args['type'] . 's/' . $assoc_args['slug'];
		}

		foreach ( $assoc_args as $key => $value ) {
			$assoc_args[ 'has_' . $key ] = ! empty( $value );
		}

		$force = Utils\get_flag_value( $assoc_args, 'force' );
		$type = $assoc_args['type'];
		$package_root = dirname( dirname( __FILE__ ) );
		$template_path = $package_root . '/templates/issue-template';
		$files_written = $this->create_files( array(
			"{$dir}/.github/ISSUE_TEMPLATE.md"  => Utils\mustache_render( "{$template_path}/{$assoc_args['type']}.mustache", $assoc_args ),
		), $force );

		if ( empty( $files_written ) ) {
			WP_CLI::log( 'No issue template created.' );
		} else {
			WP_CLI::success( 'Created issue template for ' . $assoc_args['owner'] . '/' . $assoc_args['slug'] . '.' );
		}
	}

	private static function rewrap_param_desc( $matches ) {
		$param = $matches[1];
		$desc = self::indent( "\t\t", $matches[2] );
		return "\t$param\n$desc\n\n";
	}

	private static function indent( $whitespace, $text ) {
		$lines = explode( "\n", $text );
		foreach ( $lines as &$line ) {
			$line = $whitespace . $line;
		}
		return implode( $lines, "\n" );
	}

	private function prompt_if_files_will_be_overwritten( $filename, $force ) {
		$should_write_file = true;
		if ( ! file_exists( $filename ) ) {
			return true;
		}
		WP_CLI::warning( 'File already exists!' );
		WP_CLI::log( $filename );
		if ( ! $force ) {
			do {
				$answer = \cli\prompt(
					'Skip this file, or replace it with scaffolding?',
					$default = false,
					$marker = '[s/r]: '
				);
			} while ( ! in_array( $answer, array( 's', 'r' ) ) );
			$should_write_file = 'r' === $answer;
		}
		$outcome = $should_write_file ? 'Replacing.' : 'Skipping.';
		WP_CLI::log( $outcome . PHP_EOL );
		return $should_write_file;
	}

	private function create_files( $files_and_contents, $force ) {
		$wrote_files = array();
		foreach ( $files_and_contents as $filename => $contents ) {
			$should_write_file = $this->prompt_if_files_will_be_overwritten( $filename, $force );
			if ( ! $should_write_file ) {
				continue;
			}
			if ( ! is_dir( dirname( $filename ) ) ) {
				Process::create( Utils\esc_cmd( 'mkdir -p %s', dirname( $filename ) ) )->run();
			}
			if ( ! file_put_contents( $filename, $contents ) ) {
				WP_CLI::error( "Error creating file: $filename" );
			} elseif ( $should_write_file ) {
				$wrote_files[] = $filename;
			}
		}
		return $wrote_files;
	}
}


<?php

namespace Pressbooks_CLI\I18n;

use Gettext\Extractors\PhpCode;
use Gettext\Translations;
use Pressbooks\Container;
use WP_CLI;
use WP_CLI\I18n\PhpCodeExtractor;
use WP_CLI\I18n\PhpFunctionsScanner;

final class BladeCodeExtractor extends PhpCode {
	use WP_CLI\I18n\IterableCodeExtractor;

	/**
	 * {@inheritdoc}
	 */
	public static function fromString( $string, Translations $translations, array $options = [] ) {

		// Use the same options as \WP_CLI\I18n\PhpCodeExtractor
		static::$options = PhpCodeExtractor::$options;
		$options += static::$options;

		// Pull in the BladeCompiler class, compile into regular PHP and then provide it to the regular parser.
		/** @var \Illuminate\View\Compilers\BladeCompiler $compiler */
		$compiler = Container::get( 'Blade' )->compiler();
		$string = $compiler->compileString( $string );
		$functions = new PhpFunctionsScanner( $string );

		$functions->enableCommentsExtraction( $options['extractComments'] );
		$functions->saveGettextFunctions( $translations, $options );
	}
}


<?php

namespace Pressbooks_CLI;

use Pressbooks\Cloner\Cloner;
use WP_CLI;

class CloneCommand extends PB_CLI_Command {

	/**
	 * Clone a book.
	 *
	 * ## OPTIONS
	 *
	 * <source>
	 * : URL
	 *
	 * <destination>
	 * : Book slug on the current network
	 *
	 * --user=<user>
	 * : sets request to a specific WordPress user
	 *
	 * @when after_wp_load
	 *
	 * @param array $args
	 * @param array $assoc_args
	 *
	 * @throws \WP_CLI\ExitException
	 */
	public function clone( $args, $assoc_args ) {
		if ( count( $args ) < 2 ) {
			WP_CLI::error( 'Expects 3 parameters: <source> <dest> --user=<user> ' );
		}
		if ( ! get_current_user_id() ) {
			WP_CLI::error( 'Missing --user parameter (sets request to a specific WordPress user)' );
		}

		$success = false;
		try {
			$source = esc_url( $args[0] );

			$dest = Cloner::validateNewBookName( $args[1] );
			if ( is_wp_error( $dest ) ) {
				WP_CLI::error( '<dest> ' . $dest->get_error_message() );
			}

			WP_CLI::log( "Cloning {$source} into {$dest}" );

			\Pressbooks\Metadata\init_book_data_models();
			\Pressbooks\Api\init_book();
			$cloner = new Cloner( $source, $dest );
			$success = $cloner->cloneBook();

		} catch ( \Exception $e ) {
			// Do nothing, look at $_SESSION['pb_errors'] instead
		}

		if ( ! empty( $_SESSION['pb_errors'] ) ) {
			foreach ( $_SESSION['pb_errors'] as $error ) {
				$error = wp_strip_all_tags( $error, true );
				$error = html_entity_decode( $error );
				WP_CLI::warning( $error );
			}
		}
		if ( ! $success ) {
			WP_CLI::error( 'Cloning failed!' );
		} else {
			WP_CLI::success( 'Cloning succeeded!' );
		}
	}

}


<?php

/**
 * This file is part of MetaModels/filter_tags.
 *
 * (c) 2012-2019 The MetaModels team.
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 *
 * This project is provided in good faith and hope to be usable by anyone.
 *
 * @package    MetaModels/filter_tags
 * @author     Christian Schiffler <c.schiffler@cyberspectrum.de>
 * @author     Stefan Heimes <stefan_heimes@hotmail.com>
 * @author     Christian de la Haye <service@delahaye.de>
 * @author     Andreas Isaak <info@andreas-isaak.de>
 * @author     David Maack <david.maack@arcor.de>
 * @author     David Molineus <mail@netzmacht.de>
 * @author     Ingolf Steinhardt <info@e-spin.de>
 * @copyright  2012-2019 The MetaModels team.
 * @license    https://github.com/MetaModels/filter_tags/blob/master/LICENSE LGPL-3.0-or-later
 * @filesource
 */

namespace MetaModels\FilterTagsBundle\FilterSetting;

use MetaModels\Attribute\IAttribute;
use MetaModels\Filter\Filter;
use MetaModels\Filter\IFilter;
use MetaModels\Filter\Rules\Condition\ConditionAnd;
use MetaModels\Filter\Rules\Condition\ConditionOr;
use MetaModels\Filter\Rules\SearchAttribute;
use MetaModels\Filter\Rules\StaticIdList;
use MetaModels\Filter\Setting\SimpleLookup;
use MetaModels\FrontendIntegration\FrontendFilterOptions;

/**
 * Filter "tags" for FE-filtering.
 */
class Tags extends SimpleLookup
{
    /**
     * Overrides the parent implementation to always return true, as this setting is always available for FE filtering.
     *
     * @return bool true as this setting is always available.
     */
    public function enableFEFilterWidget()
    {
        return true;
    }

    /**
     * {@inheritdoc}
     */
    protected function isActiveFrontendFilterValue($arrWidget, $arrFilterUrl, $strKeyOption)
    {
        return in_array($strKeyOption, (array) $arrWidget['value']) ? true : false;
    }

    /**
     * {@inheritdoc}
     */
    protected function getFrontendFilterValue($arrWidget, $arrFilterUrl, $strKeyOption)
    {
        $arrCurrent = (array) $arrWidget['value'];

        if ($this->isActiveFrontendFilterValue($arrWidget, $arrFilterUrl, $strKeyOption)) {
            $arrCurrent = array_diff($arrCurrent, array($strKeyOption));
        } else {
            $arrCurrent[] = $strKeyOption;
        }

        return implode(',', $arrCurrent);
    }

    /**
     * {@inheritdoc}
     */
    public function prepareRules(IFilter $objFilter, $arrFilterUrl)
    {
        $objMetaModel = $this->getMetaModel();
        $objAttribute = $objMetaModel->getAttributeById($this->get('attr_id'));
        $strParamName = $this->getParamName();

        $arrParamValue = $this->buildParamValue($arrFilterUrl, $strParamName);
        $arrOptions    = $this->getParameterFilterOptions($objAttribute, null);
        $arrParamValue = $this->filterParamValue($arrParamValue, $arrOptions);

        if ($objAttribute && $strParamName && is_array($arrParamValue) && $arrOptions) {
            // Determine which parenting rule to use, AND or OR.
            if ($this->get('useor')) {
                $objParentRule = new ConditionOr();
            } else {
                $objParentRule = new ConditionAnd();
            }

            // We allow the current and the fallback language to be searched by default.
            $arrValidLanguages = array(
                $this->getMetaModel()->getActiveLanguage(),
                $this->getMetaModel()->getFallbackLanguage()
            );

            foreach ($arrParamValue as $strParamValue) {
                // Restrict to valid options for obvious reasons.
                if (array_key_exists($strParamValue, $arrOptions)) {
                    $objSubFilter = new Filter($objMetaModel);
                    $objSubFilter->addFilterRule(
                        new SearchAttribute($objAttribute, $strParamValue, $arrValidLanguages)
                    );

                    $objParentRule->addChild($objSubFilter);
                }
            }

            $objFilter->addFilterRule($objParentRule);

            return;
        }

        // If no setting has been defined, we appear transparently as "not defined" and return all items.
        $objFilter->addFilterRule(new StaticIdList(null));
    }

    /**
     * {@inheritdoc}
     *
     * @SuppressWarnings(PHPMD.Superglobals)
     * @SuppressWarnings(PHPMD.CamelCaseVariableName)
     */
    public function getParameterFilterWidgets(
        $arrIds,
        $arrFilterUrl,
        $arrJumpTo,
        FrontendFilterOptions $objFrontendFilterOptions
    ) {
        $objAttribute = $this->getMetaModel()->getAttributeById($this->get('attr_id'));

        $arrCount   = array();
        $arrOptions = $this->getParameterFilterOptions($objAttribute, $arrIds, $arrCount);

        $arrParamValue  = null;
        $strParamName   = $this->getParamName();
        $arrMyFilterUrl = $arrFilterUrl;
        // If we have a value, we have to explode it by comma to have a valid value which the active checks may cope
        // with.
        if (array_key_exists($strParamName, $arrFilterUrl) && !empty($arrFilterUrl[$strParamName])) {
            if (is_array($arrFilterUrl[$strParamName])) {
                $arrParamValue = $arrFilterUrl[$strParamName];
            } else {
                $arrParamValue = explode(',', $arrFilterUrl[$strParamName]);
            }

            // Ok, this is rather hacky here. The magic value of '--none--' means clear in the widget.
            if (in_array('--none--', $arrParamValue)) {
                $arrParamValue = null;
            }

            // Also hacky, the magic value of '--all--' means check all items in the widget.
            if (is_array($arrParamValue) && in_array('--all--', $arrParamValue)) {
                $arrParamValue = array_keys($arrOptions);
            }

            if ($arrParamValue) {
                $arrMyFilterUrl[$strParamName] = $arrParamValue;
            }
        }

        $GLOBALS['MM_FILTER_PARAMS'][] = $strParamName;

        return $this->buildParameterFilterWidgets(
            $arrJumpTo,
            $objFrontendFilterOptions,
            $objAttribute,
            $strParamName,
            $arrOptions,
            $arrCount,
            $arrParamValue,
            $arrMyFilterUrl
        );
    }

    /**
     * Build filter param values.
     *
     * @param array  $arrFilterUrl Given filter url.
     * @param string $strParamName Name of filter param.
     *
     * @return array|null
     */
    private function buildParamValue($arrFilterUrl, $strParamName)
    {
        $arrParamValue = null;
        if (array_key_exists($strParamName, $arrFilterUrl) && !empty($arrFilterUrl[$strParamName])) {
            if (is_array($arrFilterUrl[$strParamName])) {
                $arrParamValue = $arrFilterUrl[$strParamName];
            } else {
                $arrParamValue = explode(',', $arrFilterUrl[$strParamName]);
            }
        }

        return $arrParamValue;
    }

    /**
     * Filter --all-- and --none-- values.
     *
     * @param array $arrParamValue Param value which shall be filtered.
     * @param array $arrOptions    Filter options.
     *
     * @return array
     */
    private function filterParamValue($arrParamValue, $arrOptions)
    {
        // Filter out the magic keyword for none selected.
        if ($arrParamValue && in_array('--none--', $arrParamValue)) {
            $arrParamValue = array();
        }

        // Filter out the magic keyword for all selected.
        if ($arrParamValue && in_array('--all--', $arrParamValue)) {
            $arrParamValue = array_keys($arrOptions);

            return $arrParamValue;
        }

        return $arrParamValue;
    }

    /**
     * Build parameter filter widgets.
     *
     * @param array                 $arrJumpTo                The jumpTo page.
     * @param FrontendFilterOptions $objFrontendFilterOptions Frontendfilter options.
     * @param IAttribute            $objAttribute             MetaModel Attribute.
     * @param string                $strParamName             Param name.
     * @param array                 $arrOptions               Options.
     * @param array                 $arrCount                 Count configuration.
     * @param array|null            $arrParamValue            Parameter value.
     * @param array                 $arrMyFilterUrl           Filter url of the current filter.
     *
     * @return array
     *
     * @SuppressWarnings(PHPMD.Superglobals)
     * @SuppressWarnings(PHPMD.CamelCaseVariableName)
     */
    private function buildParameterFilterWidgets(
        $arrJumpTo,
        FrontendFilterOptions $objFrontendFilterOptions,
        $objAttribute,
        $strParamName,
        $arrOptions,
        $arrCount,
        $arrParamValue,
        $arrMyFilterUrl
    ) {
        return  array(
            $this->getParamName() => $this->prepareFrontendFilterWidget(
                array(
                    'label' => array(
                        ($this->get('label') ? $this->get('label') : $objAttribute->getName()),
                        'GET: ' . $strParamName
                    ),
                    'inputType' => 'tags',
                    'options' => $arrOptions,
                    'count' => $arrCount,
                    'showCount' => $objFrontendFilterOptions->isShowCountValues(),
                    'eval' => array(
                        'includeBlankOption' => (
                            $this->get('blankoption')
                            && !$objFrontendFilterOptions->isHideClearFilter()
                        ),
                        'blankOptionLabel'   => &$GLOBALS['TL_LANG']['metamodels_frontendfilter']['do_not_filter'],
                        'multiple'           => true,
                        'colname'            => $objAttribute->getColName(),
                        'urlparam'           => $strParamName,
                        'onlyused'           => $this->get('onlyused'),
                        'onlypossible'       => $this->get('onlypossible'),
                        'template'           => $this->get('template')
                    ),
                    // We need to implode again to have it transported correctly in the frontend filter.
                    'urlvalue' => !empty($arrParamValue) ? implode(',', $arrParamValue) : ''
                ),
                $arrMyFilterUrl,
                $arrJumpTo,
                $objFrontendFilterOptions
            )
        );
    }
}


<?php

/**
 * This file is part of MetaModels/filter_tags.
 *
 * (c) 2012-2019 The MetaModels team.
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 *
 * This project is provided in good faith and hope to be usable by anyone.
 *
 * @package    MetaModels/filter_tags
 * @author     Christian Schiffler <c.schiffler@cyberspectrum.de>
 * @author     Ingolf Steinhardt <info@e-spin.de>
 * @copyright  2012-2019 The MetaModels team.
 * @license    https://github.com/MetaModels/filter_tags/blob/master/LICENSE LGPL-3.0-or-later
 * @filesource
 */

namespace MetaModels\FilterTagsBundle\FilterSetting;

use MetaModels\Filter\FilterUrlBuilder;
use MetaModels\Filter\Setting\AbstractFilterSettingTypeFactory;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;

/**
 * Attribute type factory for tags filter settings.
 */
class TagsFilterSettingTypeFactory extends AbstractFilterSettingTypeFactory
{
    /**
     * The event dispatcher.
     *
     * @var EventDispatcherInterface
     */
    private $dispatcher;

    /**
     * The filter URL builder.
     *
     * @var FilterUrlBuilder
     */
    private $filterUrlBuilder;

    /**
     * {@inheritDoc}
     */
    public function __construct(EventDispatcherInterface $dispatcher, FilterUrlBuilder $filterUrlBuilder)
    {
        parent::__construct();

        $this
            ->setTypeName('tags')
            ->setTypeIcon('bundles/metamodelsfiltertags/filter_tags.png')
            ->setTypeClass(Tags::class)
            ->allowAttributeTypes();

        foreach ([
            'select',
            'translatedselect',
            'text',
            'translatedtext',
            'tags',
            'translatedtags',
        ] as $attribute) {
            $this->addKnownAttributeType($attribute);
        }

        $this->dispatcher       = $dispatcher;
        $this->filterUrlBuilder = $filterUrlBuilder;
    }

    /**
     * {@inheritdoc}
     */
    public function createInstance($information, $filterSettings)
    {
        return new Tags($filterSettings, $information, $this->dispatcher, $this->filterUrlBuilder);
    }
}


<?php
/**
 * Spiral Framework.
 *
 * @license MIT
 * @author  Anton Titov (Wolfy-J)
 */
declare(strict_types=1);

namespace Spiral\Migrations\Operation\Column;

use Spiral\Migrations\CapsuleInterface;
use Spiral\Migrations\Exception\Operation\ColumnException;

final class Add extends Column
{
    /**
     * {@inheritdoc}
     */
    public function execute(CapsuleInterface $capsule)
    {
        $schema = $capsule->getSchema($this->getTable());

        if ($schema->hasColumn($this->name)) {
            throw new ColumnException(
                "Unable to create column '{$schema->getName()}'.'{$this->name}', column already exists"
            );
        }

        //Declaring column
        $this->declareColumn($schema);
    }
}


<?php
/**
 * Spiral Framework.
 *
 * @license MIT
 * @author  Anton Titov (Wolfy-J)
 */
declare(strict_types=1);

namespace Spiral\Migrations\Atomizer;

use Spiral\Database\Schema\AbstractTable;
use Spiral\Database\Schema\Reflector;
use Spiral\Reactor\Partial\Source;

/**
 * Atomizer provides ability to convert given AbstractTables and their changes into set of
 * migration commands.
 */
final class Atomizer
{
    /** @var RendererInterface */
    private $renderer;

    /** @var AbstractTable[] */
    protected $tables = [];

    /**
     * @param RendererInterface $renderer
     */
    public function __construct(RendererInterface $renderer)
    {
        $this->renderer = $renderer;
    }

    /**
     * Add new table into atomizer.
     *
     * @param AbstractTable $table
     * @return Atomizer
     */
    public function addTable(AbstractTable $table): self
    {
        $this->tables[] = $table;

        return $this;
    }

    /**
     * Get all atomizer tables.
     *
     * @return AbstractTable[]
     */
    public function getTables(): array
    {
        return $this->tables;
    }

    /**
     * Generate set of commands needed to describe migration (up command).
     *
     * @param Source $source
     */
    public function declareChanges(Source $source)
    {
        foreach ($this->sortedTables() as $table) {
            if (!$table->getComparator()->hasChanges()) {
                continue;
            }

            //New operations block
            $this->declareBlock($source);

            if (!$table->exists()) {
                $this->renderer->createTable($source, $table);
            } else {
                $this->renderer->updateTable($source, $table);
            }
        }
    }

    /**
     * Generate set of lines needed to rollback migration (down command).
     *
     * @param Source $source
     */
    public function revertChanges(Source $source)
    {
        foreach ($this->sortedTables(true) as $table) {
            if (!$table->getComparator()->hasChanges()) {
                continue;
            }

            //New operations block
            $this->declareBlock($source);

            if (!$table->exists()) {
                $this->renderer->dropTable($source, $table);
            } else {
                $this->renderer->revertTable($source, $table);
            }
        }
    }

    /**
     * Tables sorted in order of their dependencies.
     *
     * @param bool $reverse
     * @return AbstractTable[]
     */
    protected function sortedTables($reverse = false): array
    {
        $reflector = new Reflector();
        foreach ($this->tables as $table) {
            $reflector->addTable($table);
        }

        $sorted = $reflector->sortedTables();
        if ($reverse) {
            return array_reverse($sorted);
        }

        return $sorted;
    }

    /**
     * Add spacing between commands, only if required.
     *
     * @param Source $source
     */
    private function declareBlock(Source $source)
    {
        if (!empty($source->getLines())) {
            $source->addLine("");
        }
    }
}


<?php
/**
 * Spiral Framework.
 *
 * @license MIT
 * @author  Anton Titov (Wolfy-J)
 */
declare(strict_types=1);

namespace Spiral\Migrations;

use Spiral\Database\DatabaseManager;
use Spiral\Database\Table;
use Spiral\Migrations\Config\MigrationConfig;
use Spiral\Migrations\Exception\MigrationException;

final class Migrator
{
    /** @var MigrationConfig */
    private $config;

    /** @var DatabaseManager */
    private $dbal;

    /** @var RepositoryInterface */
    private $repository;

    /**
     * @param MigrationConfig     $config
     * @param DatabaseManager     $dbal
     * @param RepositoryInterface $repository
     */
    public function __construct(
        MigrationConfig $config,
        DatabaseManager $dbal,
        RepositoryInterface $repository
    ) {
        $this->config = $config;
        $this->repository = $repository;
        $this->dbal = $dbal;
    }

    /**
     * @return RepositoryInterface
     */
    public function getRepository(): RepositoryInterface
    {
        return $this->repository;
    }

    /**
     * Check if all related databases are configures with migrations.
     *
     * @return bool
     */
    public function isConfigured(): bool
    {
        foreach ($this->dbal->getDatabases() as $db) {
            if (!$db->hasTable($this->config->getTable())) {
                return false;
            }
        }

        return true;
    }

    /**
     * Configure all related databases with migration table.
     */
    public function configure()
    {
        if ($this->isConfigured()) {
            return;
        }

        foreach ($this->dbal->getDatabases() as $db) {
            $schema = $db->table($this->config->getTable())->getSchema();

            // Schema update will automatically sync all needed data
            $schema->primary('id');
            $schema->string('migration', 255)->nullable(false);
            $schema->datetime('time_executed')->datetime();
            $schema->index(['migration']);

            $schema->save();
        }
    }

    /**
     * Get every available migration with valid meta information.
     *
     * @return MigrationInterface[]
     */
    public function getMigrations(): array
    {
        $result = [];
        foreach ($this->repository->getMigrations() as $migration) {
            //Populating migration state and execution time (if any)
            $result[] = $migration->withState($this->resolveState($migration));
        }

        return $result;
    }

    /**
     * Execute one migration and return it's instance.
     *
     * @param CapsuleInterface $capsule
     * @return null|MigrationInterface
     *
     * @throws \Throwable
     */
    public function run(CapsuleInterface $capsule = null): ?MigrationInterface
    {
        if (!$this->isConfigured()) {
            throw new MigrationException("Unable to run migration, Migrator not configured");
        }

        foreach ($this->getMigrations() as $migration) {
            if ($migration->getState()->getStatus() != State::STATUS_PENDING) {
                continue;
            }

            $capsule = $capsule ?? new Capsule($this->dbal->database($migration->getDatabase()));
            $capsule->getDatabase($migration->getDatabase())->transaction(function () use ($migration, $capsule) {
                $migration->withCapsule($capsule)->up();
            });

            $this->migrationTable($migration->getDatabase())->insertOne([
                'migration'     => $migration->getState()->getName(),
                'time_executed' => new \DateTime('now')
            ]);

            return $migration->withState($this->resolveState($migration));
        }

        return null;
    }

    /**
     * Rollback last migration and return it's instance.
     *
     * @param CapsuleInterface $capsule
     * @return null|MigrationInterface
     *
     * @throws \Throwable
     */
    public function rollback(CapsuleInterface $capsule = null): ?MigrationInterface
    {
        if (!$this->isConfigured()) {
            throw new MigrationException("Unable to run migration, Migrator not configured");
        }

        /** @var MigrationInterface $migration */
        foreach (array_reverse($this->getMigrations()) as $migration) {
            if ($migration->getState()->getStatus() != State::STATUS_EXECUTED) {
                continue;
            }

            $capsule = $capsule ?? new Capsule($this->dbal->database($migration->getDatabase()));
            $capsule->getDatabase()->transaction(function () use ($migration, $capsule) {
                $migration->withCapsule($capsule)->down();
            });

            $this->migrationTable($migration->getDatabase())->delete([
                'migration' => $migration->getState()->getName()
            ])->run();

            return $migration->withState($this->resolveState($migration));
        }

        return null;
    }

    /**
     * Clarify migration state with valid status and execution time
     *
     * @param MigrationInterface $migration
     * @return State
     */
    protected function resolveState(MigrationInterface $migration): State
    {
        $db = $this->dbal->database($migration->getDatabase());

        //Fetch migration information from database
        $data = $this->migrationTable($migration->getDatabase())
                     ->select('id', 'time_executed')
                     ->where(['migration' => $migration->getState()->getName()])
                     ->run()->fetch();

        if (empty($data['time_executed'])) {
            return $migration->getState()->withStatus(State::STATUS_PENDING);
        }

        return $migration->getState()->withStatus(
            State::STATUS_EXECUTED,
            new \DateTime($data['time_executed'], $db->getDriver()->getTimezone())
        );
    }

    /**
     * Migration table, all migration information will be stored in it.
     *
     * @param string|null $database
     * @return Table
     */
    protected function migrationTable(string $database = null): Table
    {
        return $this->dbal->database($database)->table($this->config->getTable());
    }
}


<?php
/**
 * Spiral Framework.
 *
 * @license MIT
 * @author  Anton Titov (Wolfy-J)
 */
declare(strict_types=1);

namespace Spiral\Migrations\Atomizer;

use Spiral\Database\Schema\AbstractColumn;
use Spiral\Database\Schema\AbstractForeignKey;
use Spiral\Database\Schema\AbstractIndex;
use Spiral\Database\Schema\AbstractTable;
use Spiral\Database\Schema\Comparator;
use Spiral\Reactor\Partial\Source;
use Spiral\Reactor\Serializer;
use Spiral\Reactor\Traits\SerializerTrait;

final class Renderer implements RendererInterface
{
    use SerializerTrait;

    /**
     * Comparator alteration states.
     */
    const NEW_STATE      = 0;
    const ORIGINAL_STATE = 1;

    /**
     * {@inheritdoc}
     */
    public function createTable(Source $source, AbstractTable $table)
    {
        $this->render($source, '$this->table(%s)', $table);
        $comparator = $table->getComparator();

        $this->declareColumns($source, $comparator);
        $this->declareIndexes($source, $comparator);
        $this->declareForeignKeys($source, $comparator, $table->getPrefix());

        if (count($table->getPrimaryKeys())) {
            $this->render($source, '    ->setPrimaryKeys(%s)', $table->getPrimaryKeys());
        }

        //Finalization
        $source->addLine("    ->create();");
    }

    /**
     * {@inheritdoc}
     */
    public function updateTable(Source $source, AbstractTable $table)
    {
        $this->render($source, '$this->table(%s)', $table);
        $comparator = $table->getComparator();

        if ($comparator->isPrimaryChanged()) {
            $this->render($source, '    ->setPrimaryKeys(%s)', $table->getPrimaryKeys());
        }

        $this->declareColumns($source, $comparator);
        $this->declareIndexes($source, $comparator);
        $this->declareForeignKeys($source, $comparator, $table->getPrefix());

        //Finalization
        $source->addLine("    ->update();");
    }

    /**
     * {@inheritdoc}
     */
    public function revertTable(Source $source, AbstractTable $table)
    {
        //Get table blueprint
        $this->render($source, '$this->table(%s)', $table);
        $comparator = $table->getComparator();

        $this->revertForeigns($source, $comparator, $table->getPrefix());
        $this->revertIndexes($source, $comparator);
        $this->revertColumns($source, $comparator);

        //Finalization
        $source->addLine("    ->update();");
    }

    /**
     * {@inheritdoc}
     */
    public function dropTable(Source $source, AbstractTable $table)
    {
        $this->render($source, '$this->table(%s)->drop();', $table);
    }

    /**
     * @param Source     $source
     * @param Comparator $comparator
     */
    protected function declareColumns(Source $source, Comparator $comparator)
    {
        foreach ($comparator->addedColumns() as $column) {
            $this->render(
                $source,
                '    ->addColumn(%s, %s, %s)',
                $column->getName(),
                $column->getAbstractType(),
                $column
            );
        }

        foreach ($comparator->alteredColumns() as $pair) {
            $this->alterColumn($source, $pair[self::NEW_STATE], $pair[self::ORIGINAL_STATE]);
        }

        foreach ($comparator->droppedColumns() as $column) {
            $this->render($source, '    ->dropColumn(%s)', $column->getName());
        }
    }

    /**
     * @param Source     $source
     * @param Comparator $comparator
     */
    protected function declareIndexes(Source $source, Comparator $comparator)
    {
        foreach ($comparator->addedIndexes() as $index) {
            $this->render($source, '    ->addIndex(%s, %s)', $index->getColumns(), $index);
        }

        foreach ($comparator->alteredIndexes() as $pair) {
            /** @var AbstractIndex $index */
            $index = $pair[self::NEW_STATE];
            $this->render($source, '    ->alterIndex(%s, %s)', $index->getColumns(), $index);
        }

        foreach ($comparator->droppedIndexes() as $index) {
            $this->render($source, '    ->dropIndex(%s)', $index->getColumns());
        }
    }

    /**
     * @param Source     $source
     * @param Comparator $comparator
     * @param string     $prefix Database isolation prefix
     */
    protected function declareForeignKeys(
        Source $source,
        Comparator $comparator,
        string $prefix = ''
    ) {
        foreach ($comparator->addedForeignKeys() as $key) {
            $this->render(
                $source,
                '    ->addForeignKey(%s, %s, %s, %s)',
                $key->getColumn(),
                substr($key->getForeignTable(), strlen($prefix)),
                $key->getForeignKey(),
                $key
            );
        }

        foreach ($comparator->alteredForeignKeys() as $pair) {
            /** @var AbstractForeignKey $key */
            $key = $pair[self::NEW_STATE];
            $this->render(
                $source,
                '    ->alterForeignKey(%s, %s, %s, %s)',
                $key->getColumn(),
                substr($key->getForeignTable(), strlen($prefix)),
                $key->getForeignKey(),
                $key
            );
        }

        foreach ($comparator->droppedForeignKeys() as $key) {
            $this->render($source, '    ->dropForeignKey(%s)', $key->getColumn());
        }
    }

    /**
     * @param Source     $source
     * @param Comparator $comparator
     */
    protected function revertColumns(Source $source, Comparator $comparator)
    {
        foreach ($comparator->droppedColumns() as $column) {
            $this->render(
                $source,
                '    ->addColumn(%s, %s, %s)',
                $column->getName(),
                $column->getAbstractType(),
                $column
            );
        }

        foreach ($comparator->alteredColumns() as $pair) {
            $this->alterColumn($source, $pair[self::ORIGINAL_STATE], $pair[self::NEW_STATE]);
        }

        foreach ($comparator->addedColumns() as $column) {
            $this->render($source, '    ->dropColumn(%s)', $column->getName());
        }
    }

    /**
     * @param Source     $source
     * @param Comparator $comparator
     */
    protected function revertIndexes(Source $source, Comparator $comparator)
    {
        foreach ($comparator->droppedIndexes() as $index) {
            $this->render($source, '    ->addIndex(%s, %s)', $index->getColumns(), $index);
        }

        foreach ($comparator->alteredIndexes() as $pair) {
            /** @var AbstractIndex $index */
            $index = $pair[self::ORIGINAL_STATE];
            $this->render($source, '    ->alterIndex(%s, %s)', $index->getColumns(), $index);
        }

        foreach ($comparator->addedIndexes() as $index) {
            $this->render($source, '    ->dropIndex(%s)', $index->getColumns());
        }
    }

    /**
     * @param Source     $source
     * @param Comparator $comparator
     * @param string     $prefix Database isolation prefix.
     */
    protected function revertForeigns(
        Source $source,
        Comparator $comparator,
        string $prefix = ''
    ) {
        foreach ($comparator->droppedForeignKeys() as $key) {
            $this->render(
                $source,
                '    ->addForeignKey(%s, %s, %s, %s)',
                $key->getColumn(),
                substr($key->getForeignTable(), strlen($prefix)),
                $key->getForeignKey(),
                $key
            );
        }

        foreach ($comparator->alteredForeignKeys() as $pair) {
            /** @var AbstractForeignKey $key */
            $key = $pair[self::ORIGINAL_STATE];
            $this->render(
                $source,
                '    ->alterForeignKey(%s, %s, %s, %s)',
                $key->getColumn(),
                substr($key->getForeignTable(), strlen($prefix)),
                $key->getForeignKey(),
                $key
            );
        }

        foreach ($comparator->addedForeignKeys() as $key) {
            $this->render($source, '    ->dropForeignKey(%s)', $key->getColumn());
        }
    }

    /**
     * @param Source         $source
     * @param AbstractColumn $column
     * @param AbstractColumn $original
     */
    protected function alterColumn(
        Source $source,
        AbstractColumn $column,
        AbstractColumn $original
    ) {
        if ($column->getName() != $original->getName()) {
            $name = $original->getName();
        } else {
            $name = $column->getName();
        }

        $this->render(
            $source,
            '    ->alterColumn(%s, %s, %s)',
            $name,
            $column->getAbstractType(),
            $column
        );

        if ($column->getName() != $original->getName()) {
            $this->render($source, '    ->renameColumn(%s, %s)', $name, $column->getName());
        }
    }

    /**
     * Render values and options into source.
     *
     * @param Source $source
     * @param string $format
     * @param array  ...$values
     */
    protected function render(Source $source, string $format, ...$values)
    {
        $serializer = $this->getSerializer();

        $rendered = [];
        foreach ($values as $value) {
            if ($value instanceof AbstractTable) {
                $rendered[] = $serializer->serialize(
                    substr($value->getName(), strlen($value->getPrefix()))
                );
                continue;
            }

            if ($value instanceof AbstractColumn) {
                $rendered[] = $this->columnOptions($serializer, $value);
                continue;
            }

            if ($value instanceof AbstractIndex) {
                $rendered[] = $this->indexOptions($serializer, $value);
                continue;
            }

            if ($value instanceof AbstractForeignKey) {
                $rendered[] = $this->foreignKeyOptions($serializer, $value);
                continue;
            }

            // numeric array
            if (is_array($value) && count($value) > 0 && is_numeric(array_keys($value)[0])) {
                $rendered[] = '["' . join('", "', $value) . '"]';
                continue;
            }

            $rendered[] = $serializer->serialize($value);
        }

        $lines = sprintf($format, ...$rendered);
        foreach (explode("\n", $lines) as $line) {
            $source->addLine($line);
        }
    }

    /**
     * @param Serializer     $serializer
     * @param AbstractColumn $column
     * @return string
     */
    private function columnOptions(Serializer $serializer, AbstractColumn $column): string
    {
        $options = [
            'nullable' => $column->isNullable(),
            'default'  => $column->getDefaultValue()
        ];

        if ($column->getAbstractType() == 'enum') {
            $options['values'] = $column->getEnumValues();
        }

        if ($column->getAbstractType() == 'string') {
            $options['size'] = $column->getSize();
        }

        if ($column->getAbstractType() == 'decimal') {
            $options['scale'] = $column->getScale();
            $options['precision'] = $column->getPrecision();
        }

        return $this->mountIndents($serializer->serialize($options));
    }

    /**
     * @param Serializer    $serializer
     * @param AbstractIndex $index
     * @return string
     */
    private function indexOptions(Serializer $serializer, AbstractIndex $index): string
    {
        return $this->mountIndents($serializer->serialize([
            'name'   => $index->getName(),
            'unique' => $index->isUnique()
        ]));
    }

    /**
     * @param Serializer         $serializer
     * @param AbstractForeignKey $reference
     * @return string
     */
    private function foreignKeyOptions(
        Serializer $serializer,
        AbstractForeignKey $reference
    ): string {
        return $this->mountIndents($serializer->serialize([
            'name'   => $reference->getName(),
            'delete' => $reference->getDeleteRule(),
            'update' => $reference->getUpdateRule()
        ]));
    }

    /**
     * Mount indents for column and index options.
     *
     * @param $serialized
     * @return string
     */
    private function mountIndents(string $serialized): string
    {
        $lines = explode("\n", $serialized);
        foreach ($lines as &$line) {
            $line = "    " . $line;
            unset($line);
        }

        return ltrim(join("\n", $lines));
    }
}


<?php
/**
 * Spiral Framework.
 *
 * @license MIT
 * @author  Anton Titov (Wolfy-J)
 */
declare(strict_types=1);

namespace Spiral\Migrations;

use Spiral\Database\Schema\AbstractTable;
use Spiral\Migrations\Exception\BlueprintException;
use Spiral\Migrations\Operation;

/**
 * TableBlueprint is abstraction wrapper at top of AbstractTable which converts command based
 * definitions into declarative.
 */
final class TableBlueprint
{
    /** @var CapsuleInterface */
    private $capsule = null;

    /** @var bool */
    private $executed = false;

    /** @var array */
    private $operations = [];

    /** @var string */
    private $table = '';

    /**
     * @param CapsuleInterface $capsule
     * @param string           $table
     */
    public function __construct(CapsuleInterface $capsule, string $table)
    {
        $this->capsule = $capsule;
        $this->table = $table;
    }

    /**
     * Get associated table schema.
     *
     * @return AbstractTable
     */
    public function getSchema(): AbstractTable
    {
        return $this->capsule->getSchema($this->table);
    }

    /**
     * Example:
     * $table->addColumn('name', 'string', ['length' => 64]);
     * $table->addColumn('status', 'enum', [
     *      'values' => ['active', 'disabled']
     * ]);
     *
     * @param string $name
     * @param string $type
     * @param array  $options
     * @return TableBlueprint
     */
    public function addColumn(string $name, string $type, array $options = []): self
    {
        return $this->addOperation(
            new Operation\Column\Add($this->table, $name, $type, $options)
        );
    }

    /**
     * Example:
     * $table->alterColumn('name', 'string', ['length' => 128]);
     *
     * @param string $name
     * @param string $type
     * @param array  $options
     * @return TableBlueprint
     */
    public function alterColumn(string $name, string $type, array $options = []): self
    {
        return $this->addOperation(
            new Operation\Column\Alter($this->table, $name, $type, $options)
        );
    }

    /**
     * Example:
     * $table->renameColumn('column', 'new_name');
     *
     * @param string $name
     * @param string $newName
     * @return TableBlueprint
     */
    public function renameColumn(string $name, string $newName): self
    {
        return $this->addOperation(
            new Operation\Column\Rename($this->table, $name, $newName)
        );
    }

    /**
     * Example:
     * $table->dropColumn('email');
     *
     * @param string $name
     * @return TableBlueprint
     */
    public function dropColumn(string $name): self
    {
        return $this->addOperation(
            new Operation\Column\Drop($this->table, $name)
        );
    }

    /**
     * Example:
     * $table->addIndex(['email'], ['unique' => true]);
     *
     * @param array $columns
     * @param array $options
     * @return TableBlueprint
     */
    public function addIndex(array $columns, array $options = []): self
    {
        return $this->addOperation(
            new Operation\Index\Add($this->table, $columns, $options)
        );
    }

    /**
     * Example:
     * $table->alterIndex(['email'], ['unique' => false]);
     *
     * @param array $columns
     * @param array $options
     * @return TableBlueprint
     */
    public function alterIndex(array $columns, array $options): self
    {
        return $this->addOperation(
            new Operation\Index\Alter($this->table, $columns, $options)
        );
    }

    /**
     * Example:
     * $table->dropIndex(['email']);
     *
     * @param array $columns
     * @return TableBlueprint
     */
    public function dropIndex(array $columns): self
    {
        return $this->addOperation(
            new Operation\Index\Drop($this->table, $columns)
        );
    }

    /**
     * Example:
     * $table->addForeignKey('user_id', 'users', 'id', ['delete' => 'CASCADE']);
     *
     * @param string $column
     * @param string $foreignTable Database isolation prefix will be automatically added.
     * @param string $foreignKey
     * @param array  $options
     * @return TableBlueprint
     */
    public function addForeignKey(
        string $column,
        string $foreignTable,
        string $foreignKey,
        array $options = []
    ): self {
        return $this->addOperation(
            new Operation\ForeignKey\Add(
                $this->table,
                $column,
                $foreignTable,
                $foreignKey,
                $options
            )
        );
    }

    /**
     * Example:
     * $table->alterForeignKey('user_id', 'users', 'id', ['delete' => 'NO ACTION']);
     *
     * @param string $column
     * @param string $foreignTable
     * @param string $foreignKey
     * @param array  $options
     * @return TableBlueprint
     */
    public function alterForeignKey(
        string $column,
        string $foreignTable,
        string $foreignKey,
        array $options = []
    ): self {
        return $this->addOperation(
            new Operation\ForeignKey\Alter(
                $this->table,
                $column,
                $foreignTable,
                $foreignKey,
                $options
            )
        );
    }

    /**
     * Example:
     * $table->dropForeignKey('user_id');
     *
     * @param string $column
     * @return TableBlueprint
     */
    public function dropForeignKey(string $column): self
    {
        return $this->addOperation(
            new Operation\ForeignKey\Drop($this->table, $column)
        );
    }

    /**
     * Set table primary keys index. Attention, you can only call it when table being created.
     *
     * @param array $keys
     * @return TableBlueprint
     */
    public function setPrimaryKeys(array $keys): self
    {
        return $this->addOperation(
            new Operation\Table\PrimaryKeys($this->table, $keys)
        );
    }

    /**
     * Create table schema. Must be last operation in the sequence.
     */
    public function create()
    {
        $this->addOperation(
            new Operation\Table\Create($this->table)
        );

        $this->execute();
    }

    /**
     * Update table schema. Must be last operation in the sequence.
     */
    public function update()
    {
        $this->addOperation(
            new Operation\Table\Update($this->table)
        );

        $this->execute();
    }

    /**
     * Drop table. Must be last operation in the sequence.
     */
    public function drop()
    {
        $this->addOperation(
            new Operation\Table\Drop($this->table)
        );

        $this->execute();
    }

    /**
     * Rename table. Must be last operation in the sequence.
     *
     * @param string $newName
     */
    public function rename(string $newName)
    {
        $this->addOperation(
            new Operation\Table\Rename($this->table, $newName)
        );

        $this->execute();
    }

    /**
     * Register new operation.
     *
     * @param OperationInterface $operation
     * @return TableBlueprint
     */
    public function addOperation(OperationInterface $operation): self
    {
        $this->operations[] = $operation;

        return $this;
    }

    /**
     * Execute blueprint operations.
     */
    private function execute()
    {
        if ($this->executed) {
            throw new BlueprintException("Only one create/update/rename/drop is allowed per blueprint.");
        }

        $this->capsule->execute($this->operations);
        $this->executed = true;
    }
}


<?php
/**
 * Spiral Framework.
 *
 * @license MIT
 * @author  Anton Titov (Wolfy-J)
 */
declare(strict_types=1);

namespace Spiral\Migrations\Operation\Table;

use Spiral\Database\Driver\HandlerInterface;
use Spiral\Migrations\CapsuleInterface;
use Spiral\Migrations\Exception\Operation\TableException;
use Spiral\Migrations\Operation\AbstractOperation;

final class Drop extends AbstractOperation
{
    /**
     * {@inheritdoc}
     */
    public function execute(CapsuleInterface $capsule)
    {
        $schema = $capsule->getSchema($this->getTable());
        $database = $this->database ?? '[default]';

        if (!$schema->exists()) {
            throw new TableException(
                "Unable to drop table '{$database}'.'{$this->getTable()}', table does not exists"
            );
        }

        $schema->declareDropped();
        $schema->save(HandlerInterface::DO_ALL);
    }
}


<?php
/**
 * Spiral Framework.
 *
 * @license MIT
 * @author  Anton Titov (Wolfy-J)
 */
declare(strict_types=1);

namespace Spiral\Migrations\Operation\ForeignKey;

use Spiral\Migrations\CapsuleInterface;
use Spiral\Migrations\Exception\Operation\ForeignKeyException;

final class Drop extends ForeignKey
{
    /**
     * {@inheritdoc}
     */
    public function execute(CapsuleInterface $capsule)
    {
        $schema = $capsule->getSchema($this->getTable());

        if (!$schema->hasForeignKey($this->column)) {
            throw new ForeignKeyException(
                "Unable to drop foreign key '{$schema->getName()}'.'{$this->column}', "
                . "foreign key does not exists"
            );
        }

        $schema->dropForeignKey($this->column);
    }
}


<?php
/**
 * Spiral Framework.
 *
 * @license MIT
 * @author  Anton Titov (Wolfy-J)
 */
declare(strict_types=1);

namespace Spiral\Migrations\Operation\ForeignKey;

use Spiral\Database\ForeignKeyInterface;
use Spiral\Migrations\CapsuleInterface;
use Spiral\Migrations\Exception\Operation\ForeignKeyException;
use Spiral\Migrations\Operation\Traits\OptionsTrait;

final class Alter extends ForeignKey
{
    use OptionsTrait;

    /** @var string */
    protected $foreignTable = '';

    /** @var string */
    protected $foreignKey = '';

    /**
     * @param string $table
     * @param string $column
     * @param string $foreignTable
     * @param string $foreignKey
     * @param array  $options
     */
    public function __construct(
        string $table,
        string $column,
        string $foreignTable,
        string $foreignKey,
        array $options
    ) {
        parent::__construct($table, $column);
        $this->foreignTable = $foreignTable;
        $this->foreignKey = $foreignKey;
        $this->options = $options;
    }

    /**
     * {@inheritdoc}
     */
    public function execute(CapsuleInterface $capsule)
    {
        $schema = $capsule->getSchema($this->getTable());

        if (!$schema->hasForeignKey($this->column)) {
            throw new ForeignKeyException(
                "Unable to alter foreign key '{$schema->getName()}'.({$this->column}), "
                . "key does not exists"
            );
        }

        $outerSchema = $capsule->getSchema($this->foreignTable);

        if ($this->foreignTable != $this->table && !$outerSchema->exists()) {
            throw new ForeignKeyException(
                "Unable to alter foreign key '{$schema->getName()}'.'{$this->column}', "
                . "foreign table '{$this->foreignTable}' does not exists"
            );
        }

        if ($this->foreignTable != $this->table && !$outerSchema->hasColumn($this->foreignKey)) {
            throw new ForeignKeyException(
                "Unable to alter foreign key '{$schema->getName()}'.'{$this->column}',"
                . " foreign column '{$this->foreignTable}'.'{$this->foreignKey}' does not exists"
            );
        }

        $foreignKey = $schema->foreignKey($this->column)->references(
            $this->foreignTable,
            $this->foreignKey
        );

        /*
         * We are allowing both formats "NO_ACTION" and "NO ACTION".
         */

        $foreignKey->onDelete(str_replace(
            '_',
            ' ',
            $this->getOption('delete', ForeignKeyInterface::NO_ACTION)
        ));

        $foreignKey->onUpdate(str_replace(
            '_',
            ' ',
            $this->getOption('update', ForeignKeyInterface::NO_ACTION)
        ));
    }
}


<?php
/**
 * Spiral Framework.
 *
 * @license MIT
 * @author  Anton Titov (Wolfy-J)
 */
declare(strict_types=1);

namespace Spiral\Migrations\Operation\Index;

use Spiral\Migrations\CapsuleInterface;
use Spiral\Migrations\Exception\Operation\IndexException;
use Spiral\Migrations\Operation\Traits\OptionsTrait;

final class Alter extends Index
{
    use OptionsTrait;

    /**
     * @param string $table
     * @param array  $columns
     * @param array  $options
     */
    public function __construct(string $table, array $columns, array $options = [])
    {
        parent::__construct($table, $columns);
        $this->options = $options;
    }

    /**
     * {@inheritdoc}
     */
    public function execute(CapsuleInterface $capsule)
    {
        $schema = $capsule->getSchema($this->getTable());

        if (!$schema->hasIndex($this->columns)) {
            $columns = join(',', $this->columns);
            throw new IndexException(
                "Unable to alter index '{$schema->getName()}'.({$columns}), no such index"
            );
        }

        $schema->index($this->columns)->unique(
            $this->getOption('unique', false)
        );
    }
}


<?php
/**
 * Spiral Framework.
 *
 * @license MIT
 * @author  Anton Titov (Wolfy-J)
 */
declare(strict_types=1);

namespace Spiral\Migrations\Operation\Table;

use Spiral\Database\Driver\HandlerInterface;
use Spiral\Migrations\CapsuleInterface;
use Spiral\Migrations\Exception\Operation\TableException;
use Spiral\Migrations\Operation\AbstractOperation;

final class Rename extends AbstractOperation
{
    /** @var string */
    private $newName = '';

    /**
     * @param string $table
     * @param string $newName
     */
    public function __construct(string $table, string $newName)
    {
        parent::__construct($table);
        $this->newName = $newName;
    }

    /**
     * {@inheritdoc}
     */
    public function execute(CapsuleInterface $capsule)
    {
        $schema = $capsule->getSchema($this->getTable());
        $database = $this->database ?? '[default]';

        if (!$schema->exists()) {
            throw new TableException(
                "Unable to rename table '{$database}'.'{$this->getTable()}', table does not exists"
            );
        }

        if ($capsule->getDatabase()->hasTable($this->newName)) {
            throw new TableException(
                "Unable to rename table '{$database}'.'{$this->getTable()}', table '{$this->newName}' already exists"
            );
        }

        $schema->setName($this->newName);
        $schema->save(HandlerInterface::DO_ALL);
    }
}


<?php
/**
 * Spiral Framework.
 *
 * @license MIT
 * @author  Anton Titov (Wolfy-J)
 */
declare(strict_types=1);

namespace Spiral\Migrations;

use Doctrine\Common\Inflector\Inflector;
use Spiral\Core\FactoryInterface;
use Spiral\Files\Files;
use Spiral\Files\FilesInterface;
use Spiral\Migrations\Config\MigrationConfig;
use Spiral\Migrations\Exception\RepositoryException;
use Spiral\Tokenizer\Reflection\ReflectionFile;
use Spiral\Tokenizer\TokenizerInterface;

/**
 * Stores migrations as files.
 */
final class FileRepository implements RepositoryInterface
{
    // Migrations file name format. This format will be used when requesting new migration filename.
    private const FILENAME_FORMAT = '%s_%s_%s.php';

    // Timestamp format for files.
    private const TIMESTAMP_FORMAT = 'Ymd.His';

    /** @var MigrationConfig */
    private $config = null;

    /**
     * Required when multiple migrations added at once.
     *
     * @var int
     */
    private $chunkID = 0;

    /** @var FactoryInterface */
    private $factory;

    /** @var FilesInterface */
    private $files;

    /**
     * @param MigrationConfig  $config
     * @param FactoryInterface $factory
     */
    public function __construct(MigrationConfig $config, FactoryInterface $factory)
    {
        $this->config = $config;
        $this->files = new Files();
        $this->factory = $factory;
    }

    /**
     * {@inheritdoc}
     */
    public function getMigrations(): array
    {
        $migrations = [];

        foreach ($this->getFiles() as $f) {
            if (!class_exists($f['class'], false)) {
                //Attempting to load migration class (we can not relay on autoloading here)
                require_once($f['filename']);
            }

            /** @var MigrationInterface $migration */
            $migration = $this->factory->make($f['class']);

            $migrations[$f['created']->getTimestamp() . $f['chunk']] = $migration->withState(
                new State($f['name'], $f['created'])
            );
        }

        ksort($migrations);

        return $migrations;
    }

    /**
     * {@inheritdoc}
     */
    public function registerMigration(string $name, string $class, string $body = null): string
    {
        if (empty($body) && !class_exists($class)) {
            throw new RepositoryException(
                "Unable to register migration '{$class}', representing class does not exists"
            );
        }

        foreach ($this->getMigrations() as $migration) {
            if (get_class($migration) == $class) {
                throw new RepositoryException(
                    "Unable to register migration '{$class}', migration already exists"
                );
            }

            if ($migration->getState()->getName() == $name) {
                throw new RepositoryException(
                    "Unable to register migration '{$name}', migration under the same name already exists"
                );
            }
        }

        if (empty($body)) {
            //Let's read body from a given class filename
            $body = $this->files->read((new \ReflectionClass($class))->getFileName());
        }

        $filename = $this->createFilename($name);

        //Copying
        $this->files->write($filename, $body, FilesInterface::READONLY, true);

        return $filename;
    }

    /**
     * Internal method to fetch all migration filenames.
     */
    private function getFiles(): \Generator
    {
        foreach ($this->files->getFiles($this->config->getDirectory(), '*.php') as $filename) {
            $reflection = new ReflectionFile($filename);
            $definition = explode('_', basename($filename));

            if (count($definition) < 3) {
                throw new RepositoryException("Invalid migration filename '{$filename}'");
            }

            yield [
                'filename' => $filename,
                'class'    => $reflection->getClasses()[0],
                'created'  => \DateTime::createFromFormat(
                    self::TIMESTAMP_FORMAT,
                    $definition[0]
                ),
                'chunk'    => $definition[1],
                'name'     => str_replace(
                    '.php',
                    '',
                    join('_', array_slice($definition, 2))
                )
            ];
        }
    }

    /**
     * Request new migration filename based on user input and current timestamp.
     *
     * @param string $name
     * @return string
     */
    private function createFilename(string $name): string
    {
        $name = Inflector::tableize($name);

        $filename = sprintf(self::FILENAME_FORMAT,
            date(self::TIMESTAMP_FORMAT),
            $this->chunkID++,
            $name
        );

        return $this->files->normalizePath(
            $this->config->getDirectory() . FilesInterface::SEPARATOR . $filename
        );
    }
}


<?php
/**
 * Spiral Framework.
 *
 * @license MIT
 * @author  Anton Titov (Wolfy-J)
 */
declare(strict_types=1);

namespace Spiral\Migrations\Operation\Column;

use Spiral\Migrations\CapsuleInterface;
use Spiral\Migrations\Exception\Operation\ColumnException;
use Spiral\Migrations\Operation\AbstractOperation;

final class Rename extends AbstractOperation
{
    /** @var string */
    private $name = '';

    /** @var string */
    private $newName = '';

    /**
     * @param string $table
     * @param string $name
     * @param string $newName
     */
    public function __construct(string $table, string $name, string $newName)
    {
        parent::__construct($table);
        $this->name = $name;
        $this->newName = $newName;
    }

    /**
     * {@inheritdoc}
     */
    public function execute(CapsuleInterface $capsule)
    {
        $schema = $capsule->getSchema($this->getTable());

        if (!$schema->hasColumn($this->name)) {
            throw new ColumnException(
                "Unable to rename column '{$schema->getName()}'.'{$this->name}', column does not exists"
            );
        }

        if ($schema->hasColumn($this->newName)) {
            throw new ColumnException(
                "Unable to rename column '{$schema->getName()}'.'{$this->name}', column '{$this->newName}' already exists"
            );
        }

        //Declaring column
        $schema->renameColumn($this->name, $this->newName);
    }
}


<?php
/**
 * Spiral Framework.
 *
 * @license MIT
 * @author  Anton Titov (Wolfy-J)
 */
declare(strict_types=1);

namespace Spiral\Migrations;

use Spiral\Database\Database;
use Spiral\Database\DatabaseInterface;
use Spiral\Migrations\Exception\MigrationException;

/**
 * Simple migration class with shortcut for database and blueprint instances.
 */
abstract class Migration implements MigrationInterface
{
    // Target migration database
    protected const DATABASE = null;

    /** @var State|null */
    private $state = null;

    /** @var CapsuleInterface */
    private $capsule = null;

    /**
     * {@inheritdoc}
     */
    public function getDatabase(): ?string
    {
        return static::DATABASE;
    }

    /**
     * {@inheritdoc}
     */
    public function withCapsule(CapsuleInterface $capsule): MigrationInterface
    {
        $migration = clone $this;
        $migration->capsule = $capsule;

        return $migration;
    }

    /**
     * {@inheritdoc}
     */
    public function withState(State $state): MigrationInterface
    {
        $migration = clone $this;
        $migration->state = $state;

        return $migration;
    }

    /**
     * {@inheritdoc}
     */
    public function getState(): State
    {
        if (empty($this->state)) {
            throw new MigrationException("Unable to get migration state, no state are set");
        }

        return $this->state;
    }

    /**
     * @return Database
     */
    protected function database(): DatabaseInterface
    {
        if (empty($this->capsule)) {
            throw new MigrationException("Unable to get database, no capsule are set");
        }

        return $this->capsule->getDatabase();
    }

    /**
     * Get table schema builder (blueprint).
     *
     * @param string $table
     * @return TableBlueprint
     */
    protected function table(string $table): TableBlueprint
    {
        if (empty($this->capsule)) {
            throw new MigrationException("Unable to get table blueprint, no capsule are set");
        }

        return new TableBlueprint($this->capsule, $table);
    }
}


<?php
/**
 * Spiral Framework.
 *
 * @license MIT
 * @author  Anton Titov (Wolfy-J)
 */
declare(strict_types=1);

namespace Spiral\Migrations\Operation\Column;

use Spiral\Database\Schema\AbstractColumn;
use Spiral\Database\Schema\AbstractTable;
use Spiral\Migrations\Exception\Operation\ColumnException;
use Spiral\Migrations\Operation\AbstractOperation;
use Spiral\Migrations\Operation\Traits\OptionsTrait;

abstract class Column extends AbstractOperation
{
    use OptionsTrait;

    /**
     * Some options has set of aliases.
     *
     * @var array
     */
    protected $aliases = [
        'size'    => ['length', 'limit'],
        'default' => ['defaultValue'],
        'null'    => ['nullable']
    ];

    /** @var string */
    protected $name = '';

    /** @var string */
    protected $type = '';

    /**
     * @param string $table
     * @param string $column
     * @param string $type
     * @param array  $options
     */
    public function __construct(
        string $table,
        string $column,
        string $type = 'string',
        array $options = []
    ) {
        parent::__construct($table);
        $this->name = $column;
        $this->type = $type;
        $this->options = $options;
    }

    /**
     * @param AbstractTable $schema
     *
     * @return AbstractColumn
     * @throws ColumnException
     */
    protected function declareColumn(AbstractTable $schema): AbstractColumn
    {
        $column = $schema->column($this->name);

        //Type configuring
        if (method_exists($column, $this->type)) {
            $arguments = [];

            $method = new \ReflectionMethod($column, $this->type);
            foreach ($method->getParameters() as $parameter) {
                if ($this->hasOption($parameter->getName())) {
                    $arguments[] = $this->getOption($parameter->getName());
                } elseif (!$parameter->isOptional()) {
                    throw new ColumnException(
                        "Option '{$parameter->getName()}' are required to define column with type '{$this->type}'"
                    );
                } else {
                    $arguments[] = $parameter->getDefaultValue();
                }
            }

            call_user_func_array([$column, $this->type], $arguments);
        } else {
            $column->type($this->type);
        }

        $column->nullable($this->getOption('nullable', false));
        $column->defaultValue($this->getOption('default', null));

        return $column;
    }
}


<?php
/**
 * Spiral Framework.
 *
 * @license MIT
 * @author  Anton Titov (Wolfy-J)
 */
declare(strict_types=1);

namespace Spiral\Migrations\Operation\Traits;

/**
 * Consumer must define property "aliases".
 */
trait OptionsTrait
{
    /** @var array */
    protected $options = [];

    /**
     * @param string $name
     *
     * @return bool
     */
    protected function hasOption(string $name): bool
    {
        if (array_key_exists($name, $this->options)) {
            return true;
        }

        if (!isset($this->aliases) || !isset($this->aliases[$name])) {
            return false;
        }

        foreach ($this->aliases[$name] as $name) {
            return $this->hasOption($name);
        }

        return false;
    }

    /**
     * @param string $name
     * @param mixed  $default
     *
     * @return mixed
     */
    protected function getOption(string $name, $default = null)
    {
        if (!$this->hasOption($name)) {
            return $default;
        }

        if (array_key_exists($name, $this->options)) {
            return $this->options[$name];
        }

        if (!isset($this->aliases) || !isset($this->aliases[$name])) {
            return false;
        }

        foreach ($this->aliases[$name] as $name) {
            return $this->hasOption($name);
        }

        return false;
    }
}


<?php
/**
 * Spiral Framework.
 *
 * @license MIT
 * @author  Anton Titov (Wolfy-J)
 */
declare(strict_types=1);

namespace Spiral\Migrations;

/**
 * Migration meta information specific to current environment
 */
final class State
{
    // Migration status
    public const STATUS_UNDEFINED = -1;
    public const STATUS_PENDING   = 0;
    public const STATUS_EXECUTED  = 1;

    /** @var string */
    private $name = '';

    /** @var int */
    private $status = self::STATUS_PENDING;

    /** @var \DateTime */
    private $timeCreated = null;

    /** @var \DateTime|null */
    private $timeExecuted = null;

    /**
     * @param string    $name
     * @param \DateTime $timeCreated
     * @param int       $status
     * @param \DateTime $timeExecuted
     */
    public function __construct(
        string $name,
        \DateTime $timeCreated,
        int $status = self::STATUS_UNDEFINED,
        \DateTime $timeExecuted = null
    ) {
        $this->name = $name;
        $this->status = $status;
        $this->timeCreated = $timeCreated;
        $this->timeExecuted = $timeExecuted;
    }

    /**
     * @return string
     */
    public function getName(): string
    {
        return $this->name;
    }

    /**
     * Migration status.
     *
     * @return int
     */
    public function getStatus(): int
    {
        return $this->status;
    }

    /**
     * Get migration creation time.
     *
     * @return \DateTime
     */
    public function getTimeCreated(): \DateTime
    {
        return $this->timeCreated;
    }

    /**
     * Get migration execution time if any.
     *
     * @return \DateTime|null
     */
    public function getTimeExecuted()
    {
        return $this->timeExecuted;
    }

    /**
     * @param int            $status
     * @param \DateTime|null $timeExecuted
     *
     * @return State
     */
    public function withStatus(int $status, \DateTime $timeExecuted = null): State
    {
        $state = clone $this;
        $state->status = $status;
        $state->timeExecuted = $timeExecuted;

        return $state;
    }
}


<?php
/**
 * Spiral Framework.
 *
 * @license MIT
 * @author  Anton Titov (Wolfy-J)
 */
declare(strict_types=1);

namespace Spiral\Migrations\Operation\Index;

use Spiral\Migrations\CapsuleInterface;
use Spiral\Migrations\Exception\Operation\IndexException;

final class Drop extends Index
{
    /**
     * {@inheritdoc}
     */
    public function execute(CapsuleInterface $capsule)
    {
        $schema = $capsule->getSchema($this->getTable());

        if (!$schema->hasIndex($this->columns)) {
            $columns = join(',', $this->columns);
            throw new IndexException(
                "Unable to drop index '{$schema->getName()}'.({$columns}), index does not exists"
            );
        }

        $schema->dropIndex($this->columns);
    }
}


<?php
/**
 * Spiral Framework.
 *
 * @license MIT
 * @author  Anton Titov (Wolfy-J)
 */
declare(strict_types=1);

namespace Spiral\Migrations\Operation\Table;

use Spiral\Migrations\CapsuleInterface;
use Spiral\Migrations\Exception\Operation\TableException;
use Spiral\Migrations\Operation\AbstractOperation;

final class PrimaryKeys extends AbstractOperation
{
    /** @var array */
    private $columns = [];

    /**
     * @param string $table
     * @param array  $columns
     */
    public function __construct(string $table, array $columns)
    {
        parent::__construct($table);
        $this->columns = $columns;
    }

    /**
     * {@inheritdoc}
     */
    public function execute(CapsuleInterface $capsule)
    {
        $schema = $capsule->getSchema($this->getTable());
        $database = $this->database ?? '[default]';

        if ($schema->exists()) {
            throw new TableException(
                "Unable to set primary keys for table '{$database}'.'{$this->getTable()}', table already exists"
            );
        }

        $schema->setPrimaryKeys($this->columns);
    }
}


<?php
/**
 * Spiral Framework.
 *
 * @license MIT
 * @author  Anton Titov (Wolfy-J)
 */
declare(strict_types=1);

namespace Spiral\Migrations;

use Spiral\Database\Database;
use Spiral\Database\DatabaseInterface;
use Spiral\Database\DatabaseManager;
use Spiral\Database\Schema\AbstractTable;
use Spiral\Database\TableInterface;
use Spiral\Migrations\Exception\CapsuleException;

/**
 * Isolates set of table specific operations and schemas into one place. Kinda repository.
 */
final class Capsule implements CapsuleInterface
{
    /** @var DatabaseManager */
    private $database = null;

    /** @var array */
    private $schemas = [];

    /**
     * @param Database $database
     */
    public function __construct(Database $database)
    {
        $this->database = $database;
    }

    /**
     * {@inheritdoc}
     */
    public function getDatabase(): DatabaseInterface
    {
        return $this->database;
    }

    /**
     * {@inheritdoc}
     */
    public function getTable(string $table): TableInterface
    {
        return $this->database->table($table);
    }

    /**
     * {@inheritdoc}
     */
    public function getSchema(string $table): AbstractTable
    {
        if (!isset($this->schemas[$table])) {
            //We have to declare existed to prevent dropping existed schema
            $this->schemas[$table] = $this->database->table($table)->getSchema();
        }

        return $this->schemas[$table];
    }

    /**
     * {@inheritdoc}
     *
     * @throws \Throwable
     */
    public function execute(array $operations)
    {
        foreach ($operations as $operation) {
            if (!$operation instanceof OperationInterface) {
                throw new CapsuleException(sprintf(
                    "Migration operation expected to be an instance of `OperationInterface`, `%s` given",
                    get_class($operation)
                ));
            }

            $operation->execute($this);
        }
    }
}


<?php

namespace Laracasts\Behat\ServiceContainer;

use RuntimeException;

class LaravelBooter
{

    /**
     * The base path for the application.
     *
     * @var string
     */
    private $basePath;

    /**
     * The application's environment file.
     *
     * @var string
     */
    private $environmentFile;

    /**
     * Create a new Laravel booter instance.
     *
     * @param        $basePath
     * @param string $environmentFile
     */
    public function __construct($basePath, $environmentFile = '.env.behat')
    {
        $this->basePath = $basePath;
        $this->environmentFile = $environmentFile;
    }

    /**
     * Get the application's base path.
     *
     * @return mixed
     */
    public function basePath()
    {
        return $this->basePath;
    }

    /**
     * Get the application's environment file.
     *
     * @return string
     */
    public function environmentFile()
    {
        return $this->environmentFile;
    }

    /**
     * Boot the app.
     *
     * @return mixed
     */
    public function boot()
    {
        $bootstrapPath = $this->basePath() . '/bootstrap/app.php';

        $this->assertBootstrapFileExists($bootstrapPath);

        $app = require $bootstrapPath;

        $app->loadEnvironmentFrom($this->environmentFile());

        $app->make('Illuminate\Contracts\Console\Kernel')->bootstrap();

        $app->make('Illuminate\Http\Request')->capture();

        return $app;
    }

    /**
     * Ensure that the provided Laravel bootstrap path exists.
     *
     * @param string $bootstrapPath
     * @throws RuntimeException
     */
    private function assertBootstrapFileExists($bootstrapPath)
    {
        if ( ! file_exists($bootstrapPath)) {
            throw new RuntimeException('Could not locate the path to the Laravel bootstrap file.');
        }
    }

}