Qiang Xue
13 years ago
15 changed files with 1687 additions and 703 deletions
@ -1,138 +0,0 @@
|
||||
<?php |
||||
/** |
||||
* CConsoleCommandRunner class file. |
||||
* |
||||
* @author Qiang Xue <qiang.xue@gmail.com> |
||||
* @link http://www.yiiframework.com/ |
||||
* @copyright Copyright © 2008-2011 Yii Software LLC |
||||
* @license http://www.yiiframework.com/license/ |
||||
*/ |
||||
|
||||
namespace yii\console; |
||||
|
||||
/** |
||||
* CConsoleCommandRunner manages commands and executes the requested command. |
||||
* |
||||
* @property string $scriptName The entry script name. |
||||
* |
||||
* @author Qiang Xue <qiang.xue@gmail.com> |
||||
* @since 2.0 |
||||
*/ |
||||
class CommandRunner extends \yii\base\Component |
||||
{ |
||||
/** |
||||
* @var array list of all available commands (command name=>command configuration). |
||||
* Each command configuration can be either a string or an array. |
||||
* If the former, the string should be the class name or |
||||
* {@link YiiBase::getPathOfAlias class path alias} of the command. |
||||
* If the latter, the array must contain a 'class' element which specifies |
||||
* the command's class name or {@link YiiBase::getPathOfAlias class path alias}. |
||||
* The rest name-value pairs in the array are used to initialize |
||||
* the corresponding command properties. For example, |
||||
* <pre> |
||||
* array( |
||||
* 'email'=>array( |
||||
* 'class'=>'path.to.Mailer', |
||||
* 'interval'=>3600, |
||||
* ), |
||||
* 'log'=>'path.to.LoggerCommand', |
||||
* ) |
||||
* </pre> |
||||
*/ |
||||
public $commands=array(); |
||||
|
||||
private $_scriptName; |
||||
|
||||
/** |
||||
* Executes the requested command. |
||||
* @param array $args list of user supplied parameters (including the entry script name and the command name). |
||||
*/ |
||||
public function run($args) |
||||
{ |
||||
$this->_scriptName=$args[0]; |
||||
array_shift($args); |
||||
if(isset($args[0])) |
||||
{ |
||||
$name=$args[0]; |
||||
array_shift($args); |
||||
} else |
||||
$name='help'; |
||||
|
||||
if(($command=$this->createCommand($name))===null) |
||||
$command=$this->createCommand('help'); |
||||
$command->init(); |
||||
$command->run($args); |
||||
} |
||||
|
||||
/** |
||||
* @return string the entry script name |
||||
*/ |
||||
public function getScriptName() |
||||
{ |
||||
return $this->_scriptName; |
||||
} |
||||
|
||||
/** |
||||
* Searches for commands under the specified directory. |
||||
* @param string $path the directory containing the command class files. |
||||
* @return array list of commands (command name=>command class file) |
||||
*/ |
||||
public function findCommands($path) |
||||
{ |
||||
if(($dir=@opendir($path))===false) |
||||
return array(); |
||||
$commands=array(); |
||||
while(($name=readdir($dir))!==false) |
||||
{ |
||||
$file=$path.DIRECTORY_SEPARATOR.$name; |
||||
if(!strcasecmp(substr($name,-11),'Command.php') && is_file($file)) |
||||
$commands[strtolower(substr($name,0,-11))]=$file; |
||||
} |
||||
closedir($dir); |
||||
return $commands; |
||||
} |
||||
|
||||
/** |
||||
* Adds commands from the specified command path. |
||||
* If a command already exists, the new one will be ignored. |
||||
* @param string $path the alias of the directory containing the command class files. |
||||
*/ |
||||
public function addCommands($path) |
||||
{ |
||||
if(($commands=$this->findCommands($path))!==array()) |
||||
{ |
||||
foreach($commands as $name=>$file) |
||||
{ |
||||
if(!isset($this->commands[$name])) |
||||
$this->commands[$name]=$file; |
||||
} |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* @param string $name command name (case-insensitive) |
||||
* @return \yii\console\Command the command object. Null if the name is invalid. |
||||
*/ |
||||
public function createCommand($name) |
||||
{ |
||||
$name=strtolower($name); |
||||
if(isset($this->commands[$name])) |
||||
{ |
||||
if(is_string($this->commands[$name])) // class file path or alias |
||||
{ |
||||
if(strpos($this->commands[$name],'/')!==false || strpos($this->commands[$name],'\\')!==false) |
||||
{ |
||||
$className=substr(basename($this->commands[$name]),0,-4); |
||||
if(!class_exists($className,false)) |
||||
require_once($this->commands[$name]); |
||||
} else // an alias |
||||
$className=\Yii::import($this->commands[$name]); |
||||
return new $className($name,$this); |
||||
} else // an array configuration |
||||
return \Yii::create($this->commands[$name],$name,$this); |
||||
} else if($name==='help') |
||||
return new HelpCommand('help',$this); |
||||
else |
||||
return null; |
||||
} |
||||
} |
@ -0,0 +1,129 @@
|
||||
<?php |
||||
/** |
||||
* WebAppCommand class file. |
||||
* |
||||
* @author Qiang Xue <qiang.xue@gmail.com> |
||||
* @link http://www.yiiframework.com/ |
||||
* @copyright Copyright © 2008-2011 Yii Software LLC |
||||
* @license http://www.yiiframework.com/license/ |
||||
* @version $Id$ |
||||
*/ |
||||
|
||||
/** |
||||
* WebAppCommand creates an Yii Web application at the specified location. |
||||
* |
||||
* @author Qiang Xue <qiang.xue@gmail.com> |
||||
* @version $Id$ |
||||
* @package system.cli.commands |
||||
* @since 1.0 |
||||
*/ |
||||
class WebAppCommand extends CConsoleCommand |
||||
{ |
||||
private $_rootPath; |
||||
|
||||
public function getHelp() |
||||
{ |
||||
return <<<EOD |
||||
USAGE |
||||
yiic webapp <app-path> |
||||
|
||||
DESCRIPTION |
||||
This command generates an Yii Web Application at the specified location. |
||||
|
||||
PARAMETERS |
||||
* app-path: required, the directory where the new application will be created. |
||||
If the directory does not exist, it will be created. After the application |
||||
is created, please make sure the directory can be accessed by Web users. |
||||
|
||||
EOD; |
||||
} |
||||
|
||||
/** |
||||
* Execute the action. |
||||
* @param array command line parameters specific for this command |
||||
*/ |
||||
public function run($args) |
||||
{ |
||||
if(!isset($args[0])) |
||||
$this->usageError('the Web application location is not specified.'); |
||||
$path=strtr($args[0],'/\\',DIRECTORY_SEPARATOR); |
||||
if(strpos($path,DIRECTORY_SEPARATOR)===false) |
||||
$path='.'.DIRECTORY_SEPARATOR.$path; |
||||
$dir=rtrim(realpath(dirname($path)),'\\/'); |
||||
if($dir===false || !is_dir($dir)) |
||||
$this->usageError("The directory '$path' is not valid. Please make sure the parent directory exists."); |
||||
if(basename($path)==='.') |
||||
$this->_rootPath=$path=$dir; |
||||
else |
||||
$this->_rootPath=$path=$dir.DIRECTORY_SEPARATOR.basename($path); |
||||
if($this->confirm("Create a Web application under '$path'?")) |
||||
{ |
||||
$sourceDir=realpath(dirname(__FILE__).'/../views/webapp'); |
||||
if($sourceDir===false) |
||||
die("\nUnable to locate the source directory.\n"); |
||||
$list=$this->buildFileList($sourceDir,$path); |
||||
$list['index.php']['callback']=array($this,'generateIndex'); |
||||
$list['index-test.php']['callback']=array($this,'generateIndex'); |
||||
$list['protected/tests/bootstrap.php']['callback']=array($this,'generateTestBoostrap'); |
||||
$list['protected/yiic.php']['callback']=array($this,'generateYiic'); |
||||
$this->copyFiles($list); |
||||
@chmod($path.'/assets',0777); |
||||
@chmod($path.'/protected/runtime',0777); |
||||
@chmod($path.'/protected/data',0777); |
||||
@chmod($path.'/protected/data/testdrive.db',0777); |
||||
@chmod($path.'/protected/yiic',0755); |
||||
echo "\nYour application has been created successfully under {$path}.\n"; |
||||
} |
||||
} |
||||
|
||||
public function generateIndex($source,$params) |
||||
{ |
||||
$content=file_get_contents($source); |
||||
$yii=realpath(dirname(__FILE__).'/../../yii.php'); |
||||
$yii=$this->getRelativePath($yii,$this->_rootPath.DIRECTORY_SEPARATOR.'index.php'); |
||||
$yii=str_replace('\\','\\\\',$yii); |
||||
return preg_replace('/\$yii\s*=(.*?);/',"\$yii=$yii;",$content); |
||||
} |
||||
|
||||
public function generateTestBoostrap($source,$params) |
||||
{ |
||||
$content=file_get_contents($source); |
||||
$yii=realpath(dirname(__FILE__).'/../../yiit.php'); |
||||
$yii=$this->getRelativePath($yii,$this->_rootPath.DIRECTORY_SEPARATOR.'protected'.DIRECTORY_SEPARATOR.'tests'.DIRECTORY_SEPARATOR.'bootstrap.php'); |
||||
$yii=str_replace('\\','\\\\',$yii); |
||||
return preg_replace('/\$yiit\s*=(.*?);/',"\$yiit=$yii;",$content); |
||||
} |
||||
|
||||
public function generateYiic($source,$params) |
||||
{ |
||||
$content=file_get_contents($source); |
||||
$yiic=realpath(dirname(__FILE__).'/../../yiic.php'); |
||||
$yiic=$this->getRelativePath($yiic,$this->_rootPath.DIRECTORY_SEPARATOR.'protected'.DIRECTORY_SEPARATOR.'yiic.php'); |
||||
$yiic=str_replace('\\','\\\\',$yiic); |
||||
return preg_replace('/\$yiic\s*=(.*?);/',"\$yiic=$yiic;",$content); |
||||
} |
||||
|
||||
protected function getRelativePath($path1,$path2) |
||||
{ |
||||
$segs1=explode(DIRECTORY_SEPARATOR,$path1); |
||||
$segs2=explode(DIRECTORY_SEPARATOR,$path2); |
||||
$n1=count($segs1); |
||||
$n2=count($segs2); |
||||
|
||||
for($i=0;$i<$n1 && $i<$n2;++$i) |
||||
{ |
||||
if($segs1[$i]!==$segs2[$i]) |
||||
break; |
||||
} |
||||
|
||||
if($i===0) |
||||
return "'".$path1."'"; |
||||
$up=''; |
||||
for($j=$i;$j<$n2-1;++$j) |
||||
$up.='/..'; |
||||
for(;$i<$n1-1;++$i) |
||||
$up.='/'.$segs1[$i]; |
||||
|
||||
return 'dirname(__FILE__).\''.$up.'/'.basename($path1).'\''; |
||||
} |
||||
} |
@ -0,0 +1,223 @@
|
||||
<?php |
||||
/** |
||||
* MessageCommand class file. |
||||
* |
||||
* @author Qiang Xue <qiang.xue@gmail.com> |
||||
* @link http://www.yiiframework.com/ |
||||
* @copyright Copyright © 2008-2011 Yii Software LLC |
||||
* @license http://www.yiiframework.com/license/ |
||||
*/ |
||||
|
||||
/** |
||||
* MessageCommand extracts messages to be translated from source files. |
||||
* The extracted messages are saved as PHP message source files |
||||
* under the specified directory. |
||||
* |
||||
* @author Qiang Xue <qiang.xue@gmail.com> |
||||
* @version $Id$ |
||||
* @package system.cli.commands |
||||
* @since 1.0 |
||||
*/ |
||||
class MessageCommand extends CConsoleCommand |
||||
{ |
||||
public function getHelp() |
||||
{ |
||||
return <<<EOD |
||||
USAGE |
||||
yiic message <config-file> |
||||
|
||||
DESCRIPTION |
||||
This command searches for messages to be translated in the specified |
||||
source files and compiles them into PHP arrays as message source. |
||||
|
||||
PARAMETERS |
||||
* config-file: required, the path of the configuration file. You can find |
||||
an example in framework/messages/config.php. |
||||
|
||||
The file can be placed anywhere and must be a valid PHP script which |
||||
returns an array of name-value pairs. Each name-value pair represents |
||||
a configuration option. |
||||
|
||||
The following options are available: |
||||
|
||||
- sourcePath: string, root directory of all source files. |
||||
- messagePath: string, root directory containing message translations. |
||||
- languages: array, list of language codes that the extracted messages |
||||
should be translated to. For example, array('zh_cn','en_au'). |
||||
- fileTypes: array, a list of file extensions (e.g. 'php', 'xml'). |
||||
Only the files whose extension name can be found in this list |
||||
will be processed. If empty, all files will be processed. |
||||
- exclude: array, a list of directory and file exclusions. Each |
||||
exclusion can be either a name or a path. If a file or directory name |
||||
or path matches the exclusion, it will not be copied. For example, |
||||
an exclusion of '.svn' will exclude all files and directories whose |
||||
name is '.svn'. And an exclusion of '/a/b' will exclude file or |
||||
directory 'sourcePath/a/b'. |
||||
- translator: the name of the function for translating messages. |
||||
Defaults to 'Yii::t'. This is used as a mark to find messages to be |
||||
translated. |
||||
- overwrite: if message file must be overwritten with the merged messages. |
||||
- removeOld: if message no longer needs translation it will be removed, |
||||
instead of being enclosed between a pair of '@@' marks. |
||||
- sort: sort messages by key when merging, regardless of their translation |
||||
state (new, obsolete, translated.) |
||||
|
||||
EOD; |
||||
} |
||||
|
||||
/** |
||||
* Execute the action. |
||||
* @param array command line parameters specific for this command |
||||
*/ |
||||
public function run($args) |
||||
{ |
||||
if(!isset($args[0])) |
||||
$this->usageError('the configuration file is not specified.'); |
||||
if(!is_file($args[0])) |
||||
$this->usageError("the configuration file {$args[0]} does not exist."); |
||||
|
||||
$config=require_once($args[0]); |
||||
$translator='Yii::t'; |
||||
extract($config); |
||||
|
||||
if(!isset($sourcePath,$messagePath,$languages)) |
||||
$this->usageError('The configuration file must specify "sourcePath", "messagePath" and "languages".'); |
||||
if(!is_dir($sourcePath)) |
||||
$this->usageError("The source path $sourcePath is not a valid directory."); |
||||
if(!is_dir($messagePath)) |
||||
$this->usageError("The message path $messagePath is not a valid directory."); |
||||
if(empty($languages)) |
||||
$this->usageError("Languages cannot be empty."); |
||||
|
||||
if(!isset($overwrite)) |
||||
$overwrite = false; |
||||
|
||||
if(!isset($removeOld)) |
||||
$removeOld = false; |
||||
|
||||
if(!isset($sort)) |
||||
$sort = false; |
||||
|
||||
$options=array(); |
||||
if(isset($fileTypes)) |
||||
$options['fileTypes']=$fileTypes; |
||||
if(isset($exclude)) |
||||
$options['exclude']=$exclude; |
||||
$files=CFileHelper::findFiles(realpath($sourcePath),$options); |
||||
|
||||
$messages=array(); |
||||
foreach($files as $file) |
||||
$messages=array_merge_recursive($messages,$this->extractMessages($file,$translator)); |
||||
|
||||
foreach($languages as $language) |
||||
{ |
||||
$dir=$messagePath.DIRECTORY_SEPARATOR.$language; |
||||
if(!is_dir($dir)) |
||||
@mkdir($dir); |
||||
foreach($messages as $category=>$msgs) |
||||
{ |
||||
$msgs=array_values(array_unique($msgs)); |
||||
$this->generateMessageFile($msgs,$dir.DIRECTORY_SEPARATOR.$category.'.php',$overwrite,$removeOld,$sort); |
||||
} |
||||
} |
||||
} |
||||
|
||||
protected function extractMessages($fileName,$translator) |
||||
{ |
||||
echo "Extracting messages from $fileName...\n"; |
||||
$subject=file_get_contents($fileName); |
||||
$n=preg_match_all('/\b'.$translator.'\s*\(\s*(\'.*?(?<!\\\\)\'|".*?(?<!\\\\)")\s*,\s*(\'.*?(?<!\\\\)\'|".*?(?<!\\\\)")\s*[,\)]/s',$subject,$matches,PREG_SET_ORDER); |
||||
$messages=array(); |
||||
for($i=0;$i<$n;++$i) |
||||
{ |
||||
if(($pos=strpos($matches[$i][1],'.'))!==false) |
||||
$category=substr($matches[$i][1],$pos+1,-1); |
||||
else |
||||
$category=substr($matches[$i][1],1,-1); |
||||
$message=$matches[$i][2]; |
||||
$messages[$category][]=eval("return $message;"); // use eval to eliminate quote escape |
||||
} |
||||
return $messages; |
||||
} |
||||
|
||||
protected function generateMessageFile($messages,$fileName,$overwrite,$removeOld,$sort) |
||||
{ |
||||
echo "Saving messages to $fileName..."; |
||||
if(is_file($fileName)) |
||||
{ |
||||
$translated=require($fileName); |
||||
sort($messages); |
||||
ksort($translated); |
||||
if(array_keys($translated)==$messages) |
||||
{ |
||||
echo "nothing new...skipped.\n"; |
||||
return; |
||||
} |
||||
$merged=array(); |
||||
$untranslated=array(); |
||||
foreach($messages as $message) |
||||
{ |
||||
if(!empty($translated[$message])) |
||||
$merged[$message]=$translated[$message]; |
||||
else |
||||
$untranslated[]=$message; |
||||
} |
||||
ksort($merged); |
||||
sort($untranslated); |
||||
$todo=array(); |
||||
foreach($untranslated as $message) |
||||
$todo[$message]=''; |
||||
ksort($translated); |
||||
foreach($translated as $message=>$translation) |
||||
{ |
||||
if(!isset($merged[$message]) && !isset($todo[$message]) && !$removeOld) |
||||
{ |
||||
if(substr($translation,0,2)==='@@' && substr($translation,-2)==='@@') |
||||
$todo[$message]=$translation; |
||||
else |
||||
$todo[$message]='@@'.$translation.'@@'; |
||||
} |
||||
} |
||||
$merged=array_merge($todo,$merged); |
||||
if($sort) |
||||
ksort($merged); |
||||
if($overwrite === false) |
||||
$fileName.='.merged'; |
||||
echo "translation merged.\n"; |
||||
} |
||||
else |
||||
{ |
||||
$merged=array(); |
||||
foreach($messages as $message) |
||||
$merged[$message]=''; |
||||
ksort($merged); |
||||
echo "saved.\n"; |
||||
} |
||||
$array=str_replace("\r",'',var_export($merged,true)); |
||||
$content=<<<EOD |
||||
<?php |
||||
/** |
||||
* Message translations. |
||||
* |
||||
* This file is automatically generated by 'yiic message' 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. |
||||
* |
||||
* @version \$Id: \$ |
||||
*/ |
||||
return $array; |
||||
|
||||
EOD; |
||||
file_put_contents($fileName, $content); |
||||
} |
||||
} |
@ -0,0 +1,561 @@
|
||||
<?php |
||||
/** |
||||
* MigrateCommand class file. |
||||
* |
||||
* @author Qiang Xue <qiang.xue@gmail.com> |
||||
* @link http://www.yiiframework.com/ |
||||
* @copyright Copyright © 2008-2011 Yii Software LLC |
||||
* @license http://www.yiiframework.com/license/ |
||||
*/ |
||||
|
||||
/** |
||||
* MigrateCommand manages the database migrations. |
||||
* |
||||
* The implementation of this command and other supporting classes referenced |
||||
* the yii-dbmigrations extension ((https://github.com/pieterclaerhout/yii-dbmigrations), |
||||
* authored by Pieter Claerhout. |
||||
* |
||||
* @author Qiang Xue <qiang.xue@gmail.com> |
||||
* @version $Id$ |
||||
* @package system.cli.commands |
||||
* @since 1.1.6 |
||||
*/ |
||||
class MigrateCommand extends CConsoleCommand |
||||
{ |
||||
const BASE_MIGRATION='m000000_000000_base'; |
||||
|
||||
/** |
||||
* @var string the directory that stores the migrations. This must be specified |
||||
* in terms of a path alias, and the corresponding directory must exist. |
||||
* Defaults to 'application.migrations' (meaning 'protected/migrations'). |
||||
*/ |
||||
public $migrationPath='application.migrations'; |
||||
/** |
||||
* @var string the name of the table for keeping applied migration information. |
||||
* This table will be automatically created if not exists. Defaults to 'tbl_migration'. |
||||
* The table structure is: (version varchar(255) primary key, apply_time integer) |
||||
*/ |
||||
public $migrationTable='tbl_migration'; |
||||
/** |
||||
* @var string the application component ID that specifies the database connection for |
||||
* storing migration information. Defaults to 'db'. |
||||
*/ |
||||
public $connectionID='db'; |
||||
/** |
||||
* @var string the path of the template file for generating new migrations. This |
||||
* must be specified in terms of a path alias (e.g. application.migrations.template). |
||||
* If not set, an internal template will be used. |
||||
*/ |
||||
public $templateFile; |
||||
/** |
||||
* @var string the default command action. It defaults to 'up'. |
||||
*/ |
||||
public $defaultAction='up'; |
||||
/** |
||||
* @var boolean whether to execute the migration in an interactive mode. Defaults to true. |
||||
* Set this to false when performing migration in a cron job or background process. |
||||
*/ |
||||
public $interactive=true; |
||||
|
||||
public function beforeAction($action,$params) |
||||
{ |
||||
$path=Yii::getPathOfAlias($this->migrationPath); |
||||
if($path===false || !is_dir($path)) |
||||
die('Error: The migration directory does not exist: '.$this->migrationPath."\n"); |
||||
$this->migrationPath=$path; |
||||
|
||||
$yiiVersion=Yii::getVersion(); |
||||
echo "\nYii Migration Tool v1.0 (based on Yii v{$yiiVersion})\n\n"; |
||||
|
||||
return true; |
||||
} |
||||
|
||||
public function actionUp($args) |
||||
{ |
||||
if(($migrations=$this->getNewMigrations())===array()) |
||||
{ |
||||
echo "No new migration found. Your system is up-to-date.\n"; |
||||
return; |
||||
} |
||||
|
||||
$total=count($migrations); |
||||
$step=isset($args[0]) ? (int)$args[0] : 0; |
||||
if($step>0) |
||||
$migrations=array_slice($migrations,0,$step); |
||||
|
||||
$n=count($migrations); |
||||
if($n===$total) |
||||
echo "Total $n new ".($n===1 ? 'migration':'migrations')." to be applied:\n"; |
||||
else |
||||
echo "Total $n out of $total new ".($total===1 ? 'migration':'migrations')." to be applied:\n"; |
||||
|
||||
foreach($migrations as $migration) |
||||
echo " $migration\n"; |
||||
echo "\n"; |
||||
|
||||
if($this->confirm('Apply the above '.($n===1 ? 'migration':'migrations')."?")) |
||||
{ |
||||
foreach($migrations as $migration) |
||||
{ |
||||
if($this->migrateUp($migration)===false) |
||||
{ |
||||
echo "\nMigration failed. All later migrations are canceled.\n"; |
||||
return; |
||||
} |
||||
} |
||||
echo "\nMigrated up successfully.\n"; |
||||
} |
||||
} |
||||
|
||||
public function actionDown($args) |
||||
{ |
||||
$step=isset($args[0]) ? (int)$args[0] : 1; |
||||
if($step<1) |
||||
die("Error: The step parameter must be greater than 0.\n"); |
||||
|
||||
if(($migrations=$this->getMigrationHistory($step))===array()) |
||||
{ |
||||
echo "No migration has been done before.\n"; |
||||
return; |
||||
} |
||||
$migrations=array_keys($migrations); |
||||
|
||||
$n=count($migrations); |
||||
echo "Total $n ".($n===1 ? 'migration':'migrations')." to be reverted:\n"; |
||||
foreach($migrations as $migration) |
||||
echo " $migration\n"; |
||||
echo "\n"; |
||||
|
||||
if($this->confirm('Revert the above '.($n===1 ? 'migration':'migrations')."?")) |
||||
{ |
||||
foreach($migrations as $migration) |
||||
{ |
||||
if($this->migrateDown($migration)===false) |
||||
{ |
||||
echo "\nMigration failed. All later migrations are canceled.\n"; |
||||
return; |
||||
} |
||||
} |
||||
echo "\nMigrated down successfully.\n"; |
||||
} |
||||
} |
||||
|
||||
public function actionRedo($args) |
||||
{ |
||||
$step=isset($args[0]) ? (int)$args[0] : 1; |
||||
if($step<1) |
||||
die("Error: The step parameter must be greater than 0.\n"); |
||||
|
||||
if(($migrations=$this->getMigrationHistory($step))===array()) |
||||
{ |
||||
echo "No migration has been done before.\n"; |
||||
return; |
||||
} |
||||
$migrations=array_keys($migrations); |
||||
|
||||
$n=count($migrations); |
||||
echo "Total $n ".($n===1 ? 'migration':'migrations')." to be redone:\n"; |
||||
foreach($migrations as $migration) |
||||
echo " $migration\n"; |
||||
echo "\n"; |
||||
|
||||
if($this->confirm('Redo the above '.($n===1 ? 'migration':'migrations')."?")) |
||||
{ |
||||
foreach($migrations as $migration) |
||||
{ |
||||
if($this->migrateDown($migration)===false) |
||||
{ |
||||
echo "\nMigration failed. All later migrations are canceled.\n"; |
||||
return; |
||||
} |
||||
} |
||||
foreach(array_reverse($migrations) as $migration) |
||||
{ |
||||
if($this->migrateUp($migration)===false) |
||||
{ |
||||
echo "\nMigration failed. All later migrations are canceled.\n"; |
||||
return; |
||||
} |
||||
} |
||||
echo "\nMigration redone successfully.\n"; |
||||
} |
||||
} |
||||
|
||||
public function actionTo($args) |
||||
{ |
||||
if(isset($args[0])) |
||||
$version=$args[0]; |
||||
else |
||||
$this->usageError('Please specify which version to migrate to.'); |
||||
|
||||
$originalVersion=$version; |
||||
if(preg_match('/^m?(\d{6}_\d{6})(_.*?)?$/',$version,$matches)) |
||||
$version='m'.$matches[1]; |
||||
else |
||||
die("Error: The version option must be either a timestamp (e.g. 101129_185401)\nor the full name of a migration (e.g. m101129_185401_create_user_table).\n"); |
||||
|
||||
// try migrate up |
||||
$migrations=$this->getNewMigrations(); |
||||
foreach($migrations as $i=>$migration) |
||||
{ |
||||
if(strpos($migration,$version.'_')===0) |
||||
{ |
||||
$this->actionUp(array($i+1)); |
||||
return; |
||||
} |
||||
} |
||||
|
||||
// try migrate down |
||||
$migrations=array_keys($this->getMigrationHistory(-1)); |
||||
foreach($migrations as $i=>$migration) |
||||
{ |
||||
if(strpos($migration,$version.'_')===0) |
||||
{ |
||||
if($i===0) |
||||
echo "Already at '$originalVersion'. Nothing needs to be done.\n"; |
||||
else |
||||
$this->actionDown(array($i)); |
||||
return; |
||||
} |
||||
} |
||||
|
||||
die("Error: Unable to find the version '$originalVersion'.\n"); |
||||
} |
||||
|
||||
public function actionMark($args) |
||||
{ |
||||
if(isset($args[0])) |
||||
$version=$args[0]; |
||||
else |
||||
$this->usageError('Please specify which version to mark to.'); |
||||
$originalVersion=$version; |
||||
if(preg_match('/^m?(\d{6}_\d{6})(_.*?)?$/',$version,$matches)) |
||||
$version='m'.$matches[1]; |
||||
else |
||||
die("Error: The version option must be either a timestamp (e.g. 101129_185401)\nor the full name of a migration (e.g. m101129_185401_create_user_table).\n"); |
||||
|
||||
$db=$this->getDbConnection(); |
||||
|
||||
// try mark up |
||||
$migrations=$this->getNewMigrations(); |
||||
foreach($migrations as $i=>$migration) |
||||
{ |
||||
if(strpos($migration,$version.'_')===0) |
||||
{ |
||||
if($this->confirm("Set migration history at $originalVersion?")) |
||||
{ |
||||
$command=$db->createCommand(); |
||||
for($j=0;$j<=$i;++$j) |
||||
{ |
||||
$command->insert($this->migrationTable, array( |
||||
'version'=>$migrations[$j], |
||||
'apply_time'=>time(), |
||||
)); |
||||
} |
||||
echo "The migration history is set at $originalVersion.\nNo actual migration was performed.\n"; |
||||
} |
||||
return; |
||||
} |
||||
} |
||||
|
||||
// try mark down |
||||
$migrations=array_keys($this->getMigrationHistory(-1)); |
||||
foreach($migrations as $i=>$migration) |
||||
{ |
||||
if(strpos($migration,$version.'_')===0) |
||||
{ |
||||
if($i===0) |
||||
echo "Already at '$originalVersion'. Nothing needs to be done.\n"; |
||||
else |
||||
{ |
||||
if($this->confirm("Set migration history at $originalVersion?")) |
||||
{ |
||||
$command=$db->createCommand(); |
||||
for($j=0;$j<$i;++$j) |
||||
$command->delete($this->migrationTable, $db->quoteColumnName('version').'=:version', array(':version'=>$migrations[$j])); |
||||
echo "The migration history is set at $originalVersion.\nNo actual migration was performed.\n"; |
||||
} |
||||
} |
||||
return; |
||||
} |
||||
} |
||||
|
||||
die("Error: Unable to find the version '$originalVersion'.\n"); |
||||
} |
||||
|
||||
public function actionHistory($args) |
||||
{ |
||||
$limit=isset($args[0]) ? (int)$args[0] : -1; |
||||
$migrations=$this->getMigrationHistory($limit); |
||||
if($migrations===array()) |
||||
echo "No migration has been done before.\n"; |
||||
else |
||||
{ |
||||
$n=count($migrations); |
||||
if($limit>0) |
||||
echo "Showing the last $n applied ".($n===1 ? 'migration' : 'migrations').":\n"; |
||||
else |
||||
echo "Total $n ".($n===1 ? 'migration has' : 'migrations have')." been applied before:\n"; |
||||
foreach($migrations as $version=>$time) |
||||
echo " (".date('Y-m-d H:i:s',$time).') '.$version."\n"; |
||||
} |
||||
} |
||||
|
||||
public function actionNew($args) |
||||
{ |
||||
$limit=isset($args[0]) ? (int)$args[0] : -1; |
||||
$migrations=$this->getNewMigrations(); |
||||
if($migrations===array()) |
||||
echo "No new migrations found. Your system is up-to-date.\n"; |
||||
else |
||||
{ |
||||
$n=count($migrations); |
||||
if($limit>0 && $n>$limit) |
||||
{ |
||||
$migrations=array_slice($migrations,0,$limit); |
||||
echo "Showing $limit out of $n new ".($n===1 ? 'migration' : 'migrations').":\n"; |
||||
} |
||||
else |
||||
echo "Found $n new ".($n===1 ? 'migration' : 'migrations').":\n"; |
||||
|
||||
foreach($migrations as $migration) |
||||
echo " ".$migration."\n"; |
||||
} |
||||
} |
||||
|
||||
public function actionCreate($args) |
||||
{ |
||||
if(isset($args[0])) |
||||
$name=$args[0]; |
||||
else |
||||
$this->usageError('Please provide the name of the new migration.'); |
||||
|
||||
if(!preg_match('/^\w+$/',$name)) |
||||
die("Error: The name of the migration must contain letters, digits and/or underscore characters only.\n"); |
||||
|
||||
$name='m'.gmdate('ymd_His').'_'.$name; |
||||
$content=strtr($this->getTemplate(), array('{ClassName}'=>$name)); |
||||
$file=$this->migrationPath.DIRECTORY_SEPARATOR.$name.'.php'; |
||||
|
||||
if($this->confirm("Create new migration '$file'?")) |
||||
{ |
||||
file_put_contents($file, $content); |
||||
echo "New migration created successfully.\n"; |
||||
} |
||||
} |
||||
|
||||
public function confirm($message) |
||||
{ |
||||
if(!$this->interactive) |
||||
return true; |
||||
return parent::confirm($message); |
||||
} |
||||
|
||||
protected function migrateUp($class) |
||||
{ |
||||
if($class===self::BASE_MIGRATION) |
||||
return; |
||||
|
||||
echo "*** applying $class\n"; |
||||
$start=microtime(true); |
||||
$migration=$this->instantiateMigration($class); |
||||
if($migration->up()!==false) |
||||
{ |
||||
$this->getDbConnection()->createCommand()->insert($this->migrationTable, array( |
||||
'version'=>$class, |
||||
'apply_time'=>time(), |
||||
)); |
||||
$time=microtime(true)-$start; |
||||
echo "*** applied $class (time: ".sprintf("%.3f",$time)."s)\n\n"; |
||||
} |
||||
else |
||||
{ |
||||
$time=microtime(true)-$start; |
||||
echo "*** failed to apply $class (time: ".sprintf("%.3f",$time)."s)\n\n"; |
||||
return false; |
||||
} |
||||
} |
||||
|
||||
protected function migrateDown($class) |
||||
{ |
||||
if($class===self::BASE_MIGRATION) |
||||
return; |
||||
|
||||
echo "*** reverting $class\n"; |
||||
$start=microtime(true); |
||||
$migration=$this->instantiateMigration($class); |
||||
if($migration->down()!==false) |
||||
{ |
||||
$db=$this->getDbConnection(); |
||||
$db->createCommand()->delete($this->migrationTable, $db->quoteColumnName('version').'=:version', array(':version'=>$class)); |
||||
$time=microtime(true)-$start; |
||||
echo "*** reverted $class (time: ".sprintf("%.3f",$time)."s)\n\n"; |
||||
} |
||||
else |
||||
{ |
||||
$time=microtime(true)-$start; |
||||
echo "*** failed to revert $class (time: ".sprintf("%.3f",$time)."s)\n\n"; |
||||
return false; |
||||
} |
||||
} |
||||
|
||||
protected function instantiateMigration($class) |
||||
{ |
||||
$file=$this->migrationPath.DIRECTORY_SEPARATOR.$class.'.php'; |
||||
require_once($file); |
||||
$migration=new $class; |
||||
$migration->setDbConnection($this->getDbConnection()); |
||||
return $migration; |
||||
} |
||||
|
||||
/** |
||||
* @var CDbConnection |
||||
*/ |
||||
private $_db; |
||||
protected function getDbConnection() |
||||
{ |
||||
if($this->_db!==null) |
||||
return $this->_db; |
||||
else if(($this->_db=Yii::app()->getComponent($this->connectionID)) instanceof CDbConnection) |
||||
return $this->_db; |
||||
else |
||||
die("Error: CMigrationCommand.connectionID '{$this->connectionID}' is invalid. Please make sure it refers to the ID of a CDbConnection application component.\n"); |
||||
} |
||||
|
||||
protected function getMigrationHistory($limit) |
||||
{ |
||||
$db=$this->getDbConnection(); |
||||
if($db->schema->getTable($this->migrationTable)===null) |
||||
{ |
||||
$this->createMigrationHistoryTable(); |
||||
} |
||||
return CHtml::listData($db->createCommand() |
||||
->select('version, apply_time') |
||||
->from($this->migrationTable) |
||||
->order('version DESC') |
||||
->limit($limit) |
||||
->queryAll(), 'version', 'apply_time'); |
||||
} |
||||
|
||||
protected function createMigrationHistoryTable() |
||||
{ |
||||
$db=$this->getDbConnection(); |
||||
echo 'Creating migration history table "'.$this->migrationTable.'"...'; |
||||
$db->createCommand()->createTable($this->migrationTable,array( |
||||
'version'=>'string NOT NULL PRIMARY KEY', |
||||
'apply_time'=>'integer', |
||||
)); |
||||
$db->createCommand()->insert($this->migrationTable,array( |
||||
'version'=>self::BASE_MIGRATION, |
||||
'apply_time'=>time(), |
||||
)); |
||||
echo "done.\n"; |
||||
} |
||||
|
||||
protected function getNewMigrations() |
||||
{ |
||||
$applied=array(); |
||||
foreach($this->getMigrationHistory(-1) as $version=>$time) |
||||
$applied[substr($version,1,13)]=true; |
||||
|
||||
$migrations=array(); |
||||
$handle=opendir($this->migrationPath); |
||||
while(($file=readdir($handle))!==false) |
||||
{ |
||||
if($file==='.' || $file==='..') |
||||
continue; |
||||
$path=$this->migrationPath.DIRECTORY_SEPARATOR.$file; |
||||
if(preg_match('/^(m(\d{6}_\d{6})_.*?)\.php$/',$file,$matches) && is_file($path) && !isset($applied[$matches[2]])) |
||||
$migrations[]=$matches[1]; |
||||
} |
||||
closedir($handle); |
||||
sort($migrations); |
||||
return $migrations; |
||||
} |
||||
|
||||
public function getHelp() |
||||
{ |
||||
return <<<EOD |
||||
USAGE |
||||
yiic migrate [action] [parameter] |
||||
|
||||
DESCRIPTION |
||||
This command provides support for database migrations. The optional |
||||
'action' parameter specifies which specific migration task to perform. |
||||
It can take these values: up, down, to, create, history, new, mark. |
||||
If the 'action' parameter is not given, it defaults to 'up'. |
||||
Each action takes different parameters. Their usage can be found in |
||||
the following examples. |
||||
|
||||
EXAMPLES |
||||
* yiic migrate |
||||
Applies ALL new migrations. This is equivalent to 'yiic migrate up'. |
||||
|
||||
* yiic migrate create create_user_table |
||||
Creates a new migration named 'create_user_table'. |
||||
|
||||
* yiic migrate up 3 |
||||
Applies the next 3 new migrations. |
||||
|
||||
* yiic migrate down |
||||
Reverts the last applied migration. |
||||
|
||||
* yiic migrate down 3 |
||||
Reverts the last 3 applied migrations. |
||||
|
||||
* yiic migrate to 101129_185401 |
||||
Migrates up or down to version 101129_185401. |
||||
|
||||
* yiic migrate mark 101129_185401 |
||||
Modifies the migration history up or down to version 101129_185401. |
||||
No actual migration will be performed. |
||||
|
||||
* yiic migrate history |
||||
Shows all previously applied migration information. |
||||
|
||||
* yiic migrate history 10 |
||||
Shows the last 10 applied migrations. |
||||
|
||||
* yiic migrate new |
||||
Shows all new migrations. |
||||
|
||||
* yiic migrate new 10 |
||||
Shows the next 10 migrations that have not been applied. |
||||
|
||||
EOD; |
||||
} |
||||
|
||||
protected function getTemplate() |
||||
{ |
||||
if($this->templateFile!==null) |
||||
return file_get_contents(Yii::getPathOfAlias($this->templateFile).'.php'); |
||||
else |
||||
return <<<EOD |
||||
<?php |
||||
|
||||
class {ClassName} extends CDbMigration |
||||
{ |
||||
public function up() |
||||
{ |
||||
} |
||||
|
||||
public function down() |
||||
{ |
||||
echo "{ClassName} does not support migration down.\\n"; |
||||
return false; |
||||
} |
||||
|
||||
/* |
||||
// Use safeUp/safeDown to do migration with transaction |
||||
public function safeUp() |
||||
{ |
||||
} |
||||
|
||||
public function safeDown() |
||||
{ |
||||
} |
||||
*/ |
||||
} |
||||
EOD; |
||||
} |
||||
} |
@ -0,0 +1,148 @@
|
||||
<?php |
||||
/** |
||||
* ShellCommand class file. |
||||
* |
||||
* @author Qiang Xue <qiang.xue@gmail.com> |
||||
* @link http://www.yiiframework.com/ |
||||
* @copyright Copyright © 2008-2011 Yii Software LLC |
||||
* @license http://www.yiiframework.com/license/ |
||||
* @version $Id$ |
||||
*/ |
||||
|
||||
/** |
||||
* ShellCommand executes the specified Web application and provides a shell for interaction. |
||||
* |
||||
* @property string $help The help information for the shell command. |
||||
* |
||||
* @author Qiang Xue <qiang.xue@gmail.com> |
||||
* @version $Id$ |
||||
* @package system.cli.commands |
||||
* @since 1.0 |
||||
*/ |
||||
class ShellCommand extends CConsoleCommand |
||||
{ |
||||
/** |
||||
* @return string the help information for the shell command |
||||
*/ |
||||
public function getHelp() |
||||
{ |
||||
return <<<EOD |
||||
USAGE |
||||
yiic shell [entry-script | config-file] |
||||
|
||||
DESCRIPTION |
||||
This command allows you to interact with a Web application |
||||
on the command line. It also provides tools to automatically |
||||
generate new controllers, views and data models. |
||||
|
||||
It is recommended that you execute this command under |
||||
the directory that contains the entry script file of |
||||
the Web application. |
||||
|
||||
PARAMETERS |
||||
* entry-script | config-file: optional, the path to |
||||
the entry script file or the configuration file for |
||||
the Web application. If not given, it is assumed to be |
||||
the 'index.php' file under the current directory. |
||||
|
||||
EOD; |
||||
} |
||||
|
||||
/** |
||||
* Execute the action. |
||||
* @param array $args command line parameters specific for this command |
||||
*/ |
||||
public function run($args) |
||||
{ |
||||
if(!isset($args[0])) |
||||
$args[0]='index.php'; |
||||
$entryScript=isset($args[0]) ? $args[0] : 'index.php'; |
||||
if(($entryScript=realpath($args[0]))===false || !is_file($entryScript)) |
||||
$this->usageError("{$args[0]} does not exist or is not an entry script file."); |
||||
|
||||
// fake the web server setting |
||||
$cwd=getcwd(); |
||||
chdir(dirname($entryScript)); |
||||
$_SERVER['SCRIPT_NAME']='/'.basename($entryScript); |
||||
$_SERVER['REQUEST_URI']=$_SERVER['SCRIPT_NAME']; |
||||
$_SERVER['SCRIPT_FILENAME']=$entryScript; |
||||
$_SERVER['HTTP_HOST']='localhost'; |
||||
$_SERVER['SERVER_NAME']='localhost'; |
||||
$_SERVER['SERVER_PORT']=80; |
||||
|
||||
// reset context to run the web application |
||||
restore_error_handler(); |
||||
restore_exception_handler(); |
||||
Yii::setApplication(null); |
||||
Yii::setPathOfAlias('application',null); |
||||
|
||||
ob_start(); |
||||
$config=require($entryScript); |
||||
ob_end_clean(); |
||||
|
||||
// oops, the entry script turns out to be a config file |
||||
if(is_array($config)) |
||||
{ |
||||
chdir($cwd); |
||||
$_SERVER['SCRIPT_NAME']='/index.php'; |
||||
$_SERVER['REQUEST_URI']=$_SERVER['SCRIPT_NAME']; |
||||
$_SERVER['SCRIPT_FILENAME']=$cwd.DIRECTORY_SEPARATOR.'index.php'; |
||||
Yii::createWebApplication($config); |
||||
} |
||||
|
||||
restore_error_handler(); |
||||
restore_exception_handler(); |
||||
|
||||
$yiiVersion=Yii::getVersion(); |
||||
echo <<<EOD |
||||
Yii Interactive Tool v1.1 (based on Yii v{$yiiVersion}) |
||||
Please type 'help' for help. Type 'exit' to quit. |
||||
EOD; |
||||
$this->runShell(); |
||||
} |
||||
|
||||
protected function runShell() |
||||
{ |
||||
// disable E_NOTICE so that the shell is more friendly |
||||
error_reporting(E_ALL ^ E_NOTICE); |
||||
|
||||
$_runner_=new CConsoleCommandRunner; |
||||
$_runner_->addCommands(dirname(__FILE__).'/shell'); |
||||
$_runner_->addCommands(Yii::getPathOfAlias('application.commands.shell')); |
||||
if(($_path_=@getenv('YIIC_SHELL_COMMAND_PATH'))!==false) |
||||
$_runner_->addCommands($_path_); |
||||
$_commands_=$_runner_->commands; |
||||
$log=Yii::app()->log; |
||||
|
||||
while(($_line_=$this->prompt("\n>>"))!==false) |
||||
{ |
||||
$_line_=trim($_line_); |
||||
if($_line_==='exit') |
||||
return; |
||||
try |
||||
{ |
||||
$_args_=preg_split('/[\s,]+/',rtrim($_line_,';'),-1,PREG_SPLIT_NO_EMPTY); |
||||
if(isset($_args_[0]) && isset($_commands_[$_args_[0]])) |
||||
{ |
||||
$_command_=$_runner_->createCommand($_args_[0]); |
||||
array_shift($_args_); |
||||
$_command_->init(); |
||||
$_command_->run($_args_); |
||||
} |
||||
else |
||||
echo eval($_line_.';'); |
||||
} |
||||
catch(Exception $e) |
||||
{ |
||||
if($e instanceof ShellException) |
||||
echo $e->getMessage(); |
||||
else |
||||
echo $e; |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
class ShellException extends CException |
||||
{ |
||||
} |
Loading…
Reference in new issue