This commit is contained in:
2020-03-27 10:13:51 +07:00
commit da1024a5b3
16614 changed files with 3274282 additions and 0 deletions

View File

@@ -0,0 +1,242 @@
<?php
/**
* @link http://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license http://www.yiiframework.com/license/
*/
namespace yii\console;
use Yii;
use yii\base\InvalidRouteException;
// define STDIN, STDOUT and STDERR if the PHP SAPI did not define them (e.g. creating console application in web env)
// http://php.net/manual/en/features.commandline.io-streams.php
defined('STDIN') or define('STDIN', fopen('php://stdin', 'r'));
defined('STDOUT') or define('STDOUT', fopen('php://stdout', 'w'));
defined('STDERR') or define('STDERR', fopen('php://stderr', 'w'));
/**
* Application represents a console application.
*
* Application extends from [[\yii\base\Application]] by providing functionalities that are
* specific to console requests. In particular, it deals with console requests
* through a command-based approach:
*
* - A console application consists of one or several possible user commands;
* - Each user command is implemented as a class extending [[\yii\console\Controller]];
* - User specifies which command to run on the command line;
* - The command processes the user request with the specified parameters.
*
* The command classes should be under the namespace specified by [[controllerNamespace]].
* Their naming should follow the same naming convention as controllers. For example, the `help` command
* is implemented using the `HelpController` class.
*
* To run the console application, enter the following on the command line:
*
* ```
* yii <route> [--param1=value1 --param2 ...]
* ```
*
* where `<route>` refers to a controller route in the form of `ModuleID/ControllerID/ActionID`
* (e.g. `sitemap/create`), and `param1`, `param2` refers to a set of named parameters that
* will be used to initialize the controller action (e.g. `--since=0` specifies a `since` parameter
* whose value is 0 and a corresponding `$since` parameter is passed to the action method).
*
* A `help` command is provided by default, which lists available commands and shows their usage.
* To use this command, simply type:
*
* ```
* yii help
* ```
*
* @property ErrorHandler $errorHandler The error handler application component. This property is read-only.
* @property Request $request The request component. This property is read-only.
* @property Response $response The response component. This property is read-only.
*
* @author Qiang Xue <qiang.xue@gmail.com>
* @since 2.0
*/
class Application extends \yii\base\Application
{
/**
* The option name for specifying the application configuration file path.
*/
const OPTION_APPCONFIG = 'appconfig';
/**
* @var string the default route of this application. Defaults to 'help',
* meaning the `help` command.
*/
public $defaultRoute = 'help';
/**
* @var bool whether to enable the commands provided by the core framework.
* Defaults to true.
*/
public $enableCoreCommands = true;
/**
* @var Controller the currently active controller instance
*/
public $controller;
/**
* {@inheritdoc}
*/
public function __construct($config = [])
{
$config = $this->loadConfig($config);
parent::__construct($config);
}
/**
* Loads the configuration.
* This method will check if the command line option [[OPTION_APPCONFIG]] is specified.
* If so, the corresponding file will be loaded as the application configuration.
* Otherwise, the configuration provided as the parameter will be returned back.
* @param array $config the configuration provided in the constructor.
* @return array the actual configuration to be used by the application.
*/
protected function loadConfig($config)
{
if (!empty($_SERVER['argv'])) {
$option = '--' . self::OPTION_APPCONFIG . '=';
foreach ($_SERVER['argv'] as $param) {
if (strpos($param, $option) !== false) {
$path = substr($param, strlen($option));
if (!empty($path) && is_file($file = Yii::getAlias($path))) {
return require $file;
}
exit("The configuration file does not exist: $path\n");
}
}
}
return $config;
}
/**
* Initialize the application.
*/
public function init()
{
parent::init();
if ($this->enableCoreCommands) {
foreach ($this->coreCommands() as $id => $command) {
if (!isset($this->controllerMap[$id])) {
$this->controllerMap[$id] = $command;
}
}
}
// ensure we have the 'help' command so that we can list the available commands
if (!isset($this->controllerMap['help'])) {
$this->controllerMap['help'] = 'yii\console\controllers\HelpController';
}
}
/**
* Handles the specified request.
* @param Request $request the request to be handled
* @return Response the resulting response
*/
public function handleRequest($request)
{
list($route, $params) = $request->resolve();
$this->requestedRoute = $route;
$result = $this->runAction($route, $params);
if ($result instanceof Response) {
return $result;
}
$response = $this->getResponse();
$response->exitStatus = $result;
return $response;
}
/**
* Runs a controller action specified by a route.
* This method parses the specified route and creates the corresponding child module(s), controller and action
* instances. It then calls [[Controller::runAction()]] to run the action with the given parameters.
* If the route is empty, the method will use [[defaultRoute]].
*
* For example, to run `public function actionTest($a, $b)` assuming that the controller has options the following
* code should be used:
*
* ```php
* \Yii::$app->runAction('controller/test', ['option' => 'value', $a, $b]);
* ```
*
* @param string $route the route that specifies the action.
* @param array $params the parameters to be passed to the action
* @return int|Response the result of the action. This can be either an exit code or Response object.
* Exit code 0 means normal, and other values mean abnormal. Exit code of `null` is treaded as `0` as well.
* @throws Exception if the route is invalid
*/
public function runAction($route, $params = [])
{
try {
$res = parent::runAction($route, $params);
return is_object($res) ? $res : (int) $res;
} catch (InvalidRouteException $e) {
throw new UnknownCommandException($route, $this, 0, $e);
}
}
/**
* Returns the configuration of the built-in commands.
* @return array the configuration of the built-in commands.
*/
public function coreCommands()
{
return [
'asset' => 'yii\console\controllers\AssetController',
'cache' => 'yii\console\controllers\CacheController',
'fixture' => 'yii\console\controllers\FixtureController',
'help' => 'yii\console\controllers\HelpController',
'message' => 'yii\console\controllers\MessageController',
'migrate' => 'yii\console\controllers\MigrateController',
'serve' => 'yii\console\controllers\ServeController',
];
}
/**
* Returns the error handler component.
* @return ErrorHandler the error handler application component.
*/
public function getErrorHandler()
{
return $this->get('errorHandler');
}
/**
* Returns the request component.
* @return Request the request component.
*/
public function getRequest()
{
return $this->get('request');
}
/**
* Returns the response component.
* @return Response the response component.
*/
public function getResponse()
{
return $this->get('response');
}
/**
* {@inheritdoc}
*/
public function coreComponents()
{
return array_merge(parent::coreComponents(), [
'request' => ['class' => 'yii\console\Request'],
'response' => ['class' => 'yii\console\Response'],
'errorHandler' => ['class' => 'yii\console\ErrorHandler'],
]);
}
}

View File

@@ -0,0 +1,673 @@
<?php
/**
* @link http://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license http://www.yiiframework.com/license/
*/
namespace yii\console;
use Yii;
use yii\base\Action;
use yii\base\InlineAction;
use yii\base\InvalidRouteException;
use yii\helpers\Console;
use yii\helpers\Inflector;
/**
* Controller is the base class of console command classes.
*
* A console controller consists of one or several actions known as sub-commands.
* Users call a console command by specifying the corresponding route which identifies a controller action.
* The `yii` program is used when calling a console command, like the following:
*
* ```
* yii <route> [--param1=value1 --param2 ...]
* ```
*
* where `<route>` is a route to a controller action and the params will be populated as properties of a command.
* See [[options()]] for details.
*
* @property string $help This property is read-only.
* @property string $helpSummary This property is read-only.
* @property array $passedOptionValues The properties corresponding to the passed options. This property is
* read-only.
* @property array $passedOptions The names of the options passed during execution. This property is
* read-only.
*
* @author Qiang Xue <qiang.xue@gmail.com>
* @since 2.0
*/
class Controller extends \yii\base\Controller
{
/**
* @deprecated since 2.0.13. Use [[ExitCode::OK]] instead.
*/
const EXIT_CODE_NORMAL = 0;
/**
* @deprecated since 2.0.13. Use [[ExitCode::UNSPECIFIED_ERROR]] instead.
*/
const EXIT_CODE_ERROR = 1;
/**
* @var bool whether to run the command interactively.
*/
public $interactive = true;
/**
* @var bool whether to enable ANSI color in the output.
* If not set, ANSI color will only be enabled for terminals that support it.
*/
public $color;
/**
* @var bool whether to display help information about current command.
* @since 2.0.10
*/
public $help;
/**
* @var array the options passed during execution.
*/
private $_passedOptions = [];
/**
* Returns a value indicating whether ANSI color is enabled.
*
* ANSI color is enabled only if [[color]] is set true or is not set
* and the terminal supports ANSI color.
*
* @param resource $stream the stream to check.
* @return bool Whether to enable ANSI style in output.
*/
public function isColorEnabled($stream = \STDOUT)
{
return $this->color === null ? Console::streamSupportsAnsiColors($stream) : $this->color;
}
/**
* Runs an action with the specified action ID and parameters.
* If the action ID is empty, the method will use [[defaultAction]].
* @param string $id the ID of the action to be executed.
* @param array $params the parameters (name-value pairs) to be passed to the action.
* @return int the status of the action execution. 0 means normal, other values mean abnormal.
* @throws InvalidRouteException if the requested action ID cannot be resolved into an action successfully.
* @throws Exception if there are unknown options or missing arguments
* @see createAction
*/
public function runAction($id, $params = [])
{
if (!empty($params)) {
// populate options here so that they are available in beforeAction().
$options = $this->options($id === '' ? $this->defaultAction : $id);
if (isset($params['_aliases'])) {
$optionAliases = $this->optionAliases();
foreach ($params['_aliases'] as $name => $value) {
if (array_key_exists($name, $optionAliases)) {
$params[$optionAliases[$name]] = $value;
} else {
throw new Exception(Yii::t('yii', 'Unknown alias: -{name}', ['name' => $name]));
}
}
unset($params['_aliases']);
}
foreach ($params as $name => $value) {
// Allow camelCase options to be entered in kebab-case
if (!in_array($name, $options, true) && strpos($name, '-') !== false) {
$kebabName = $name;
$altName = lcfirst(Inflector::id2camel($kebabName));
if (in_array($altName, $options, true)) {
$name = $altName;
}
}
if (in_array($name, $options, true)) {
$default = $this->$name;
if (is_array($default)) {
$this->$name = preg_split('/\s*,\s*(?![^()]*\))/', $value);
} elseif ($default !== null) {
settype($value, gettype($default));
$this->$name = $value;
} else {
$this->$name = $value;
}
$this->_passedOptions[] = $name;
unset($params[$name]);
if (isset($kebabName)) {
unset($params[$kebabName]);
}
} elseif (!is_int($name)) {
throw new Exception(Yii::t('yii', 'Unknown option: --{name}', ['name' => $name]));
}
}
}
if ($this->help) {
$route = $this->getUniqueId() . '/' . $id;
return Yii::$app->runAction('help', [$route]);
}
return parent::runAction($id, $params);
}
/**
* Binds the parameters to the action.
* This method is invoked by [[Action]] when it begins to run with the given parameters.
* This method will first bind the parameters with the [[options()|options]]
* available to the action. It then validates the given arguments.
* @param Action $action the action to be bound with parameters
* @param array $params the parameters to be bound to the action
* @return array the valid parameters that the action can run with.
* @throws Exception if there are unknown options or missing arguments
*/
public function bindActionParams($action, $params)
{
if ($action instanceof InlineAction) {
$method = new \ReflectionMethod($this, $action->actionMethod);
} else {
$method = new \ReflectionMethod($action, 'run');
}
$args = array_values($params);
$missing = [];
foreach ($method->getParameters() as $i => $param) {
if ($param->isArray() && isset($args[$i])) {
$args[$i] = $args[$i] === '' ? [] : preg_split('/\s*,\s*/', $args[$i]);
}
if (!isset($args[$i])) {
if ($param->isDefaultValueAvailable()) {
$args[$i] = $param->getDefaultValue();
} else {
$missing[] = $param->getName();
}
}
}
if (!empty($missing)) {
throw new Exception(Yii::t('yii', 'Missing required arguments: {params}', ['params' => implode(', ', $missing)]));
}
return $args;
}
/**
* Formats a string with ANSI codes.
*
* You may pass additional parameters using the constants defined in [[\yii\helpers\Console]].
*
* Example:
*
* ```
* echo $this->ansiFormat('This will be red and underlined.', Console::FG_RED, Console::UNDERLINE);
* ```
*
* @param string $string the string to be formatted
* @return string
*/
public function ansiFormat($string)
{
if ($this->isColorEnabled()) {
$args = func_get_args();
array_shift($args);
$string = Console::ansiFormat($string, $args);
}
return $string;
}
/**
* Prints a string to STDOUT.
*
* You may optionally format the string with ANSI codes by
* passing additional parameters using the constants defined in [[\yii\helpers\Console]].
*
* Example:
*
* ```
* $this->stdout('This will be red and underlined.', Console::FG_RED, Console::UNDERLINE);
* ```
*
* @param string $string the string to print
* @return int|bool Number of bytes printed or false on error
*/
public function stdout($string)
{
if ($this->isColorEnabled()) {
$args = func_get_args();
array_shift($args);
$string = Console::ansiFormat($string, $args);
}
return Console::stdout($string);
}
/**
* Prints a string to STDERR.
*
* You may optionally format the string with ANSI codes by
* passing additional parameters using the constants defined in [[\yii\helpers\Console]].
*
* Example:
*
* ```
* $this->stderr('This will be red and underlined.', Console::FG_RED, Console::UNDERLINE);
* ```
*
* @param string $string the string to print
* @return int|bool Number of bytes printed or false on error
*/
public function stderr($string)
{
if ($this->isColorEnabled(\STDERR)) {
$args = func_get_args();
array_shift($args);
$string = Console::ansiFormat($string, $args);
}
return fwrite(\STDERR, $string);
}
/**
* Prompts the user for input and validates it.
*
* @param string $text prompt string
* @param array $options the options to validate the input:
*
* - required: whether it is required or not
* - default: default value if no input is inserted by the user
* - pattern: regular expression pattern to validate user input
* - validator: a callable function to validate input. The function must accept two parameters:
* - $input: the user input to validate
* - $error: the error value passed by reference if validation failed.
*
* An example of how to use the prompt method with a validator function.
*
* ```php
* $code = $this->prompt('Enter 4-Chars-Pin', ['required' => true, 'validator' => function($input, &$error) {
* if (strlen($input) !== 4) {
* $error = 'The Pin must be exactly 4 chars!';
* return false;
* }
* return true;
* }]);
* ```
*
* @return string the user input
*/
public function prompt($text, $options = [])
{
if ($this->interactive) {
return Console::prompt($text, $options);
}
return isset($options['default']) ? $options['default'] : '';
}
/**
* Asks user to confirm by typing y or n.
*
* A typical usage looks like the following:
*
* ```php
* if ($this->confirm("Are you sure?")) {
* echo "user typed yes\n";
* } else {
* echo "user typed no\n";
* }
* ```
*
* @param string $message to echo out before waiting for user input
* @param bool $default this value is returned if no selection is made.
* @return bool whether user confirmed.
* Will return true if [[interactive]] is false.
*/
public function confirm($message, $default = false)
{
if ($this->interactive) {
return Console::confirm($message, $default);
}
return true;
}
/**
* Gives the user an option to choose from. Giving '?' as an input will show
* a list of options to choose from and their explanations.
*
* @param string $prompt the prompt message
* @param array $options Key-value array of options to choose from
*
* @return string An option character the user chose
*/
public function select($prompt, $options = [])
{
return Console::select($prompt, $options);
}
/**
* Returns the names of valid options for the action (id)
* An option requires the existence of a public member variable whose
* name is the option name.
* Child classes may override this method to specify possible options.
*
* Note that the values setting via options are not available
* until [[beforeAction()]] is being called.
*
* @param string $actionID the action id of the current request
* @return string[] the names of the options valid for the action
*/
public function options($actionID)
{
// $actionId might be used in subclasses to provide options specific to action id
return ['color', 'interactive', 'help'];
}
/**
* Returns option alias names.
* Child classes may override this method to specify alias options.
*
* @return array the options alias names valid for the action
* where the keys is alias name for option and value is option name.
*
* @since 2.0.8
* @see options()
*/
public function optionAliases()
{
return [
'h' => 'help',
];
}
/**
* Returns properties corresponding to the options for the action id
* Child classes may override this method to specify possible properties.
*
* @param string $actionID the action id of the current request
* @return array properties corresponding to the options for the action
*/
public function getOptionValues($actionID)
{
// $actionId might be used in subclasses to provide properties specific to action id
$properties = [];
foreach ($this->options($this->action->id) as $property) {
$properties[$property] = $this->$property;
}
return $properties;
}
/**
* Returns the names of valid options passed during execution.
*
* @return array the names of the options passed during execution
*/
public function getPassedOptions()
{
return $this->_passedOptions;
}
/**
* Returns the properties corresponding to the passed options.
*
* @return array the properties corresponding to the passed options
*/
public function getPassedOptionValues()
{
$properties = [];
foreach ($this->_passedOptions as $property) {
$properties[$property] = $this->$property;
}
return $properties;
}
/**
* Returns one-line short summary describing this controller.
*
* You may override this method to return customized summary.
* The default implementation returns first line from the PHPDoc comment.
*
* @return string
*/
public function getHelpSummary()
{
return $this->parseDocCommentSummary(new \ReflectionClass($this));
}
/**
* Returns help information for this controller.
*
* You may override this method to return customized help.
* The default implementation returns help information retrieved from the PHPDoc comment.
* @return string
*/
public function getHelp()
{
return $this->parseDocCommentDetail(new \ReflectionClass($this));
}
/**
* Returns a one-line short summary describing the specified action.
* @param Action $action action to get summary for
* @return string a one-line short summary describing the specified action.
*/
public function getActionHelpSummary($action)
{
return $this->parseDocCommentSummary($this->getActionMethodReflection($action));
}
/**
* Returns the detailed help information for the specified action.
* @param Action $action action to get help for
* @return string the detailed help information for the specified action.
*/
public function getActionHelp($action)
{
return $this->parseDocCommentDetail($this->getActionMethodReflection($action));
}
/**
* Returns the help information for the anonymous arguments for the action.
*
* The returned value should be an array. The keys are the argument names, and the values are
* the corresponding help information. Each value must be an array of the following structure:
*
* - required: boolean, whether this argument is required.
* - type: string, the PHP type of this argument.
* - default: string, the default value of this argument
* - comment: string, the comment of this argument
*
* The default implementation will return the help information extracted from the doc-comment of
* the parameters corresponding to the action method.
*
* @param Action $action
* @return array the help information of the action arguments
*/
public function getActionArgsHelp($action)
{
$method = $this->getActionMethodReflection($action);
$tags = $this->parseDocCommentTags($method);
$params = isset($tags['param']) ? (array) $tags['param'] : [];
$args = [];
/** @var \ReflectionParameter $reflection */
foreach ($method->getParameters() as $i => $reflection) {
if ($reflection->getClass() !== null) {
continue;
}
$name = $reflection->getName();
$tag = isset($params[$i]) ? $params[$i] : '';
if (preg_match('/^(\S+)\s+(\$\w+\s+)?(.*)/s', $tag, $matches)) {
$type = $matches[1];
$comment = $matches[3];
} else {
$type = null;
$comment = $tag;
}
if ($reflection->isDefaultValueAvailable()) {
$args[$name] = [
'required' => false,
'type' => $type,
'default' => $reflection->getDefaultValue(),
'comment' => $comment,
];
} else {
$args[$name] = [
'required' => true,
'type' => $type,
'default' => null,
'comment' => $comment,
];
}
}
return $args;
}
/**
* Returns the help information for the options for the action.
*
* The returned value should be an array. The keys are the option names, and the values are
* the corresponding help information. Each value must be an array of the following structure:
*
* - type: string, the PHP type of this argument.
* - default: string, the default value of this argument
* - comment: string, the comment of this argument
*
* The default implementation will return the help information extracted from the doc-comment of
* the properties corresponding to the action options.
*
* @param Action $action
* @return array the help information of the action options
*/
public function getActionOptionsHelp($action)
{
$optionNames = $this->options($action->id);
if (empty($optionNames)) {
return [];
}
$class = new \ReflectionClass($this);
$options = [];
foreach ($class->getProperties() as $property) {
$name = $property->getName();
if (!in_array($name, $optionNames, true)) {
continue;
}
$defaultValue = $property->getValue($this);
$tags = $this->parseDocCommentTags($property);
// Display camelCase options in kebab-case
$name = Inflector::camel2id($name, '-', true);
if (isset($tags['var']) || isset($tags['property'])) {
$doc = isset($tags['var']) ? $tags['var'] : $tags['property'];
if (is_array($doc)) {
$doc = reset($doc);
}
if (preg_match('/^(\S+)(.*)/s', $doc, $matches)) {
$type = $matches[1];
$comment = $matches[2];
} else {
$type = null;
$comment = $doc;
}
$options[$name] = [
'type' => $type,
'default' => $defaultValue,
'comment' => $comment,
];
} else {
$options[$name] = [
'type' => null,
'default' => $defaultValue,
'comment' => '',
];
}
}
return $options;
}
private $_reflections = [];
/**
* @param Action $action
* @return \ReflectionMethod
*/
protected function getActionMethodReflection($action)
{
if (!isset($this->_reflections[$action->id])) {
if ($action instanceof InlineAction) {
$this->_reflections[$action->id] = new \ReflectionMethod($this, $action->actionMethod);
} else {
$this->_reflections[$action->id] = new \ReflectionMethod($action, 'run');
}
}
return $this->_reflections[$action->id];
}
/**
* Parses the comment block into tags.
* @param \Reflector $reflection the comment block
* @return array the parsed tags
*/
protected function parseDocCommentTags($reflection)
{
$comment = $reflection->getDocComment();
$comment = "@description \n" . strtr(trim(preg_replace('/^\s*\**( |\t)?/m', '', trim($comment, '/'))), "\r", '');
$parts = preg_split('/^\s*@/m', $comment, -1, PREG_SPLIT_NO_EMPTY);
$tags = [];
foreach ($parts as $part) {
if (preg_match('/^(\w+)(.*)/ms', trim($part), $matches)) {
$name = $matches[1];
if (!isset($tags[$name])) {
$tags[$name] = trim($matches[2]);
} elseif (is_array($tags[$name])) {
$tags[$name][] = trim($matches[2]);
} else {
$tags[$name] = [$tags[$name], trim($matches[2])];
}
}
}
return $tags;
}
/**
* Returns the first line of docblock.
*
* @param \Reflector $reflection
* @return string
*/
protected function parseDocCommentSummary($reflection)
{
$docLines = preg_split('~\R~u', $reflection->getDocComment());
if (isset($docLines[1])) {
return trim($docLines[1], "\t *");
}
return '';
}
/**
* Returns full description from the docblock.
*
* @param \Reflector $reflection
* @return string
*/
protected function parseDocCommentDetail($reflection)
{
$comment = strtr(trim(preg_replace('/^\s*\**( |\t)?/m', '', trim($reflection->getDocComment(), '/'))), "\r", '');
if (preg_match('/^\s*@\w+/m', $comment, $matches, PREG_OFFSET_CAPTURE)) {
$comment = trim(substr($comment, 0, $matches[0][1]));
}
if ($comment !== '') {
return rtrim(Console::renderColoredString(Console::markdownToAnsi($comment)));
}
return '';
}
}

View File

@@ -0,0 +1,88 @@
<?php
/**
* @link http://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license http://www.yiiframework.com/license/
*/
namespace yii\console;
use Yii;
use yii\base\ErrorException;
use yii\base\UserException;
use yii\helpers\Console;
/**
* ErrorHandler handles uncaught PHP errors and exceptions.
*
* ErrorHandler is configured as an application component in [[\yii\base\Application]] by default.
* You can access that instance via `Yii::$app->errorHandler`.
*
* @author Carsten Brandt <mail@cebe.cc>
* @since 2.0
*/
class ErrorHandler extends \yii\base\ErrorHandler
{
/**
* Renders an exception using ansi format for console output.
* @param \Exception $exception the exception to be rendered.
*/
protected function renderException($exception)
{
if ($exception instanceof UnknownCommandException) {
// display message and suggest alternatives in case of unknown command
$message = $this->formatMessage($exception->getName() . ': ') . $exception->command;
$alternatives = $exception->getSuggestedAlternatives();
if (count($alternatives) === 1) {
$message .= "\n\nDid you mean \"" . reset($alternatives) . '"?';
} elseif (count($alternatives) > 1) {
$message .= "\n\nDid you mean one of these?\n - " . implode("\n - ", $alternatives);
}
} elseif ($exception instanceof Exception && ($exception instanceof UserException || !YII_DEBUG)) {
$message = $this->formatMessage($exception->getName() . ': ') . $exception->getMessage();
} elseif (YII_DEBUG) {
if ($exception instanceof Exception) {
$message = $this->formatMessage("Exception ({$exception->getName()})");
} elseif ($exception instanceof ErrorException) {
$message = $this->formatMessage($exception->getName());
} else {
$message = $this->formatMessage('Exception');
}
$message .= $this->formatMessage(" '" . get_class($exception) . "'", [Console::BOLD, Console::FG_BLUE])
. ' with message ' . $this->formatMessage("'{$exception->getMessage()}'", [Console::BOLD]) //. "\n"
. "\n\nin " . dirname($exception->getFile()) . DIRECTORY_SEPARATOR . $this->formatMessage(basename($exception->getFile()), [Console::BOLD])
. ':' . $this->formatMessage($exception->getLine(), [Console::BOLD, Console::FG_YELLOW]) . "\n";
if ($exception instanceof \yii\db\Exception && !empty($exception->errorInfo)) {
$message .= "\n" . $this->formatMessage("Error Info:\n", [Console::BOLD]) . print_r($exception->errorInfo, true);
}
$message .= "\n" . $this->formatMessage("Stack trace:\n", [Console::BOLD]) . $exception->getTraceAsString();
} else {
$message = $this->formatMessage('Error: ') . $exception->getMessage();
}
if (PHP_SAPI === 'cli') {
Console::stderr($message . "\n");
} else {
echo $message . "\n";
}
}
/**
* Colorizes a message for console output.
* @param string $message the message to colorize.
* @param array $format the message format.
* @return string the colorized message.
* @see Console::ansiFormat() for details on how to specify the message format.
*/
protected function formatMessage($message, $format = [Console::FG_RED, Console::BOLD])
{
$stream = (PHP_SAPI === 'cli') ? \STDERR : \STDOUT;
// try controller first to allow check for --color switch
if (Yii::$app->controller instanceof \yii\console\Controller && Yii::$app->controller->isColorEnabled($stream)
|| Yii::$app instanceof \yii\console\Application && Console::streamSupportsAnsiColors($stream)) {
$message = Console::ansiFormat($message, $format);
}
return $message;
}
}

View File

@@ -0,0 +1,27 @@
<?php
/**
* @link http://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license http://www.yiiframework.com/license/
*/
namespace yii\console;
use yii\base\UserException;
/**
* Exception represents an exception caused by incorrect usage of a console command.
*
* @author Qiang Xue <qiang.xue@gmail.com>
* @since 2.0
*/
class Exception extends UserException
{
/**
* @return string the user-friendly name of this exception
*/
public function getName()
{
return 'Error';
}
}

160
vendor/yiisoft/yii2/console/ExitCode.php vendored Normal file
View File

@@ -0,0 +1,160 @@
<?php
/**
* @link http://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license http://www.yiiframework.com/license/
*/
namespace yii\console;
/**
* This class provides constants for defining console command exit codes.
*
* The exit codes follow the codes defined in the [FreeBSD sysexits(3)](http://man.openbsd.org/sysexits) manual page.
*
* These constants can be used in console controllers for example like this:
*
* ```php
* public function actionIndex()
* {
* if (!$this->isAllowedToPerformAction()) {
* $this->stderr('Error: ' . ExitCode::getReason(ExitCode::NOPERM));
* return ExitCode::NOPERM;
* }
*
* // do something
*
* return ExitCode::OK;
* }
* ```
*
* @author Tom Worster <fsb@thefsb.org>
* @author Alexander Makarov <sam@rmcreative.ru>
* @see http://man.openbsd.org/sysexits
* @since 2.0.13
*/
class ExitCode
{
/**
* The command completed successfully.
*/
const OK = 0;
/**
* The command exited with an error code that says nothing about the error.
*/
const UNSPECIFIED_ERROR = 1;
/**
* The command was used incorrectly, e.g., with the wrong number of
* arguments, a bad flag, a bad syntax in a parameter, or whatever.
*/
const USAGE = 64;
/**
* The input data was incorrect in some way. This should only be used for
* user's data and not system files.
*/
const DATAERR = 65;
/**
* An input file (not a system file) did not exist or was not readable.
* This could also include errors like ``No message'' to a mailer (if it
* cared to catch it).
*/
const NOINPUT = 66;
/**
* The user specified did not exist. This might be used for mail addresses
* or remote logins.
*/
const NOUSER = 67;
/**
* The host specified did not exist. This is used in mail addresses or
* network requests.
*/
const NOHOST = 68;
/**
* A service is unavailable. This can occur if a support program or file
* does not exist. This can also be used as a catchall message when
* something you wanted to do does not work, but you do not know why.
*/
const UNAVAILABLE = 69;
/**
* An internal software error has been detected. This should be limited to
* non-operating system related errors as possible.
*/
const SOFTWARE = 70;
/**
* An operating system error has been detected. This is intended to be
* used for such things as ``cannot fork'', ``cannot create pipe'', or the
* like. It includes things like getuid returning a user that does not
* exist in the passwd file.
*/
const OSERR = 71;
/**
* Some system file (e.g., /etc/passwd, /var/run/utx.active, etc.) does not
* exist, cannot be opened, or has some sort of error (e.g., syntax error).
*/
const OSFILE = 72;
/**
* A (user specified) output file cannot be created.
*/
const CANTCREAT = 73;
/**
* An error occurred while doing I/O on some file.
*/
const IOERR = 74;
/**
* Temporary failure, indicating something that is not really an error. In
* sendmail, this means that a mailer (e.g.) could not create a connection,
* and the request should be reattempted later.
*/
const TEMPFAIL = 75;
/**
* The remote system returned something that was ``not possible'' during a
* protocol exchange.
*/
const PROTOCOL = 76;
/**
* You did not have sufficient permission to perform the operation. This
* is not intended for file system problems, which should use NOINPUT or
* CANTCREAT, but rather for higher level permissions.
*/
const NOPERM = 77;
/**
* Something was found in an unconfigured or misconfigured state.
*/
const CONFIG = 78;
/**
* @var array a map of reason descriptions for exit codes.
*/
public static $reasons = [
self::OK => 'Success',
self::UNSPECIFIED_ERROR => 'Unspecified error',
self::USAGE => 'Incorrect usage, argument or option error',
self::DATAERR => 'Error in input data',
self::NOINPUT => 'Input file not found or unreadable',
self::NOUSER => 'User not found',
self::NOHOST => 'Host not found',
self::UNAVAILABLE => 'A requied service is unavailable',
self::SOFTWARE => 'Internal error',
self::OSERR => 'Error making system call or using OS service',
self::OSFILE => 'Error accessing system file',
self::CANTCREAT => 'Cannot create output file',
self::IOERR => 'I/O error',
self::TEMPFAIL => 'Temporary failure',
self::PROTOCOL => 'Unexpected remote service behavior',
self::NOPERM => 'Insufficient permissions',
self::CONFIG => 'Configuration error',
];
/**
* Returns a short reason text for the given exit code.
*
* This method uses [[$reasons]] to determine the reason for an exit code.
* @param int $exitCode one of the constants defined in this class.
* @return string the reason text, or `"Unknown exit code"` if the code is not listed in [[$reasons]].
*/
public static function getReason($exitCode)
{
return isset(static::$reasons[$exitCode]) ? static::$reasons[$exitCode] : 'Unknown exit code';
}
}

106
vendor/yiisoft/yii2/console/Markdown.php vendored Normal file
View File

@@ -0,0 +1,106 @@
<?php
/**
* @link http://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license http://www.yiiframework.com/license/
*/
namespace yii\console;
use cebe\markdown\block\FencedCodeTrait;
use cebe\markdown\inline\CodeTrait;
use cebe\markdown\inline\EmphStrongTrait;
use cebe\markdown\inline\StrikeoutTrait;
use yii\helpers\Console;
/**
* A Markdown parser that enhances markdown for reading in console environments.
*
* Based on [cebe/markdown](https://github.com/cebe/markdown).
*
* @author Carsten Brandt <mail@cebe.cc>
* @since 2.0
*/
class Markdown extends \cebe\markdown\Parser
{
use FencedCodeTrait;
use CodeTrait;
use EmphStrongTrait;
use StrikeoutTrait;
/**
* @var array these are "escapeable" characters. When using one of these prefixed with a
* backslash, the character will be outputted without the backslash and is not interpreted
* as markdown.
*/
protected $escapeCharacters = [
'\\', // backslash
'`', // backtick
'*', // asterisk
'_', // underscore
'~', // tilde
];
/**
* Renders a code block.
*
* @param array $block
* @return string
*/
protected function renderCode($block)
{
return Console::ansiFormat($block['content'], [Console::NEGATIVE]) . "\n\n";
}
/**
* Render a paragraph block.
*
* @param string $block
* @return string
*/
protected function renderParagraph($block)
{
return rtrim($this->renderAbsy($block['content'])) . "\n\n";
}
/**
* Renders an inline code span `` ` ``.
* @param array $element
* @return string
*/
protected function renderInlineCode($element)
{
return Console::ansiFormat($element[1], [Console::UNDERLINE]);
}
/**
* Renders empathized elements.
* @param array $element
* @return string
*/
protected function renderEmph($element)
{
return Console::ansiFormat($this->renderAbsy($element[1]), [Console::ITALIC]);
}
/**
* Renders strong elements.
* @param array $element
* @return string
*/
protected function renderStrong($element)
{
return Console::ansiFormat($this->renderAbsy($element[1]), [Console::BOLD]);
}
/**
* Renders the strike through feature.
* @param array $element
* @return string
*/
protected function renderStrike($element)
{
return Console::ansiFormat($this->parseInline($this->renderAbsy($element[1])), [Console::CROSSED_OUT]);
}
}

108
vendor/yiisoft/yii2/console/Request.php vendored Normal file
View File

@@ -0,0 +1,108 @@
<?php
/**
* @link http://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license http://www.yiiframework.com/license/
*/
namespace yii\console;
/**
* The console Request represents the environment information for a console application.
*
* It is a wrapper for the PHP `$_SERVER` variable which holds information about the
* currently running PHP script and the command line arguments given to it.
*
* @property array $params The command line arguments. It does not include the entry script name.
*
* @author Qiang Xue <qiang.xue@gmail.com>
* @since 2.0
*/
class Request extends \yii\base\Request
{
private $_params;
/**
* Returns the command line arguments.
* @return array the command line arguments. It does not include the entry script name.
*/
public function getParams()
{
if ($this->_params === null) {
if (isset($_SERVER['argv'])) {
$this->_params = $_SERVER['argv'];
array_shift($this->_params);
} else {
$this->_params = [];
}
}
return $this->_params;
}
/**
* Sets the command line arguments.
* @param array $params the command line arguments
*/
public function setParams($params)
{
$this->_params = $params;
}
/**
* Resolves the current request into a route and the associated parameters.
* @return array the first element is the route, and the second is the associated parameters.
* @throws Exception when parameter is wrong and can not be resolved
*/
public function resolve()
{
$rawParams = $this->getParams();
$endOfOptionsFound = false;
if (isset($rawParams[0])) {
$route = array_shift($rawParams);
if ($route === '--') {
$endOfOptionsFound = true;
$route = array_shift($rawParams);
}
} else {
$route = '';
}
$params = [];
$prevOption = null;
foreach ($rawParams as $param) {
if ($endOfOptionsFound) {
$params[] = $param;
} elseif ($param === '--') {
$endOfOptionsFound = true;
} elseif (preg_match('/^--([\w-]+)(?:=(.*))?$/', $param, $matches)) {
$name = $matches[1];
if (is_numeric(substr($name, 0, 1))) {
throw new Exception('Parameter "' . $name . '" is not valid');
}
if ($name !== Application::OPTION_APPCONFIG) {
$params[$name] = isset($matches[2]) ? $matches[2] : true;
$prevOption = &$params[$name];
}
} elseif (preg_match('/^-([\w-]+)(?:=(.*))?$/', $param, $matches)) {
$name = $matches[1];
if (is_numeric($name)) {
$params[] = $param;
} else {
$params['_aliases'][$name] = isset($matches[2]) ? $matches[2] : true;
$prevOption = &$params['_aliases'][$name];
}
} elseif ($prevOption === true) {
// `--option value` syntax
$prevOption = $param;
} else {
$params[] = $param;
}
}
return [$route, $params];
}
}

View File

@@ -0,0 +1,18 @@
<?php
/**
* @link http://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license http://www.yiiframework.com/license/
*/
namespace yii\console;
/**
* The console Response represents the result of a console application.
*
* @author Qiang Xue <qiang.xue@gmail.com>
* @since 2.0
*/
class Response extends \yii\base\Response
{
}

View File

@@ -0,0 +1,142 @@
<?php
/**
* @link http://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license http://www.yiiframework.com/license/
*/
namespace yii\console;
use yii\console\controllers\HelpController;
/**
* UnknownCommandException represents an exception caused by incorrect usage of a console command.
*
* @author Carsten Brandt <mail@cebe.cc>
* @since 2.0.11
*/
class UnknownCommandException extends Exception
{
/**
* @var string the name of the command that could not be recognized.
*/
public $command;
/**
* @var Application
*/
protected $application;
/**
* Construct the exception.
*
* @param string $route the route of the command that could not be found.
* @param Application $application the console application instance involved.
* @param int $code the Exception code.
* @param \Exception $previous the previous exception used for the exception chaining.
*/
public function __construct($route, $application, $code = 0, \Exception $previous = null)
{
$this->command = $route;
$this->application = $application;
parent::__construct("Unknown command \"$route\".", $code, $previous);
}
/**
* @return string the user-friendly name of this exception
*/
public function getName()
{
return 'Unknown command';
}
/**
* Suggest alternative commands for [[$command]] based on string similarity.
*
* Alternatives are searched using the following steps:
*
* - suggest alternatives that begin with `$command`
* - find typos by calculating the Levenshtein distance between the unknown command and all
* available commands. The Levenshtein distance is defined as the minimal number of
* characters you have to replace, insert or delete to transform str1 into str2.
*
* @see http://php.net/manual/en/function.levenshtein.php
* @return array a list of suggested alternatives sorted by similarity.
*/
public function getSuggestedAlternatives()
{
$help = $this->application->createController('help');
if ($help === false || $this->command === '') {
return [];
}
/** @var $helpController HelpController */
list($helpController, $actionID) = $help;
$availableActions = [];
foreach ($helpController->getCommands() as $command) {
$result = $this->application->createController($command);
if ($result === false) {
continue;
}
// add the command itself (default action)
$availableActions[] = $command;
// add all actions of this controller
/** @var $controller Controller */
list($controller, $actionID) = $result;
$actions = $helpController->getActions($controller);
if (!empty($actions)) {
$prefix = $controller->getUniqueId();
foreach ($actions as $action) {
$availableActions[] = $prefix . '/' . $action;
}
}
}
return $this->filterBySimilarity($availableActions, $this->command);
}
/**
* Find suggest alternative commands based on string similarity.
*
* Alternatives are searched using the following steps:
*
* - suggest alternatives that begin with `$command`
* - find typos by calculating the Levenshtein distance between the unknown command and all
* available commands. The Levenshtein distance is defined as the minimal number of
* characters you have to replace, insert or delete to transform str1 into str2.
*
* @see http://php.net/manual/en/function.levenshtein.php
* @param array $actions available command names.
* @param string $command the command to compare to.
* @return array a list of suggested alternatives sorted by similarity.
*/
private function filterBySimilarity($actions, $command)
{
$alternatives = [];
// suggest alternatives that begin with $command first
foreach ($actions as $action) {
if (strpos($action, $command) === 0) {
$alternatives[] = $action;
}
}
// calculate the Levenshtein distance between the unknown command and all available commands.
$distances = array_map(function ($action) use ($command) {
$action = strlen($action) > 255 ? substr($action, 0, 255) : $action;
$command = strlen($command) > 255 ? substr($command, 0, 255) : $command;
return levenshtein($action, $command);
}, array_combine($actions, $actions));
// we assume a typo if the levensthein distance is no more than 3, i.e. 3 replacements needed
$relevantTypos = array_filter($distances, function ($distance) {
return $distance <= 3;
});
asort($relevantTypos);
$alternatives = array_merge($alternatives, array_flip($relevantTypos));
return array_unique($alternatives);
}
}

View File

@@ -0,0 +1,841 @@
<?php
/**
* @link http://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license http://www.yiiframework.com/license/
*/
namespace yii\console\controllers;
use Yii;
use yii\console\Controller;
use yii\console\Exception;
use yii\console\ExitCode;
use yii\helpers\Console;
use yii\helpers\FileHelper;
use yii\helpers\VarDumper;
use yii\web\AssetBundle;
/**
* Allows you to combine and compress your JavaScript and CSS files.
*
* Usage:
*
* 1. Create a configuration file using the `template` action:
*
* yii asset/template /path/to/myapp/config.php
*
* 2. Edit the created config file, adjusting it for your web application needs.
* 3. Run the 'compress' action, using created config:
*
* yii asset /path/to/myapp/config.php /path/to/myapp/config/assets_compressed.php
*
* 4. Adjust your web application config to use compressed assets.
*
* Note: in the console environment some [path aliases](guide:concept-aliases) like `@webroot` and `@web` may not exist,
* so corresponding paths inside the configuration should be specified directly.
*
* Note: by default this command relies on an external tools to perform actual files compression,
* check [[jsCompressor]] and [[cssCompressor]] for more details.
*
* @property \yii\web\AssetManager $assetManager Asset manager instance. Note that the type of this property
* differs in getter and setter. See [[getAssetManager()]] and [[setAssetManager()]] for details.
*
* @author Qiang Xue <qiang.xue@gmail.com>
* @author Paul Klimov <klimov.paul@gmail.com>
* @since 2.0
*/
class AssetController extends Controller
{
/**
* @var string controller default action ID.
*/
public $defaultAction = 'compress';
/**
* @var array list of asset bundles to be compressed.
*/
public $bundles = [];
/**
* @var array list of asset bundles, which represents output compressed files.
* You can specify the name of the output compressed file using 'css' and 'js' keys:
* For example:
*
* ```php
* 'app\config\AllAsset' => [
* 'js' => 'js/all-{hash}.js',
* 'css' => 'css/all-{hash}.css',
* 'depends' => [ ... ],
* ]
* ```
*
* File names can contain placeholder "{hash}", which will be filled by the hash of the resulting file.
*
* You may specify several target bundles in order to compress different groups of assets.
* In this case you should use 'depends' key to specify, which bundles should be covered with particular
* target bundle. You may leave 'depends' to be empty for single bundle, which will compress all remaining
* bundles in this case.
* For example:
*
* ```php
* 'allShared' => [
* 'js' => 'js/all-shared-{hash}.js',
* 'css' => 'css/all-shared-{hash}.css',
* 'depends' => [
* // Include all assets shared between 'backend' and 'frontend'
* 'yii\web\YiiAsset',
* 'app\assets\SharedAsset',
* ],
* ],
* 'allBackEnd' => [
* 'js' => 'js/all-{hash}.js',
* 'css' => 'css/all-{hash}.css',
* 'depends' => [
* // Include only 'backend' assets:
* 'app\assets\AdminAsset'
* ],
* ],
* 'allFrontEnd' => [
* 'js' => 'js/all-{hash}.js',
* 'css' => 'css/all-{hash}.css',
* 'depends' => [], // Include all remaining assets
* ],
* ```
*/
public $targets = [];
/**
* @var string|callable JavaScript file compressor.
* If a string, it is treated as shell command template, which should contain
* placeholders {from} - source file name - and {to} - output file name.
* Otherwise, it is treated as PHP callback, which should perform the compression.
*
* Default value relies on usage of "Closure Compiler"
* @see https://developers.google.com/closure/compiler/
*/
public $jsCompressor = 'java -jar compiler.jar --js {from} --js_output_file {to}';
/**
* @var string|callable CSS file compressor.
* If a string, it is treated as shell command template, which should contain
* placeholders {from} - source file name - and {to} - output file name.
* Otherwise, it is treated as PHP callback, which should perform the compression.
*
* Default value relies on usage of "YUI Compressor"
* @see https://github.com/yui/yuicompressor/
*/
public $cssCompressor = 'java -jar yuicompressor.jar --type css {from} -o {to}';
/**
* @var bool whether to delete asset source files after compression.
* This option affects only those bundles, which have [[\yii\web\AssetBundle::sourcePath]] is set.
* @since 2.0.10
*/
public $deleteSource = false;
/**
* @var array|\yii\web\AssetManager [[\yii\web\AssetManager]] instance or its array configuration, which will be used
* for assets processing.
*/
private $_assetManager = [];
/**
* Returns the asset manager instance.
* @throws \yii\console\Exception on invalid configuration.
* @return \yii\web\AssetManager asset manager instance.
*/
public function getAssetManager()
{
if (!is_object($this->_assetManager)) {
$options = $this->_assetManager;
if (!isset($options['class'])) {
$options['class'] = 'yii\\web\\AssetManager';
}
if (!isset($options['basePath'])) {
throw new Exception("Please specify 'basePath' for the 'assetManager' option.");
}
if (!isset($options['baseUrl'])) {
throw new Exception("Please specify 'baseUrl' for the 'assetManager' option.");
}
if (!isset($options['forceCopy'])) {
$options['forceCopy'] = true;
}
$this->_assetManager = Yii::createObject($options);
}
return $this->_assetManager;
}
/**
* Sets asset manager instance or configuration.
* @param \yii\web\AssetManager|array $assetManager asset manager instance or its array configuration.
* @throws \yii\console\Exception on invalid argument type.
*/
public function setAssetManager($assetManager)
{
if (is_scalar($assetManager)) {
throw new Exception('"' . get_class($this) . '::assetManager" should be either object or array - "' . gettype($assetManager) . '" given.');
}
$this->_assetManager = $assetManager;
}
/**
* Combines and compresses the asset files according to the given configuration.
* During the process new asset bundle configuration file will be created.
* You should replace your original asset bundle configuration with this file in order to use compressed files.
* @param string $configFile configuration file name.
* @param string $bundleFile output asset bundles configuration file name.
*/
public function actionCompress($configFile, $bundleFile)
{
$this->loadConfiguration($configFile);
$bundles = $this->loadBundles($this->bundles);
$targets = $this->loadTargets($this->targets, $bundles);
foreach ($targets as $name => $target) {
$this->stdout("Creating output bundle '{$name}':\n");
if (!empty($target->js)) {
$this->buildTarget($target, 'js', $bundles);
}
if (!empty($target->css)) {
$this->buildTarget($target, 'css', $bundles);
}
$this->stdout("\n");
}
$targets = $this->adjustDependency($targets, $bundles);
$this->saveTargets($targets, $bundleFile);
if ($this->deleteSource) {
$this->deletePublishedAssets($bundles);
}
}
/**
* Applies configuration from the given file to self instance.
* @param string $configFile configuration file name.
* @throws \yii\console\Exception on failure.
*/
protected function loadConfiguration($configFile)
{
$this->stdout("Loading configuration from '{$configFile}'...\n");
$config = require $configFile;
foreach ($config as $name => $value) {
if (property_exists($this, $name) || $this->canSetProperty($name)) {
$this->$name = $value;
} else {
throw new Exception("Unknown configuration option: $name");
}
}
$this->getAssetManager(); // check if asset manager configuration is correct
}
/**
* Creates full list of source asset bundles.
* @param string[] $bundles list of asset bundle names
* @return \yii\web\AssetBundle[] list of source asset bundles.
*/
protected function loadBundles($bundles)
{
$this->stdout("Collecting source bundles information...\n");
$am = $this->getAssetManager();
$result = [];
foreach ($bundles as $name) {
$result[$name] = $am->getBundle($name);
}
foreach ($result as $bundle) {
$this->loadDependency($bundle, $result);
}
return $result;
}
/**
* Loads asset bundle dependencies recursively.
* @param \yii\web\AssetBundle $bundle bundle instance
* @param array $result already loaded bundles list.
* @throws Exception on failure.
*/
protected function loadDependency($bundle, &$result)
{
$am = $this->getAssetManager();
foreach ($bundle->depends as $name) {
if (!isset($result[$name])) {
$dependencyBundle = $am->getBundle($name);
$result[$name] = false;
$this->loadDependency($dependencyBundle, $result);
$result[$name] = $dependencyBundle;
} elseif ($result[$name] === false) {
throw new Exception("A circular dependency is detected for bundle '{$name}': " . $this->composeCircularDependencyTrace($name, $result) . '.');
}
}
}
/**
* Creates full list of output asset bundles.
* @param array $targets output asset bundles configuration.
* @param \yii\web\AssetBundle[] $bundles list of source asset bundles.
* @return \yii\web\AssetBundle[] list of output asset bundles.
* @throws Exception on failure.
*/
protected function loadTargets($targets, $bundles)
{
// build the dependency order of bundles
$registered = [];
foreach ($bundles as $name => $bundle) {
$this->registerBundle($bundles, $name, $registered);
}
$bundleOrders = array_combine(array_keys($registered), range(0, count($bundles) - 1));
// fill up the target which has empty 'depends'.
$referenced = [];
foreach ($targets as $name => $target) {
if (empty($target['depends'])) {
if (!isset($all)) {
$all = $name;
} else {
throw new Exception("Only one target can have empty 'depends' option. Found two now: $all, $name");
}
} else {
foreach ($target['depends'] as $bundle) {
if (!isset($referenced[$bundle])) {
$referenced[$bundle] = $name;
} else {
throw new Exception("Target '{$referenced[$bundle]}' and '$name' cannot contain the bundle '$bundle' at the same time.");
}
}
}
}
if (isset($all)) {
$targets[$all]['depends'] = array_diff(array_keys($registered), array_keys($referenced));
}
// adjust the 'depends' order for each target according to the dependency order of bundles
// create an AssetBundle object for each target
foreach ($targets as $name => $target) {
if (!isset($target['basePath'])) {
throw new Exception("Please specify 'basePath' for the '$name' target.");
}
if (!isset($target['baseUrl'])) {
throw new Exception("Please specify 'baseUrl' for the '$name' target.");
}
usort($target['depends'], function ($a, $b) use ($bundleOrders) {
if ($bundleOrders[$a] == $bundleOrders[$b]) {
return 0;
}
return $bundleOrders[$a] > $bundleOrders[$b] ? 1 : -1;
});
if (!isset($target['class'])) {
$target['class'] = $name;
}
$targets[$name] = Yii::createObject($target);
}
return $targets;
}
/**
* Builds output asset bundle.
* @param \yii\web\AssetBundle $target output asset bundle
* @param string $type either 'js' or 'css'.
* @param \yii\web\AssetBundle[] $bundles source asset bundles.
* @throws Exception on failure.
*/
protected function buildTarget($target, $type, $bundles)
{
$inputFiles = [];
foreach ($target->depends as $name) {
if (isset($bundles[$name])) {
if (!$this->isBundleExternal($bundles[$name])) {
foreach ($bundles[$name]->$type as $file) {
if (is_array($file)) {
$inputFiles[] = $bundles[$name]->basePath . '/' . $file[0];
} else {
$inputFiles[] = $bundles[$name]->basePath . '/' . $file;
}
}
}
} else {
throw new Exception("Unknown bundle: '{$name}'");
}
}
if (empty($inputFiles)) {
$target->$type = [];
} else {
FileHelper::createDirectory($target->basePath, $this->getAssetManager()->dirMode);
$tempFile = $target->basePath . '/' . strtr($target->$type, ['{hash}' => 'temp']);
if ($type === 'js') {
$this->compressJsFiles($inputFiles, $tempFile);
} else {
$this->compressCssFiles($inputFiles, $tempFile);
}
$targetFile = strtr($target->$type, ['{hash}' => md5_file($tempFile)]);
$outputFile = $target->basePath . '/' . $targetFile;
rename($tempFile, $outputFile);
$target->$type = [$targetFile];
}
}
/**
* Adjust dependencies between asset bundles in the way source bundles begin to depend on output ones.
* @param \yii\web\AssetBundle[] $targets output asset bundles.
* @param \yii\web\AssetBundle[] $bundles source asset bundles.
* @return \yii\web\AssetBundle[] output asset bundles.
*/
protected function adjustDependency($targets, $bundles)
{
$this->stdout("Creating new bundle configuration...\n");
$map = [];
foreach ($targets as $name => $target) {
foreach ($target->depends as $bundle) {
$map[$bundle] = $name;
}
}
foreach ($targets as $name => $target) {
$depends = [];
foreach ($target->depends as $bn) {
foreach ($bundles[$bn]->depends as $bundle) {
$depends[$map[$bundle]] = true;
}
}
unset($depends[$name]);
$target->depends = array_keys($depends);
}
// detect possible circular dependencies
foreach ($targets as $name => $target) {
$registered = [];
$this->registerBundle($targets, $name, $registered);
}
foreach ($map as $bundle => $target) {
$sourceBundle = $bundles[$bundle];
$depends = $sourceBundle->depends;
if (!$this->isBundleExternal($sourceBundle)) {
$depends[] = $target;
}
$targetBundle = clone $sourceBundle;
$targetBundle->depends = $depends;
$targets[$bundle] = $targetBundle;
}
return $targets;
}
/**
* Registers asset bundles including their dependencies.
* @param \yii\web\AssetBundle[] $bundles asset bundles list.
* @param string $name bundle name.
* @param array $registered stores already registered names.
* @throws Exception if circular dependency is detected.
*/
protected function registerBundle($bundles, $name, &$registered)
{
if (!isset($registered[$name])) {
$registered[$name] = false;
$bundle = $bundles[$name];
foreach ($bundle->depends as $depend) {
$this->registerBundle($bundles, $depend, $registered);
}
unset($registered[$name]);
$registered[$name] = $bundle;
} elseif ($registered[$name] === false) {
throw new Exception("A circular dependency is detected for target '{$name}': " . $this->composeCircularDependencyTrace($name, $registered) . '.');
}
}
/**
* Saves new asset bundles configuration.
* @param \yii\web\AssetBundle[] $targets list of asset bundles to be saved.
* @param string $bundleFile output file name.
* @throws \yii\console\Exception on failure.
*/
protected function saveTargets($targets, $bundleFile)
{
$array = [];
foreach ($targets as $name => $target) {
if (isset($this->targets[$name])) {
$array[$name] = array_merge($this->targets[$name], [
'class' => get_class($target),
'sourcePath' => null,
'basePath' => $this->targets[$name]['basePath'],
'baseUrl' => $this->targets[$name]['baseUrl'],
'js' => $target->js,
'css' => $target->css,
'depends' => [],
]);
} else {
if ($this->isBundleExternal($target)) {
$array[$name] = $this->composeBundleConfig($target);
} else {
$array[$name] = [
'sourcePath' => null,
'js' => [],
'css' => [],
'depends' => $target->depends,
];
}
}
}
$array = VarDumper::export($array);
$version = date('Y-m-d H:i:s');
$bundleFileContent = <<<EOD
<?php
/**
* This file is generated by the "yii {$this->id}" command.
* DO NOT MODIFY THIS FILE DIRECTLY.
* @version {$version}
*/
return {$array};
EOD;
if (!file_put_contents($bundleFile, $bundleFileContent, LOCK_EX)) {
throw new Exception("Unable to write output bundle configuration at '{$bundleFile}'.");
}
$this->stdout("Output bundle configuration created at '{$bundleFile}'.\n", Console::FG_GREEN);
}
/**
* Compresses given JavaScript files and combines them into the single one.
* @param array $inputFiles list of source file names.
* @param string $outputFile output file name.
* @throws \yii\console\Exception on failure
*/
protected function compressJsFiles($inputFiles, $outputFile)
{
if (empty($inputFiles)) {
return;
}
$this->stdout(" Compressing JavaScript files...\n");
if (is_string($this->jsCompressor)) {
$tmpFile = $outputFile . '.tmp';
$this->combineJsFiles($inputFiles, $tmpFile);
$this->stdout(shell_exec(strtr($this->jsCompressor, [
'{from}' => escapeshellarg($tmpFile),
'{to}' => escapeshellarg($outputFile),
])));
@unlink($tmpFile);
} else {
call_user_func($this->jsCompressor, $this, $inputFiles, $outputFile);
}
if (!file_exists($outputFile)) {
throw new Exception("Unable to compress JavaScript files into '{$outputFile}'.");
}
$this->stdout(" JavaScript files compressed into '{$outputFile}'.\n");
}
/**
* Compresses given CSS files and combines them into the single one.
* @param array $inputFiles list of source file names.
* @param string $outputFile output file name.
* @throws \yii\console\Exception on failure
*/
protected function compressCssFiles($inputFiles, $outputFile)
{
if (empty($inputFiles)) {
return;
}
$this->stdout(" Compressing CSS files...\n");
if (is_string($this->cssCompressor)) {
$tmpFile = $outputFile . '.tmp';
$this->combineCssFiles($inputFiles, $tmpFile);
$this->stdout(shell_exec(strtr($this->cssCompressor, [
'{from}' => escapeshellarg($tmpFile),
'{to}' => escapeshellarg($outputFile),
])));
@unlink($tmpFile);
} else {
call_user_func($this->cssCompressor, $this, $inputFiles, $outputFile);
}
if (!file_exists($outputFile)) {
throw new Exception("Unable to compress CSS files into '{$outputFile}'.");
}
$this->stdout(" CSS files compressed into '{$outputFile}'.\n");
}
/**
* Combines JavaScript files into a single one.
* @param array $inputFiles source file names.
* @param string $outputFile output file name.
* @throws \yii\console\Exception on failure.
*/
public function combineJsFiles($inputFiles, $outputFile)
{
$content = '';
foreach ($inputFiles as $file) {
// Add a semicolon to source code if trailing semicolon missing.
// Notice: It needs a new line before `;` to avoid affection of line comment. (// ...;)
$fileContent = rtrim(file_get_contents($file));
if (substr($fileContent, -1) !== ';') {
$fileContent .= "\n;";
}
$content .= "/*** BEGIN FILE: $file ***/\n"
. $fileContent . "\n"
. "/*** END FILE: $file ***/\n";
}
if (!file_put_contents($outputFile, $content)) {
throw new Exception("Unable to write output JavaScript file '{$outputFile}'.");
}
}
/**
* Combines CSS files into a single one.
* @param array $inputFiles source file names.
* @param string $outputFile output file name.
* @throws \yii\console\Exception on failure.
*/
public function combineCssFiles($inputFiles, $outputFile)
{
$content = '';
$outputFilePath = dirname($this->findRealPath($outputFile));
foreach ($inputFiles as $file) {
$content .= "/*** BEGIN FILE: $file ***/\n"
. $this->adjustCssUrl(file_get_contents($file), dirname($this->findRealPath($file)), $outputFilePath)
. "/*** END FILE: $file ***/\n";
}
if (!file_put_contents($outputFile, $content)) {
throw new Exception("Unable to write output CSS file '{$outputFile}'.");
}
}
/**
* Adjusts CSS content allowing URL references pointing to the original resources.
* @param string $cssContent source CSS content.
* @param string $inputFilePath input CSS file name.
* @param string $outputFilePath output CSS file name.
* @return string adjusted CSS content.
*/
protected function adjustCssUrl($cssContent, $inputFilePath, $outputFilePath)
{
$inputFilePath = str_replace('\\', '/', $inputFilePath);
$outputFilePath = str_replace('\\', '/', $outputFilePath);
$sharedPathParts = [];
$inputFilePathParts = explode('/', $inputFilePath);
$inputFilePathPartsCount = count($inputFilePathParts);
$outputFilePathParts = explode('/', $outputFilePath);
$outputFilePathPartsCount = count($outputFilePathParts);
for ($i = 0; $i < $inputFilePathPartsCount && $i < $outputFilePathPartsCount; $i++) {
if ($inputFilePathParts[$i] == $outputFilePathParts[$i]) {
$sharedPathParts[] = $inputFilePathParts[$i];
} else {
break;
}
}
$sharedPath = implode('/', $sharedPathParts);
$inputFileRelativePath = trim(str_replace($sharedPath, '', $inputFilePath), '/');
$outputFileRelativePath = trim(str_replace($sharedPath, '', $outputFilePath), '/');
if (empty($inputFileRelativePath)) {
$inputFileRelativePathParts = [];
} else {
$inputFileRelativePathParts = explode('/', $inputFileRelativePath);
}
if (empty($outputFileRelativePath)) {
$outputFileRelativePathParts = [];
} else {
$outputFileRelativePathParts = explode('/', $outputFileRelativePath);
}
$callback = function ($matches) use ($inputFileRelativePathParts, $outputFileRelativePathParts) {
$fullMatch = $matches[0];
$inputUrl = $matches[1];
if (strncmp($inputUrl, '/', 1) === 0 || strncmp($inputUrl, '#', 1) === 0 || preg_match('/^https?:\/\//i', $inputUrl) || preg_match('/^data:/i', $inputUrl)) {
return $fullMatch;
}
if ($inputFileRelativePathParts === $outputFileRelativePathParts) {
return $fullMatch;
}
if (empty($outputFileRelativePathParts)) {
$outputUrlParts = [];
} else {
$outputUrlParts = array_fill(0, count($outputFileRelativePathParts), '..');
}
$outputUrlParts = array_merge($outputUrlParts, $inputFileRelativePathParts);
if (strpos($inputUrl, '/') !== false) {
$inputUrlParts = explode('/', $inputUrl);
foreach ($inputUrlParts as $key => $inputUrlPart) {
if ($inputUrlPart === '..') {
array_pop($outputUrlParts);
unset($inputUrlParts[$key]);
}
}
$outputUrlParts[] = implode('/', $inputUrlParts);
} else {
$outputUrlParts[] = $inputUrl;
}
$outputUrl = implode('/', $outputUrlParts);
return str_replace($inputUrl, $outputUrl, $fullMatch);
};
$cssContent = preg_replace_callback('/url\(["\']?([^)^"^\']*)["\']?\)/i', $callback, $cssContent);
return $cssContent;
}
/**
* Creates template of configuration file for [[actionCompress]].
* @param string $configFile output file name.
* @return int CLI exit code
* @throws \yii\console\Exception on failure.
*/
public function actionTemplate($configFile)
{
$jsCompressor = VarDumper::export($this->jsCompressor);
$cssCompressor = VarDumper::export($this->cssCompressor);
$template = <<<EOD
<?php
/**
* Configuration file for the "yii asset" console command.
*/
// In the console environment, some path aliases may not exist. Please define these:
// Yii::setAlias('@webroot', __DIR__ . '/../web');
// Yii::setAlias('@web', '/');
return [
// Adjust command/callback for JavaScript files compressing:
'jsCompressor' => {$jsCompressor},
// Adjust command/callback for CSS files compressing:
'cssCompressor' => {$cssCompressor},
// Whether to delete asset source after compression:
'deleteSource' => false,
// The list of asset bundles to compress:
'bundles' => [
// 'app\assets\AppAsset',
// 'yii\web\YiiAsset',
// 'yii\web\JqueryAsset',
],
// Asset bundle for compression output:
'targets' => [
'all' => [
'class' => 'yii\web\AssetBundle',
'basePath' => '@webroot/assets',
'baseUrl' => '@web/assets',
'js' => 'js/all-{hash}.js',
'css' => 'css/all-{hash}.css',
],
],
// Asset manager configuration:
'assetManager' => [
//'basePath' => '@webroot/assets',
//'baseUrl' => '@web/assets',
],
];
EOD;
if (file_exists($configFile)) {
if (!$this->confirm("File '{$configFile}' already exists. Do you wish to overwrite it?")) {
return ExitCode::OK;
}
}
if (!file_put_contents($configFile, $template, LOCK_EX)) {
throw new Exception("Unable to write template file '{$configFile}'.");
}
$this->stdout("Configuration file template created at '{$configFile}'.\n\n", Console::FG_GREEN);
return ExitCode::OK;
}
/**
* Returns canonicalized absolute pathname.
* Unlike regular `realpath()` this method does not expand symlinks and does not check path existence.
* @param string $path raw path
* @return string canonicalized absolute pathname
*/
private function findRealPath($path)
{
$path = str_replace(['/', '\\'], DIRECTORY_SEPARATOR, $path);
$pathParts = explode(DIRECTORY_SEPARATOR, $path);
$realPathParts = [];
foreach ($pathParts as $pathPart) {
if ($pathPart === '..') {
array_pop($realPathParts);
} else {
$realPathParts[] = $pathPart;
}
}
return implode(DIRECTORY_SEPARATOR, $realPathParts);
}
/**
* @param AssetBundle $bundle
* @return bool whether asset bundle external or not.
*/
private function isBundleExternal($bundle)
{
return empty($bundle->sourcePath) && empty($bundle->basePath);
}
/**
* @param AssetBundle $bundle asset bundle instance.
* @return array bundle configuration.
*/
private function composeBundleConfig($bundle)
{
$config = Yii::getObjectVars($bundle);
$config['class'] = get_class($bundle);
return $config;
}
/**
* Composes trace info for bundle circular dependency.
* @param string $circularDependencyName name of the bundle, which have circular dependency
* @param array $registered list of bundles registered while detecting circular dependency.
* @return string bundle circular dependency trace string.
*/
private function composeCircularDependencyTrace($circularDependencyName, array $registered)
{
$dependencyTrace = [];
$startFound = false;
foreach ($registered as $name => $value) {
if ($name === $circularDependencyName) {
$startFound = true;
}
if ($startFound && $value === false) {
$dependencyTrace[] = $name;
}
}
$dependencyTrace[] = $circularDependencyName;
return implode(' -> ', $dependencyTrace);
}
/**
* Deletes bundle asset files, which have been published from `sourcePath`.
* @param \yii\web\AssetBundle[] $bundles asset bundles to be processed.
* @since 2.0.10
*/
private function deletePublishedAssets($bundles)
{
$this->stdout("Deleting source files...\n");
if ($this->getAssetManager()->linkAssets) {
$this->stdout("`AssetManager::linkAssets` option is enabled. Deleting of source files canceled.\n", Console::FG_YELLOW);
return;
}
foreach ($bundles as $bundle) {
if ($bundle->sourcePath !== null) {
foreach ($bundle->js as $jsFile) {
@unlink($bundle->basePath . DIRECTORY_SEPARATOR . $jsFile);
}
foreach ($bundle->css as $cssFile) {
@unlink($bundle->basePath . DIRECTORY_SEPARATOR . $cssFile);
}
}
}
$this->stdout("Source files deleted.\n", Console::FG_GREEN);
}
}

View File

@@ -0,0 +1,978 @@
<?php
/**
* @link http://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license http://www.yiiframework.com/license/
*/
namespace yii\console\controllers;
use Yii;
use yii\base\BaseObject;
use yii\base\InvalidConfigException;
use yii\base\NotSupportedException;
use yii\console\Controller;
use yii\console\Exception;
use yii\console\ExitCode;
use yii\db\MigrationInterface;
use yii\helpers\Console;
use yii\helpers\FileHelper;
/**
* BaseMigrateController is the base class for migrate controllers.
*
* @author Qiang Xue <qiang.xue@gmail.com>
* @since 2.0
*/
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.
*
* Since version 2.0.12 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/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 `Yii::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 Yii.
*
* For example:
*
* ```php
* [
* 'app\migrations',
* 'some\extension\migrations',
* ]
* ```
*
* @since 2.0.10
* @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.
* @since 2.0.13
*/
public $compact = false;
/**
* {@inheritdoc}
*/
public function options($actionID)
{
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 \yii\base\Action $action 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($action)
{
if (parent::beforeAction($action)) {
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] = Yii::getAlias($path);
}
} elseif ($this->migrationPath !== null) {
$path = Yii::getAlias($this->migrationPath);
if (!is_dir($path)) {
if ($action->id !== 'create') {
throw new InvalidConfigException("Migration failed. Directory specified in migrationPath doesn't exist: {$this->migrationPath}");
}
FileHelper::createDirectory($path);
}
$this->migrationPath = $path;
}
$version = Yii::getVersion();
$this->stdout("Yii Migration Tool (based on Yii v{$version})\n\n");
return true;
}
return false;
}
/**
* Upgrades the application by applying new migrations.
*
* For example,
*
* ```
* yii migrate # apply all new migrations
* yii migrate 3 # apply the first 3 new migrations
* ```
*
* @param int $limit the number of new migrations to be applied. If 0, it means
* applying all available new migrations.
*
* @return int the status of the action execution. 0 means normal, other values mean abnormal.
*/
public function actionUp($limit = 0)
{
$migrations = $this->getNewMigrations();
if (empty($migrations)) {
$this->stdout("No new migrations found. Your system is up-to-date.\n", Console::FG_GREEN);
return ExitCode::OK;
}
$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) {
$this->stdout("\nThe migration name '$migration' is too long. Its not possible to apply this migration.\n", Console::FG_RED);
return ExitCode::UNSPECIFIED_ERROR;
}
$this->stdout("\t$migration\n");
}
$this->stdout("\n");
$applied = 0;
if ($this->confirm('Apply the above ' . ($n === 1 ? 'migration' : 'migrations') . '?')) {
foreach ($migrations as $migration) {
if (!$this->migrateUp($migration)) {
$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);
return ExitCode::UNSPECIFIED_ERROR;
}
$applied++;
}
$this->stdout("\n$n " . ($n === 1 ? 'migration was' : 'migrations were') . " applied.\n", Console::FG_GREEN);
$this->stdout("\nMigrated up successfully.\n", Console::FG_GREEN);
}
}
/**
* Downgrades the application by reverting old migrations.
*
* For example,
*
* ```
* yii migrate/down # revert the last migration
* yii migrate/down 3 # revert the last 3 migrations
* yii migrate/down all # revert all migrations
* ```
*
* @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.
* @throws Exception if the number of the steps specified is less than 1.
*
* @return int the status of the action execution. 0 means normal, other values mean abnormal.
*/
public function actionDown($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.');
}
}
$migrations = $this->getMigrationHistory($limit);
if (empty($migrations)) {
$this->stdout("No migration has been done before.\n", Console::FG_YELLOW);
return ExitCode::OK;
}
$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;
if ($this->confirm('Revert the above ' . ($n === 1 ? 'migration' : 'migrations') . '?')) {
foreach ($migrations as $migration) {
if (!$this->migrateDown($migration)) {
$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 ExitCode::UNSPECIFIED_ERROR;
}
$reverted++;
}
$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,
*
* ```
* yii migrate/redo # redo the last applied migration
* yii migrate/redo 3 # redo the last 3 applied migrations
* yii migrate/redo all # redo all migrations
* ```
*
* @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.
* @throws Exception if the number of the steps specified is less than 1.
*
* @return int the status of the action execution. 0 means normal, other values mean abnormal.
*/
public function actionRedo($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.');
}
}
$migrations = $this->getMigrationHistory($limit);
if (empty($migrations)) {
$this->stdout("No migration has been done before.\n", Console::FG_YELLOW);
return ExitCode::OK;
}
$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");
if ($this->confirm('Redo the above ' . ($n === 1 ? 'migration' : 'migrations') . '?')) {
foreach ($migrations as $migration) {
if (!$this->migrateDown($migration)) {
$this->stdout("\nMigration failed. The rest of the migrations are canceled.\n", Console::FG_RED);
return ExitCode::UNSPECIFIED_ERROR;
}
}
foreach (array_reverse($migrations) as $migration) {
if (!$this->migrateUp($migration)) {
$this->stdout("\nMigration failed. The rest of the migrations are canceled.\n", Console::FG_RED);
return ExitCode::UNSPECIFIED_ERROR;
}
}
$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,
*
* ```
* yii migrate/to 101129_185401 # using timestamp
* yii migrate/to m101129_185401_create_user_table # using full name
* yii migrate/to 1392853618 # using UNIX timestamp
* yii migrate/to "2014-02-15 13:00:50" # using strtotime() parseable string
* yii migrate/to app\migrations\M101129185401CreateUser # using full namespace name
* ```
*
* @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.
* @throws Exception if the version argument is invalid.
*/
public function actionTo($version)
{
if (($namespaceVersion = $this->extractNamespaceMigrationVersion($version)) !== false) {
$this->migrateToVersion($namespaceVersion);
} elseif (($migrationName = $this->extractMigrationVersion($version)) !== false) {
$this->migrateToVersion($migrationName);
} elseif ((string) (int) $version == $version) {
$this->migrateToTime($version);
} elseif (($time = strtotime($version)) !== false) {
$this->migrateToTime($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.
*
* ```
* yii migrate/mark 101129_185401 # using timestamp
* yii migrate/mark m101129_185401_create_user_table # using full name
* yii migrate/mark app\migrations\M101129185401CreateUser # using full namespace name
* yii migrate/mark m000000_000000_base # reset the complete migration history
* ```
*
* @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 int CLI exit code
* @throws Exception if the version argument is invalid or the version cannot be found.
*/
public function actionMark($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
$migrations = $this->getNewMigrations();
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 ExitCode::OK;
}
}
// try mark down
$migrations = array_keys($this->getMigrationHistory(null));
$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);
} else {
if ($this->confirm("Set migration history at $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 ExitCode::OK;
}
}
throw new Exception("Unable to find the version '$originalVersion'.");
}
/**
* Truncates the whole database and starts the migration from the beginning.
*
* ```
* yii migrate/fresh
* ```
*
* @since 2.0.13
*/
public function actionFresh()
{
if (YII_ENV_PROD) {
$this->stdout("YII_ENV is set to 'prod'.\nRefreshing migrations is not possible on production systems.\n");
return ExitCode::OK;
}
if ($this->confirm(
"Are you sure you want to reset the database and start the migration from the beginning?\nAll data will be lost irreversibly!")) {
$this->truncateDatabase();
$this->actionUp();
} else {
$this->stdout('Action was cancelled by user. Nothing has been performed.');
}
}
/**
* 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.
* @since 2.0.10
*/
private 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.
* @since 2.0.10
*/
private function extractMigrationVersion($rawVersion)
{
if (preg_match('/^m?(\d{6}_?\d{6})(\D.*)?$/is', $rawVersion, $matches)) {
return 'm' . $matches[1];
}
return false;
}
/**
* Displays the migration history.
*
* This command will show the list of migrations that have been applied
* so far. For example,
*
* ```
* yii migrate/history # showing the last 10 migrations
* yii migrate/history 5 # showing the last 5 migrations
* yii migrate/history all # showing the whole history
* ```
*
* @param int|string $limit the maximum number of migrations to be displayed.
* If it is "all", the whole migration history will be displayed.
* @throws \yii\console\Exception if invalid limit value passed
*/
public function actionHistory($limit = 10)
{
if ($limit === 'all') {
$limit = null;
} else {
$limit = (int) $limit;
if ($limit < 1) {
throw new Exception('The limit must be greater than 0.');
}
}
$migrations = $this->getMigrationHistory($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");
}
}
}
/**
* Displays the un-applied new migrations.
*
* This command will show the new migrations that have not been applied.
* For example,
*
* ```
* yii migrate/new # showing the first 10 new migrations
* yii migrate/new 5 # showing the first 5 new migrations
* yii migrate/new all # showing all new migrations
* ```
*
* @param int|string $limit the maximum number of new migrations to be displayed.
* If it is `all`, all available new migrations will be displayed.
* @throws \yii\console\Exception if invalid limit value passed
*/
public function actionNew($limit = 10)
{
if ($limit === 'all') {
$limit = null;
} else {
$limit = (int) $limit;
if ($limit < 1) {
throw new Exception('The limit must be greater than 0.');
}
}
$migrations = $this->getNewMigrations();
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");
}
}
}
/**
* 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.
*
* ```
* yii 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:
*
* ```
* yii 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 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.
*
* @throws Exception if the name argument is invalid.
*/
public function actionCreate($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';
if ($this->confirm("Create new migration '$file'?")) {
$content = $this->generateMigrationSourceCode([
'name' => $name,
'className' => $className,
'namespace' => $namespace,
]);
FileHelper::createDirectory($migrationPath);
file_put_contents($file, $content, LOCK_EX);
$this->stdout("New migration created successfully.\n", Console::FG_GREEN);
}
}
/**
* 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'
* @since 2.0.10
*/
private 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.
* @since 2.0.10
*/
private 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.
* @since 2.0.10
*/
private function getNamespacePath($namespace)
{
return str_replace('/', DIRECTORY_SEPARATOR, Yii::getAlias('@' . str_replace('\\', '/', $namespace)));
}
/**
* Upgrades with the specified migration class.
* @param string $class the migration class name
* @return bool whether the migration is successful
*/
protected function migrateUp($class)
{
if ($class === self::BASE_MIGRATION) {
return true;
}
$this->stdout("*** applying $class\n", Console::FG_YELLOW);
$start = microtime(true);
$migration = $this->createMigration($class);
if ($migration->up() !== false) {
$this->addMigrationHistory($class);
$time = microtime(true) - $start;
$this->stdout("*** applied $class (time: " . sprintf('%.3f', $time) . "s)\n\n", Console::FG_GREEN);
return true;
}
$time = microtime(true) - $start;
$this->stdout("*** failed to apply $class (time: " . sprintf('%.3f', $time) . "s)\n\n", Console::FG_RED);
return false;
}
/**
* Downgrades with the specified migration class.
* @param string $class the migration class name
* @return bool whether the migration is successful
*/
protected function migrateDown($class)
{
if ($class === self::BASE_MIGRATION) {
return true;
}
$this->stdout("*** reverting $class\n", Console::FG_YELLOW);
$start = microtime(true);
$migration = $this->createMigration($class);
if ($migration->down() !== false) {
$this->removeMigrationHistory($class);
$time = microtime(true) - $start;
$this->stdout("*** reverted $class (time: " . sprintf('%.3f', $time) . "s)\n\n", Console::FG_GREEN);
return true;
}
$time = microtime(true) - $start;
$this->stdout("*** failed to revert $class (time: " . sprintf('%.3f', $time) . "s)\n\n", Console::FG_RED);
return false;
}
/**
* Creates a new migration instance.
* @param string $class the migration class name
* @return \yii\db\MigrationInterface the migration instance
*/
protected function createMigration($class)
{
$this->includeMigrationFile($class);
/** @var MigrationInterface $migration */
$migration = Yii::createObject($class);
if ($migration instanceof BaseObject && $migration->canSetProperty('compact')) {
$migration->compact = $this->compact;
}
return $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.
* @since 2.0.12
*/
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 int $time UNIX timestamp value.
*/
protected function migrateToTime($time)
{
$count = 0;
$migrations = array_values($this->getMigrationHistory(null));
while ($count < count($migrations) && $migrations[$count] > $time) {
++$count;
}
if ($count === 0) {
$this->stdout("Nothing needs to be done.\n", Console::FG_GREEN);
} else {
$this->actionDown($count);
}
}
/**
* Migrates to the certain version.
* @param string $version name in the full format.
* @return int CLI exit code
* @throws Exception if the provided version cannot be found.
*/
protected function migrateToVersion($version)
{
$originalVersion = $version;
// try migrate up
$migrations = $this->getNewMigrations();
foreach ($migrations as $i => $migration) {
if (strpos($migration, $version) === 0) {
$this->actionUp($i + 1);
return ExitCode::OK;
}
}
// try migrate down
$migrations = array_keys($this->getMigrationHistory(null));
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);
} else {
$this->actionDown($i);
}
return ExitCode::OK;
}
}
throw new Exception("Unable to find the version '$originalVersion'.");
}
/**
* Returns the migrations that are not applied.
* @return array list of new migrations
*/
protected function getNewMigrations()
{
$applied = [];
foreach ($this->getMigrationHistory(null) 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
*
* @return string generated PHP code.
* @since 2.0.8
*/
protected function generateMigrationSourceCode($params)
{
return $this->renderFile(Yii::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
* @since 2.0.13
*/
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.
* @since 2.0.13
*/
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 array the migration history
*/
abstract protected function getMigrationHistory($limit);
/**
* Adds new migration entry to the history.
* @param string $version migration version name.
*/
abstract protected function addMigrationHistory($version);
/**
* Removes existing migration from the history.
* @param string $version migration version name.
*/
abstract protected function removeMigrationHistory($version);
}

View File

@@ -0,0 +1,305 @@
<?php
/**
* @link http://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license http://www.yiiframework.com/license/
*/
namespace yii\console\controllers;
use Yii;
use yii\caching\ApcCache;
use yii\caching\CacheInterface;
use yii\console\Controller;
use yii\console\Exception;
use yii\console\ExitCode;
use yii\helpers\Console;
/**
* Allows you to flush cache.
*
* see list of available components to flush:
*
* yii cache
*
* flush particular components specified by their names:
*
* yii cache/flush first second third
*
* flush all cache components that can be found in the system
*
* yii cache/flush-all
*
* Note that the command uses cache components defined in your console application configuration file. If components
* configured are different from web application, web application cache won't be cleared. In order to fix it please
* duplicate web application cache components in console config. You can use any component names.
*
* APC is not shared between PHP processes so flushing cache from command line has no effect on web.
* Flushing web cache could be either done by:
*
* - Putting a php file under web root and calling it via HTTP
* - Using [Cachetool](http://gordalina.github.io/cachetool/)
*
* @author Alexander Makarov <sam@rmcreative.ru>
* @author Mark Jebri <mark.github@yandex.ru>
* @since 2.0
*/
class CacheController extends Controller
{
/**
* Lists the caches that can be flushed.
*/
public function actionIndex()
{
$caches = $this->findCaches();
if (!empty($caches)) {
$this->notifyCachesCanBeFlushed($caches);
} else {
$this->notifyNoCachesFound();
}
}
/**
* Flushes given cache components.
*
* For example,
*
* ```
* # flushes caches specified by their id: "first", "second", "third"
* yii cache/flush first second third
* ```
*/
public function actionFlush()
{
$cachesInput = func_get_args();
if (empty($cachesInput)) {
throw new Exception('You should specify cache components names');
}
$caches = $this->findCaches($cachesInput);
$cachesInfo = [];
$foundCaches = array_keys($caches);
$notFoundCaches = array_diff($cachesInput, array_keys($caches));
if ($notFoundCaches) {
$this->notifyNotFoundCaches($notFoundCaches);
}
if (!$foundCaches) {
$this->notifyNoCachesFound();
return ExitCode::OK;
}
if (!$this->confirmFlush($foundCaches)) {
return ExitCode::OK;
}
foreach ($caches as $name => $class) {
$cachesInfo[] = [
'name' => $name,
'class' => $class,
'is_flushed' => $this->canBeFlushed($class) ? Yii::$app->get($name)->flush() : false,
];
}
$this->notifyFlushed($cachesInfo);
}
/**
* Flushes all caches registered in the system.
*/
public function actionFlushAll()
{
$caches = $this->findCaches();
$cachesInfo = [];
if (empty($caches)) {
$this->notifyNoCachesFound();
return ExitCode::OK;
}
foreach ($caches as $name => $class) {
$cachesInfo[] = [
'name' => $name,
'class' => $class,
'is_flushed' => $this->canBeFlushed($class) ? Yii::$app->get($name)->flush() : false,
];
}
$this->notifyFlushed($cachesInfo);
}
/**
* Clears DB schema cache for a given connection component.
*
* ```
* # clears cache schema specified by component id: "db"
* yii cache/flush-schema db
* ```
*
* @param string $db id connection component
* @return int exit code
* @throws Exception
* @throws \yii\base\InvalidConfigException
*
* @since 2.0.1
*/
public function actionFlushSchema($db = 'db')
{
$connection = Yii::$app->get($db, false);
if ($connection === null) {
$this->stdout("Unknown component \"$db\".\n", Console::FG_RED);
return ExitCode::UNSPECIFIED_ERROR;
}
if (!$connection instanceof \yii\db\Connection) {
$this->stdout("\"$db\" component doesn't inherit \\yii\\db\\Connection.\n", Console::FG_RED);
return ExitCode::UNSPECIFIED_ERROR;
} elseif (!$this->confirm("Flush cache schema for \"$db\" connection?")) {
return ExitCode::OK;
}
try {
$schema = $connection->getSchema();
$schema->refresh();
$this->stdout("Schema cache for component \"$db\", was flushed.\n\n", Console::FG_GREEN);
} catch (\Exception $e) {
$this->stdout($e->getMessage() . "\n\n", Console::FG_RED);
}
}
/**
* Notifies user that given caches are found and can be flushed.
* @param array $caches array of cache component classes
*/
private function notifyCachesCanBeFlushed($caches)
{
$this->stdout("The following caches were found in the system:\n\n", Console::FG_YELLOW);
foreach ($caches as $name => $class) {
if ($this->canBeFlushed($class)) {
$this->stdout("\t* $name ($class)\n", Console::FG_GREEN);
} else {
$this->stdout("\t* $name ($class) - can not be flushed via console\n", Console::FG_YELLOW);
}
}
$this->stdout("\n");
}
/**
* Notifies user that there was not found any cache in the system.
*/
private function notifyNoCachesFound()
{
$this->stdout("No cache components were found in the system.\n", Console::FG_RED);
}
/**
* Notifies user that given cache components were not found in the system.
* @param array $cachesNames
*/
private function notifyNotFoundCaches($cachesNames)
{
$this->stdout("The following cache components were NOT found:\n\n", Console::FG_RED);
foreach ($cachesNames as $name) {
$this->stdout("\t* $name \n", Console::FG_GREEN);
}
$this->stdout("\n");
}
/**
* @param array $caches
*/
private function notifyFlushed($caches)
{
$this->stdout("The following cache components were processed:\n\n", Console::FG_YELLOW);
foreach ($caches as $cache) {
$this->stdout("\t* " . $cache['name'] . ' (' . $cache['class'] . ')', Console::FG_GREEN);
if (!$cache['is_flushed']) {
$this->stdout(" - not flushed\n", Console::FG_RED);
} else {
$this->stdout("\n");
}
}
$this->stdout("\n");
}
/**
* Prompts user with confirmation if caches should be flushed.
* @param array $cachesNames
* @return bool
*/
private function confirmFlush($cachesNames)
{
$this->stdout("The following cache components will be flushed:\n\n", Console::FG_YELLOW);
foreach ($cachesNames as $name) {
$this->stdout("\t* $name \n", Console::FG_GREEN);
}
return $this->confirm("\nFlush above cache components?");
}
/**
* Returns array of caches in the system, keys are cache components names, values are class names.
* @param array $cachesNames caches to be found
* @return array
*/
private function findCaches(array $cachesNames = [])
{
$caches = [];
$components = Yii::$app->getComponents();
$findAll = ($cachesNames === []);
foreach ($components as $name => $component) {
if (!$findAll && !in_array($name, $cachesNames, true)) {
continue;
}
if ($component instanceof CacheInterface) {
$caches[$name] = get_class($component);
} elseif (is_array($component) && isset($component['class']) && $this->isCacheClass($component['class'])) {
$caches[$name] = $component['class'];
} elseif (is_string($component) && $this->isCacheClass($component)) {
$caches[$name] = $component;
} elseif ($component instanceof \Closure) {
$cache = Yii::$app->get($name);
if ($this->isCacheClass($cache)) {
$cacheClass = get_class($cache);
$caches[$name] = $cacheClass;
}
}
}
return $caches;
}
/**
* Checks if given class is a Cache class.
* @param string $className class name.
* @return bool
*/
private function isCacheClass($className)
{
return is_subclass_of($className, 'yii\caching\CacheInterface');
}
/**
* Checks if cache of a certain class can be flushed.
* @param string $className class name.
* @return bool
*/
private function canBeFlushed($className)
{
return !is_a($className, ApcCache::className(), true) || PHP_SAPI !== 'cli';
}
}

View File

@@ -0,0 +1,522 @@
<?php
/**
* @link http://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license http://www.yiiframework.com/license/
*/
namespace yii\console\controllers;
use Yii;
use yii\base\InvalidConfigException;
use yii\base\InvalidParamException;
use yii\console\Controller;
use yii\console\Exception;
use yii\console\ExitCode;
use yii\helpers\Console;
use yii\helpers\FileHelper;
use yii\test\FixtureTrait;
/**
* Manages fixture data loading and unloading.
*
* ```
* #load fixtures from UsersFixture class with default namespace "tests\unit\fixtures"
* yii fixture/load User
*
* #also a short version of this command (generate action is default)
* yii fixture User
*
* #load all fixtures
* yii fixture "*"
*
* #load all fixtures except User
* yii fixture "*, -User"
*
* #load fixtures with different namespace.
* yii fixture/load User --namespace=alias\my\custom\namespace\goes\here
* ```
*
* The `unload` sub-command can be used similarly to unload fixtures.
*
* @author Mark Jebri <mark.github@yandex.ru>
* @since 2.0
*/
class FixtureController extends Controller
{
use FixtureTrait;
/**
* @var string controller default action ID.
*/
public $defaultAction = 'load';
/**
* @var string default namespace to search fixtures in
*/
public $namespace = 'tests\unit\fixtures';
/**
* @var array global fixtures that should be applied when loading and unloading. By default it is set to `InitDbFixture`
* that disables and enables integrity check, so your data can be safely loaded.
*/
public $globalFixtures = [
'yii\test\InitDbFixture',
];
/**
* {@inheritdoc}
*/
public function options($actionID)
{
return array_merge(parent::options($actionID), [
'namespace', 'globalFixtures',
]);
}
/**
* {@inheritdoc}
* @since 2.0.8
*/
public function optionAliases()
{
return array_merge(parent::optionAliases(), [
'g' => 'globalFixtures',
'n' => 'namespace',
]);
}
/**
* Loads the specified fixture data.
*
* For example,
*
* ```
* # load the fixture data specified by User and UserProfile.
* # any existing fixture data will be removed first
* yii fixture/load "User, UserProfile"
*
* # load all available fixtures found under 'tests\unit\fixtures'
* yii fixture/load "*"
*
* # load all fixtures except User and UserProfile
* yii fixture/load "*, -User, -UserProfile"
* ```
*
* @param array $fixturesInput
* @return int return code
* @throws Exception if the specified fixture does not exist.
*/
public function actionLoad(array $fixturesInput = [])
{
if ($fixturesInput === []) {
$this->stdout($this->getHelpSummary() . "\n");
$helpCommand = Console::ansiFormat('yii help fixture', [Console::FG_CYAN]);
$this->stdout("Use $helpCommand to get usage info.\n");
return ExitCode::OK;
}
$filtered = $this->filterFixtures($fixturesInput);
$except = $filtered['except'];
if (!$this->needToApplyAll($fixturesInput[0])) {
$fixtures = $filtered['apply'];
$foundFixtures = $this->findFixtures($fixtures);
$notFoundFixtures = array_diff($fixtures, $foundFixtures);
if ($notFoundFixtures) {
$this->notifyNotFound($notFoundFixtures);
}
} else {
$foundFixtures = $this->findFixtures();
}
$fixturesToLoad = array_diff($foundFixtures, $except);
if (!$foundFixtures) {
throw new Exception(
'No files were found for: "' . implode(', ', $fixturesInput) . "\".\n" .
"Check that files exist under fixtures path: \n\"" . $this->getFixturePath() . '".'
);
}
if (!$fixturesToLoad) {
$this->notifyNothingToLoad($foundFixtures, $except);
return ExitCode::OK;
}
if (!$this->confirmLoad($fixturesToLoad, $except)) {
return ExitCode::OK;
}
$fixtures = $this->getFixturesConfig(array_merge($this->globalFixtures, $fixturesToLoad));
if (!$fixtures) {
throw new Exception('No fixtures were found in namespace: "' . $this->namespace . '"' . '');
}
$fixturesObjects = $this->createFixtures($fixtures);
$this->unloadFixtures($fixturesObjects);
$this->loadFixtures($fixturesObjects);
$this->notifyLoaded($fixtures);
return ExitCode::OK;
}
/**
* Unloads the specified fixtures.
*
* For example,
*
* ```
* # unload the fixture data specified by User and UserProfile.
* yii fixture/unload "User, UserProfile"
*
* # unload all fixtures found under 'tests\unit\fixtures'
* yii fixture/unload "*"
*
* # unload all fixtures except User and UserProfile
* yii fixture/unload "*, -User, -UserProfile"
* ```
*
* @param array $fixturesInput
* @return int return code
* @throws Exception if the specified fixture does not exist.
*/
public function actionUnload(array $fixturesInput = [])
{
$filtered = $this->filterFixtures($fixturesInput);
$except = $filtered['except'];
if (!$this->needToApplyAll($fixturesInput[0])) {
$fixtures = $filtered['apply'];
$foundFixtures = $this->findFixtures($fixtures);
$notFoundFixtures = array_diff($fixtures, $foundFixtures);
if ($notFoundFixtures) {
$this->notifyNotFound($notFoundFixtures);
}
} else {
$foundFixtures = $this->findFixtures();
}
$fixturesToUnload = array_diff($foundFixtures, $except);
if (!$foundFixtures) {
throw new Exception(
'No files were found for: "' . implode(', ', $fixturesInput) . "\".\n" .
"Check that files exist under fixtures path: \n\"" . $this->getFixturePath() . '".'
);
}
if (!$fixturesToUnload) {
$this->notifyNothingToUnload($foundFixtures, $except);
return ExitCode::OK;
}
if (!$this->confirmUnload($fixturesToUnload, $except)) {
return ExitCode::OK;
}
$fixtures = $this->getFixturesConfig(array_merge($this->globalFixtures, $fixturesToUnload));
if (!$fixtures) {
throw new Exception('No fixtures were found in namespace: ' . $this->namespace . '".');
}
$this->unloadFixtures($this->createFixtures($fixtures));
$this->notifyUnloaded($fixtures);
}
/**
* Notifies user that fixtures were successfully loaded.
* @param array $fixtures
*/
private function notifyLoaded($fixtures)
{
$this->stdout("Fixtures were successfully loaded from namespace:\n", Console::FG_YELLOW);
$this->stdout("\t\"" . Yii::getAlias($this->namespace) . "\"\n\n", Console::FG_GREEN);
$this->outputList($fixtures);
}
/**
* Notifies user that there are no fixtures to load according input conditions.
* @param array $foundFixtures array of found fixtures
* @param array $except array of names of fixtures that should not be loaded
*/
public function notifyNothingToLoad($foundFixtures, $except)
{
$this->stdout("Fixtures to load could not be found according given conditions:\n\n", Console::FG_RED);
$this->stdout("Fixtures namespace is: \n", Console::FG_YELLOW);
$this->stdout("\t" . $this->namespace . "\n", Console::FG_GREEN);
if (count($foundFixtures)) {
$this->stdout("\nFixtures founded under the namespace:\n\n", Console::FG_YELLOW);
$this->outputList($foundFixtures);
}
if (count($except)) {
$this->stdout("\nFixtures that will NOT be loaded: \n\n", Console::FG_YELLOW);
$this->outputList($except);
}
}
/**
* Notifies user that there are no fixtures to unload according input conditions.
* @param array $foundFixtures array of found fixtures
* @param array $except array of names of fixtures that should not be loaded
*/
public function notifyNothingToUnload($foundFixtures, $except)
{
$this->stdout("Fixtures to unload could not be found according to given conditions:\n\n", Console::FG_RED);
$this->stdout("Fixtures namespace is: \n", Console::FG_YELLOW);
$this->stdout("\t" . $this->namespace . "\n", Console::FG_GREEN);
if (count($foundFixtures)) {
$this->stdout("\nFixtures found under the namespace:\n\n", Console::FG_YELLOW);
$this->outputList($foundFixtures);
}
if (count($except)) {
$this->stdout("\nFixtures that will NOT be unloaded: \n\n", Console::FG_YELLOW);
$this->outputList($except);
}
}
/**
* Notifies user that fixtures were successfully unloaded.
* @param array $fixtures
*/
private function notifyUnloaded($fixtures)
{
$this->stdout("\nFixtures were successfully unloaded from namespace: ", Console::FG_YELLOW);
$this->stdout(Yii::getAlias($this->namespace) . "\"\n\n", Console::FG_GREEN);
$this->outputList($fixtures);
}
/**
* Notifies user that fixtures were not found under fixtures path.
* @param array $fixtures
*/
private function notifyNotFound($fixtures)
{
$this->stdout("Some fixtures were not found under path:\n", Console::BG_RED);
$this->stdout("\t" . $this->getFixturePath() . "\n\n", Console::FG_GREEN);
$this->stdout("Check that they have correct namespace \"{$this->namespace}\" \n", Console::BG_RED);
$this->outputList($fixtures);
$this->stdout("\n");
}
/**
* Prompts user with confirmation if fixtures should be loaded.
* @param array $fixtures
* @param array $except
* @return bool
*/
private function confirmLoad($fixtures, $except)
{
$this->stdout("Fixtures namespace is: \n", Console::FG_YELLOW);
$this->stdout("\t" . $this->namespace . "\n\n", Console::FG_GREEN);
if (count($this->globalFixtures)) {
$this->stdout("Global fixtures will be used:\n\n", Console::FG_YELLOW);
$this->outputList($this->globalFixtures);
}
if (count($fixtures)) {
$this->stdout("\nFixtures below will be loaded:\n\n", Console::FG_YELLOW);
$this->outputList($fixtures);
}
if (count($except)) {
$this->stdout("\nFixtures that will NOT be loaded: \n\n", Console::FG_YELLOW);
$this->outputList($except);
}
$this->stdout("\nBe aware that:\n", Console::BOLD);
$this->stdout("Applying leads to purging of certain data in the database!\n", Console::FG_RED);
return $this->confirm("\nLoad above fixtures?");
}
/**
* Prompts user with confirmation for fixtures that should be unloaded.
* @param array $fixtures
* @param array $except
* @return bool
*/
private function confirmUnload($fixtures, $except)
{
$this->stdout("Fixtures namespace is: \n", Console::FG_YELLOW);
$this->stdout("\t" . $this->namespace . "\n\n", Console::FG_GREEN);
if (count($this->globalFixtures)) {
$this->stdout("Global fixtures will be used:\n\n", Console::FG_YELLOW);
$this->outputList($this->globalFixtures);
}
if (count($fixtures)) {
$this->stdout("\nFixtures below will be unloaded:\n\n", Console::FG_YELLOW);
$this->outputList($fixtures);
}
if (count($except)) {
$this->stdout("\nFixtures that will NOT be unloaded:\n\n", Console::FG_YELLOW);
$this->outputList($except);
}
return $this->confirm("\nUnload fixtures?");
}
/**
* Outputs data to the console as a list.
* @param array $data
*/
private function outputList($data)
{
foreach ($data as $index => $item) {
$this->stdout("\t" . ($index + 1) . ". {$item}\n", Console::FG_GREEN);
}
}
/**
* Checks if needed to apply all fixtures.
* @param string $fixture
* @return bool
*/
public function needToApplyAll($fixture)
{
return $fixture === '*';
}
/**
* Finds fixtures to be loaded, for example "User", if no fixtures were specified then all of them
* will be searching by suffix "Fixture.php".
* @param array $fixtures fixtures to be loaded
* @return array Array of found fixtures. These may differ from input parameter as not all fixtures may exists.
*/
private function findFixtures(array $fixtures = [])
{
$fixturesPath = $this->getFixturePath();
$filesToSearch = ['*Fixture.php'];
$findAll = ($fixtures === []);
if (!$findAll) {
$filesToSearch = [];
foreach ($fixtures as $fileName) {
$filesToSearch[] = $fileName . 'Fixture.php';
}
}
$files = FileHelper::findFiles($fixturesPath, ['only' => $filesToSearch]);
$foundFixtures = [];
foreach ($files as $fixture) {
$foundFixtures[] = $this->getFixtureRelativeName($fixture);
}
return $foundFixtures;
}
/**
* Calculates fixture's name
* Basically, strips [[getFixturePath()]] and `Fixture.php' suffix from fixture's full path.
* @see getFixturePath()
* @param string $fullFixturePath Full fixture path
* @return string Relative fixture name
*/
private function getFixtureRelativeName($fullFixturePath)
{
$fixturesPath = FileHelper::normalizePath($this->getFixturePath());
$fullFixturePath = FileHelper::normalizePath($fullFixturePath);
$relativeName = substr($fullFixturePath, strlen($fixturesPath) + 1);
$relativeDir = dirname($relativeName) === '.' ? '' : dirname($relativeName) . DIRECTORY_SEPARATOR;
return $relativeDir . basename($fullFixturePath, 'Fixture.php');
}
/**
* Returns valid fixtures config that can be used to load them.
* @param array $fixtures fixtures to configure
* @return array
*/
private function getFixturesConfig($fixtures)
{
$config = [];
foreach ($fixtures as $fixture) {
$isNamespaced = (strpos($fixture, '\\') !== false);
// replace linux' path slashes to namespace backslashes, in case if $fixture is non-namespaced relative path
$fixture = str_replace('/', '\\', $fixture);
$fullClassName = $isNamespaced ? $fixture : $this->namespace . '\\' . $fixture;
if (class_exists($fullClassName)) {
$config[] = $fullClassName;
} elseif (class_exists($fullClassName . 'Fixture')) {
$config[] = $fullClassName . 'Fixture';
}
}
return $config;
}
/**
* Filters fixtures by splitting them in two categories: one that should be applied and not.
*
* If fixture is prefixed with "-", for example "-User", that means that fixture should not be loaded,
* if it is not prefixed it is considered as one to be loaded. Returns array:
*
* ```php
* [
* 'apply' => [
* 'User',
* ...
* ],
* 'except' => [
* 'Custom',
* ...
* ],
* ]
* ```
* @param array $fixtures
* @return array fixtures array with 'apply' and 'except' elements.
*/
private function filterFixtures($fixtures)
{
$filtered = [
'apply' => [],
'except' => [],
];
foreach ($fixtures as $fixture) {
if (mb_strpos($fixture, '-') !== false) {
$filtered['except'][] = str_replace('-', '', $fixture);
} else {
$filtered['apply'][] = $fixture;
}
}
return $filtered;
}
/**
* Returns fixture path that determined on fixtures namespace.
* @throws InvalidConfigException if fixture namespace is invalid
* @return string fixture path
*/
private function getFixturePath()
{
try {
return Yii::getAlias('@' . str_replace('\\', '/', $this->namespace));
} catch (InvalidParamException $e) {
throw new InvalidConfigException('Invalid fixture namespace: "' . $this->namespace . '". Please, check your FixtureController::namespace parameter');
}
}
}

View File

@@ -0,0 +1,560 @@
<?php
/**
* @link http://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license http://www.yiiframework.com/license/
*/
namespace yii\console\controllers;
use Yii;
use yii\base\Application;
use yii\console\Controller;
use yii\console\Exception;
use yii\helpers\Console;
use yii\helpers\Inflector;
/**
* Provides help information about console commands.
*
* This command displays the available command list in
* the application or the detailed instructions about using
* a specific command.
*
* This command can be used as follows on command line:
*
* ```
* yii help [command name]
* ```
*
* In the above, if the command name is not provided, all
* available commands will be displayed.
*
* @property array $commands All available command names. This property is read-only.
*
* @author Qiang Xue <qiang.xue@gmail.com>
* @since 2.0
*/
class HelpController extends Controller
{
/**
* Displays available commands or the detailed information
* about a particular command.
*
* @param string $command The name of the command to show help about.
* If not provided, all available commands will be displayed.
* @return int the exit status
* @throws Exception if the command for help is unknown
*/
public function actionIndex($command = null)
{
if ($command !== null) {
$result = Yii::$app->createController($command);
if ($result === false) {
$name = $this->ansiFormat($command, Console::FG_YELLOW);
throw new Exception("No help for unknown command \"$name\".");
}
list($controller, $actionID) = $result;
$actions = $this->getActions($controller);
if ($actionID !== '' || count($actions) === 1 && $actions[0] === $controller->defaultAction) {
$this->getSubCommandHelp($controller, $actionID);
} else {
$this->getCommandHelp($controller);
}
} else {
$this->getDefaultHelp();
}
}
/**
* List all available controllers and actions in machine readable format.
* This is used for shell completion.
* @since 2.0.11
*/
public function actionList()
{
foreach ($this->getCommandDescriptions() as $command => $description) {
$result = Yii::$app->createController($command);
if ($result === false || !($result[0] instanceof Controller)) {
continue;
}
/** @var $controller Controller */
list($controller, $actionID) = $result;
$actions = $this->getActions($controller);
if (!empty($actions)) {
$prefix = $controller->getUniqueId();
$this->stdout("$prefix\n");
foreach ($actions as $action) {
$this->stdout("$prefix/$action\n");
}
}
}
}
/**
* List all available options for the $action in machine readable format.
* This is used for shell completion.
*
* @param string $action route to action
* @since 2.0.11
*/
public function actionListActionOptions($action)
{
$result = Yii::$app->createController($action);
if ($result === false || !($result[0] instanceof Controller)) {
return;
}
/** @var Controller $controller */
list($controller, $actionID) = $result;
$action = $controller->createAction($actionID);
if ($action === null) {
return;
}
foreach ($controller->getActionArgsHelp($action) as $argument => $help) {
$description = str_replace("\n", '', addcslashes($help['comment'], ':')) ?: $argument;
$this->stdout($argument . ':' . $description . "\n");
}
$this->stdout("\n");
foreach ($controller->getActionOptionsHelp($action) as $argument => $help) {
$description = str_replace("\n", '', addcslashes($help['comment'], ':'));
$this->stdout('--' . $argument . ($description ? ':' . $description : '') . "\n");
}
}
/**
* Displays usage information for $action.
*
* @param string $action route to action
* @since 2.0.11
*/
public function actionUsage($action)
{
$result = Yii::$app->createController($action);
if ($result === false || !($result[0] instanceof Controller)) {
return;
}
/** @var Controller $controller */
list($controller, $actionID) = $result;
$action = $controller->createAction($actionID);
if ($action === null) {
return;
}
$scriptName = $this->getScriptName();
if ($action->id === $controller->defaultAction) {
$this->stdout($scriptName . ' ' . $this->ansiFormat($controller->getUniqueId(), Console::FG_YELLOW));
} else {
$this->stdout($scriptName . ' ' . $this->ansiFormat($action->getUniqueId(), Console::FG_YELLOW));
}
foreach ($controller->getActionArgsHelp($action) as $name => $arg) {
if ($arg['required']) {
$this->stdout(' <' . $name . '>', Console::FG_CYAN);
} else {
$this->stdout(' [' . $name . ']', Console::FG_CYAN);
}
}
$this->stdout("\n");
}
/**
* Returns all available command names.
* @return array all available command names
*/
public function getCommands()
{
$commands = $this->getModuleCommands(Yii::$app);
sort($commands);
return array_unique($commands);
}
/**
* Returns an array of commands an their descriptions.
* @return array all available commands as keys and their description as values.
*/
protected function getCommandDescriptions()
{
$descriptions = [];
foreach ($this->getCommands() as $command) {
$description = '';
$result = Yii::$app->createController($command);
if ($result !== false && $result[0] instanceof Controller) {
list($controller, $actionID) = $result;
/** @var Controller $controller */
$description = $controller->getHelpSummary();
}
$descriptions[$command] = $description;
}
return $descriptions;
}
/**
* Returns all available actions of the specified controller.
* @param Controller $controller the controller instance
* @return array all available action IDs.
*/
public function getActions($controller)
{
$actions = array_keys($controller->actions());
$class = new \ReflectionClass($controller);
foreach ($class->getMethods() as $method) {
$name = $method->getName();
if ($name !== 'actions' && $method->isPublic() && !$method->isStatic() && strncmp($name, 'action', 6) === 0) {
$actions[] = Inflector::camel2id(substr($name, 6), '-', true);
}
}
sort($actions);
return array_unique($actions);
}
/**
* Returns available commands of a specified module.
* @param \yii\base\Module $module the module instance
* @return array the available command names
*/
protected function getModuleCommands($module)
{
$prefix = $module instanceof Application ? '' : $module->getUniqueId() . '/';
$commands = [];
foreach (array_keys($module->controllerMap) as $id) {
$commands[] = $prefix . $id;
}
foreach ($module->getModules() as $id => $child) {
if (($child = $module->getModule($id)) === null) {
continue;
}
foreach ($this->getModuleCommands($child) as $command) {
$commands[] = $command;
}
}
$controllerPath = $module->getControllerPath();
if (is_dir($controllerPath)) {
$iterator = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($controllerPath, \RecursiveDirectoryIterator::KEY_AS_PATHNAME));
$iterator = new \RegexIterator($iterator, '/.*Controller\.php$/', \RecursiveRegexIterator::GET_MATCH);
foreach ($iterator as $matches) {
$file = $matches[0];
$relativePath = str_replace($controllerPath, '', $file);
$class = strtr($relativePath, [
DIRECTORY_SEPARATOR => '\\',
'.php' => '',
]);
$controllerClass = $module->controllerNamespace . $class;
if ($this->validateControllerClass($controllerClass)) {
$dir = ltrim(pathinfo($relativePath, PATHINFO_DIRNAME), DIRECTORY_SEPARATOR);
$command = Inflector::camel2id(substr(basename($file), 0, -14), '-', true);
if (!empty($dir)) {
$command = $dir . DIRECTORY_SEPARATOR . $command;
}
$commands[] = $prefix . $command;
}
}
}
return $commands;
}
/**
* Validates if the given class is a valid console controller class.
* @param string $controllerClass
* @return bool
*/
protected function validateControllerClass($controllerClass)
{
if (class_exists($controllerClass)) {
$class = new \ReflectionClass($controllerClass);
return !$class->isAbstract() && $class->isSubclassOf('yii\console\Controller');
}
return false;
}
/**
* Displays all available commands.
*/
protected function getDefaultHelp()
{
$commands = $this->getCommandDescriptions();
$this->stdout($this->getDefaultHelpHeader());
if (!empty($commands)) {
$this->stdout("\nThe following commands are available:\n\n", Console::BOLD);
$len = 0;
foreach ($commands as $command => $description) {
$result = Yii::$app->createController($command);
if ($result !== false && $result[0] instanceof Controller) {
/** @var $controller Controller */
list($controller, $actionID) = $result;
$actions = $this->getActions($controller);
if (!empty($actions)) {
$prefix = $controller->getUniqueId();
foreach ($actions as $action) {
$string = $prefix . '/' . $action;
if ($action === $controller->defaultAction) {
$string .= ' (default)';
}
if (($l = strlen($string)) > $len) {
$len = $l;
}
}
}
} elseif (($l = strlen($command)) > $len) {
$len = $l;
}
}
foreach ($commands as $command => $description) {
$this->stdout('- ' . $this->ansiFormat($command, Console::FG_YELLOW));
$this->stdout(str_repeat(' ', $len + 4 - strlen($command)));
$this->stdout(Console::wrapText($description, $len + 4 + 2), Console::BOLD);
$this->stdout("\n");
$result = Yii::$app->createController($command);
if ($result !== false && $result[0] instanceof Controller) {
list($controller, $actionID) = $result;
$actions = $this->getActions($controller);
if (!empty($actions)) {
$prefix = $controller->getUniqueId();
foreach ($actions as $action) {
$string = ' ' . $prefix . '/' . $action;
$this->stdout(' ' . $this->ansiFormat($string, Console::FG_GREEN));
if ($action === $controller->defaultAction) {
$string .= ' (default)';
$this->stdout(' (default)', Console::FG_YELLOW);
}
$summary = $controller->getActionHelpSummary($controller->createAction($action));
if ($summary !== '') {
$this->stdout(str_repeat(' ', $len + 4 - strlen($string)));
$this->stdout(Console::wrapText($summary, $len + 4 + 2));
}
$this->stdout("\n");
}
}
$this->stdout("\n");
}
}
$scriptName = $this->getScriptName();
$this->stdout("\nTo see the help of each command, enter:\n", Console::BOLD);
$this->stdout("\n $scriptName " . $this->ansiFormat('help', Console::FG_YELLOW) . ' '
. $this->ansiFormat('<command-name>', Console::FG_CYAN) . "\n\n");
} else {
$this->stdout("\nNo commands are found.\n\n", Console::BOLD);
}
}
/**
* Displays the overall information of the command.
* @param Controller $controller the controller instance
*/
protected function getCommandHelp($controller)
{
$controller->color = $this->color;
$this->stdout("\nDESCRIPTION\n", Console::BOLD);
$comment = $controller->getHelp();
if ($comment !== '') {
$this->stdout("\n$comment\n\n");
}
$actions = $this->getActions($controller);
if (!empty($actions)) {
$this->stdout("\nSUB-COMMANDS\n\n", Console::BOLD);
$prefix = $controller->getUniqueId();
$maxlen = 5;
foreach ($actions as $action) {
$len = strlen($prefix . '/' . $action) + 2 + ($action === $controller->defaultAction ? 10 : 0);
if ($maxlen < $len) {
$maxlen = $len;
}
}
foreach ($actions as $action) {
$this->stdout('- ' . $this->ansiFormat($prefix . '/' . $action, Console::FG_YELLOW));
$len = strlen($prefix . '/' . $action) + 2;
if ($action === $controller->defaultAction) {
$this->stdout(' (default)', Console::FG_GREEN);
$len += 10;
}
$summary = $controller->getActionHelpSummary($controller->createAction($action));
if ($summary !== '') {
$this->stdout(str_repeat(' ', $maxlen - $len + 2) . Console::wrapText($summary, $maxlen + 2));
}
$this->stdout("\n");
}
$scriptName = $this->getScriptName();
$this->stdout("\nTo see the detailed information about individual sub-commands, enter:\n");
$this->stdout("\n $scriptName " . $this->ansiFormat('help', Console::FG_YELLOW) . ' '
. $this->ansiFormat('<sub-command>', Console::FG_CYAN) . "\n\n");
}
}
/**
* Displays the detailed information of a command action.
* @param Controller $controller the controller instance
* @param string $actionID action ID
* @throws Exception if the action does not exist
*/
protected function getSubCommandHelp($controller, $actionID)
{
$action = $controller->createAction($actionID);
if ($action === null) {
$name = $this->ansiFormat(rtrim($controller->getUniqueId() . '/' . $actionID, '/'), Console::FG_YELLOW);
throw new Exception("No help for unknown sub-command \"$name\".");
}
$description = $controller->getActionHelp($action);
if ($description !== '') {
$this->stdout("\nDESCRIPTION\n", Console::BOLD);
$this->stdout("\n$description\n\n");
}
$this->stdout("\nUSAGE\n\n", Console::BOLD);
$scriptName = $this->getScriptName();
if ($action->id === $controller->defaultAction) {
$this->stdout($scriptName . ' ' . $this->ansiFormat($controller->getUniqueId(), Console::FG_YELLOW));
} else {
$this->stdout($scriptName . ' ' . $this->ansiFormat($action->getUniqueId(), Console::FG_YELLOW));
}
$args = $controller->getActionArgsHelp($action);
foreach ($args as $name => $arg) {
if ($arg['required']) {
$this->stdout(' <' . $name . '>', Console::FG_CYAN);
} else {
$this->stdout(' [' . $name . ']', Console::FG_CYAN);
}
}
$options = $controller->getActionOptionsHelp($action);
$options[\yii\console\Application::OPTION_APPCONFIG] = [
'type' => 'string',
'default' => null,
'comment' => "custom application configuration file path.\nIf not set, default application configuration is used.",
];
ksort($options);
if (!empty($options)) {
$this->stdout(' [...options...]', Console::FG_RED);
}
$this->stdout("\n\n");
if (!empty($args)) {
foreach ($args as $name => $arg) {
$this->stdout($this->formatOptionHelp(
'- ' . $this->ansiFormat($name, Console::FG_CYAN),
$arg['required'],
$arg['type'],
$arg['default'],
$arg['comment']) . "\n\n");
}
}
if (!empty($options)) {
$this->stdout("\nOPTIONS\n\n", Console::BOLD);
foreach ($options as $name => $option) {
$this->stdout($this->formatOptionHelp(
$this->ansiFormat('--' . $name . $this->formatOptionAliases($controller, $name),
Console::FG_RED, empty($option['required']) ? Console::FG_RED : Console::BOLD),
!empty($option['required']),
$option['type'],
$option['default'],
$option['comment']) . "\n\n");
}
}
}
/**
* Generates a well-formed string for an argument or option.
* @param string $name the name of the argument or option
* @param bool $required whether the argument is required
* @param string $type the type of the option or argument
* @param mixed $defaultValue the default value of the option or argument
* @param string $comment comment about the option or argument
* @return string the formatted string for the argument or option
*/
protected function formatOptionHelp($name, $required, $type, $defaultValue, $comment)
{
$comment = trim($comment);
$type = trim($type);
if (strncmp($type, 'bool', 4) === 0) {
$type = 'boolean, 0 or 1';
}
if ($defaultValue !== null && !is_array($defaultValue)) {
if ($type === null) {
$type = gettype($defaultValue);
}
if (is_bool($defaultValue)) {
// show as integer to avoid confusion
$defaultValue = (int)$defaultValue;
}
if (is_string($defaultValue)) {
$defaultValue = "'" . $defaultValue . "'";
} else {
$defaultValue = var_export($defaultValue, true);
}
$doc = "$type (defaults to $defaultValue)";
} else {
$doc = $type;
}
if ($doc === '') {
$doc = $comment;
} elseif ($comment !== '') {
$doc .= "\n" . preg_replace('/^/m', ' ', $comment);
}
$name = $required ? "$name (required)" : $name;
return $doc === '' ? $name : "$name: $doc";
}
/**
* @param Controller $controller the controller instance
* @param string $option the option name
* @return string the formatted string for the alias argument or option
* @since 2.0.8
*/
protected function formatOptionAliases($controller, $option)
{
foreach ($controller->optionAliases() as $name => $value) {
if ($value === $option) {
return ', -' . $name;
}
}
return '';
}
/**
* @return string the name of the cli script currently running.
*/
protected function getScriptName()
{
return basename(Yii::$app->request->scriptFile);
}
/**
* Return a default help header.
* @return string default help header.
* @since 2.0.11
*/
protected function getDefaultHelpHeader()
{
return "\nThis is Yii version " . \Yii::getVersion() . ".\n";
}
}

View File

@@ -0,0 +1,946 @@
<?php
/**
* @link http://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license http://www.yiiframework.com/license/
*/
namespace yii\console\controllers;
use Yii;
use yii\console\Exception;
use yii\console\ExitCode;
use yii\db\Connection;
use yii\db\Query;
use yii\di\Instance;
use yii\helpers\Console;
use yii\helpers\FileHelper;
use yii\helpers\VarDumper;
use yii\i18n\GettextPoFile;
/**
* Extracts messages to be translated from source files.
*
* The extracted messages can be saved the following depending on `format`
* setting in config file:
*
* - PHP message source files.
* - ".po" files.
* - Database.
*
* Usage:
* 1. Create a configuration file using the 'message/config' command:
* yii message/config /path/to/myapp/messages/config.php
* 2. Edit the created config file, adjusting it for your web application needs.
* 3. Run the 'message/extract' command, using created config:
* yii message /path/to/myapp/messages/config.php
*
* @author Qiang Xue <qiang.xue@gmail.com>
* @since 2.0
*/
class MessageController extends \yii\console\Controller
{
/**
* @var string controller default action ID.
*/
public $defaultAction = 'extract';
/**
* @var string required, root directory of all source files.
*/
public $sourcePath = '@yii';
/**
* @var string required, root directory containing message translations.
*/
public $messagePath = '@yii/messages';
/**
* @var array required, list of language codes that the extracted messages
* should be translated to. For example, ['zh-CN', 'de'].
*/
public $languages = [];
/**
* @var string the name of the function for translating messages.
* Defaults to 'Yii::t'. This is used as a mark to find the messages to be
* translated. You may use a string for single function name or an array for
* multiple function names.
*/
public $translator = 'Yii::t';
/**
* @var bool whether to sort messages by keys when merging new messages
* with the existing ones. Defaults to false, which means the new (untranslated)
* messages will be separated from the old (translated) ones.
*/
public $sort = false;
/**
* @var bool whether the message file should be overwritten with the merged messages
*/
public $overwrite = true;
/**
* @var bool whether to remove messages that no longer appear in the source code.
* Defaults to false, which means these messages will NOT be removed.
*/
public $removeUnused = false;
/**
* @var bool whether to mark messages that no longer appear in the source code.
* Defaults to true, which means each of these messages will be enclosed with a pair of '@@' marks.
*/
public $markUnused = true;
/**
* @var array list of patterns that specify which files/directories should NOT be processed.
* If empty or not set, all files/directories will be processed.
* See helpers/FileHelper::findFiles() description for pattern matching rules.
* If a file/directory matches both a pattern in "only" and "except", it will NOT be processed.
*/
public $except = [
'.svn',
'.git',
'.gitignore',
'.gitkeep',
'.hgignore',
'.hgkeep',
'/messages',
'/BaseYii.php', // contains examples about Yii:t()
];
/**
* @var array list of patterns that specify which files (not directories) should be processed.
* If empty or not set, all files will be processed.
* See helpers/FileHelper::findFiles() description for pattern matching rules.
* If a file/directory matches both a pattern in "only" and "except", it will NOT be processed.
*/
public $only = ['*.php'];
/**
* @var string generated file format. Can be "php", "db", "po" or "pot".
*/
public $format = 'php';
/**
* @var string connection component ID for "db" format.
*/
public $db = 'db';
/**
* @var string custom name for source message table for "db" format.
*/
public $sourceMessageTable = '{{%source_message}}';
/**
* @var string custom name for translation message table for "db" format.
*/
public $messageTable = '{{%message}}';
/**
* @var string name of the file that will be used for translations for "po" format.
*/
public $catalog = 'messages';
/**
* @var array message categories to ignore. For example, 'yii', 'app*', 'widgets/menu', etc.
* @see isCategoryIgnored
*/
public $ignoreCategories = [];
/**
* @var string File header in generated PHP file with messages. This property is used only if [[$format]] is "php".
* @since 2.0.13
*/
public $phpFileHeader = '';
/**
* @var string|null DocBlock used for messages array in generated PHP file. If `null`, default DocBlock will be used.
* This property is used only if [[$format]] is "php".
* @since 2.0.13
*/
public $phpDocBlock;
/**
* @var array Config for messages extraction.
* @see actionExtract()
* @see initConfig()
* @since 2.0.13
*/
protected $config;
/**
* {@inheritdoc}
*/
public function options($actionID)
{
return array_merge(parent::options($actionID), [
'sourcePath',
'messagePath',
'languages',
'translator',
'sort',
'overwrite',
'removeUnused',
'markUnused',
'except',
'only',
'format',
'db',
'sourceMessageTable',
'messageTable',
'catalog',
'ignoreCategories',
'phpFileHeader',
'phpDocBlock',
]);
}
/**
* {@inheritdoc}
* @since 2.0.8
*/
public function optionAliases()
{
return array_merge(parent::optionAliases(), [
'c' => 'catalog',
'e' => 'except',
'f' => 'format',
'i' => 'ignoreCategories',
'l' => 'languages',
'u' => 'markUnused',
'p' => 'messagePath',
'o' => 'only',
'w' => 'overwrite',
'S' => 'sort',
't' => 'translator',
'm' => 'sourceMessageTable',
's' => 'sourcePath',
'r' => 'removeUnused',
]);
}
/**
* Creates a configuration file for the "extract" command using command line options specified.
*
* The generated configuration file contains parameters required
* for source code messages extraction.
* You may use this configuration file with the "extract" command.
*
* @param string $filePath output file name or alias.
* @return int CLI exit code
* @throws Exception on failure.
*/
public function actionConfig($filePath)
{
$filePath = Yii::getAlias($filePath);
if (file_exists($filePath)) {
if (!$this->confirm("File '{$filePath}' already exists. Do you wish to overwrite it?")) {
return ExitCode::OK;
}
}
$array = VarDumper::export($this->getOptionValues($this->action->id));
$content = <<<EOD
<?php
/**
* Configuration file for 'yii {$this->id}/{$this->defaultAction}' command.
*
* This file is automatically generated by 'yii {$this->id}/{$this->action->id}' command.
* It contains parameters for source code messages extraction.
* You may modify this file to suit your needs.
*
* You can use 'yii {$this->id}/{$this->action->id}-template' command to create
* template configuration file with detailed description for each parameter.
*/
return $array;
EOD;
if (file_put_contents($filePath, $content, LOCK_EX) === false) {
$this->stdout("Configuration file was NOT created: '{$filePath}'.\n\n", Console::FG_RED);
return ExitCode::UNSPECIFIED_ERROR;
}
$this->stdout("Configuration file created: '{$filePath}'.\n\n", Console::FG_GREEN);
return ExitCode::OK;
}
/**
* Creates a configuration file template for the "extract" command.
*
* The created configuration file contains detailed instructions on
* how to customize it to fit for your needs. After customization,
* you may use this configuration file with the "extract" command.
*
* @param string $filePath output file name or alias.
* @return int CLI exit code
* @throws Exception on failure.
*/
public function actionConfigTemplate($filePath)
{
$filePath = Yii::getAlias($filePath);
if (file_exists($filePath)) {
if (!$this->confirm("File '{$filePath}' already exists. Do you wish to overwrite it?")) {
return ExitCode::OK;
}
}
if (!copy(Yii::getAlias('@yii/views/messageConfig.php'), $filePath)) {
$this->stdout("Configuration file template was NOT created at '{$filePath}'.\n\n", Console::FG_RED);
return ExitCode::UNSPECIFIED_ERROR;
}
$this->stdout("Configuration file template created at '{$filePath}'.\n\n", Console::FG_GREEN);
return ExitCode::OK;
}
/**
* Extracts messages to be translated from source code.
*
* This command will search through source code files and extract
* messages that need to be translated in different languages.
*
* @param string $configFile the path or alias of the configuration file.
* You may use the "yii message/config" command to generate
* this file and then customize it for your needs.
* @throws Exception on failure.
*/
public function actionExtract($configFile = null)
{
$this->initConfig($configFile);
$files = FileHelper::findFiles(realpath($this->config['sourcePath']), $this->config);
$messages = [];
foreach ($files as $file) {
$messages = array_merge_recursive($messages, $this->extractMessages($file, $this->config['translator'], $this->config['ignoreCategories']));
}
$catalog = isset($this->config['catalog']) ? $this->config['catalog'] : 'messages';
if (in_array($this->config['format'], ['php', 'po'])) {
foreach ($this->config['languages'] as $language) {
$dir = $this->config['messagePath'] . DIRECTORY_SEPARATOR . $language;
if (!is_dir($dir) && !@mkdir($dir)) {
throw new Exception("Directory '{$dir}' can not be created.");
}
if ($this->config['format'] === 'po') {
$this->saveMessagesToPO($messages, $dir, $this->config['overwrite'], $this->config['removeUnused'], $this->config['sort'], $catalog, $this->config['markUnused']);
} else {
$this->saveMessagesToPHP($messages, $dir, $this->config['overwrite'], $this->config['removeUnused'], $this->config['sort'], $this->config['markUnused']);
}
}
} elseif ($this->config['format'] === 'db') {
/** @var Connection $db */
$db = Instance::ensure($this->config['db'], Connection::className());
$sourceMessageTable = isset($this->config['sourceMessageTable']) ? $this->config['sourceMessageTable'] : '{{%source_message}}';
$messageTable = isset($this->config['messageTable']) ? $this->config['messageTable'] : '{{%message}}';
$this->saveMessagesToDb(
$messages,
$db,
$sourceMessageTable,
$messageTable,
$this->config['removeUnused'],
$this->config['languages'],
$this->config['markUnused']
);
} elseif ($this->config['format'] === 'pot') {
$this->saveMessagesToPOT($messages, $this->config['messagePath'], $catalog);
}
}
/**
* Saves messages to database.
*
* @param array $messages
* @param Connection $db
* @param string $sourceMessageTable
* @param string $messageTable
* @param bool $removeUnused
* @param array $languages
* @param bool $markUnused
*/
protected function saveMessagesToDb($messages, $db, $sourceMessageTable, $messageTable, $removeUnused, $languages, $markUnused)
{
$currentMessages = [];
$rows = (new Query())->select(['id', 'category', 'message'])->from($sourceMessageTable)->all($db);
foreach ($rows as $row) {
$currentMessages[$row['category']][$row['id']] = $row['message'];
}
$currentLanguages = [];
$rows = (new Query())->select(['language'])->from($messageTable)->groupBy('language')->all($db);
foreach ($rows as $row) {
$currentLanguages[] = $row['language'];
}
$missingLanguages = [];
if (!empty($currentLanguages)) {
$missingLanguages = array_diff($languages, $currentLanguages);
}
$new = [];
$obsolete = [];
foreach ($messages as $category => $msgs) {
$msgs = array_unique($msgs);
if (isset($currentMessages[$category])) {
$new[$category] = array_diff($msgs, $currentMessages[$category]);
$obsolete += array_diff($currentMessages[$category], $msgs);
} else {
$new[$category] = $msgs;
}
}
foreach (array_diff(array_keys($currentMessages), array_keys($messages)) as $category) {
$obsolete += $currentMessages[$category];
}
if (!$removeUnused) {
foreach ($obsolete as $pk => $msg) {
if (mb_substr($msg, 0, 2) === '@@' && mb_substr($msg, -2) === '@@') {
unset($obsolete[$pk]);
}
}
}
$obsolete = array_keys($obsolete);
$this->stdout('Inserting new messages...');
$savedFlag = false;
foreach ($new as $category => $msgs) {
foreach ($msgs as $msg) {
$savedFlag = true;
$lastPk = $db->schema->insert($sourceMessageTable, ['category' => $category, 'message' => $msg]);
foreach ($languages as $language) {
$db->createCommand()
->insert($messageTable, ['id' => $lastPk['id'], 'language' => $language])
->execute();
}
}
}
if (!empty($missingLanguages)) {
$updatedMessages = [];
$rows = (new Query())->select(['id', 'category', 'message'])->from($sourceMessageTable)->all($db);
foreach ($rows as $row) {
$updatedMessages[$row['category']][$row['id']] = $row['message'];
}
foreach ($updatedMessages as $category => $msgs) {
foreach ($msgs as $id => $msg) {
$savedFlag = true;
foreach ($missingLanguages as $language) {
$db->createCommand()
->insert($messageTable, ['id' => $id, 'language' => $language])
->execute();
}
}
}
}
$this->stdout($savedFlag ? "saved.\n" : "Nothing to save.\n");
$this->stdout($removeUnused ? 'Deleting obsoleted messages...' : 'Updating obsoleted messages...');
if (empty($obsolete)) {
$this->stdout("Nothing obsoleted...skipped.\n");
return;
}
if ($removeUnused) {
$db->createCommand()
->delete($sourceMessageTable, ['in', 'id', $obsolete])
->execute();
$this->stdout("deleted.\n");
} elseif ($markUnused) {
$rows = (new Query())
->select(['id', 'message'])
->from($sourceMessageTable)
->where(['in', 'id', $obsolete])
->all($db);
foreach ($rows as $row) {
$db->createCommand()->update(
$sourceMessageTable,
['message' => '@@' . $row['message'] . '@@'],
['id' => $row['id']]
)->execute();
}
$this->stdout("updated.\n");
} else {
$this->stdout("kept untouched.\n");
}
}
/**
* Extracts messages from a file.
*
* @param string $fileName name of the file to extract messages from
* @param string $translator name of the function used to translate messages
* @param array $ignoreCategories message categories to ignore.
* This parameter is available since version 2.0.4.
* @return array
*/
protected function extractMessages($fileName, $translator, $ignoreCategories = [])
{
$this->stdout('Extracting messages from ');
$this->stdout($fileName, Console::FG_CYAN);
$this->stdout("...\n");
$subject = file_get_contents($fileName);
$messages = [];
$tokens = token_get_all($subject);
foreach ((array) $translator as $currentTranslator) {
$translatorTokens = token_get_all('<?php ' . $currentTranslator);
array_shift($translatorTokens);
$messages = array_merge_recursive($messages, $this->extractMessagesFromTokens($tokens, $translatorTokens, $ignoreCategories));
}
$this->stdout("\n");
return $messages;
}
/**
* Extracts messages from a parsed PHP tokens list.
* @param array $tokens tokens to be processed.
* @param array $translatorTokens translator tokens.
* @param array $ignoreCategories message categories to ignore.
* @return array messages.
*/
protected function extractMessagesFromTokens(array $tokens, array $translatorTokens, array $ignoreCategories)
{
$messages = [];
$translatorTokensCount = count($translatorTokens);
$matchedTokensCount = 0;
$buffer = [];
$pendingParenthesisCount = 0;
foreach ($tokens as $token) {
// finding out translator call
if ($matchedTokensCount < $translatorTokensCount) {
if ($this->tokensEqual($token, $translatorTokens[$matchedTokensCount])) {
$matchedTokensCount++;
} else {
$matchedTokensCount = 0;
}
} elseif ($matchedTokensCount === $translatorTokensCount) {
// translator found
// end of function call
if ($this->tokensEqual(')', $token)) {
$pendingParenthesisCount--;
if ($pendingParenthesisCount === 0) {
// end of translator call or end of something that we can't extract
if (isset($buffer[0][0], $buffer[1], $buffer[2][0]) && $buffer[0][0] === T_CONSTANT_ENCAPSED_STRING && $buffer[1] === ',' && $buffer[2][0] === T_CONSTANT_ENCAPSED_STRING) {
// is valid call we can extract
$category = stripcslashes($buffer[0][1]);
$category = mb_substr($category, 1, -1);
if (!$this->isCategoryIgnored($category, $ignoreCategories)) {
$fullMessage = mb_substr($buffer[2][1], 1, -1);
$i = 3;
while ($i < count($buffer) - 1 && !is_array($buffer[$i]) && $buffer[$i] === '.') {
$fullMessage .= mb_substr($buffer[$i + 1][1], 1, -1);
$i += 2;
}
$message = stripcslashes($fullMessage);
$messages[$category][] = $message;
}
$nestedTokens = array_slice($buffer, 3);
if (count($nestedTokens) > $translatorTokensCount) {
// search for possible nested translator calls
$messages = array_merge_recursive($messages, $this->extractMessagesFromTokens($nestedTokens, $translatorTokens, $ignoreCategories));
}
} else {
// invalid call or dynamic call we can't extract
$line = Console::ansiFormat($this->getLine($buffer), [Console::FG_CYAN]);
$skipping = Console::ansiFormat('Skipping line', [Console::FG_YELLOW]);
$this->stdout("$skipping $line. Make sure both category and message are static strings.\n");
}
// prepare for the next match
$matchedTokensCount = 0;
$pendingParenthesisCount = 0;
$buffer = [];
} else {
$buffer[] = $token;
}
} elseif ($this->tokensEqual('(', $token)) {
// count beginning of function call, skipping translator beginning
if ($pendingParenthesisCount > 0) {
$buffer[] = $token;
}
$pendingParenthesisCount++;
} elseif (isset($token[0]) && !in_array($token[0], [T_WHITESPACE, T_COMMENT])) {
// ignore comments and whitespaces
$buffer[] = $token;
}
}
}
return $messages;
}
/**
* The method checks, whether the $category is ignored according to $ignoreCategories array.
*
* Examples:
*
* - `myapp` - will be ignored only `myapp` category;
* - `myapp*` - will be ignored by all categories beginning with `myapp` (`myapp`, `myapplication`, `myapprove`, `myapp/widgets`, `myapp.widgets`, etc).
*
* @param string $category category that is checked
* @param array $ignoreCategories message categories to ignore.
* @return bool
* @since 2.0.7
*/
protected function isCategoryIgnored($category, array $ignoreCategories)
{
if (!empty($ignoreCategories)) {
if (in_array($category, $ignoreCategories, true)) {
return true;
}
foreach ($ignoreCategories as $pattern) {
if (strpos($pattern, '*') > 0 && strpos($category, rtrim($pattern, '*')) === 0) {
return true;
}
}
}
return false;
}
/**
* Finds out if two PHP tokens are equal.
*
* @param array|string $a
* @param array|string $b
* @return bool
* @since 2.0.1
*/
protected function tokensEqual($a, $b)
{
if (is_string($a) && is_string($b)) {
return $a === $b;
}
if (isset($a[0], $a[1], $b[0], $b[1])) {
return $a[0] === $b[0] && $a[1] == $b[1];
}
return false;
}
/**
* Finds out a line of the first non-char PHP token found.
*
* @param array $tokens
* @return int|string
* @since 2.0.1
*/
protected function getLine($tokens)
{
foreach ($tokens as $token) {
if (isset($token[2])) {
return $token[2];
}
}
return 'unknown';
}
/**
* Writes messages into PHP files.
*
* @param array $messages
* @param string $dirName name of the directory to write to
* @param bool $overwrite if existing file should be overwritten without backup
* @param bool $removeUnused if obsolete translations should be removed
* @param bool $sort if translations should be sorted
* @param bool $markUnused if obsolete translations should be marked
*/
protected function saveMessagesToPHP($messages, $dirName, $overwrite, $removeUnused, $sort, $markUnused)
{
foreach ($messages as $category => $msgs) {
$file = str_replace('\\', '/', "$dirName/$category.php");
$path = dirname($file);
FileHelper::createDirectory($path);
$msgs = array_values(array_unique($msgs));
$coloredFileName = Console::ansiFormat($file, [Console::FG_CYAN]);
$this->stdout("Saving messages to $coloredFileName...\n");
$this->saveMessagesCategoryToPHP($msgs, $file, $overwrite, $removeUnused, $sort, $category, $markUnused);
}
}
/**
* Writes category messages into PHP file.
*
* @param array $messages
* @param string $fileName name of the file to write to
* @param bool $overwrite if existing file should be overwritten without backup
* @param bool $removeUnused if obsolete translations should be removed
* @param bool $sort if translations should be sorted
* @param string $category message category
* @param bool $markUnused if obsolete translations should be marked
* @return int exit code
*/
protected function saveMessagesCategoryToPHP($messages, $fileName, $overwrite, $removeUnused, $sort, $category, $markUnused)
{
if (is_file($fileName)) {
$rawExistingMessages = require $fileName;
$existingMessages = $rawExistingMessages;
sort($messages);
ksort($existingMessages);
if (array_keys($existingMessages) === $messages && (!$sort || array_keys($rawExistingMessages) === $messages)) {
$this->stdout("Nothing new in \"$category\" category... Nothing to save.\n\n", Console::FG_GREEN);
return ExitCode::OK;
}
unset($rawExistingMessages);
$merged = [];
$untranslated = [];
foreach ($messages as $message) {
if (array_key_exists($message, $existingMessages) && $existingMessages[$message] !== '') {
$merged[$message] = $existingMessages[$message];
} else {
$untranslated[] = $message;
}
}
ksort($merged);
sort($untranslated);
$todo = [];
foreach ($untranslated as $message) {
$todo[$message] = '';
}
ksort($existingMessages);
foreach ($existingMessages as $message => $translation) {
if (!$removeUnused && !isset($merged[$message]) && !isset($todo[$message])) {
if (!$markUnused || (!empty($translation) && (strncmp($translation, '@@', 2) === 0 && substr_compare($translation, '@@', -2, 2) === 0))) {
$todo[$message] = $translation;
} else {
$todo[$message] = '@@' . $translation . '@@';
}
}
}
$merged = array_merge($todo, $merged);
if ($sort) {
ksort($merged);
}
if (false === $overwrite) {
$fileName .= '.merged';
}
$this->stdout("Translation merged.\n");
} else {
$merged = [];
foreach ($messages as $message) {
$merged[$message] = '';
}
ksort($merged);
}
$array = VarDumper::export($merged);
$content = <<<EOD
<?php
{$this->config['phpFileHeader']}{$this->config['phpDocBlock']}
return $array;
EOD;
if (file_put_contents($fileName, $content, LOCK_EX) === false) {
$this->stdout("Translation was NOT saved.\n\n", Console::FG_RED);
return ExitCode::UNSPECIFIED_ERROR;
}
$this->stdout("Translation saved.\n\n", Console::FG_GREEN);
return ExitCode::OK;
}
/**
* Writes messages into PO file.
*
* @param array $messages
* @param string $dirName name of the directory to write to
* @param bool $overwrite if existing file should be overwritten without backup
* @param bool $removeUnused if obsolete translations should be removed
* @param bool $sort if translations should be sorted
* @param string $catalog message catalog
* @param bool $markUnused if obsolete translations should be marked
*/
protected function saveMessagesToPO($messages, $dirName, $overwrite, $removeUnused, $sort, $catalog, $markUnused)
{
$file = str_replace('\\', '/', "$dirName/$catalog.po");
FileHelper::createDirectory(dirname($file));
$this->stdout("Saving messages to $file...\n");
$poFile = new GettextPoFile();
$merged = [];
$todos = [];
$hasSomethingToWrite = false;
foreach ($messages as $category => $msgs) {
$notTranslatedYet = [];
$msgs = array_values(array_unique($msgs));
if (is_file($file)) {
$existingMessages = $poFile->load($file, $category);
sort($msgs);
ksort($existingMessages);
if (array_keys($existingMessages) == $msgs) {
$this->stdout("Nothing new in \"$category\" category...\n");
sort($msgs);
foreach ($msgs as $message) {
$merged[$category . chr(4) . $message] = $existingMessages[$message];
}
ksort($merged);
continue;
}
// merge existing message translations with new message translations
foreach ($msgs as $message) {
if (array_key_exists($message, $existingMessages) && $existingMessages[$message] !== '') {
$merged[$category . chr(4) . $message] = $existingMessages[$message];
} else {
$notTranslatedYet[] = $message;
}
}
ksort($merged);
sort($notTranslatedYet);
// collect not yet translated messages
foreach ($notTranslatedYet as $message) {
$todos[$category . chr(4) . $message] = '';
}
// add obsolete unused messages
foreach ($existingMessages as $message => $translation) {
if (!$removeUnused && !isset($merged[$category . chr(4) . $message]) && !isset($todos[$category . chr(4) . $message])) {
if (!$markUnused || (!empty($translation) && (substr($translation, 0, 2) === '@@' && substr($translation, -2) === '@@'))) {
$todos[$category . chr(4) . $message] = $translation;
} else {
$todos[$category . chr(4) . $message] = '@@' . $translation . '@@';
}
}
}
$merged = array_merge($todos, $merged);
if ($sort) {
ksort($merged);
}
if ($overwrite === false) {
$file .= '.merged';
}
} else {
sort($msgs);
foreach ($msgs as $message) {
$merged[$category . chr(4) . $message] = '';
}
ksort($merged);
}
$this->stdout("Category \"$category\" merged.\n");
$hasSomethingToWrite = true;
}
if ($hasSomethingToWrite) {
$poFile->save($file, $merged);
$this->stdout("Translation saved.\n", Console::FG_GREEN);
} else {
$this->stdout("Nothing to save.\n", Console::FG_GREEN);
}
}
/**
* Writes messages into POT file.
*
* @param array $messages
* @param string $dirName name of the directory to write to
* @param string $catalog message catalog
* @since 2.0.6
*/
protected function saveMessagesToPOT($messages, $dirName, $catalog)
{
$file = str_replace('\\', '/', "$dirName/$catalog.pot");
FileHelper::createDirectory(dirname($file));
$this->stdout("Saving messages to $file...\n");
$poFile = new GettextPoFile();
$merged = [];
$hasSomethingToWrite = false;
foreach ($messages as $category => $msgs) {
$msgs = array_values(array_unique($msgs));
sort($msgs);
foreach ($msgs as $message) {
$merged[$category . chr(4) . $message] = '';
}
$this->stdout("Category \"$category\" merged.\n");
$hasSomethingToWrite = true;
}
if ($hasSomethingToWrite) {
ksort($merged);
$poFile->save($file, $merged);
$this->stdout("Translation saved.\n", Console::FG_GREEN);
} else {
$this->stdout("Nothing to save.\n", Console::FG_GREEN);
}
}
/**
* @param string $configFile
* @throws Exception If configuration file does not exists.
* @since 2.0.13
*/
protected function initConfig($configFile)
{
$configFileContent = [];
if ($configFile !== null) {
$configFile = Yii::getAlias($configFile);
if (!is_file($configFile)) {
throw new Exception("The configuration file does not exist: $configFile");
}
$configFileContent = require $configFile;
}
$this->config = array_merge(
$this->getOptionValues($this->action->id),
$configFileContent,
$this->getPassedOptionValues()
);
$this->config['sourcePath'] = Yii::getAlias($this->config['sourcePath']);
$this->config['messagePath'] = Yii::getAlias($this->config['messagePath']);
if (!isset($this->config['sourcePath'], $this->config['languages'])) {
throw new Exception('The configuration file must specify "sourcePath" and "languages".');
}
if (!is_dir($this->config['sourcePath'])) {
throw new Exception("The source path {$this->config['sourcePath']} is not a valid directory.");
}
if (empty($this->config['format']) || !in_array($this->config['format'], ['php', 'po', 'pot', 'db'])) {
throw new Exception('Format should be either "php", "po", "pot" or "db".');
}
if (in_array($this->config['format'], ['php', 'po', 'pot'])) {
if (!isset($this->config['messagePath'])) {
throw new Exception('The configuration file must specify "messagePath".');
}
if (!is_dir($this->config['messagePath'])) {
throw new Exception("The message path {$this->config['messagePath']} is not a valid directory.");
}
}
if (empty($this->config['languages'])) {
throw new Exception('Languages cannot be empty.');
}
if ($this->config['format'] === 'php' && $this->config['phpDocBlock'] === null) {
$this->config['phpDocBlock'] = <<<DOCBLOCK
/**
* Message translations.
*
* This file is automatically generated by 'yii {$this->id}/{$this->action->id}' command.
* It contains the localizable messages extracted from source code.
* You may modify this file by translating the extracted messages.
*
* Each array element represents the translation (value) of a message (key).
* If the value is empty, the message is considered as not translated.
* Messages that no longer need translation will have their translations
* enclosed between a pair of '@@' marks.
*
* Message string can be used with plural forms format. Check i18n section
* of the guide for details.
*
* NOTE: this file must be saved in UTF-8 encoding.
*/
DOCBLOCK;
}
}
}

View File

@@ -0,0 +1,531 @@
<?php
/**
* @link http://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license http://www.yiiframework.com/license/
*/
namespace yii\console\controllers;
use Yii;
use yii\db\Connection;
use yii\db\Query;
use yii\di\Instance;
use yii\helpers\ArrayHelper;
use yii\helpers\Console;
/**
* Manages application migrations.
*
* A migration means a set of persistent changes to the application environment
* that is shared among different developers. For example, in an application
* backed by a database, a migration may refer to a set of changes to
* the database, such as creating a new table, adding a new table column.
*
* This command provides support for tracking the migration history, upgrading
* or downloading with migrations, and creating new migration skeletons.
*
* The migration history is stored in a database table named
* as [[migrationTable]]. The table will be automatically created the first time
* this command is executed, if it does not exist. You may also manually
* create it as follows:
*
* ```sql
* CREATE TABLE migration (
* version varchar(180) PRIMARY KEY,
* apply_time integer
* )
* ```
*
* Below are some common usages of this command:
*
* ```
* # creates a new migration named 'create_user_table'
* yii migrate/create create_user_table
*
* # applies ALL new migrations
* yii migrate
*
* # reverts the last applied migration
* yii migrate/down
* ```
*
* Since 2.0.10 you can use namespaced migrations. In order to enable this feature you should configure [[migrationNamespaces]]
* property for the controller at application configuration:
*
* ```php
* return [
* 'controllerMap' => [
* 'migrate' => [
* 'class' => 'yii\console\controllers\MigrateController',
* 'migrationNamespaces' => [
* 'app\migrations',
* 'some\extension\migrations',
* ],
* //'migrationPath' => null, // allows to disable not namespaced migration completely
* ],
* ],
* ];
* ```
*
* @author Qiang Xue <qiang.xue@gmail.com>
* @since 2.0
*/
class MigrateController extends BaseMigrateController
{
/**
* Maximum length of a migration name.
* @since 2.0.13
*/
const MAX_NAME_LENGTH = 180;
/**
* @var string the name of the table for keeping applied migration information.
*/
public $migrationTable = '{{%migration}}';
/**
* {@inheritdoc}
*/
public $templateFile = '@yii/views/migration.php';
/**
* @var array a set of template paths for generating migration code automatically.
*
* The key is the template type, the value is a path or the alias. Supported types are:
* - `create_table`: table creating template
* - `drop_table`: table dropping template
* - `add_column`: adding new column template
* - `drop_column`: dropping column template
* - `create_junction`: create junction template
*
* @since 2.0.7
*/
public $generatorTemplateFiles = [
'create_table' => '@yii/views/createTableMigration.php',
'drop_table' => '@yii/views/dropTableMigration.php',
'add_column' => '@yii/views/addColumnMigration.php',
'drop_column' => '@yii/views/dropColumnMigration.php',
'create_junction' => '@yii/views/createTableMigration.php',
];
/**
* @var bool indicates whether the table names generated should consider
* the `tablePrefix` setting of the DB connection. For example, if the table
* name is `post` the generator wil return `{{%post}}`.
* @since 2.0.8
*/
public $useTablePrefix = false;
/**
* @var array column definition strings used for creating migration code.
*
* The format of each definition is `COLUMN_NAME:COLUMN_TYPE:COLUMN_DECORATOR`. Delimiter is `,`.
* For example, `--fields="name:string(12):notNull:unique"`
* produces a string column of size 12 which is not null and unique values.
*
* Note: primary key is added automatically and is named id by default.
* If you want to use another name you may specify it explicitly like
* `--fields="id_key:primaryKey,name:string(12):notNull:unique"`
* @since 2.0.7
*/
public $fields = [];
/**
* @var Connection|array|string the DB connection object or the application component ID of the DB connection to use
* when applying migrations. Starting from version 2.0.3, this can also be a configuration array
* for creating the object.
*/
public $db = 'db';
/**
* @var string the comment for the table being created.
* @since 2.0.14
*/
public $comment = '';
/**
* {@inheritdoc}
*/
public function options($actionID)
{
return array_merge(
parent::options($actionID),
['migrationTable', 'db'], // global for all actions
$actionID === 'create'
? ['templateFile', 'fields', 'useTablePrefix', 'comment']
: []
);
}
/**
* {@inheritdoc}
* @since 2.0.8
*/
public function optionAliases()
{
return array_merge(parent::optionAliases(), [
'C' => 'comment',
'f' => 'fields',
'p' => 'migrationPath',
't' => 'migrationTable',
'F' => 'templateFile',
'P' => 'useTablePrefix',
'c' => 'compact',
]);
}
/**
* This method is invoked right before an action is to be executed (after all possible filters.)
* It checks the existence of the [[migrationPath]].
* @param \yii\base\Action $action the action to be executed.
* @return bool whether the action should continue to be executed.
*/
public function beforeAction($action)
{
if (parent::beforeAction($action)) {
$this->db = Instance::ensure($this->db, Connection::className());
return true;
}
return false;
}
/**
* Creates a new migration instance.
* @param string $class the migration class name
* @return \yii\db\Migration the migration instance
*/
protected function createMigration($class)
{
$this->includeMigrationFile($class);
return Yii::createObject([
'class' => $class,
'db' => $this->db,
'compact' => $this->compact,
]);
}
/**
* {@inheritdoc}
*/
protected function getMigrationHistory($limit)
{
if ($this->db->schema->getTableSchema($this->migrationTable, true) === null) {
$this->createMigrationHistoryTable();
}
$query = (new Query())
->select(['version', 'apply_time'])
->from($this->migrationTable)
->orderBy(['apply_time' => SORT_DESC, 'version' => SORT_DESC]);
if (empty($this->migrationNamespaces)) {
$query->limit($limit);
$rows = $query->all($this->db);
$history = ArrayHelper::map($rows, 'version', 'apply_time');
unset($history[self::BASE_MIGRATION]);
return $history;
}
$rows = $query->all($this->db);
$history = [];
foreach ($rows as $key => $row) {
if ($row['version'] === self::BASE_MIGRATION) {
continue;
}
if (preg_match('/m?(\d{6}_?\d{6})(\D.*)?$/is', $row['version'], $matches)) {
$time = str_replace('_', '', $matches[1]);
$row['canonicalVersion'] = $time;
} else {
$row['canonicalVersion'] = $row['version'];
}
$row['apply_time'] = (int) $row['apply_time'];
$history[] = $row;
}
usort($history, function ($a, $b) {
if ($a['apply_time'] === $b['apply_time']) {
if (($compareResult = strcasecmp($b['canonicalVersion'], $a['canonicalVersion'])) !== 0) {
return $compareResult;
}
return strcasecmp($b['version'], $a['version']);
}
return ($a['apply_time'] > $b['apply_time']) ? -1 : +1;
});
$history = array_slice($history, 0, $limit);
$history = ArrayHelper::map($history, 'version', 'apply_time');
return $history;
}
/**
* Creates the migration history table.
*/
protected function createMigrationHistoryTable()
{
$tableName = $this->db->schema->getRawTableName($this->migrationTable);
$this->stdout("Creating migration history table \"$tableName\"...", Console::FG_YELLOW);
$this->db->createCommand()->createTable($this->migrationTable, [
'version' => 'varchar(' . static::MAX_NAME_LENGTH . ') NOT NULL PRIMARY KEY',
'apply_time' => 'integer',
])->execute();
$this->db->createCommand()->insert($this->migrationTable, [
'version' => self::BASE_MIGRATION,
'apply_time' => time(),
])->execute();
$this->stdout("Done.\n", Console::FG_GREEN);
}
/**
* {@inheritdoc}
*/
protected function addMigrationHistory($version)
{
$command = $this->db->createCommand();
$command->insert($this->migrationTable, [
'version' => $version,
'apply_time' => time(),
])->execute();
}
/**
* {@inheritdoc}
* @since 2.0.13
*/
protected function truncateDatabase()
{
$db = $this->db;
$schemas = $db->schema->getTableSchemas();
// First drop all foreign keys,
foreach ($schemas as $schema) {
if ($schema->foreignKeys) {
foreach ($schema->foreignKeys as $name => $foreignKey) {
$db->createCommand()->dropForeignKey($name, $schema->name)->execute();
$this->stdout("Foreign key $name dropped.\n");
}
}
}
// Then drop the tables:
foreach ($schemas as $schema) {
$db->createCommand()->dropTable($schema->name)->execute();
$this->stdout("Table {$schema->name} dropped.\n");
}
}
/**
* {@inheritdoc}
*/
protected function removeMigrationHistory($version)
{
$command = $this->db->createCommand();
$command->delete($this->migrationTable, [
'version' => $version,
])->execute();
}
private $_migrationNameLimit;
/**
* {@inheritdoc}
* @since 2.0.13
*/
protected function getMigrationNameLimit()
{
if ($this->_migrationNameLimit !== null) {
return $this->_migrationNameLimit;
}
$tableSchema = $this->db->schema ? $this->db->schema->getTableSchema($this->migrationTable, true) : null;
if ($tableSchema !== null) {
return $this->_migrationNameLimit = $tableSchema->columns['version']->size;
}
return static::MAX_NAME_LENGTH;
}
/**
* {@inheritdoc}
* @since 2.0.8
*/
protected function generateMigrationSourceCode($params)
{
$parsedFields = $this->parseFields();
$fields = $parsedFields['fields'];
$foreignKeys = $parsedFields['foreignKeys'];
$name = $params['name'];
$templateFile = $this->templateFile;
$table = null;
if (preg_match('/^create_junction(?:_table_for_|_for_|_)(.+)_and_(.+)_tables?$/', $name, $matches)) {
$templateFile = $this->generatorTemplateFiles['create_junction'];
$firstTable = $matches[1];
$secondTable = $matches[2];
$fields = array_merge(
[
[
'property' => $firstTable . '_id',
'decorators' => 'integer()',
],
[
'property' => $secondTable . '_id',
'decorators' => 'integer()',
],
],
$fields,
[
[
'property' => 'PRIMARY KEY(' .
$firstTable . '_id, ' .
$secondTable . '_id)',
],
]
);
$foreignKeys[$firstTable . '_id']['table'] = $firstTable;
$foreignKeys[$secondTable . '_id']['table'] = $secondTable;
$foreignKeys[$firstTable . '_id']['column'] = null;
$foreignKeys[$secondTable . '_id']['column'] = null;
$table = $firstTable . '_' . $secondTable;
} elseif (preg_match('/^add_(.+)_columns?_to_(.+)_table$/', $name, $matches)) {
$templateFile = $this->generatorTemplateFiles['add_column'];
$table = $matches[2];
} elseif (preg_match('/^drop_(.+)_columns?_from_(.+)_table$/', $name, $matches)) {
$templateFile = $this->generatorTemplateFiles['drop_column'];
$table = $matches[2];
} elseif (preg_match('/^create_(.+)_table$/', $name, $matches)) {
$this->addDefaultPrimaryKey($fields);
$templateFile = $this->generatorTemplateFiles['create_table'];
$table = $matches[1];
} elseif (preg_match('/^drop_(.+)_table$/', $name, $matches)) {
$this->addDefaultPrimaryKey($fields);
$templateFile = $this->generatorTemplateFiles['drop_table'];
$table = $matches[1];
}
foreach ($foreignKeys as $column => $foreignKey) {
$relatedColumn = $foreignKey['column'];
$relatedTable = $foreignKey['table'];
// Since 2.0.11 if related column name is not specified,
// we're trying to get it from table schema
// @see https://github.com/yiisoft/yii2/issues/12748
if ($relatedColumn === null) {
$relatedColumn = 'id';
try {
$this->db = Instance::ensure($this->db, Connection::className());
$relatedTableSchema = $this->db->getTableSchema($relatedTable);
if ($relatedTableSchema !== null) {
$primaryKeyCount = count($relatedTableSchema->primaryKey);
if ($primaryKeyCount === 1) {
$relatedColumn = $relatedTableSchema->primaryKey[0];
} elseif ($primaryKeyCount > 1) {
$this->stdout("Related table for field \"{$column}\" exists, but primary key is composite. Default name \"id\" will be used for related field\n", Console::FG_YELLOW);
} elseif ($primaryKeyCount === 0) {
$this->stdout("Related table for field \"{$column}\" exists, but does not have a primary key. Default name \"id\" will be used for related field.\n", Console::FG_YELLOW);
}
}
} catch (\ReflectionException $e) {
$this->stdout("Cannot initialize database component to try reading referenced table schema for field \"{$column}\". Default name \"id\" will be used for related field.\n", Console::FG_YELLOW);
}
}
$foreignKeys[$column] = [
'idx' => $this->generateTableName("idx-$table-$column"),
'fk' => $this->generateTableName("fk-$table-$column"),
'relatedTable' => $this->generateTableName($relatedTable),
'relatedColumn' => $relatedColumn,
];
}
return $this->renderFile(Yii::getAlias($templateFile), array_merge($params, [
'table' => $this->generateTableName($table),
'fields' => $fields,
'foreignKeys' => $foreignKeys,
'tableComment' => $this->comment,
]));
}
/**
* If `useTablePrefix` equals true, then the table name will contain the
* prefix format.
*
* @param string $tableName the table name to generate.
* @return string
* @since 2.0.8
*/
protected function generateTableName($tableName)
{
if (!$this->useTablePrefix) {
return $tableName;
}
return '{{%' . $tableName . '}}';
}
/**
* Parse the command line migration fields.
* @return array parse result with following fields:
*
* - fields: array, parsed fields
* - foreignKeys: array, detected foreign keys
*
* @since 2.0.7
*/
protected function parseFields()
{
$fields = [];
$foreignKeys = [];
foreach ($this->fields as $index => $field) {
$chunks = preg_split('/\s?:\s?/', $field, null);
$property = array_shift($chunks);
foreach ($chunks as $i => &$chunk) {
if (strncmp($chunk, 'foreignKey', 10) === 0) {
preg_match('/foreignKey\((\w*)\s?(\w*)\)/', $chunk, $matches);
$foreignKeys[$property] = [
'table' => isset($matches[1])
? $matches[1]
: preg_replace('/_id$/', '', $property),
'column' => !empty($matches[2])
? $matches[2]
: null,
];
unset($chunks[$i]);
continue;
}
if (!preg_match('/^(.+?)\(([^(]+)\)$/', $chunk)) {
$chunk .= '()';
}
}
$fields[] = [
'property' => $property,
'decorators' => implode('->', $chunks),
];
}
return [
'fields' => $fields,
'foreignKeys' => $foreignKeys,
];
}
/**
* Adds default primary key to fields list if there's no primary key specified.
* @param array $fields parsed fields
* @since 2.0.7
*/
protected function addDefaultPrimaryKey(&$fields)
{
foreach ($fields as $field) {
if (false !== strripos($field['decorators'], 'primarykey()')) {
return;
}
}
array_unshift($fields, ['property' => 'id', 'decorators' => 'primaryKey()']);
}
}

View File

@@ -0,0 +1,124 @@
<?php
/**
* @link http://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license http://www.yiiframework.com/license/
*/
namespace yii\console\controllers;
use Yii;
use yii\console\Controller;
use yii\helpers\Console;
/**
* Runs PHP built-in web server.
*
* In order to access server from remote machines use 0.0.0.0:8000. That is especially useful when running server in
* a virtual machine.
*
* @author Alexander Makarov <sam@rmcreative.ru>
* @since 2.0.7
*/
class ServeController extends Controller
{
const EXIT_CODE_NO_DOCUMENT_ROOT = 2;
const EXIT_CODE_NO_ROUTING_FILE = 3;
const EXIT_CODE_ADDRESS_TAKEN_BY_ANOTHER_SERVER = 4;
const EXIT_CODE_ADDRESS_TAKEN_BY_ANOTHER_PROCESS = 5;
/**
* @var int port to serve on.
*/
public $port = 8080;
/**
* @var string path or [path alias](guide:concept-aliases) to directory to serve
*/
public $docroot = '@app/web';
/**
* @var string path to router script.
* See https://secure.php.net/manual/en/features.commandline.webserver.php
*/
public $router;
/**
* Runs PHP built-in web server.
*
* @param string $address address to serve on. Either "host" or "host:port".
*
* @return int
*/
public function actionIndex($address = 'localhost')
{
$documentRoot = Yii::getAlias($this->docroot);
if (strpos($address, ':') === false) {
$address = $address . ':' . $this->port;
}
if (!is_dir($documentRoot)) {
$this->stdout("Document root \"$documentRoot\" does not exist.\n", Console::FG_RED);
return self::EXIT_CODE_NO_DOCUMENT_ROOT;
}
if ($this->isAddressTaken($address)) {
$this->stdout("http://$address is taken by another process.\n", Console::FG_RED);
return self::EXIT_CODE_ADDRESS_TAKEN_BY_ANOTHER_PROCESS;
}
if ($this->router !== null && !file_exists($this->router)) {
$this->stdout("Routing file \"$this->router\" does not exist.\n", Console::FG_RED);
return self::EXIT_CODE_NO_ROUTING_FILE;
}
$this->stdout("Server started on http://{$address}/\n");
$this->stdout("Document root is \"{$documentRoot}\"\n");
if ($this->router) {
$this->stdout("Routing file is \"$this->router\"\n");
}
$this->stdout("Quit the server with CTRL-C or COMMAND-C.\n");
passthru('"' . PHP_BINARY . '"' . " -S {$address} -t \"{$documentRoot}\" $this->router");
}
/**
* {@inheritdoc}
*/
public function options($actionID)
{
return array_merge(parent::options($actionID), [
'docroot',
'router',
'port',
]);
}
/**
* {@inheritdoc}
* @since 2.0.8
*/
public function optionAliases()
{
return array_merge(parent::optionAliases(), [
't' => 'docroot',
'p' => 'port',
'r' => 'router',
]);
}
/**
* @param string $address server address
* @return bool if address is already in use
*/
protected function isAddressTaken($address)
{
list($hostname, $port) = explode(':', $address);
$fp = @fsockopen($hostname, $port, $errno, $errstr, 3);
if ($fp === false) {
return false;
}
fclose($fp);
return true;
}
}

View File

@@ -0,0 +1,2 @@
*
!.gitignore

View File

@@ -0,0 +1,386 @@
<?php
/**
* @link http://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license http://www.yiiframework.com/license/
*/
namespace yii\console\widgets;
use Yii;
use yii\base\Widget;
use yii\helpers\ArrayHelper;
use yii\helpers\Console;
/**
* Table class displays a table in console.
*
* For example,
*
* ```php
* $table = new Table();
*
* echo $table
* ->setHeaders(['test1', 'test2', 'test3'])
* ->setRows([
* ['col1', 'col2', 'col3'],
* ['col1', 'col2', ['col3-0', 'col3-1', 'col3-2']],
* ])
* ->run();
* ```
*
* or
*
* ```php
* echo Table::widget([
* 'headers' => ['test1', 'test2', 'test3'],
* 'rows' => [
* ['col1', 'col2', 'col3'],
* ['col1', 'col2', ['col3-0', 'col3-1', 'col3-2']],
* ],
* ]);
*
* @property string $listPrefix List prefix. This property is write-only.
* @property int $screenWidth Screen width. This property is write-only.
*
* @author Daniel Gomez Pan <pana_1990@hotmail.com>
* @since 2.0.13
*/
class Table extends Widget
{
const DEFAULT_CONSOLE_SCREEN_WIDTH = 120;
const CONSOLE_SCROLLBAR_OFFSET = 3;
const CHAR_TOP = 'top';
const CHAR_TOP_MID = 'top-mid';
const CHAR_TOP_LEFT = 'top-left';
const CHAR_TOP_RIGHT = 'top-right';
const CHAR_BOTTOM = 'bottom';
const CHAR_BOTTOM_MID = 'bottom-mid';
const CHAR_BOTTOM_LEFT = 'bottom-left';
const CHAR_BOTTOM_RIGHT = 'bottom-right';
const CHAR_LEFT = 'left';
const CHAR_LEFT_MID = 'left-mid';
const CHAR_MID = 'mid';
const CHAR_MID_MID = 'mid-mid';
const CHAR_RIGHT = 'right';
const CHAR_RIGHT_MID = 'right-mid';
const CHAR_MIDDLE = 'middle';
/**
* @var array table headers
*/
private $_headers = [];
/**
* @var array table rows
*/
private $_rows = [];
/**
* @var array table chars
*/
private $_chars = [
self::CHAR_TOP => '═',
self::CHAR_TOP_MID => '╤',
self::CHAR_TOP_LEFT => '╔',
self::CHAR_TOP_RIGHT => '╗',
self::CHAR_BOTTOM => '═',
self::CHAR_BOTTOM_MID => '╧',
self::CHAR_BOTTOM_LEFT => '╚',
self::CHAR_BOTTOM_RIGHT => '╝',
self::CHAR_LEFT => '║',
self::CHAR_LEFT_MID => '╟',
self::CHAR_MID => '─',
self::CHAR_MID_MID => '┼',
self::CHAR_RIGHT => '║',
self::CHAR_RIGHT_MID => '╢',
self::CHAR_MIDDLE => '│',
];
/**
* @var array table column widths
*/
private $_columnWidths = [];
/**
* @var int screen width
*/
private $_screenWidth;
/**
* @var string list prefix
*/
private $_listPrefix = '• ';
/**
* Set table headers.
*
* @param array $headers table headers
* @return $this
*/
public function setHeaders(array $headers)
{
$this->_headers = array_values($headers);
return $this;
}
/**
* Set table rows.
*
* @param array $rows table rows
* @return $this
*/
public function setRows(array $rows)
{
$this->_rows = array_map('array_values', $rows);
return $this;
}
/**
* Set table chars.
*
* @param array $chars table chars
* @return $this
*/
public function setChars(array $chars)
{
$this->_chars = $chars;
return $this;
}
/**
* Set screen width.
*
* @param int $width screen width
* @return $this
*/
public function setScreenWidth($width)
{
$this->_screenWidth = $width;
return $this;
}
/**
* Set list prefix.
*
* @param string $listPrefix list prefix
* @return $this
*/
public function setListPrefix($listPrefix)
{
$this->_listPrefix = $listPrefix;
return $this;
}
/**
* @return string the rendered table
*/
public function run()
{
$this->calculateRowsSize();
$buffer = $this->renderSeparator(
$this->_chars[self::CHAR_TOP_LEFT],
$this->_chars[self::CHAR_TOP_MID],
$this->_chars[self::CHAR_TOP],
$this->_chars[self::CHAR_TOP_RIGHT]
);
// Header
$buffer .= $this->renderRow($this->_headers,
$this->_chars[self::CHAR_LEFT],
$this->_chars[self::CHAR_MIDDLE],
$this->_chars[self::CHAR_RIGHT]
);
// Content
foreach ($this->_rows as $row) {
$buffer .= $this->renderSeparator(
$this->_chars[self::CHAR_LEFT_MID],
$this->_chars[self::CHAR_MID_MID],
$this->_chars[self::CHAR_MID],
$this->_chars[self::CHAR_RIGHT_MID]
);
$buffer .= $this->renderRow($row,
$this->_chars[self::CHAR_LEFT],
$this->_chars[self::CHAR_MIDDLE],
$this->_chars[self::CHAR_RIGHT]);
}
$buffer .= $this->renderSeparator(
$this->_chars[self::CHAR_BOTTOM_LEFT],
$this->_chars[self::CHAR_BOTTOM_MID],
$this->_chars[self::CHAR_BOTTOM],
$this->_chars[self::CHAR_BOTTOM_RIGHT]
);
return $buffer;
}
/**
* Renders a row of data into a string.
*
* @param array $row row of data
* @param string $spanLeft character for left border
* @param string $spanMiddle character for middle border
* @param string $spanRight character for right border
* @return string
* @see \yii\console\widgets\Table::render()
*/
protected function renderRow(array $row, $spanLeft, $spanMiddle, $spanRight)
{
$size = $this->_columnWidths;
$buffer = '';
$arrayPointer = [];
$finalChunk = [];
for ($i = 0, ($max = $this->calculateRowHeight($row)) ?: $max = 1; $i < $max; $i++) {
$buffer .= $spanLeft . ' ';
foreach ($size as $index => $cellSize) {
$cell = isset($row[$index]) ? $row[$index] : null;
$prefix = '';
if ($index !== 0) {
$buffer .= $spanMiddle . ' ';
}
if (is_array($cell)) {
if (empty($finalChunk[$index])) {
$finalChunk[$index] = '';
$start = 0;
$prefix = $this->_listPrefix;
if (!isset($arrayPointer[$index])) {
$arrayPointer[$index] = 0;
}
} else {
$start = mb_strwidth($finalChunk[$index], Yii::$app->charset);
}
$chunk = mb_substr($cell[$arrayPointer[$index]], $start, $cellSize - 4, Yii::$app->charset);
$finalChunk[$index] .= $chunk;
if (isset($cell[$arrayPointer[$index] + 1]) && $finalChunk[$index] === $cell[$arrayPointer[$index]]) {
$arrayPointer[$index]++;
$finalChunk[$index] = '';
}
} else {
$chunk = mb_substr($cell, ($cellSize * $i) - ($i * 2), $cellSize - 2, Yii::$app->charset);
}
$chunk = $prefix . $chunk;
$repeat = $cellSize - mb_strwidth($chunk, Yii::$app->charset) - 1;
$buffer .= $chunk;
if ($repeat >= 0) {
$buffer .= str_repeat(' ', $repeat);
}
}
$buffer .= "$spanRight\n";
}
return $buffer;
}
/**
* Renders separator.
*
* @param string $spanLeft character for left border
* @param string $spanMid character for middle border
* @param string $spanMidMid character for middle-middle border
* @param string $spanRight character for right border
* @return string the generated separator row
* @see \yii\console\widgets\Table::render()
*/
protected function renderSeparator($spanLeft, $spanMid, $spanMidMid, $spanRight)
{
$separator = $spanLeft;
foreach ($this->_columnWidths as $index => $rowSize) {
if ($index !== 0) {
$separator .= $spanMid;
}
$separator .= str_repeat($spanMidMid, $rowSize);
}
$separator .= $spanRight . "\n";
return $separator;
}
/**
* Calculate the size of rows to draw anchor of columns in console.
*
* @see \yii\console\widgets\Table::render()
*/
protected function calculateRowsSize()
{
$this->_columnWidths = $columns = [];
$totalWidth = 0;
$screenWidth = $this->getScreenWidth() - self::CONSOLE_SCROLLBAR_OFFSET;
for ($i = 0, $count = count($this->_headers); $i < $count; $i++) {
$columns[] = ArrayHelper::getColumn($this->_rows, $i);
$columns[$i][] = $this->_headers[$i];
}
foreach ($columns as $column) {
$columnWidth = max(array_map(function ($val) {
if (is_array($val)) {
$encodings = array_fill(0, count($val), Yii::$app->charset);
return max(array_map('mb_strwidth', $val, $encodings)) + mb_strwidth($this->_listPrefix, Yii::$app->charset);
}
return mb_strwidth($val, Yii::$app->charset);
}, $column)) + 2;
$this->_columnWidths[] = $columnWidth;
$totalWidth += $columnWidth;
}
$relativeWidth = $screenWidth / $totalWidth;
if ($totalWidth > $screenWidth) {
foreach ($this->_columnWidths as $j => $width) {
$this->_columnWidths[$j] = (int) ($width * $relativeWidth);
if ($j === count($this->_columnWidths)) {
$this->_columnWidths = $totalWidth;
}
$totalWidth -= $this->_columnWidths[$j];
}
}
}
/**
* Calculate the height of a row.
*
* @param array $row
* @return int maximum row per cell
* @see \yii\console\widgets\Table::render()
*/
protected function calculateRowHeight($row)
{
$rowsPerCell = array_map(function ($size, $columnWidth) {
if (is_array($columnWidth)) {
$rows = 0;
foreach ($columnWidth as $width) {
$rows += ceil($width / ($size - 2));
}
return $rows;
}
return ceil($columnWidth / ($size - 2));
}, $this->_columnWidths, array_map(function ($val) {
if (is_array($val)) {
$encodings = array_fill(0, count($val), Yii::$app->charset);
return array_map('mb_strwidth', $val, $encodings);
}
return mb_strwidth($val, Yii::$app->charset);
}, $row)
);
return max($rowsPerCell);
}
/**
* Getting screen width.
* If it is not able to determine screen width, default value `123` will be set.
*
* @return int screen width
*/
protected function getScreenWidth()
{
if (!$this->_screenWidth) {
$size = Console::getScreenSize();
$this->_screenWidth = isset($size[0])
? $size[0]
: self::DEFAULT_CONSOLE_SCREEN_WIDTH + self::CONSOLE_SCROLLBAR_OFFSET;
}
return $this->_screenWidth;
}
}