<?php

namespace Reaction\Console\Controllers;

use Reaction\Base\BaseObject;
use Reaction\Base\ComponentInitBlockingInterface;
use Reaction\Exceptions\InvalidConfigException;
use Reaction\Exceptions\NotSupportedException;
use Reaction\Console\Routes\Controller;
use Reaction\Console\Exception;
use Reaction\Helpers\ClassFinderHelper;
use function Reaction\Promise\allInOrder;
use Reaction\Promise\ExtendedPromiseInterface;
use Reaction\Promise\LazyPromise;
use Reaction\Promise\LazyPromiseInterface;
use function Reaction\Promise\reject;
use function Reaction\Promise\resolve;
use function Reaction\Promise\resolveLazy;
use Reaction\RequestApplicationInterface;
use Reaction\Db\MigrationInterface;
use Reaction\Helpers\Console;
use Reaction\Helpers\FileHelper;

/**
 * BaseMigrateController is the base class for migrate controllers.
 */
abstract class BaseMigrateController extends Controller
{
    /**
     * The name of the dummy migration that marks the beginning of the whole migration history.
     */
    const BASE_MIGRATION = 'm000000_000000_base';

    /**
     * @var string the default command action.
     */
    public $defaultAction = 'up';
    /**
     * @var string|array the directory containing the migration classes. This can be either
     * a [path alias](guide:concept-aliases) or a directory path.
     *
     * Migration classes located at this path should be declared without a namespace.
     * Use [[migrationNamespaces]] property in case you are using namespaced migrations.
     *
     * If you have set up [[migrationNamespaces]], you may set this field to `null` in order
     * to disable usage of migrations that are not namespaced.
     *
     * You may also specify an array of migration paths that should be searched for
     * migrations to load. This is mainly useful to support old extensions that provide migrations
     * without namespace and to adopt the new feature of namespaced migrations while keeping existing migrations.
     *
     * In general, to load migrations from different locations, [[migrationNamespaces]] is the preferable solution
     * as the migration name contains the origin of the migration in the history, which is not the case when
     * using multiple migration paths.
     *
     * @see $migrationNamespaces
     */
    public $migrationPath = ['@app/Console/Migrations'];
    /**
     * @var array list of namespaces containing the migration classes.
     *
     * Migration namespaces should be resolvable as a [path alias](guide:concept-aliases) if prefixed with `@`, e.g. if you specify
     * the namespace `app\migrations`, the code `Reaction::getAlias('@app/migrations')` should be able to return
     * the file path to the directory this namespace refers to.
     * This corresponds with the [autoloading conventions](guide:concept-autoloading) of framework.
     *
     * For example:
     *
     * ```php
     * [
     *     'App\Migrations',
     *     'Some\Namespace\Migrations',
     * ]
     * ```
     *
     * @see $migrationPath
     */
    public $migrationNamespaces = [];
    /**
     * @var string the template file for generating new migrations.
     * This can be either a [path alias](guide:concept-aliases) (e.g. "@app/migrations/template.php")
     * or a file path.
     */
    public $templateFile;
    /**
     * @var bool indicates whether the console output should be compacted.
     * If this is set to true, the individual commands ran within the migration will not be output to the console.
     * Default is false, in other words the output is fully verbose by default.
     */
    public $compact = false;

    /**
     * {@inheritdoc}
     */
    public function options($actionId = null)
    {
        return array_merge(
            parent::options($actionId),
            ['migrationPath', 'migrationNamespaces', 'compact'], // global for all actions
            $actionId === 'create' ? ['templateFile'] : [] // action create
        );
    }

    /**
     * This method is invoked right before an action is to be executed (after all possible filters.)
     * It checks the existence of the [[migrationPath]].
     * @param string $actionId the action to be executed.
     * @throws InvalidConfigException if directory specified in migrationPath doesn't exist and action isn't "create".
     * @return bool whether the action should continue to be executed.
     */
    public function beforeAction($actionId)
    {
        if (parent::beforeAction($actionId)) {
            if (empty($this->migrationNamespaces) && empty($this->migrationPath)) {
                throw new InvalidConfigException('At least one of `migrationPath` or `migrationNamespaces` should be specified.');
            }

            foreach ($this->migrationNamespaces as $key => $value) {
                $this->migrationNamespaces[$key] = trim($value, '\\');
            }

            if (is_array($this->migrationPath)) {
                foreach ($this->migrationPath as $i => $path) {
                    $this->migrationPath[$i] = \Reaction::$app->getAlias($path);
                }
            } elseif ($this->migrationPath !== null) {
                $path = \Reaction::$app->getAlias($this->migrationPath);
                if (!is_dir($path)) {
                    if ($actionId !== 'create') {
                        throw new InvalidConfigException("Migration failed. Directory specified in migrationPath doesn't exist: {$this->migrationPath}");
                    }
                    FileHelper::createDirectory($path);
                }
                $this->migrationPath = $path;
            }

            $version = \Reaction::getVersion();
            $this->stdout("Reaction Migration Tool (based on Reaction v{$version})\n\n");

            return true;
        }

        return false;
    }

    /**
     * Upgrades the application by applying new migrations.
     *
     * For example,
     *
     * ```
     * console migrate     # apply all new migrations
     * console migrate 3   # apply the first 3 new migrations
     * ```
     *
     * @param RequestApplicationInterface $app
     * @param int                         $limit the number of new migrations to be applied. If 0, it means
     * applying all available new migrations.
     *
     * @return ExtendedPromiseInterface
     */
    public function actionUp(RequestApplicationInterface $app, $limit = 0)
    {
        return $this->getNewMigrations()->then(
            function($migrations) use ($limit) {
                if (empty($migrations)) {
                    $this->stdout("No new migrations found. Your system is up-to-date.\n", Console::FG_GREEN);

                    return true;
                }

                $total = count($migrations);
                $limit = (int)$limit;
                if ($limit > 0) {
                    $migrations = array_slice($migrations, 0, $limit);
                }

                $n = count($migrations);
                if ($n === $total) {
                    $this->stdout("Total $n new " . ($n === 1 ? 'migration' : 'migrations') . " to be applied:\n", Console::FG_YELLOW);
                } else {
                    $this->stdout("Total $n out of $total new " . ($total === 1 ? 'migration' : 'migrations') . " to be applied:\n", Console::FG_YELLOW);
                }

                foreach ($migrations as $migration) {
                    $nameLimit = $this->getMigrationNameLimit();
                    if ($nameLimit !== null && strlen($migration) > $nameLimit) {
                        $message = "The migration name '$migration' is too long. Its not possible to apply this migration.";
                        $this->stdout("\n$message\n", Console::FG_RED);
                        throw new Exception($message);
                    }
                    $this->stdout("\t$migration\n");
                }
                $this->stdout("\n");

                $applied = 0;
                return $this->confirm('Apply the above ' . ($n === 1 ? 'migration' : 'migrations') . '?')->then(
                    function() use ($migrations, $n, &$applied) {
                        $promises = [];
                        foreach ($migrations as $migration) {
                            $promises[] = $this->migrateUp($migration)->thenLazy(
                                function() use (&$applied) {
                                    $applied++;
                                    return true;
                                },
                                function($error = null) use ($applied, $n) {
                                    $this->stdout("\n$applied from $n " . ($applied === 1 ? 'migration was' : 'migrations were') . " applied.\n", Console::FG_RED);
                                    $this->stdout("\nMigration failed. The rest of the migrations are canceled.\n", Console::FG_RED);
                                    $prevException = $error instanceof \Throwable ? $error : null;
                                    $exception = new Exception("Migration failed. The rest of the migrations are canceled", 0, $prevException);
                                    return reject($exception);
                                }
                            );
                        }

                        return allInOrder($promises)->then(
                            function() use ($n) {
                                $this->stdout("\n$n " . ($n === 1 ? 'migration was' : 'migrations were') . " applied.\n", Console::FG_GREEN);
                                $this->stdout("\nMigrated up successfully.\n", Console::FG_GREEN);
                                return true;
                            }
                        );
                    }
                );
            }
        );
    }

    /**
     * Downgrades the application by reverting old migrations.
     *
     * For example,
     *
     * ```
     * console migrate/down     # revert the last migration
     * console migrate/down 3   # revert the last 3 migrations
     * console migrate/down all # revert all migrations
     * ```
     *
     * @param RequestApplicationInterface $app
     * @param int|string                  $limit the number of migrations to be reverted. Defaults to 1,
     * meaning the last applied migration will be reverted. When value is "all", all migrations will be reverted.
     * @return ExtendedPromiseInterface
     * @throws Exception if the number of the steps specified is less than 1.
     */
    public function actionDown(RequestApplicationInterface $app, $limit = 1)
    {
        if ($limit === 'all') {
            $limit = null;
        } else {
            $limit = (int)$limit;
            if ($limit < 1) {
                throw new Exception('The step argument must be greater than 0.');
            }
        }

        return $this->getMigrationHistory($limit)->then(
            function($migrations) {
                if (empty($migrations)) {
                    $this->stdout("No migration has been done before.\n", Console::FG_YELLOW);

                    return true;
                }
                $migrations = array_keys($migrations);
                $n = count($migrations);
                $this->stdout("Total $n " . ($n === 1 ? 'migration' : 'migrations') . " to be reverted:\n", Console::FG_YELLOW);
                foreach ($migrations as $migration) {
                    $this->stdout("\t$migration\n");
                }
                $this->stdout("\n");

                $reverted = 0;

                return $this->confirm('Revert the above ' . ($n === 1 ? 'migration' : 'migrations') . '?')->then(
                    function() use ($migrations, $n, &$reverted) {
                        $promises = [];
                        foreach ($migrations as $migration) {
                            $promises[] = $this->migrateDown($migration)->thenLazy(
                                function() use (&$reverted) {
                                    $reverted++;
                                    return true;
                                },
                                function($error = null) use ($reverted, $n) {
                                    $this->stdout("\n$reverted from $n " . ($reverted === 1 ? 'migration was' : 'migrations were') . " reverted.\n", Console::FG_RED);
                                    $this->stdout("\nMigration failed. The rest of the migrations are canceled.\n", Console::FG_RED);
                                    return reject($error);
                                }
                            );
                        }
                        return allInOrder($promises)->then(
                            function() use ($n) {
                                $this->stdout("\n$n " . ($n === 1 ? 'migration was' : 'migrations were') . " reverted.\n", Console::FG_GREEN);
                                $this->stdout("\nMigrated down successfully.\n", Console::FG_GREEN);
                            }
                        );
                    }
                );
            }
        );
    }

    /**
     * Redoes the last few migrations.
     *
     * This command will first revert the specified migrations, and then apply
     * them again. For example,
     *
     * ```
     * console migrate/redo     # redo the last applied migration
     * console migrate/redo 3   # redo the last 3 applied migrations
     * console migrate/redo all # redo all migrations
     * ```
     *
     * @param RequestApplicationInterface $app
     * @param int|string                  $limit the number of migrations to be redone. Defaults to 1,
     * meaning the last applied migration will be redone. When equals "all", all migrations will be redone.
     * @return ExtendedPromiseInterface
     * @throws Exception if the number of the steps specified is less than 1.
     */
    public function actionRedo(RequestApplicationInterface $app, $limit = 1)
    {
        if ($limit === 'all') {
            $limit = null;
        } else {
            $limit = (int)$limit;
            if ($limit < 1) {
                throw new Exception('The step argument must be greater than 0.');
            }
        }

        return $this->getMigrationHistory($limit)->then(
            function($migrations) {
                if (empty($migrations)) {
                    $this->stdout("No migration has been done before.\n", Console::FG_YELLOW);

                    return true;
                }
                $migrations = array_keys($migrations);

                $n = count($migrations);
                $this->stdout("Total $n " . ($n === 1 ? 'migration' : 'migrations') . " to be redone:\n", Console::FG_YELLOW);
                foreach ($migrations as $migration) {
                    $this->stdout("\t$migration\n");
                }
                $this->stdout("\n");

                return $this->confirm('Redo the above ' . ($n === 1 ? 'migration' : 'migrations') . '?')
                    ->then(
                        function() use ($migrations, $n) {
                            $promises = [];
                            foreach ($migrations as $migration) {
                                $promises[] = $this->migrateDown($migration)->thenLazy(
                                    null,
                                    function($error) {
                                        $this->stdout("\nMigration failed. The rest of the migrations are canceled.\n", Console::FG_RED);
                                        return reject($error);
                                    }
                                );
                            }
                            foreach (array_reverse($migrations) as $migration) {
                                $promises[] = $this->migrateUp($migration)->thenLazy(
                                    null,
                                    function($error) {
                                        $this->stdout("\nMigration failed. The rest of the migrations are canceled.\n", Console::FG_RED);
                                        return reject($error);
                                    }
                                );
                            }
                            return allInOrder($promises)->then(function() use ($n) {
                                $this->stdout("\n$n " . ($n === 1 ? 'migration was' : 'migrations were') . " redone.\n", Console::FG_GREEN);
                                $this->stdout("\nMigration redone successfully.\n", Console::FG_GREEN);
                            });
                        }
                    );
            }
        );
    }

    /**
     * Upgrades or downgrades till the specified version.
     *
     * Can also downgrade versions to the certain apply time in the past by providing
     * a UNIX timestamp or a string parseable by the strtotime() function. This means
     * that all the versions applied after the specified certain time would be reverted.
     *
     * This command will first revert the specified migrations, and then apply
     * them again. For example,
     *
     * ```
     * console migrate/to 101129_185401                          # using timestamp
     * console migrate/to m101129_185401_create_user_table       # using full name
     * console migrate/to 1392853618                             # using UNIX timestamp
     * console migrate/to "2014-02-15 13:00:50"                  # using strtotime() parseable string
     * console migrate/to App\Migrations\M101129185401CreateUser # using full namespace name
     * ```
     *
     * @param RequestApplicationInterface $app
     * @param string                      $version either the version name or the certain time value in the past
     * that the application should be migrated to. This can be either the timestamp,
     * the full name of the migration, the UNIX timestamp, or the parseable datetime
     * string.
     * @return ExtendedPromiseInterface
     * @throws Exception if the version argument is invalid.
     */
    public function actionTo(RequestApplicationInterface $app, $version)
    {
        if (($namespaceVersion = $this->extractNamespaceMigrationVersion($version)) !== false) {
            return $this->migrateToVersion($app, $namespaceVersion);
        } elseif (($migrationName = $this->extractMigrationVersion($version)) !== false) {
            return $this->migrateToVersion($app, $migrationName);
        } elseif ((string)(int)$version == $version) {
            return $this->migrateToTime($app, $version);
        } elseif (($time = strtotime($version)) !== false) {
            return $this->migrateToTime($app, $time);
        } else {
            throw new Exception("The version argument must be either a timestamp (e.g. 101129_185401),\n the full name of a migration (e.g. m101129_185401_create_user_table),\n the full namespaced name of a migration (e.g. app\\migrations\\M101129185401CreateUserTable),\n a UNIX timestamp (e.g. 1392853000), or a datetime string parseable\nby the strtotime() function (e.g. 2014-02-15 13:00:50).");
        }
    }

    /**
     * Modifies the migration history to the specified version.
     *
     * No actual migration will be performed.
     *
     * ```
     * console migrate/mark 101129_185401                        # using timestamp
     * console migrate/mark m101129_185401_create_user_table     # using full name
     * console migrate/mark app\migrations\M101129185401CreateUser # using full namespace name
     * console migrate/mark m000000_000000_base # reset the complete migration history
     * ```
     *
     * @param RequestApplicationInterface $app
     * @param string                      $version the version at which the migration history should be marked.
     * This can be either the timestamp or the full name of the migration.
     * You may specify the name `m000000_000000_base` to set the migration history to a
     * state where no migration has been applied.
     * @return ExtendedPromiseInterface
     * @throws Exception if the version argument is invalid or the version cannot be found.
     */
    public function actionMark(RequestApplicationInterface $app, $version)
    {
        $originalVersion = $version;
        if (($namespaceVersion = $this->extractNamespaceMigrationVersion($version)) !== false) {
            $version = $namespaceVersion;
        } elseif (($migrationName = $this->extractMigrationVersion($version)) !== false) {
            $version = $migrationName;
        } elseif ($version !== static::BASE_MIGRATION) {
            throw new Exception("The version argument must be either a timestamp (e.g. 101129_185401)\nor the full name of a migration (e.g. m101129_185401_create_user_table)\nor the full name of a namespaced migration (e.g. app\\migrations\\M101129185401CreateUserTable).");
        }

        // try mark up
        return $this->getNewMigrations()->then(
            function($migrations = []) use ($version, $originalVersion) {
                foreach ($migrations as $i => $migration) {
                    if (strpos($migration, $version) === 0) {
                        if ($this->confirm("Set migration history at $originalVersion?")) {
                            for ($j = 0; $j <= $i; ++$j) {
                                $this->addMigrationHistory($migrations[$j]);
                            }
                            $this->stdout("The migration history is set at $originalVersion.\nNo actual migration was performed.\n", Console::FG_GREEN);
                        }

                        return true;
                    }
                }
                return false;
            }
        )->then(
            function($prevResult = false) use ($version, $originalVersion) {
                if ($prevResult) {
                    return true;
                }
                // try mark down
                return $this->getMigrationHistory(null)->then(
                    function($migrations = []) use ($version, $originalVersion) {
                        $migrations = array_keys($migrations);
                        $migrations[] = static::BASE_MIGRATION;
                        foreach ($migrations as $i => $migration) {
                            if (strpos($migration, $version) === 0) {
                                if ($i === 0) {
                                    $this->stdout("Already at '$originalVersion'. Nothing needs to be done.\n", Console::FG_YELLOW);
                                    return resolve(true);
                                } else {
                                    return $this->confirm("Set migration history at $originalVersion?")->then(
                                        function() use ($migrations, $i, $originalVersion) {
                                            for ($j = 0; $j < $i; ++$j) {
                                                $this->removeMigrationHistory($migrations[$j]);
                                            }
                                            $this->stdout("The migration history is set at $originalVersion.\nNo actual migration was performed.\n", Console::FG_GREEN);
                                        }
                                    );
                                }
                            }
                        }
                        return reject(false);
                    }
                );
            }
        )->then(null, function($error = null) use ($originalVersion) {
            $prevException = $error instanceof \Throwable ? $error : null;
            $exception = new Exception("Unable to find the version '$originalVersion'.", 0, $prevException);
            throw $exception;
        });
    }

    /**
     * Truncates the whole database and starts the migration from the beginning.
     *
     * ```
     * console migrate/fresh
     * ```
     *
     * @param RequestApplicationInterface $app
     * @return ExtendedPromiseInterface
     */
    public function actionFresh(RequestApplicationInterface $app)
    {
        if (\Reaction::isProd()) {
            $this->stdout("App env is set to 'production'.\nRefreshing migrations is not possible on production systems.\n");
            return resolve(true);
        }

        $confirmMessage = "Are you sure you want to reset the database and start the migration from the beginning?\nAll data will be lost irreversibly!";
        return $this->confirm($confirmMessage)->then(
            function() {
                return $this->truncateDatabase();
            },
            function() {
                $this->stdout('Action was cancelled by user. Nothing has been performed.');
                return resolve(true);
            }
        )->then(
            function() use ($app) {
                return $this->actionUp($app);
            }
        );
    }

    /**
     * Displays the migration history.
     *
     * This command will show the list of migrations that have been applied
     * so far. For example,
     *
     * ```
     * console migrate/history     # showing the last 10 migrations
     * console migrate/history 5   # showing the last 5 migrations
     * console migrate/history all # showing the whole history
     * ```
     *
     * @param RequestApplicationInterface $app
     * @param int|string                  $limit the maximum number of migrations to be displayed.
     * If it is "all", the whole migration history will be displayed.
     * @return ExtendedPromiseInterface
     * @throws Exception if invalid limit value passed
     */
    public function actionHistory(RequestApplicationInterface $app, $limit = 10)
    {
        if ($limit === 'all') {
            $limit = null;
        } else {
            $limit = (int)$limit;
            if ($limit < 1) {
                throw new Exception('The limit must be greater than 0.');
            }
        }

        return $this->getMigrationHistory($limit)->then(
            function($migrations) use ($limit) {
                if (empty($migrations)) {
                    $this->stdout("No migration has been done before.\n", Console::FG_YELLOW);
                } else {
                    $n = count($migrations);
                    if ($limit > 0) {
                        $this->stdout("Showing the last $n applied " . ($n === 1 ? 'migration' : 'migrations') . ":\n", Console::FG_YELLOW);
                    } else {
                        $this->stdout("Total $n " . ($n === 1 ? 'migration has' : 'migrations have') . " been applied before:\n", Console::FG_YELLOW);
                    }
                    foreach ($migrations as $version => $time) {
                        $this->stdout("\t(" . date('Y-m-d H:i:s', $time) . ') ' . $version . "\n");
                    }
                }
                return true;
            }
        );
    }

    /**
     * Displays the un-applied new migrations.
     *
     * This command will show the new migrations that have not been applied.
     * For example,
     *
     * ```
     * console migrate/new     # showing the first 10 new migrations
     * console migrate/new 5   # showing the first 5 new migrations
     * console migrate/new all # showing all new migrations
     * ```
     *
     * @param RequestApplicationInterface $app
     * @param int|string                  $limit the maximum number of new migrations to be displayed.
     * If it is `all`, all available new migrations will be displayed.
     * @return ExtendedPromiseInterface
     * @throws Exception if invalid limit value passed
     */
    public function actionNew(RequestApplicationInterface $app, $limit = 10)
    {
        if ($limit === 'all') {
            $limit = null;
        } else {
            $limit = (int)$limit;
            if ($limit < 1) {
                throw new Exception('The limit must be greater than 0.');
            }
        }

        return $this->getNewMigrations()->then(
            function($migrations) use ($limit) {
                if (empty($migrations)) {
                    $this->stdout("No new migrations found. Your system is up-to-date.\n", Console::FG_GREEN);
                } else {
                    $n = count($migrations);
                    if ($limit && $n > $limit) {
                        $migrations = array_slice($migrations, 0, $limit);
                        $this->stdout("Showing $limit out of $n new " . ($n === 1 ? 'migration' : 'migrations') . ":\n", Console::FG_YELLOW);
                    } else {
                        $this->stdout("Found $n new " . ($n === 1 ? 'migration' : 'migrations') . ":\n", Console::FG_YELLOW);
                    }

                    foreach ($migrations as $migration) {
                        $this->stdout("\t" . $migration . "\n");
                    }
                }
                return true;
            }
        );
    }

    /**
     * Creates a new migration.
     *
     * This command creates a new migration using the available migration template.
     * After using this command, developers should modify the created migration
     * skeleton by filling up the actual migration logic.
     *
     * ```
     * console migrate/create create_user_table
     * ```
     *
     * In order to generate a namespaced migration, you should specify a namespace before the migration's name.
     * Note that backslash (`\`) is usually considered a special character in the shell, so you need to escape it
     * properly to avoid shell errors or incorrect behavior.
     * For example:
     *
     * ```
     * console migrate/create 'app\\migrations\\createUserTable'
     * ```
     *
     * In case [[migrationPath]] is not set and no namespace is provided, the first entry of [[migrationNamespaces]] will be used.
     *
     * @param RequestApplicationInterface $app
     * @param string                      $name the name of the new migration. This should only contain
     * letters, digits, underscores and/or backslashes.
     *
     * Note: If the migration name is of a special form, for example create_xxx or
     * drop_xxx, then the generated migration file will contain extra code,
     * in this case for creating/dropping tables.
     *
     * @return ExtendedPromiseInterface
     * @throws Exception if the name argument is invalid.
     * @throws \Reaction\Exceptions\Exception
     */
    public function actionCreate(RequestApplicationInterface $app, $name)
    {
        if (!preg_match('/^[\w\\\\]+$/', $name)) {
            throw new Exception('The migration name should contain letters, digits, underscore and/or backslash characters only.');
        }

        list($namespace, $className) = $this->generateClassName($name);
        // Abort if name is too long
        $nameLimit = $this->getMigrationNameLimit();
        if ($nameLimit !== null && strlen($className) > $nameLimit) {
            throw new Exception('The migration name is too long.');
        }

        $migrationPath = $this->findMigrationPath($namespace);

        $file = $migrationPath . DIRECTORY_SEPARATOR . $className . '.php';
        return $this->confirm("Create new migration '$file'?")->then(
            function() use ($app, $name, $className, $namespace, $migrationPath, $file) {
                $content = $this->generateMigrationSourceCode([
                    'name' => $name,
                    'className' => $className,
                    'namespace' => $namespace,
                ], $app);
                FileHelper::createDirectory($migrationPath);
                file_put_contents($file, $content, LOCK_EX);
                $this->stdout("New migration created successfully.\n", Console::FG_GREEN);
                return true;
            }
        );
    }

    /**
     * Checks if given migration version specification matches namespaced migration name.
     * @param string $rawVersion raw version specification received from user input.
     * @return string|false actual migration version, `false` - if not match.
     */
    protected function extractNamespaceMigrationVersion($rawVersion)
    {
        if (preg_match('/^\\\\?([\w_]+\\\\)+m(\d{6}_?\d{6})(\D.*)?$/is', $rawVersion, $matches)) {
            return trim($rawVersion, '\\');
        }

        return false;
    }

    /**
     * Checks if given migration version specification matches migration base name.
     * @param string $rawVersion raw version specification received from user input.
     * @return string|false actual migration version, `false` - if not match.
     */
    protected function extractMigrationVersion($rawVersion)
    {
        if (preg_match('/^m?(\d{6}_?\d{6})(\D.*)?$/is', $rawVersion, $matches)) {
            return 'm' . $matches[1];
        }

        return false;
    }

    /**
     * Generates class base name and namespace from migration name from user input.
     * @param string $name migration name from user input.
     * @return array list of 2 elements: 'namespace' and 'class base name'
     */
    protected function generateClassName($name)
    {
        $namespace = null;
        $name = trim($name, '\\');
        if (strpos($name, '\\') !== false) {
            $namespace = substr($name, 0, strrpos($name, '\\'));
            $name = substr($name, strrpos($name, '\\') + 1);
        } else {
            if ($this->migrationPath === null) {
                $migrationNamespaces = $this->migrationNamespaces;
                $namespace = array_shift($migrationNamespaces);
            }
        }

        if ($namespace === null) {
            $class = 'm' . gmdate('ymd_His') . '_' . $name;
        } else {
            $class = 'M' . gmdate('ymdHis') . ucfirst($name);
        }

        return [$namespace, $class];
    }

    /**
     * Finds the file path for the specified migration namespace.
     * @param string|null $namespace migration namespace.
     * @return string migration file path.
     * @throws Exception on failure.
     */
    protected function findMigrationPath($namespace)
    {
        if (empty($namespace)) {
            return is_array($this->migrationPath) ? reset($this->migrationPath) : $this->migrationPath;
        }

        if (!in_array($namespace, $this->migrationNamespaces, true)) {
            throw new Exception("Namespace '{$namespace}' not found in `migrationNamespaces`");
        }

        return $this->getNamespacePath($namespace);
    }

    /**
     * Returns the file path matching the give namespace.
     * @param string $namespace namespace.
     * @return string file path.
     */
    protected function getNamespacePath($namespace)
    {
        return ClassFinderHelper::getNamespacePath($namespace);
    }

    /**
     * Upgrades with the specified migration class.
     * @param string $class the migration class name
     * @return LazyPromiseInterface whether the migration is successful
     */
    protected function migrateUp($class)
    {
        if ($class === self::BASE_MIGRATION) {
            return resolveLazy(true);
        }

        $this->stdout("*** applying $class\n", Console::FG_YELLOW);
        $start = microtime(true);
        $promise = new LazyPromise(function() use ($class, $start) {
            return $this->createMigration($class)
                ->then(function($migration) {
                    /** @var MigrationInterface $migration */
                    return $migration->up();
                })
                ->then(function() use ($class, $start) {
                    $time = microtime(true) - $start;
                    $this->stdout("*** applied $class (time: " . sprintf('%.3f', $time) . "s)\n\n", Console::FG_GREEN);
                    return $this->addMigrationHistory($class);
                }, function($error = null) use ($class, $start) {
                    $time = microtime(true) - $start;
                    $this->stdout("*** failed to apply $class (time: " . sprintf('%.3f', $time) . "s)\n\n", Console::FG_RED);
                    $prevException = $error instanceof \Throwable ? $error : null;
                    $exception = new Exception("Failed to apply $class (time: " . sprintf('%.3f', $time) . "s)", 0, $prevException);
                    throw new $exception;
                });
        });
        return $promise;
    }

    /**
     * Downgrades with the specified migration class.
     * @param string $class the migration class name
     * @return LazyPromiseInterface whether the migration is successful
     */
    protected function migrateDown($class)
    {
        if ($class === self::BASE_MIGRATION) {
            return resolveLazy(true);
        }

        $this->stdout("*** reverting $class\n", Console::FG_YELLOW);
        $start = microtime(true);
        $promise = new LazyPromise(function() use (&$migration, $class, $start) {
            return $this->createMigration($class)
                ->then(function($migration) {
                    /** @var MigrationInterface $migration */
                    return $migration->down();
                })->then(function() use ($class, $start) {
                    $time = microtime(true) - $start;
                    $this->stdout("*** reverted $class (time: " . sprintf('%.3f', $time) . "s)\n\n", Console::FG_GREEN);
                    return $this->removeMigrationHistory($class);
                }, function($error = null) use ($class, $start) {
                    $time = microtime(true) - $start;
                    $this->stdout("*** failed to revert $class (time: " . sprintf('%.3f', $time) . "s)\n\n", Console::FG_RED);
                    $prevException = $error instanceof \Throwable ? $error : null;
                    $exception = new Exception("Failed to revert $class (time: " . sprintf('%.3f', $time) . "s)", 0, $prevException);
                    throw new $exception;
                });
        });
        return $promise;
    }

    /**
     * Creates a new migration instance.
     * @param string $class the migration class name
     * @return ExtendedPromiseInterface with \Reaction\Db\MigrationInterface the migration instance
     */
    protected function createMigration($class)
    {
        $this->includeMigrationFile($class);

        /** @var MigrationInterface $migration */
        $migration = \Reaction::create($class);
        if ($migration instanceof BaseObject && $migration->canSetProperty('compact')) {
            $migration->compact = $this->compact;
        }
        if ($migration instanceof ComponentInitBlockingInterface) {
            return $migration->initComponent()
                ->then(function() use ($migration) {
                    return $migration;
                });
        }

        return resolve($migration);
    }

    /**
     * Includes the migration file for a given migration class name.
     *
     * This function will do nothing on namespaced migrations, which are loaded by
     * autoloading automatically. It will include the migration file, by searching
     * [[migrationPath]] for classes without namespace.
     * @param string $class the migration class name.
     */
    protected function includeMigrationFile($class)
    {
        $class = trim($class, '\\');
        if (strpos($class, '\\') === false) {
            if (is_array($this->migrationPath)) {
                foreach ($this->migrationPath as $path) {
                    $file = $path . DIRECTORY_SEPARATOR . $class . '.php';
                    if (is_file($file)) {
                        require_once $file;
                        break;
                    }
                }
            } else {
                $file = $this->migrationPath . DIRECTORY_SEPARATOR . $class . '.php';
                require_once $file;
            }
        }
    }

    /**
     * Migrates to the specified apply time in the past.
     * @param RequestApplicationInterface $app
     * @param int                         $time UNIX timestamp value.
     * @return ExtendedPromiseInterface
     */
    protected function migrateToTime(RequestApplicationInterface $app, $time)
    {
        return $this->getMigrationHistory(null)->then(
            function($migrations) use ($app, $time) {
                $migrations = array_values($migrations);
                $count = 0;
                while ($count < count($migrations) && $migrations[$count] > $time) {
                    ++$count;
                }
                if ($count === 0) {
                    $this->stdout("Nothing needs to be done.\n", Console::FG_GREEN);
                } else {
                    return $this->actionDown($app, $count);
                }
                return reject(new Exception("Nothing needs to be done"));
            }
        );
    }

    /**
     * Migrates to the certain version.
     * @param RequestApplicationInterface $app
     * @param string                      $version name in the full format.
     * @return ExtendedPromiseInterface
     * @throws Exception if the provided version cannot be found.
     */
    protected function migrateToVersion(RequestApplicationInterface $app, $version)
    {
        $originalVersion = $version;

        return $this->getNewMigrations()->then(
            function($migrations) use ($app, $version) {
                foreach ($migrations as $i => $migration) {
                    if (strpos($migration, $version) === 0) {
                        return $this->actionUp($app, $i + 1);
                    }
                }
                return reject(null);
            }
        )->then(null, function() use ($app, $version, $originalVersion) {
            return $this->getMigrationHistory(null)
                ->then(function($migrations) use ($app, $version, $originalVersion) {
                    $migrations = array_keys($migrations);
                    foreach ($migrations as $i => $migration) {
                        if (strpos($migration, $version, $originalVersion) === 0) {
                            if ($i === 0) {
                                $this->stdout("Already at '$originalVersion'. Nothing needs to be done.\n", Console::FG_YELLOW);
                            } else {
                                $this->actionDown($app, $i);
                            }

                            return resolve(true);
                        }
                    }
                    throw new Exception("Unable to find the version '$originalVersion'.");
                });
        });
    }

    /**
     * Returns the migrations that are not applied.
     * @return ExtendedPromiseInterface with array list of new migrations
     */
    protected function getNewMigrations()
    {
        return $this->getMigrationHistory(null)->then(function($history) {
            $applied = [];
            foreach ($history as $class => $time) {
                $applied[trim($class, '\\')] = true;
            }
            $migrationPaths = [];
            if (is_array($this->migrationPath)) {
                foreach ($this->migrationPath as $path) {
                    $migrationPaths[] = [$path, ''];
                }
            } elseif (!empty($this->migrationPath)) {
                $migrationPaths[] = [$this->migrationPath, ''];
            }
            foreach ($this->migrationNamespaces as $namespace) {
                $migrationPaths[] = [$this->getNamespacePath($namespace), $namespace];
            }

            $migrations = [];
            foreach ($migrationPaths as $item) {
                list($migrationPath, $namespace) = $item;
                if (!file_exists($migrationPath)) {
                    continue;
                }
                $handle = opendir($migrationPath);
                while (($file = readdir($handle)) !== false) {
                    if ($file === '.' || $file === '..') {
                        continue;
                    }
                    $path = $migrationPath . DIRECTORY_SEPARATOR . $file;
                    if (preg_match('/^(m(\d{6}_?\d{6})\D.*?)\.php$/is', $file, $matches) && is_file($path)) {
                        $class = $matches[1];
                        if (!empty($namespace)) {
                            $class = $namespace . '\\' . $class;
                        }
                        $time = str_replace('_', '', $matches[2]);
                        if (!isset($applied[$class])) {
                            $migrations[$time . '\\' . $class] = $class;
                        }
                    }
                }
                closedir($handle);
            }
            ksort($migrations);

            return array_values($migrations);
        });
    }

    /**
     * Generates new migration source PHP code.
     * Child class may override this method, adding extra logic or variation to the process.
     * @param array                       $params generation parameters, usually following parameters are present:
     *
     *  - name: string migration base name
     *  - className: string migration class name
     *
     * @param RequestApplicationInterface $app
     * @return string generated PHP code.
     */
    protected function generateMigrationSourceCode($params, RequestApplicationInterface $app)
    {
        return $this->renderFile($app, \Reaction::$app->getAlias($this->templateFile), $params);
    }

    /**
     * Truncates the database.
     * This method should be overwritten in subclasses to implement the task of clearing the database.
     * @throws NotSupportedException if not overridden
     * @return ExtendedPromiseInterface when finished
     */
    protected function truncateDatabase()
    {
        throw new NotSupportedException('This command is not implemented in ' . get_class($this));
    }

    /**
     * Return the maximum name length for a migration.
     *
     * Subclasses may override this method to define a limit.
     * @return int|null the maximum name length for a migration or `null` if no limit applies.
     */
    protected function getMigrationNameLimit()
    {
        return null;
    }

    /**
     * Returns the migration history.
     * @param int $limit the maximum number of records in the history to be returned. `null` for "no limit".
     * @return ExtendedPromiseInterface with array the migration history
     */
    abstract protected function getMigrationHistory($limit);

    /**
     * Adds new migration entry to the history.
     * @param string $version migration version name.
     * @return ExtendedPromiseInterface when finished
     */
    abstract protected function addMigrationHistory($version);

    /**
     * Removes existing migration from the history.
     * @param string $version migration version name.
     * @return ExtendedPromiseInterface when finished
     */
    abstract protected function removeMigrationHistory($version);
}


<?php

namespace Ekyna\Bundle\CoreBundle\Menu\Voter;

use Knp\Menu\ItemInterface;
use Knp\Menu\Matcher\Voter\VoterInterface;
use Symfony\Component\HttpFoundation\Request;

/**
 * Class UriVoter
 * @package Ekyna\Bundle\CoreBundle\Menu\Voter
 * @author Étienne Dauvergne <contact@ekyna.com>
 */
class UriVoter implements VoterInterface
{
    /**
     * @var Request
     */
    private $request;

    /**
     * @param Request $request
     */
    public function setRequest(Request $request)
    {
        $this->request = $request;
    }

    /**
     * {@inheritdoc}
     */
    public function matchItem(ItemInterface $item)
    {
        if (null === $this->request) {
            return null;
        }

        $uri = $item->getUri();
        if (0 < strlen($scriptName = $this->request->getScriptName())) {
            if (0 === strpos($uri, $this->request->getScriptName())) {
                $uri = substr($uri, strlen($this->request->getScriptName()));
            }
        }

        if (1 >= strlen($uri)) {
            return null;
        }

        if (preg_match(sprintf('#^%s#', $uri), $this->request->getPathInfo())) {
            return true;
        }

        return null;
    }
}


<?php

namespace Think\Controller;

use Think\App;
use Think\Controller;

/**
 * ThinkPHP REST控制器类
 */
class RestController extends Controller
{
    // 当前请求类型
    protected $_method = '';
    // 当前请求的资源类型
    protected $_type = '';
    // REST允许的请求类型列表
    protected $allowMethod = ['get', 'post', 'put', 'delete'];
    // REST默认请求类型
    protected $defaultMethod = 'get';
    // REST允许请求的资源类型列表
    protected $allowType = ['html', 'xml', 'json', 'rss'];
    // 默认的资源类型
    protected $defaultType = 'html';
    // REST允许输出的资源类型列表
    protected $allowOutputType = [
        'xml'  => 'application/xml',
        'json' => 'application/json',
        'html' => 'text/html',
    ];

    /**
     * RestController constructor.
     */
    public function __construct()
    {
        // 资源类型检测
        // 自动检测资源类型
        if ('' == __EXT__) {
            $this->_type = $this->getAcceptType();
        } elseif (!in_array(__EXT__, $this->allowType)) {
            // 资源类型非法 则用默认资源类型访问
            $this->_type = $this->defaultType;
        } else {
            $this->_type = __EXT__;
        }

        // 请求方式检测
        $method = strtolower(REQUEST_METHOD);
        if (!in_array($method, $this->allowMethod)) {
            // 请求方式非法 则用默认请求方法
            $method = $this->defaultMethod;
        }
        $this->_method = $method;

        parent::__construct();
    }

    /**
     * 魔术方法 有不存在的操作的时候执行
     * @param string $method 方法名
     * @param array $args 参数
     * @return mixed
     * @throws \ReflectionException
     * @throws \Think\BaseException
     */
    public function __call($method, $args)
    {
        if (0 === strcasecmp($method, ACTION_NAME.C('ACTION_SUFFIX'))) {
            // RESTFul方法支持
            if (method_exists($this, $method.'_'.$this->_method.'_'.$this->_type)) {
                $fun = $method.'_'.$this->_method.'_'.$this->_type;
                App::invokeAction($this, $fun);
            } elseif ($this->_method == $this->defaultMethod && method_exists($this, $method.'_'.$this->_type)) {
                $fun = $method.'_'.$this->_type;
                App::invokeAction($this, $fun);
            } elseif ($this->_type == $this->defaultType && method_exists($this, $method.'_'.$this->_method)) {
                $fun = $method.'_'.$this->_method;
                App::invokeAction($this, $fun);
            } elseif (method_exists($this, '_empty')) {
                // 如果定义了_empty操作 则调用
                $this->_empty($method, $args);
            } elseif (file_exists_case($this->view->parseTemplate())) {
                // 检查是否存在默认模版 如果有直接输出模版
                $this->display();
            } else {
                E(L('_ERROR_ACTION_').':'.ACTION_NAME);
            }
        }
    }

    /**
     * 获取当前请求的Accept头信息
     * @return string
     */
    protected function getAcceptType()
    {
        $type = [
            'xml'  => 'application/xml,text/xml,application/x-xml',
            'json' => 'application/json,text/x-json,application/jsonrequest,text/json',
            'js'   => 'text/javascript,application/javascript,application/x-javascript',
            'css'  => 'text/css',
            'rss'  => 'application/rss+xml',
            'yaml' => 'application/x-yaml,text/yaml',
            'atom' => 'application/atom+xml',
            'pdf'  => 'application/pdf',
            'text' => 'text/plain',
            'png'  => 'image/png',
            'jpg'  => 'image/jpg,image/jpeg,image/pjpeg',
            'gif'  => 'image/gif',
            'csv'  => 'text/csv',
            'html' => 'text/html,application/xhtml+xml,*/*',
        ];

        foreach ($type as $key => $val) {
            $array = explode(',', $val);
            foreach ($array as $k => $v) {
                if (stristr($_SERVER['HTTP_ACCEPT'], $v)) {
                    return $key;
                }
            }
        }

        return false;
    }

    /**
     * 发送Http状态信息
     * @param $code
     */
    protected function sendHttpStatus($code)
    {
        static $_status = [
            // Informational 1xx
            100 => 'Continue',
            101 => 'Switching Protocols',
            // Success 2xx
            200 => 'OK',
            201 => 'Created',
            202 => 'Accepted',
            203 => 'Non-Authoritative Information',
            204 => 'No Content',
            205 => 'Reset Content',
            206 => 'Partial Content',
            // Redirection 3xx
            300 => 'Multiple Choices',
            301 => 'Moved Permanently',
            302 => 'Moved Temporarily ',  // 1.1
            303 => 'See Other',
            304 => 'Not Modified',
            305 => 'Use Proxy',
            // 306 is deprecated but reserved
            307 => 'Temporary Redirect',
            // Client Error 4xx
            400 => 'Bad Request',
            401 => 'Unauthorized',
            402 => 'Payment Required',
            403 => 'Forbidden',
            404 => 'Not Found',
            405 => 'Method Not Allowed',
            406 => 'Not Acceptable',
            407 => 'Proxy Authentication Required',
            408 => 'Request Timeout',
            409 => 'Conflict',
            410 => 'Gone',
            411 => 'Length Required',
            412 => 'Precondition Failed',
            413 => 'Request Entity Too Large',
            414 => 'Request-URI Too Long',
            415 => 'Unsupported Media Type',
            416 => 'Requested Range Not Satisfiable',
            417 => 'Expectation Failed',
            // Server Error 5xx
            500 => 'Internal Server Error',
            501 => 'Not Implemented',
            502 => 'Bad Gateway',
            503 => 'Service Unavailable',
            504 => 'Gateway Timeout',
            505 => 'HTTP Version Not Supported',
            509 => 'Bandwidth Limit Exceeded',
        ];
        if (isset($_status[$code])) {
            header('HTTP/1.1 '.$code.' '.$_status[$code]);
            // 确保FastCGI模式下正常
            header('Status:'.$code.' '.$_status[$code]);
        }
    }

    /**
     * 编码数据
     * @param mixed $data 要返回的数据
     * @param String $type 返回类型 JSON XML
     * @return string
     */
    protected function encodeData($data, $type = '')
    {
        if (empty($data)) {
            return '';
        }
        if ('json' == $type) {
            // 返回JSON数据格式到客户端 包含状态信息
            $data = json_encode($data);
        } elseif ('xml' == $type) {
            // 返回xml格式数据
            $data = xml_encode($data);
        } elseif ('php' == $type) {
            $data = serialize($data);
        }// 默认直接输出
        $this->setContentType($type);

        //header('Content-Length: ' . strlen($data));
        return $data;
    }

    /**
     * 设置页面输出的CONTENT_TYPE和编码
     * @param string $type content_type 类型对应的扩展名
     * @param string $charset 页面输出编码
     * @return void
     */
    public function setContentType($type, $charset = '')
    {
        if (headers_sent()) {
            return;
        }
        if (empty($charset)) {
            $charset = C('DEFAULT_CHARSET');
        }
        $type = strtolower($type);
        //过滤content_type
        if (isset($this->allowOutputType[$type])) {
            header('Content-Type: '.$this->allowOutputType[$type].'; charset='.$charset);
        }
    }

    /**
     * 输出返回数据
     * @param mixed $data 要返回的数据
     * @param String $type 返回类型 JSON XML
     * @param integer $code HTTP状态
     * @return void
     */
    protected function response($data, $type = '', $code = 200)
    {
        $this->sendHttpStatus($code);
        exit($this->encodeData($data, strtolower($type)));
    }
}


<?php

namespace Ekyna\Bundle\CoreBundle\Form\Extension;

use Symfony\Component\Form\AbstractTypeExtension;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\Form\FormEvents;
use Symfony\Component\Form\FormEvent;
use Symfony\Component\Form\FormView;
use Symfony\Component\Form\FormInterface;

/**
 * Class FormTypeRedirectExtension
 * @package Ekyna\Bundle\CoreBundle\Form\Extension
 * @author Étienne Dauvergne <contact@ekyna.com>
 */
class FormTypeRedirectExtension extends AbstractTypeExtension
{
    /**
     * @var \Symfony\Component\HttpFoundation\RequestStack
     */
    protected $requestStack;

    /**
     * Constructor.
     *
     * @param RequestStack $requestStack
     */
    public function __construct(RequestStack $requestStack)
    {
        $this->requestStack = $requestStack;
    }

    /**
     * {@inheritDoc}
     */
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        // Abort if not enabled
        if (! (array_key_exists('_redirect_enabled', $options) && $options['_redirect_enabled'] === true)) {
            return;
        }

        // Retrieve the _redirect path from request (GET)
        $redirectPath = null;
        if (null !== $request = $this->requestStack->getCurrentRequest()) {
            $redirectPath = $request->query->get('_redirect', null);
        }

        // Add the hidden field
        $builder->addEventListener(
            FormEvents::PRE_SET_DATA,
            function (FormEvent $event) {
                $form = $event->getForm();
                // Only "root" form
                if(null !== $form->getParent()) {
                    return;
                }
                $form->add('_redirect', 'hidden', ['mapped' => false]);
            }
        );

        // Sets the data
        $builder->addEventListener(
            FormEvents::POST_SET_DATA,
            function (FormEvent $event) use ($redirectPath, $request) {
                $form = $event->getForm();
                // Only "root" form
                if(null !== $form->getParent()) {
                    return;
                }
                // If form has been posted => retrieve _redirect path from request (POST)
                if(null === $redirectPath && null !== $request) {
                    $datas = $request->request->get($form->getName(), ['_redirect' => null]);
                    if (array_key_exists('_redirect', $datas)) {
                        $redirectPath = $datas['_redirect'];
                    }
                }
                $form->get('_redirect')->setData($redirectPath);
            }
        );
    }

	public function buildView(FormView $view, FormInterface $form, array $options)
	{
	    // Only "root" form
	    if(null !== $form->getParent()) {
	        return;
	    }
	}

    /**
     * {@inheritDoc}
     */
    public function configureOptions(OptionsResolver $resolver)
    {
        $resolver
            ->setDefined(['_redirect_enabled'])
            ->setAllowedTypes('_redirect_enabled', 'bool')
        ;
    }

    /**
     * {@inheritDoc}
     */
    public function getExtendedType()
    {
    	return 'form';
    }
}


<?php
/**
 * Поле типа «timestamp»
 *
 * @version ${product.version}
 *
 * @copyright 2013, Михаил Красильников <m.krasilnikov@yandex.ru>
 * @license http://www.gnu.org/licenses/gpl.txt	GPL License 3
 * @author Михаил Красильников <m.krasilnikov@yandex.ru>
 *
 * Данная программа является свободным программным обеспечением. Вы
 * вправе распространять ее и/или модифицировать в соответствии с
 * условиями версии 3 либо (по вашему выбору) с условиями более поздней
 * версии Стандартной Общественной Лицензии GNU, опубликованной Free
 * Software Foundation.
 *
 * Мы распространяем эту программу в надежде на то, что она будет вам
 * полезной, однако НЕ ПРЕДОСТАВЛЯЕМ НА НЕЕ НИКАКИХ ГАРАНТИЙ, в том
 * числе ГАРАНТИИ ТОВАРНОГО СОСТОЯНИЯ ПРИ ПРОДАЖЕ и ПРИГОДНОСТИ ДЛЯ
 * ИСПОЛЬЗОВАНИЯ В КОНКРЕТНЫХ ЦЕЛЯХ. Для получения более подробной
 * информации ознакомьтесь со Стандартной Общественной Лицензией GNU.
 *
 * Вы должны были получить копию Стандартной Общественной Лицензии
 * GNU с этой программой. Если Вы ее не получили, смотрите документ на
 * <http://www.gnu.org/licenses/>
 *
 * @package ORM
 */

/**
 * Поле типа «timestamp»
 *
 * @package ORM
 * @since 3.00
 */
class ORM_Field_Timestamp extends ORM_Field_Abstract
{
    /**
     * Возвращает имя типа
     *
     * @return string
     *
     * @since 3.00
     */
    public function getTypeName()
    {
        return 'timestamp';
    }

    /**
     * Возвращает соответствующий тип PDO (PDO::PARAM_…) или null
     *
     * @return null|int
     *
     * @since 3.00
     */
    public function getPdoType()
    {
        return PDO::PARAM_INT;
    }

    /**
     * Преобразует значение ORM в значение PDO
     *
     * @param mixed $ormValue
     *
     * @throws InvalidArgumentException
     *
     * @return mixed
     *
     * @since 3.00
     */
    public function orm2pdo($ormValue)
    {
        if (is_null($ormValue))
        {
            return null;
        }
        if ($ormValue instanceof DateTime)
        {
            /* @var DateTime $ormValue */
            return $ormValue->getTimestamp();
        }
        else
        {
            return intval($ormValue);
        }
    }

    /**
     * Преобразует значение PDO в значение ORM
     *
     * @param mixed $pdoValue
     *
     * @return mixed
     *
     * @since 3.00
     */
    public function pdo2orm($pdoValue)
    {
        return new DateTime('@' . strval($pdoValue));
    }

    /**
     * Возвращает выражение SQL для описания поля при создании таблицы
     *
     * @return string
     */
    public function getSqlFieldDefinition()
    {
        return $this->getName() . ' TIMESTAMP';
    }
}


<?php namespace Ordercloud\Entities\Organisations;

use JsonSerializable;

class OrganisationOperatingHours implements JsonSerializable
{
    /** @var integer */
    private $id;
    /** @var integer */
    private $day;
    /** @var string */
    private $openTime;
    /** @var string */
    private $closeTime;
    /** @var string */
    private $dayName;

    public function __construct($id, $day, $openTime, $closeTime, $dayName)
    {
        $this->id = $id;
        $this->day = $day;
        $this->openTime = $openTime;
        $this->closeTime = $closeTime;
        $this->dayName = $dayName;
    }

    /**
     * @return int
     */
    public function getId()
    {
        return $this->id;
    }

    /**
     * @return int
     */
    public function getDay()
    {
        return $this->day;
    }

    /**
     * @param int $day
     */
    public function setDay($day)
    {
        $this->day = $day;
    }

    /**
     * @return string
     */
    public function getOpenTime()
    {
        return $this->openTime;
    }

    /**
     * @param string $openTime
     */
    public function setOpenTime($openTime)
    {
        $this->openTime = $openTime;
    }

    /**
     * @return string
     */
    public function getCloseTime()
    {
        return $this->closeTime;
    }

    /**
     * @param string $closeTime
     */
    public function setCloseTime($closeTime)
    {
        $this->closeTime = $closeTime;
    }

    /**
     * @return string
     */
    public function getDayName()
    {
        return $this->dayName;
    }

    /**
     * @param string $dayName
     */
    public function setDayName($dayName)
    {
        $this->dayName = $dayName;
    }

    /**
     * Specify data which should be serialized to JSON
     */
    public function jsonSerialize()
    {
        return [
            'id' => $this->getId(),
            'day' => $this->getDay(),
            'openTime' => $this->getOpenTime(),
            'closeTime' => $this->getCloseTime(),
            'dayName' => $this->getDayName(),
        ];
    }
}


<?php

/*
 * This file is part of the SmarTwig (Twig Extension).
 *
 * (c) Omar Yepez <omar.yepez@yepsua.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Yepsua\SmarTwig\Twig\Extension;

use \YsJQuery;
use \YsArgument;

class JQueryCoreExtension extends \Twig_Extension {

  /**
   * Returns the token parser instance to add to the existing list.
   *
   * @return array An array of Twig_TokenParser instances
   */
  public function getTokenParsers() {
    return array(
        new \Yepsua\SmarTwig\Twig\TokenParser\RCTokenParser(),
        new \Yepsua\SmarTwig\Twig\TokenParser\AjaxStatusTokenParser(),
    );
  }

  /**
  * Returns a list of functions to add to the existing list.
  *
  * @return array An array of functions
  */
  public function getFunctions(){
    return array(
        'jQuery' => new \Twig_Function_Method($this, 'jsJquery',array('is_safe' => array('html'))),
        'arg' => new \Twig_Function_Method($this, 'argument',array('is_safe' => array('html'))),
    );
  }

  public function jsJquery($selector, $event = null, $sintax = null){
    $jquery = new YsJQuery($selector, $event);
    if($sintax !== null){
      $jquery->execute($sintax);
    }
    return $jquery;
  }

  public function argument($value){
    return new YsArgument($value);
  }

  /**
   * Returns the name of the extension.
   *
   * @return string The extension name
   */
  public function getName() {
    return 'jquery.core';
  }
}


<?php

namespace Think\Model;

use Think\Model;

/**
 * 关联模型
 */
abstract class RelationModel extends Model
{

    const   HAS_ONE = 1;
    const   BELONGS_TO = 2;
    const   HAS_MANY = 3;
    const   MANY_TO_MANY = 4;

    /**
     * @var array $_link 关联定义
     */
    protected $_link = [];

    /**
     * 动态方法实现
     *
     * @param string $method 方法名称
     * @param array $args 调用参数
     *
     * @return mixed
     * @throws \Think\BaseException
     */
    public function __call($method, $args)
    {
        if (strtolower(substr($method, 0, 8)) == 'relation') {
            $type = strtoupper(substr($method, 8));
            if (in_array($type, ['ADD', 'SAVE', 'DEL'], true)) {
                array_unshift($args, $type);

                return call_user_func_array([&$this, 'opRelation'], $args);
            }
        } else {
            return parent::__call($method, $args);
        }
    }

    /**
     * 进行关联查询

     *
     * @param mixed $name 关联名称
     *
     * @return Model
     */
    public function relation($name)
    {
        $this->options['link'] = $name;

        return $this;
    }

    /**
     * 关联数据获取 仅用于查询后

     *
     * @param string $name 关联名称
     *
     * @return array|bool
     */
    public function relationGet($name)
    {
        if (empty($this->data)) {
            return false;
        }

        return $this->getRelation($this->data, $name, true);
    }

    /**
     * 获取返回数据的关联记录

     *
     * @param mixed $result 返回数据
     * @param string|array $name 关联名称
     * @param bool $return 是否返回关联数据本身
     *
     * @return array
     */
    protected function getRelation(&$result, $name = '', $return = false)
    {
        if (!empty($this->_link)) {
            foreach ($this->_link as $key => $val) {
                // 映射名称
                $mappingName = $val['mapping_name'] ?? $key;
                if (empty($name) || true === $name || $mappingName == $name || (is_array($name) && in_array(
                            $mappingName,
                            $name
                        ))
                ) {
                    //  关联类型
                    $mappingType = $val['mapping_type'] ?? $val;
                    //  关联类名
                    $mappingClass = $val['class_name'] ?? $key;
                    // 映射字段
                    $mappingFields = $val['mapping_fields'] ?? '*';
                    // 关联条件
                    $mappingCondition = $val['condition'] ?? [];
                    // 关联键名
                    $mappingKey = $val['mapping_key'] ?? $this->getPk();
                    if (strtoupper($mappingClass) == strtoupper($this->name)) {
                        // 自引用关联 获取父键名
                        $mappingFk = $val['parent_key'] ?? 'parent_id';
                    } else {
                        //  关联外键
                        $mappingFk = $val['foreign_key'] ?? strtolower(
                                $this->name
                            ).'_id';
                    }
                    // 获取关联模型对象
                    $model = D($mappingClass);
                    switch ($mappingType) {
                        case static::HAS_ONE:
                            $pk = $result[$mappingKey];
                            $mappingCondition[$mappingFk] = $pk;
                            $relationData = $model->where($mappingCondition)->field(
                                $mappingFields
                            )->find();
                            break;
                        case static::BELONGS_TO:
                            if (strtoupper($mappingClass) == strtoupper($this->name)) {
                                // 自引用关联 获取父键名
                                $mappingFk = $val['parent_key'] ?? 'parent_id';
                            } else {
                                //  关联外键
                                $mappingFk = $val['foreign_key'] ?? strtolower(
                                        $model->getModelName()
                                    ).'_id';
                            }
                            $fk = $result[$mappingFk];
                            $mappingCondition[$model->getPk()] = $fk;
                            $relationData = $model->where($mappingCondition)->field(
                                $mappingFields
                            )->find();
                            break;
                        case static::HAS_MANY:
                            $pk = $result[$mappingKey];
                            $mappingCondition[$mappingFk] = $pk;
                            $mappingOrder = $val['mapping_order'] ?? '';
                            $mappingLimit = $val['mapping_limit'] ?? '';
                            // 延时获取关联记录
                            $relationData = $model->where($mappingCondition)
                                ->field($mappingFields)
                                ->order($mappingOrder)
                                ->limit($mappingLimit)
                                ->select();
                            break;
                        case static::MANY_TO_MANY:
                            $pk = $result[$mappingKey];
                            $prefix = $this->tablePrefix;
                            $mappingCondition[$mappingFk] = $pk;
                            $mappingOrder = $val['mapping_order'];
                            $mappingLimit = $val['mapping_limit'];
                            $mappingRelationFk = $val['relation_foreign_key'] ?? $model->getModelName(
                                ).'_id';
                            if (isset($val['relation_table'])) {
                                $mappingRelationTable = preg_replace_callback(
                                    '/__([A-Z_-]+)__/sU',
                                    function ($match) use ($prefix) {
                                        return $prefix.strtolower($match[1]);
                                    },
                                    $val['relation_table']
                                );
                            } else {
                                $mappingRelationTable = $this->getRelationTableName($model);
                            }
                            $mappingConditionStr = '';
                            foreach ($mappingCondition as $k => $v) {
                                $mappingConditionStr .= '`'.$k.'`='.$v;
                            }
                            $sql = "SELECT b.{$mappingFields} FROM {$mappingRelationTable} AS a, "
                                .$model->getTableName()." AS b WHERE a.{$mappingRelationFk} =".
                                " b.{$model->getPk()} AND a.{$mappingConditionStr}";
                            if (!empty($val['condition'])) {
                                $sql .= ' AND '.$val['condition'];
                            }
                            if (!empty($mappingOrder)) {
                                $sql .= ' ORDER BY '.$mappingOrder;
                            }
                            if (!empty($mappingLimit)) {
                                $sql .= ' LIMIT '.$mappingLimit;
                            }
                            $relationData = $this->query($sql);
                            break;
                    }
                    if (!$return) {
                        if (isset($val['as_fields']) && in_array($mappingType, [static::HAS_ONE, static::BELONGS_TO])) {
                            // 支持直接把关联的字段值映射成数据对象中的某个字段
                            // 仅仅支持HAS_ONE BELONGS_TO
                            $fields = explode(',', $val['as_fields']);
                            foreach ($fields as $field) {
                                if (strpos($field, ':')) {
                                    list($relationName, $nick) = explode(':', $field);
                                    $result[$nick] = $relationData[$relationName];
                                } else {
                                    $result[$field] = $relationData[$field];
                                }
                            }
                        } else {
                            $result[$mappingName] = $relationData;
                        }
                        unset($relationData);
                    } else {
                        return $relationData;
                    }
                }
            }
        }

        return $result;
    }

    /**
     * 得到关联的数据表名

     *
     * @param Model $relation
     *
     * @return string
     */
    public function getRelationTableName(Model $relation)
    {
        $relationTable = $this->tablePrefix ?? '';
        $relationTable .= $this->tableName ?? $this->name;
        $relationTable .= '_'.$relation->getModelName();

        return strtolower($relationTable);
    }

    /**
     * 查询成功后的回调方法
     *
     * @param $result
     * @param $options
     */
    protected function _after_find(&$result, $options)
    {
        // 获取关联数据 并附加到结果中
        if (!empty($options['link'])) {
            $this->getRelation($result, $options['link']);
        }
    }

    /**
     * 查询数据集成功后的回调方法
     *
     * @param $result
     * @param $options
     */
    protected function _after_select(&$result, $options)
    {
        // 获取关联数据 并附加到结果中
        if (!empty($options['link'])) {
            $this->getRelations($result, $options['link']);
        }
    }

    /**
     * 获取返回数据集的关联记录

     *
     * @param array $resultSet 返回数据
     * @param string|array $name 关联名称
     *
     * @return array
     */
    protected function getRelations(&$resultSet, $name = '')
    {
        // 获取记录集的主键列表
        foreach ($resultSet as $key => $val) {
            $val = $this->getRelation($val, $name);
            $resultSet[$key] = $val;
        }

        return $resultSet;
    }

    /**
     * 写入成功后的回调方法
     *
     * @param $data
     * @param $options
     */
    protected function _after_insert($data, $options)
    {
        // 关联写入
        if (!empty($options['link'])) {
            $this->opRelation('ADD', $data, $options['link']);
        }
    }

    /**
     * 操作关联数据

     *
     * @param string $opType 操作方式 ADD SAVE DEL
     * @param mixed $data 数据对象
     * @param string $name 关联名称
     *
     * @return mixed
     */
    protected function opRelation($opType, $data = '', $name = '')
    {
        $result = false;
        if (empty($data) && !empty($this->data)) {
            $data = $this->data;
        } elseif (!is_array($data)) {
            // 数据无效返回
            return false;
        }
        if (!empty($this->_link)) {
            // 遍历关联定义
            foreach ($this->_link as $key => $val) {
                // 操作制定关联类型
                // 映射名称
                $mappingName = $val['mapping_name'] ?? $key;
                if (empty($name) || true === $name || $mappingName == $name || (is_array($name) && in_array(
                            $mappingName,
                            $name
                        ))
                ) {
                    // 操作制定的关联
                    //  关联类型
                    $mappingType = $val['mapping_type'] ?? $val;
                    //  关联类名
                    $mappingClass = $val['class_name'] ?? $key;
                    // 关联键名
                    $mappingKey = $val['mapping_key'] ?? $this->getPk();
                    // 当前数据对象主键值
                    $pk = $data[$mappingKey];
                    if (strtoupper($mappingClass) == strtoupper($this->name)) {
                        // 自引用关联 获取父键名
                        $mappingFk = $val['parent_key'] ?? 'parent_id';
                    } else {
                        //  关联外键
                        $mappingFk = $val['foreign_key'] ?? strtolower(
                                $this->name
                            ).'_id';
                    }
                    if (!empty($val['condition'])) {
                        $mappingCondition = $val['condition'];
                    } else {
                        $mappingCondition = [];
                        $mappingCondition[$mappingFk] = $pk;
                    }
                    // 获取关联model对象
                    $model = D($mappingClass);
                    $mappingData = $data[$mappingName] ?? false;
                    if (!empty($mappingData) || $opType == 'DEL') {
                        switch ($mappingType) {
                            case static::HAS_ONE:
                                switch (strtoupper($opType)) {
                                    // 增加关联数据
                                    case 'ADD':
                                        $mappingData[$mappingFk] = $pk;
                                        $result = $model->add($mappingData);
                                        break;
                                    // 更新关联数据
                                    case 'SAVE':
                                        $result = $model->where($mappingCondition)->save($mappingData);
                                        break;
                                    // 根据外键删除关联数据
                                    case 'DEL':
                                        $result = $model->where($mappingCondition)->delete();
                                        break;
                                    default:
                                        break;
                                }
                                break;
                            case static::BELONGS_TO:
                                break;
                            case static::HAS_MANY:
                                switch (strtoupper($opType)) {
                                    // 增加关联数据
                                    case 'ADD'   :
                                        $model->startTrans();
                                        foreach ($mappingData as $val) {
                                            $val[$mappingFk] = $pk;
                                            $result = $model->add($val);
                                        }
                                        $model->commit();
                                        break;
                                    // 更新关联数据
                                    case 'SAVE' :
                                        $model->startTrans();
                                        $pk = $model->getPk();
                                        foreach ($mappingData as $vo) {
                                            // 更新数据
                                            if (isset($vo[$pk])) {
                                                $mappingCondition[$pk] = $vo[$pk];
                                                $result = $model->where($mappingCondition)->save(
                                                    $vo
                                                );
                                                // 新增数据
                                            } else {
                                                $vo[$mappingFk] = $data[$mappingKey];
                                                $result = $model->add($vo);
                                            }
                                        }
                                        $model->commit();
                                        break;
                                    // 删除关联数据
                                    case 'DEL' :
                                        $result = $model->where($mappingCondition)->delete();
                                        break;
                                    default:
                                        break;
                                }
                                break;
                            case static::MANY_TO_MANY:
                                // 关联
                                $mappingRelationFk = $val['relation_foreign_key'] ?? $model->getModelName().'_id';
                                $prefix = $this->tablePrefix;
                                if (isset($val['relation_table'])) {
                                    $mappingRelationTable = preg_replace_callback(
                                        "/__([A-Z_-]+)__/sU",
                                        function ($match) use ($prefix) {
                                            return $prefix.strtolower($match[1]);
                                        },
                                        $val['relation_table']
                                    );
                                } else {
                                    $mappingRelationTable = $this->getRelationTableName($model);
                                }
                                if (is_array($mappingData)) {
                                    $ids = [];
                                    foreach ($mappingData as $vo) {
                                        $ids[] = $vo[$mappingKey];
                                    }
                                    $relationId = implode(',', $ids);
                                }
                                switch (strtoupper($opType)) {
                                    // 增加关联数据
                                    case 'ADD':
                                        if (isset($relationId)) {
                                            $this->startTrans();
                                            // 插入关联表数据
                                            $sql = 'INSERT INTO '.$mappingRelationTable.' ('.$mappingFk.','.$mappingRelationFk.') SELECT a.'.$this->getPk(
                                                ).',b.'.$model->getPk().' FROM '.$this->getTableName(
                                                ).' AS a ,'.$model->getTableName()." AS b where a.".$this->getPk(
                                                ).' ='.$pk.' AND  b.'.$model->getPk().' IN ('.$relationId.") ";
                                            $result = $model->execute($sql);
                                            // 提交事务
                                            if (false !== $result) {
                                                $this->commit();
                                            } // 事务回滚
                                            else {
                                                $this->rollback();
                                            }
                                        }
                                        break;
                                    // 更新关联数据
                                    case 'SAVE':
                                        if (isset($relationId)) {
                                            $this->startTrans();
                                            // 删除关联表数据
                                            $this->table($mappingRelationTable)->where($mappingCondition)->delete();
                                            // 插入关联表数据
                                            $sql = 'INSERT INTO '.$mappingRelationTable.' ('.$mappingFk.','.$mappingRelationFk.') SELECT a.'.$this->getPk(
                                                ).',b.'.$model->getPk().' FROM '.$this->getTableName(
                                                ).' AS a ,'.$model->getTableName()." AS b where a.".$this->getPk(
                                                ).' ='.$pk.' AND  b.'.$model->getPk().' IN ('.$relationId.") ";
                                            $result = $model->execute($sql);
                                            // 提交事务
                                            if (false !== $result) {
                                                $this->commit();
                                            } // 事务回滚
                                            else {
                                                $this->rollback();
                                            }
                                        }
                                        break;
                                    // 根据外键删除中间表关联数据
                                    case 'DEL':
                                        $result = $this->table($mappingRelationTable)->where(
                                            $mappingCondition
                                        )->delete();
                                        break;
                                    default:
                                        break;
                                }
                                break;
                        }
                    }
                }
            }
        }

        return $result;
    }

    /**
     * 更新成功后的回调方法
     *
     * @param $data
     * @param $options
     */
    protected function _after_update($data, $options)
    {
        // 关联更新
        if (!empty($options['link'])) {
            $this->opRelation('SAVE', $data, $options['link']);
        }
    }

    /**
     * 删除成功后的回调方法
     *
     * @param $data
     * @param $options
     */
    protected function _after_delete($data, $options)
    {
        // 关联删除
        if (!empty($options['link'])) {
            $this->opRelation('DEL', $data, $options['link']);
        }
    }

    /**
     * 对保存到数据库的数据进行处理

     *
     * @param mixed $data 要操作的数据
     *
     * @return bool
     */
    protected function _facade($data)
    {
        $this->_before_write($data);

        return $data;
    }
}