1
0

Added new (clean) yii boilerplate

This commit is contained in:
2014-05-13 12:40:42 +02:00
parent 1d6d975a16
commit 99d29b432b
1983 changed files with 653465 additions and 17 deletions

View File

@@ -0,0 +1,228 @@
<?php
/**
* MessageCommand class file.
*
* @author Qiang Xue <qiang.xue@gmail.com>
* @link http://www.yiiframework.com/
* @copyright 2008-2013 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>
* @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. Accepts both string for single function name or array for
multiple function names.
- 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 $args 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($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);
$messages=array();
if(!is_array($translator))
$translator=array($translator);
foreach ($translator as $currentTranslator)
{
$n=preg_match_all('/\b'.$currentTranslator.'\s*\(\s*(\'[\w.\/]*?(?<!\.)\'|"[\w.]*?(?<!\.)")\s*,\s*(\'.*?(?<!\\\\)\'|".*?(?<!\\\\)")\s*[,\)]/s',$subject,$matches,PREG_SET_ORDER);
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(array_key_exists($message,$translated) && strlen($translated[$message])>0)
$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.
*/
return $array;
EOD;
file_put_contents($fileName, $content);
}
}

View File

@@ -0,0 +1,585 @@
<?php
/**
* MigrateCommand class file.
*
* @author Qiang Xue <qiang.xue@gmail.com>
* @link http://www.yiiframework.com/
* @copyright 2008-2013 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.
*
* Since version 1.1.11 this command will exit with the following exit codes:
* <ul>
* <li>0 on success</li>
* <li>1 on general error</li>
* <li>2 on failed migration.</li>
* </ul>
*
* @author Qiang Xue <qiang.xue@gmail.com>
* @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))
{
echo 'Error: The migration directory does not exist: '.$this->migrationPath."\n";
exit(1);
}
$this->migrationPath=$path;
$yiiVersion=Yii::getVersion();
echo "\nYii Migration Tool v1.0 (based on Yii v{$yiiVersion})\n\n";
return parent::beforeAction($action,$params);
}
public function actionUp($args)
{
if(($migrations=$this->getNewMigrations())===array())
{
echo "No new migration found. Your system is up-to-date.\n";
return 0;
}
$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 2;
}
}
echo "\nMigrated up successfully.\n";
}
}
public function actionDown($args)
{
$step=isset($args[0]) ? (int)$args[0] : 1;
if($step<1)
{
echo "Error: The step parameter must be greater than 0.\n";
return 1;
}
if(($migrations=$this->getMigrationHistory($step))===array())
{
echo "No migration has been done before.\n";
return 0;
}
$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 2;
}
}
echo "\nMigrated down successfully.\n";
}
}
public function actionRedo($args)
{
$step=isset($args[0]) ? (int)$args[0] : 1;
if($step<1)
{
echo "Error: The step parameter must be greater than 0.\n";
return 1;
}
if(($migrations=$this->getMigrationHistory($step))===array())
{
echo "No migration has been done before.\n";
return 0;
}
$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 2;
}
}
foreach(array_reverse($migrations) as $migration)
{
if($this->migrateUp($migration)===false)
{
echo "\nMigration failed. All later migrations are canceled.\n";
return 2;
}
}
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
{
echo "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";
return 1;
}
// try migrate up
$migrations=$this->getNewMigrations();
foreach($migrations as $i=>$migration)
{
if(strpos($migration,$version.'_')===0)
return $this->actionUp(array($i+1));
}
// 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";
return 0;
}
else
return $this->actionDown(array($i));
}
}
echo "Error: Unable to find the version '$originalVersion'.\n";
return 1;
}
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 {
echo "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";
return 1;
}
$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 0;
}
}
// 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 0;
}
}
echo "Error: Unable to find the version '$originalVersion'.\n";
return 1;
}
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)) {
echo "Error: The name of the migration must contain letters, digits and/or underscore characters only.\n";
return 1;
}
$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,$default=false)
{
if(!$this->interactive)
return true;
return parent::confirm($message,$default);
}
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;
elseif(($this->_db=Yii::app()->getComponent($this->connectionID)) instanceof CDbConnection)
return $this->_db;
echo "Error: CMigrationCommand.connectionID '{$this->connectionID}' is invalid. Please make sure it refers to the ID of a CDbConnection application component.\n";
exit(1);
}
protected function getMigrationHistory($limit)
{
$db=$this->getDbConnection();
if($db->schema->getTable($this->migrationTable,true)===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;
}
}

View File

@@ -0,0 +1,146 @@
<?php
/**
* ShellCommand class file.
*
* @author Qiang Xue <qiang.xue@gmail.com>
* @link http://www.yiiframework.com/
* @copyright 2008-2013 Yii Software LLC
* @license http://www.yiiframework.com/license/
*/
/**
* 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>
* @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
{
}

View File

@@ -0,0 +1,213 @@
<?php
/**
* WebAppCommand class file.
*
* @author Qiang Xue <qiang.xue@gmail.com>
* @link http://www.yiiframework.com/
* @copyright 2008-2013 Yii Software LLC
* @license http://www.yiiframework.com/license/
*/
/**
* WebAppCommand creates an Yii Web application at the specified location.
*
* @author Qiang Xue <qiang.xue@gmail.com>
* @package system.cli.commands
* @since 1.0
*/
class WebAppCommand extends CConsoleCommand
{
private $_rootPath;
public function getHelp()
{
return <<<EOD
USAGE
yiic webapp <app-path> [<vcs>]
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.
* vcs: optional, version control system you're going to use in the new project.
Application generator will create all needed files to the specified VCS
(such as .gitignore, .gitkeep, etc.). Possible values: git, hg. Do not
use this argument if you're going to create VCS files yourself.
EOD;
}
/**
* Execute the action.
* @param array $args command line parameters specific for this command
*/
public function run($args)
{
$vcs=false;
if(isset($args[1]))
{
if($args[1]!='git' && $args[1]!='hg')
$this->usageError('Unsupported VCS specified. Currently only git and hg supported.');
$vcs=$args[1];
}
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;
if(basename($path)=='..')
$path.=DIRECTORY_SEPARATOR.'.';
$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=$this->getSourceDir();
if($sourceDir===false)
die("\nUnable to locate the source directory.\n");
$ignoreFiles=array();
$renameMap=array();
switch($vcs)
{
case 'git':
$renameMap=array('git-gitignore'=>'.gitignore','git-gitkeep'=>'.gitkeep'); // move with rename git files
$ignoreFiles=array('hg-hgignore','hg-hgkeep'); // ignore only hg files
break;
case 'hg':
$renameMap=array('hg-hgignore'=>'.hgignore','hg-hgkeep'=>'.hgkeep'); // move with rename hg files
$ignoreFiles=array('git-gitignore','git-gitkeep'); // ignore only git files
break;
default:
// no files for renaming
$ignoreFiles=array('git-gitignore','git-gitkeep','hg-hgignore','hg-hgkeep'); // ignore both git and hg files
break;
}
$list=$this->buildFileList($sourceDir,$path,'',$ignoreFiles,$renameMap);
$this->addFileModificationCallbacks($list);
$this->copyFiles($list);
$this->setPermissions($path);
echo "\nYour application has been created successfully under {$path}.\n";
}
}
/**
* Adjusts created application file and directory permissions
*
* @param string $targetDir path to created application
*/
protected function setPermissions($targetDir)
{
@chmod($targetDir.'/assets',0777);
@chmod($targetDir.'/protected/runtime',0777);
@chmod($targetDir.'/protected/data',0777);
@chmod($targetDir.'/protected/data/testdrive.db',0777);
@chmod($targetDir.'/protected/yiic',0755);
}
/**
* @return string path to application bootstrap source files
*/
protected function getSourceDir()
{
return realpath(dirname(__FILE__).'/../views/webapp');
}
/**
* Adds callbacks that will modify source files
*
* @param array $fileList
*/
protected function addFileModificationCallbacks(&$fileList)
{
$fileList['index.php']['callback']=array($this,'generateIndex');
$fileList['index-test.php']['callback']=array($this,'generateIndex');
$fileList['protected/tests/bootstrap.php']['callback']=array($this,'generateTestBoostrap');
$fileList['protected/yiic.php']['callback']=array($this,'generateYiic');
}
/**
* Inserts path to framework's yii.php into application's index.php
*
* @param string $source source file path
* @param array $params
* @return string modified source file content
*/
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);
}
/**
* Inserts path to framework's yiit.php into application's index-test.php
*
* @param string $source source file path
* @param array $params
* @return string modified source file 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);
}
/**
* Inserts path to framework's yiic.php into application's yiic.php
*
* @param string $source source file path
* @param array $params
* @return string modified source file 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);
}
/**
* Returns variant of $path1 relative to $path2
*
* @param string $path1
* @param string $path2
* @return string $path1 relative to $path2
*/
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).'\'';
}
}

View File

@@ -0,0 +1,175 @@
<?php
/**
* ControllerCommand class file.
*
* @author Qiang Xue <qiang.xue@gmail.com>
* @link http://www.yiiframework.com/
* @copyright 2008-2013 Yii Software LLC
* @license http://www.yiiframework.com/license/
*/
/**
* ControllerCommand generates a controller class.
*
* @author Qiang Xue <qiang.xue@gmail.com>
* @package system.cli.commands.shell
* @since 1.0
*/
class ControllerCommand extends CConsoleCommand
{
/**
* @var string the directory that contains templates for the model command.
* Defaults to null, meaning using 'framework/cli/views/shell/controller'.
* If you set this path and some views are missing in the directory,
* the default views will be used.
*/
public $templatePath;
public function getHelp()
{
return <<<EOD
USAGE
controller <controller-ID> [action-ID] ...
DESCRIPTION
This command generates a controller and views associated with
the specified actions.
PARAMETERS
* controller-ID: required, controller ID, e.g., 'post'.
If the controller should be located under a subdirectory,
please specify the controller ID as 'path/to/ControllerID',
e.g., 'admin/user'.
If the controller belongs to a module, please specify
the controller ID as 'ModuleID/ControllerID' or
'ModuleID/path/to/Controller' (assuming the controller is
under a subdirectory of that module).
* action-ID: optional, action ID. You may supply one or several
action IDs. A default 'index' action will always be generated.
EXAMPLES
* Generates the 'post' controller:
controller post
* Generates the 'post' controller with additional actions 'contact'
and 'about':
controller post contact about
* Generates the 'post' controller which should be located under
the 'admin' subdirectory of the base controller path:
controller admin/post
* Generates the 'post' controller which should belong to
the 'admin' module:
controller admin/post
NOTE: in the last two examples, the commands are the same, but
the generated controller file is located under different directories.
Yii is able to detect whether 'admin' refers to a module or a subdirectory.
EOD;
}
/**
* Execute the action.
* @param array $args command line parameters specific for this command
* @return integer|null non zero application exit code for help or null on success
*/
public function run($args)
{
if(!isset($args[0]))
{
echo "Error: controller name is required.\n";
echo $this->getHelp();
return 1;
}
$module=Yii::app();
$controllerID=$args[0];
if(($pos=strrpos($controllerID,'/'))===false)
{
$controllerClass=ucfirst($controllerID).'Controller';
$controllerFile=$module->controllerPath.DIRECTORY_SEPARATOR.$controllerClass.'.php';
$controllerID[0]=strtolower($controllerID[0]);
}
else
{
$last=substr($controllerID,$pos+1);
$last[0]=strtolower($last[0]);
$pos2=strpos($controllerID,'/');
$first=substr($controllerID,0,$pos2);
$middle=$pos===$pos2?'':substr($controllerID,$pos2+1,$pos-$pos2);
$controllerClass=ucfirst($last).'Controller';
$controllerFile=($middle===''?'':$middle.'/').$controllerClass.'.php';
$controllerID=$middle===''?$last:$middle.'/'.$last;
if(($m=Yii::app()->getModule($first))!==null)
$module=$m;
else
{
$controllerFile=$first.'/'.$controllerClass.'.php';
$controllerID=$first.'/'.$controllerID;
}
$controllerFile=$module->controllerPath.DIRECTORY_SEPARATOR.str_replace('/',DIRECTORY_SEPARATOR,$controllerFile);
}
$args[]='index';
$actions=array_unique(array_splice($args,1));
$templatePath=$this->templatePath===null?YII_PATH.'/cli/views/shell/controller':$this->templatePath;
$list=array(
basename($controllerFile)=>array(
'source'=>$templatePath.DIRECTORY_SEPARATOR.'controller.php',
'target'=>$controllerFile,
'callback'=>array($this,'generateController'),
'params'=>array($controllerClass, $actions),
),
);
$viewPath=$module->viewPath.DIRECTORY_SEPARATOR.str_replace('/',DIRECTORY_SEPARATOR,$controllerID);
foreach($actions as $name)
{
$list[$name.'.php']=array(
'source'=>$templatePath.DIRECTORY_SEPARATOR.'view.php',
'target'=>$viewPath.DIRECTORY_SEPARATOR.$name.'.php',
'callback'=>array($this,'generateAction'),
'params'=>array('controller'=>$controllerClass, 'action'=>$name),
);
}
$this->copyFiles($list);
if($module instanceof CWebModule)
$moduleID=$module->id.'/';
else
$moduleID='';
echo <<<EOD
Controller '{$controllerID}' has been created in the following file:
$controllerFile
You may access it in the browser using the following URL:
http://hostname/path/to/index.php?r={$moduleID}{$controllerID}
EOD;
}
public function generateController($source,$params)
{
if(!is_file($source)) // fall back to default ones
$source=YII_PATH.'/cli/views/shell/controller/'.basename($source);
return $this->renderFile($source,array('className'=>$params[0],'actions'=>$params[1]),true);
}
public function generateAction($source,$params)
{
if(!is_file($source)) // fall back to default ones
$source=YII_PATH.'/cli/views/shell/controller/'.basename($source);
return $this->renderFile($source,$params,true);
}
}

View File

@@ -0,0 +1,326 @@
<?php
/**
* CrudCommand class file.
*
* @author Qiang Xue <qiang.xue@gmail.com>
* @link http://www.yiiframework.com/
* @copyright 2008-2013 Yii Software LLC
* @license http://www.yiiframework.com/license/
*/
/**
* CrudCommand generates code implementing CRUD operations.
*
* @author Qiang Xue <qiang.xue@gmail.com>
* @package system.cli.commands.shell
* @since 1.0
*/
class CrudCommand extends CConsoleCommand
{
/**
* @var string the directory that contains templates for crud commands.
* Defaults to null, meaning using 'framework/cli/views/shell/crud'.
* If you set this path and some views are missing in the directory,
* the default views will be used.
*/
public $templatePath;
/**
* @var string the directory that contains functional test classes.
* Defaults to null, meaning using 'protected/tests/functional'.
* If this is false, it means functional test file should NOT be generated.
*/
public $functionalTestPath;
/**
* @var array list of actions to be created. Each action must be associated with a template file with the same name.
*/
public $actions=array('create','update','index','view','admin','_form','_view','_search');
public function getHelp()
{
return <<<EOD
USAGE
crud <model-class> [controller-ID] ...
DESCRIPTION
This command generates a controller and views that accomplish
CRUD operations for the specified data model.
PARAMETERS
* model-class: required, the name of the data model class. This can
also be specified as a path alias (e.g. application.models.Post).
If the model class belongs to a module, it should be specified
as 'ModuleID.models.ClassName'.
* controller-ID: optional, the controller ID (e.g. 'post').
If this is not specified, the model class name will be used
as the controller ID. In this case, if the model belongs to
a module, the controller will also be created under the same
module.
If the controller should be located under a subdirectory,
please specify the controller ID as 'path/to/ControllerID'
(e.g. 'admin/user').
If the controller belongs to a module (different from the module
that the model belongs to), please specify the controller ID
as 'ModuleID/ControllerID' or 'ModuleID/path/to/Controller'.
EXAMPLES
* Generates CRUD for the Post model:
crud Post
* Generates CRUD for the Post model which belongs to module 'admin':
crud admin.models.Post
* Generates CRUD for the Post model. The generated controller should
belong to module 'admin', but not the model class:
crud Post admin/post
EOD;
}
/**
* Execute the action.
* @param array $args command line parameters specific for this command
* @return integer|null non zero application exit code for help or null on success
*/
public function run($args)
{
if(!isset($args[0]))
{
echo "Error: data model class is required.\n";
echo $this->getHelp();
return 1;
}
$module=Yii::app();
$modelClass=$args[0];
if(($pos=strpos($modelClass,'.'))===false)
$modelClass='application.models.'.$modelClass;
else
{
$id=substr($modelClass,0,$pos);
if(($m=Yii::app()->getModule($id))!==null)
$module=$m;
}
$modelClass=Yii::import($modelClass);
if(isset($args[1]))
{
$controllerID=$args[1];
if(($pos=strrpos($controllerID,'/'))===false)
{
$controllerClass=ucfirst($controllerID).'Controller';
$controllerFile=$module->controllerPath.DIRECTORY_SEPARATOR.$controllerClass.'.php';
$controllerID[0]=strtolower($controllerID[0]);
}
else
{
$last=substr($controllerID,$pos+1);
$last[0]=strtolower($last);
$pos2=strpos($controllerID,'/');
$first=substr($controllerID,0,$pos2);
$middle=$pos===$pos2?'':substr($controllerID,$pos2+1,$pos-$pos2);
$controllerClass=ucfirst($last).'Controller';
$controllerFile=($middle===''?'':$middle.'/').$controllerClass.'.php';
$controllerID=$middle===''?$last:$middle.'/'.$last;
if(($m=Yii::app()->getModule($first))!==null)
$module=$m;
else
{
$controllerFile=$first.'/'.$controllerFile;
$controllerID=$first.'/'.$controllerID;
}
$controllerFile=$module->controllerPath.DIRECTORY_SEPARATOR.str_replace('/',DIRECTORY_SEPARATOR,$controllerFile);
}
}
else
{
$controllerID=$modelClass;
$controllerClass=ucfirst($controllerID).'Controller';
$controllerFile=$module->controllerPath.DIRECTORY_SEPARATOR.$controllerClass.'.php';
$controllerID[0]=strtolower($controllerID[0]);
}
$templatePath=$this->templatePath===null?YII_PATH.'/cli/views/shell/crud':$this->templatePath;
$functionalTestPath=$this->functionalTestPath===null?Yii::getPathOfAlias('application.tests.functional'):$this->functionalTestPath;
$viewPath=$module->viewPath.DIRECTORY_SEPARATOR.str_replace('.',DIRECTORY_SEPARATOR,$controllerID);
$fixtureName=$this->pluralize($modelClass);
$fixtureName[0]=strtolower($fixtureName);
$list=array(
basename($controllerFile)=>array(
'source'=>$templatePath.'/controller.php',
'target'=>$controllerFile,
'callback'=>array($this,'generateController'),
'params'=>array($controllerClass,$modelClass),
),
);
if($functionalTestPath!==false)
{
$list[$modelClass.'Test.php']=array(
'source'=>$templatePath.'/test.php',
'target'=>$functionalTestPath.DIRECTORY_SEPARATOR.$modelClass.'Test.php',
'callback'=>array($this,'generateTest'),
'params'=>array($controllerID,$fixtureName,$modelClass),
);
}
foreach($this->actions as $action)
{
$list[$action.'.php']=array(
'source'=>$templatePath.'/'.$action.'.php',
'target'=>$viewPath.'/'.$action.'.php',
'callback'=>array($this,'generateView'),
'params'=>$modelClass,
);
}
$this->copyFiles($list);
if($module instanceof CWebModule)
$moduleID=$module->id.'/';
else
$moduleID='';
echo "\nCrud '{$controllerID}' has been successfully created. You may access it via:\n";
echo "http://hostname/path/to/index.php?r={$moduleID}{$controllerID}\n";
}
public function generateController($source,$params)
{
list($controllerClass,$modelClass)=$params;
$model=CActiveRecord::model($modelClass);
$id=$model->tableSchema->primaryKey;
if($id===null)
throw new ShellException(Yii::t('yii','Error: Table "{table}" does not have a primary key.',array('{table}'=>$model->tableName())));
elseif(is_array($id))
throw new ShellException(Yii::t('yii','Error: Table "{table}" has a composite primary key which is not supported by crud command.',array('{table}'=>$model->tableName())));
if(!is_file($source)) // fall back to default ones
$source=YII_PATH.'/cli/views/shell/crud/'.basename($source);
return $this->renderFile($source,array(
'ID'=>$id,
'controllerClass'=>$controllerClass,
'modelClass'=>$modelClass,
),true);
}
public function generateView($source,$modelClass)
{
$model=CActiveRecord::model($modelClass);
$table=$model->getTableSchema();
$columns=$table->columns;
if(!is_file($source)) // fall back to default ones
$source=YII_PATH.'/cli/views/shell/crud/'.basename($source);
return $this->renderFile($source,array(
'ID'=>$table->primaryKey,
'modelClass'=>$modelClass,
'columns'=>$columns),true);
}
public function generateTest($source,$params)
{
list($controllerID,$fixtureName,$modelClass)=$params;
if(!is_file($source)) // fall back to default ones
$source=YII_PATH.'/cli/views/shell/crud/'.basename($source);
return $this->renderFile($source, array(
'controllerID'=>$controllerID,
'fixtureName'=>$fixtureName,
'modelClass'=>$modelClass,
),true);
}
public function generateInputLabel($modelClass,$column)
{
return "CHtml::activeLabelEx(\$model,'{$column->name}')";
}
public function generateInputField($modelClass,$column)
{
if($column->type==='boolean')
return "CHtml::activeCheckBox(\$model,'{$column->name}')";
elseif(stripos($column->dbType,'text')!==false)
return "CHtml::activeTextArea(\$model,'{$column->name}',array('rows'=>6, 'cols'=>50))";
else
{
if(preg_match('/^(password|pass|passwd|passcode)$/i',$column->name))
$inputField='activePasswordField';
else
$inputField='activeTextField';
if($column->type!=='string' || $column->size===null)
return "CHtml::{$inputField}(\$model,'{$column->name}')";
else
{
if(($size=$maxLength=$column->size)>60)
$size=60;
return "CHtml::{$inputField}(\$model,'{$column->name}',array('size'=>$size,'maxlength'=>$maxLength))";
}
}
}
public function generateActiveLabel($modelClass,$column)
{
return "\$form->labelEx(\$model,'{$column->name}')";
}
public function generateActiveField($modelClass,$column)
{
if($column->type==='boolean')
return "\$form->checkBox(\$model,'{$column->name}')";
elseif(stripos($column->dbType,'text')!==false)
return "\$form->textArea(\$model,'{$column->name}',array('rows'=>6, 'cols'=>50))";
else
{
if(preg_match('/^(password|pass|passwd|passcode)$/i',$column->name))
$inputField='passwordField';
else
$inputField='textField';
if($column->type!=='string' || $column->size===null)
return "\$form->{$inputField}(\$model,'{$column->name}')";
else
{
if(($size=$maxLength=$column->size)>60)
$size=60;
return "\$form->{$inputField}(\$model,'{$column->name}',array('size'=>$size,'maxlength'=>$maxLength))";
}
}
}
public function guessNameColumn($columns)
{
foreach($columns as $column)
{
if(!strcasecmp($column->name,'name'))
return $column->name;
}
foreach($columns as $column)
{
if(!strcasecmp($column->name,'title'))
return $column->name;
}
foreach($columns as $column)
{
if($column->isPrimaryKey)
return $column->name;
}
return 'id';
}
public function class2id($className)
{
return trim(strtolower(str_replace('_','-',preg_replace('/(?<![A-Z])[A-Z]/', '-\0', $className))),'-');
}
public function class2name($className,$pluralize=false)
{
if($pluralize)
$className=$this->pluralize($className);
return ucwords(trim(strtolower(str_replace(array('-','_'),' ',preg_replace('/(?<![A-Z])[A-Z]/', ' \0', $className)))));
}
}

View File

@@ -0,0 +1,122 @@
<?php
/**
* FormCommand class file.
*
* @author Qiang Xue <qiang.xue@gmail.com>
* @link http://www.yiiframework.com/
* @copyright 2008-2013 Yii Software LLC
* @license http://www.yiiframework.com/license/
*/
/**
* FormCommand generates a form view based on a specified model.
*
* @author Qiang Xue <qiang.xue@gmail.com>
* @package system.cli.commands.shell
* @since 1.0
*/
class FormCommand extends CConsoleCommand
{
/**
* @var string the directory that contains templates for the form command.
* Defaults to null, meaning using 'framework/cli/views/shell/form'.
* If you set this path and some views are missing in the directory,
* the default views will be used.
*/
public $templatePath;
public function getHelp()
{
return <<<EOD
USAGE
form <model-class> <view-name> [scenario]
DESCRIPTION
This command generates a form view that can be used to collect inputs
for the specified model.
PARAMETERS
* model-class: required, model class. This can be either the name of
the model class (e.g. 'ContactForm') or the path alias of the model
class file (e.g. 'application.models.ContactForm'). The former can
be used only if the class can be autoloaded.
* view-name: required, the name of the view to be generated. This should
be the path alias of the view script (e.g. 'application.views.site.contact').
* scenario: optional, the name of the scenario in which the model is used
(e.g. 'update', 'login'). This determines which model attributes the
generated form view will be used to collect user inputs for. If this
is not provided, the scenario will be assumed to be '' (empty string).
EXAMPLES
* Generates the view script for the 'ContactForm' model:
form ContactForm application.views.site.contact
EOD;
}
/**
* Execute the action.
* @param array $args command line parameters specific for this command
* @return integer|null non zero application exit code for help or null on success
*/
public function run($args)
{
if(!isset($args[0],$args[1]))
{
echo "Error: both model class and view name are required.\n";
echo $this->getHelp();
return 1;
}
$scenario=isset($args[2]) ? $args[2] : '';
$modelClass=Yii::import($args[0],true);
$model=new $modelClass($scenario);
$attributes=$model->getSafeAttributeNames();
$templatePath=$this->templatePath===null?YII_PATH.'/cli/views/shell/form':$this->templatePath;
$viewPath=Yii::getPathOfAlias($args[1]);
$viewName=basename($viewPath);
$viewPath.='.php';
$params=array(
'modelClass'=>$modelClass,
'viewName'=>$viewName,
'attributes'=>$attributes,
);
$list=array(
basename($viewPath)=>array(
'source'=>$templatePath.'/form.php',
'target'=>$viewPath,
'callback'=>array($this,'generateForm'),
'params'=>$params,
),
);
$this->copyFiles($list);
$actionFile=$templatePath.'/action.php';
if(!is_file($actionFile)) // fall back to default ones
$actionFile=YII_PATH.'/cli/views/shell/form/action.php';
echo "The following form view has been successfully created:\n";
echo "\t$viewPath\n\n";
echo "You may use the following code in your controller action:\n\n";
echo $this->renderFile($actionFile,$params,true);
echo "\n";
}
public function generateForm($source,$params)
{
if(!is_file($source)) // fall back to default ones
$source=YII_PATH.'/cli/views/shell/form/'.basename($source);
return $this->renderFile($source,$params,true);
}
public function class2id($className)
{
if(strrpos($className,'Form')===strlen($className)-4)
$className=substr($className,0,strlen($className)-4);
return trim(strtolower(str_replace('_','-',preg_replace('/(?<![A-Z])[A-Z]/', '-\0', $className))),'-');
}
}

View File

@@ -0,0 +1,78 @@
<?php
/**
* HelpCommand class file.
*
* @author Qiang Xue <qiang.xue@gmail.com>
* @link http://www.yiiframework.com/
* @copyright 2008-2013 Yii Software LLC
* @license http://www.yiiframework.com/license/
*/
/**
* HelpCommand displays help information for commands under yiic shell.
*
* @property string $help The command description.
*
* @author Qiang Xue <qiang.xue@gmail.com>
* @package system.cli.commands.shell
* @since 1.0
*/
class HelpCommand extends CConsoleCommand
{
/**
* Execute the action.
* @param array $args command line parameters specific for this command
* @return integer non zero application exit code for help
*/
public function run($args)
{
$runner=$this->getCommandRunner();
$commands=$runner->commands;
if(isset($args[0]))
$name=strtolower($args[0]);
if(!isset($args[0]) || !isset($commands[$name]))
{
echo <<<EOD
At the prompt, you may enter a PHP statement or one of the following commands:
EOD;
$commandNames=array_keys($commands);
sort($commandNames);
echo ' - '.implode("\n - ",$commandNames);
echo <<<EOD
Type 'help <command-name>' for details about a command.
To expand the above command list, place your command class files
under 'protected/commands/shell', or a directory specified
by the 'YIIC_SHELL_COMMAND_PATH' environment variable. The command class
must extend from CConsoleCommand.
EOD;
}
else
echo $runner->createCommand($name)->getHelp();
return 1;
}
/**
* Provides the command description.
* @return string the command description.
*/
public function getHelp()
{
return <<<EOD
USAGE
help [command-name]
DESCRIPTION
Display the help information for the specified command.
If the command name is not given, all commands will be listed.
PARAMETERS
* command-name: optional, the name of the command to show help information.
EOD;
}
}

View File

@@ -0,0 +1,488 @@
<?php
/**
* ModelCommand class file.
*
* @author Qiang Xue <qiang.xue@gmail.com>
* @link http://www.yiiframework.com/
* @copyright 2008-2013 Yii Software LLC
* @license http://www.yiiframework.com/license/
*/
/**
* ModelCommand generates a model class.
*
* @author Qiang Xue <qiang.xue@gmail.com>
* @package system.cli.commands.shell
* @since 1.0
*/
class ModelCommand extends CConsoleCommand
{
/**
* @var string the directory that contains templates for the model command.
* Defaults to null, meaning using 'framework/cli/views/shell/model'.
* If you set this path and some views are missing in the directory,
* the default views will be used.
*/
public $templatePath;
/**
* @var string the directory that contains test fixtures.
* Defaults to null, meaning using 'protected/tests/fixtures'.
* If this is false, it means fixture file should NOT be generated.
*/
public $fixturePath;
/**
* @var string the directory that contains unit test classes.
* Defaults to null, meaning using 'protected/tests/unit'.
* If this is false, it means unit test file should NOT be generated.
*/
public $unitTestPath;
private $_schema;
private $_relations; // where we keep table relations
private $_tables;
private $_classes;
public function getHelp()
{
return <<<EOD
USAGE
model <class-name> [table-name]
DESCRIPTION
This command generates a model class with the specified class name.
PARAMETERS
* class-name: required, model class name. By default, the generated
model class file will be placed under the directory aliased as
'application.models'. To override this default, specify the class
name in terms of a path alias, e.g., 'application.somewhere.ClassName'.
If the model class belongs to a module, it should be specified
as 'ModuleID.models.ClassName'.
If the class name ends with '*', then a model class will be generated
for EVERY table in the database.
If the class name contains a regular expression deliminated by slashes,
then a model class will be generated for those tables whose name
matches the regular expression. If the regular expression contains
sub-patterns, the first sub-pattern will be used to generate the model
class name.
* table-name: optional, the associated database table name. If not given,
it is assumed to be the model class name.
Note, when the class name ends with '*', this parameter will be
ignored.
EXAMPLES
* Generates the Post model:
model Post
* Generates the Post model which is associated with table 'posts':
model Post posts
* Generates the Post model which should belong to module 'admin':
model admin.models.Post
* Generates a model class for every table in the current database:
model *
* Same as above, but the model class files should be generated
under 'protected/models2':
model application.models2.*
* Generates a model class for every table whose name is prefixed
with 'tbl_' in the current database. The model class will not
contain the table prefix.
model /^tbl_(.*)$/
* Same as above, but the model class files should be generated
under 'protected/models2':
model application.models2./^tbl_(.*)$/
EOD;
}
/**
* Checks if the given table is a "many to many" helper table.
* Their PK has 2 fields, and both of those fields are also FK to other separate tables.
* @param CDbTableSchema $table table to inspect
* @return boolean true if table matches description of helper table.
*/
protected function isRelationTable($table)
{
$pk=$table->primaryKey;
return (count($pk) === 2 // we want 2 columns
&& isset($table->foreignKeys[$pk[0]]) // pk column 1 is also a foreign key
&& isset($table->foreignKeys[$pk[1]]) // pk column 2 is also a foreign key
&& $table->foreignKeys[$pk[0]][0] !== $table->foreignKeys[$pk[1]][0]); // and the foreign keys point different tables
}
/**
* Generate code to put in ActiveRecord class's relations() function.
* @return array indexed by table names, each entry contains array of php code to go in appropriate ActiveRecord class.
* Empty array is returned if database couldn't be connected.
*/
protected function generateRelations()
{
$this->_relations=array();
$this->_classes=array();
foreach($this->_schema->getTables() as $table)
{
$tableName=$table->name;
if ($this->isRelationTable($table))
{
$pks=$table->primaryKey;
$fks=$table->foreignKeys;
$table0=$fks[$pks[1]][0];
$table1=$fks[$pks[0]][0];
$className0=$this->getClassName($table0);
$className1=$this->getClassName($table1);
$unprefixedTableName=$this->removePrefix($tableName,true);
$relationName=$this->generateRelationName($table0, $table1, true);
$this->_relations[$className0][$relationName]="array(self::MANY_MANY, '$className1', '$unprefixedTableName($pks[0], $pks[1])')";
$relationName=$this->generateRelationName($table1, $table0, true);
$this->_relations[$className1][$relationName]="array(self::MANY_MANY, '$className0', '$unprefixedTableName($pks[0], $pks[1])')";
}
else
{
$this->_classes[$tableName]=$className=$this->getClassName($tableName);
foreach ($table->foreignKeys as $fkName => $fkEntry)
{
// Put table and key name in variables for easier reading
$refTable=$fkEntry[0]; // Table name that current fk references to
$refKey=$fkEntry[1]; // Key in that table being referenced
$refClassName=$this->getClassName($refTable);
// Add relation for this table
$relationName=$this->generateRelationName($tableName, $fkName, false);
$this->_relations[$className][$relationName]="array(self::BELONGS_TO, '$refClassName', '$fkName')";
// Add relation for the referenced table
$relationType=$table->primaryKey === $fkName ? 'HAS_ONE' : 'HAS_MANY';
$relationName=$this->generateRelationName($refTable, $this->removePrefix($tableName), $relationType==='HAS_MANY');
$this->_relations[$refClassName][$relationName]="array(self::$relationType, '$className', '$fkName')";
}
}
}
}
protected function getClassName($tableName)
{
return isset($this->_tables[$tableName]) ? $this->_tables[$tableName] : $this->generateClassName($tableName);
}
/**
* Generates model class name based on a table name
* @param string $tableName the table name
* @return string the generated model class name
*/
protected function generateClassName($tableName)
{
return str_replace(' ','',
ucwords(
trim(
strtolower(
str_replace(array('-','_'),' ',
preg_replace('/(?<![A-Z])[A-Z]/', ' \0', $tableName))))));
}
/**
* Generates the mapping table between table names and class names.
* @param CDbSchema $schema the database schema
* @param string $pattern a regular expression that may be used to filter table names
*/
protected function generateClassNames($schema,$pattern=null)
{
$this->_tables=array();
foreach($schema->getTableNames() as $name)
{
if($pattern===null)
$this->_tables[$name]=$this->generateClassName($this->removePrefix($name));
elseif(preg_match($pattern,$name,$matches))
{
if(count($matches)>1 && !empty($matches[1]))
$className=$this->generateClassName($matches[1]);
else
$className=$this->generateClassName($matches[0]);
$this->_tables[$name]=empty($className) ? $name : $className;
}
}
}
/**
* Generate a name for use as a relation name (inside relations() function in a model).
* @param string $tableName the name of the table to hold the relation
* @param string $fkName the foreign key name
* @param boolean $multiple whether the relation would contain multiple objects
* @return string the generated relation name
*/
protected function generateRelationName($tableName, $fkName, $multiple)
{
if(strcasecmp(substr($fkName,-2),'id')===0 && strcasecmp($fkName,'id'))
$relationName=rtrim(substr($fkName, 0, -2),'_');
else
$relationName=$fkName;
$relationName[0]=strtolower($relationName);
$rawName=$relationName;
if($multiple)
$relationName=$this->pluralize($relationName);
$table=$this->_schema->getTable($tableName);
$i=0;
while(isset($table->columns[$relationName]))
$relationName=$rawName.($i++);
return $relationName;
}
/**
* Execute the action.
* @param array $args command line parameters specific for this command
* @return integer|null non zero application exit code for help or null on success
*/
public function run($args)
{
if(!isset($args[0]))
{
echo "Error: model class name is required.\n";
echo $this->getHelp();
return 1;
}
$className=$args[0];
if(($db=Yii::app()->getDb())===null)
{
echo "Error: an active 'db' connection is required.\n";
echo "If you already added 'db' component in application configuration,\n";
echo "please quit and re-enter the yiic shell.\n";
return 1;
}
$db->active=true;
$this->_schema=$db->schema;
if(!preg_match('/^[\w\.\-\*]*(.*?)$/',$className,$matches))
{
echo "Error: model class name is invalid.\n";
return 1;
}
if(empty($matches[1])) // without regular expression
{
$this->generateClassNames($this->_schema);
if(($pos=strrpos($className,'.'))===false)
$basePath=Yii::getPathOfAlias('application.models');
else
{
$basePath=Yii::getPathOfAlias(substr($className,0,$pos));
$className=substr($className,$pos+1);
}
if($className==='*') // generate all models
$this->generateRelations();
else
{
$tableName=isset($args[1])?$args[1]:$className;
$tableName=$this->addPrefix($tableName);
$this->_tables[$tableName]=$className;
$this->generateRelations();
$this->_classes=array($tableName=>$className);
}
}
else // with regular expression
{
$pattern=$matches[1];
$pos=strrpos($className,$pattern);
if($pos>0) // only regexp is given
$basePath=Yii::getPathOfAlias(rtrim(substr($className,0,$pos),'.'));
else
$basePath=Yii::getPathOfAlias('application.models');
$this->generateClassNames($this->_schema,$pattern);
$classes=$this->_tables;
$this->generateRelations();
$this->_classes=$classes;
}
if(count($this->_classes)>1)
{
$entries=array();
$count=0;
foreach($this->_classes as $tableName=>$className)
$entries[]=++$count.". $className ($tableName)";
echo "The following model classes (tables) match your criteria:\n";
echo implode("\n",$entries)."\n\n";
if(!$this->confirm("Do you want to generate the above classes?"))
return;
}
$templatePath=$this->templatePath===null?YII_PATH.'/cli/views/shell/model':$this->templatePath;
$fixturePath=$this->fixturePath===null?Yii::getPathOfAlias('application.tests.fixtures'):$this->fixturePath;
$unitTestPath=$this->unitTestPath===null?Yii::getPathOfAlias('application.tests.unit'):$this->unitTestPath;
$list=array();
$files=array();
foreach ($this->_classes as $tableName=>$className)
{
$files[$className]=$classFile=$basePath.DIRECTORY_SEPARATOR.$className.'.php';
$list['models/'.$className.'.php']=array(
'source'=>$templatePath.DIRECTORY_SEPARATOR.'model.php',
'target'=>$classFile,
'callback'=>array($this,'generateModel'),
'params'=>array($className,$tableName),
);
if($fixturePath!==false)
{
$list['fixtures/'.$tableName.'.php']=array(
'source'=>$templatePath.DIRECTORY_SEPARATOR.'fixture.php',
'target'=>$fixturePath.DIRECTORY_SEPARATOR.$tableName.'.php',
'callback'=>array($this,'generateFixture'),
'params'=>$this->_schema->getTable($tableName),
);
}
if($unitTestPath!==false)
{
$fixtureName=$this->pluralize($className);
$fixtureName[0]=strtolower($fixtureName);
$list['unit/'.$className.'Test.php']=array(
'source'=>$templatePath.DIRECTORY_SEPARATOR.'test.php',
'target'=>$unitTestPath.DIRECTORY_SEPARATOR.$className.'Test.php',
'callback'=>array($this,'generateTest'),
'params'=>array($className,$fixtureName),
);
}
}
$this->copyFiles($list);
foreach($files as $className=>$file)
{
if(!class_exists($className,false))
include_once($file);
}
$classes=implode(", ", $this->_classes);
echo <<<EOD
The following model classes are successfully generated:
$classes
If you have a 'db' database connection, you can test these models now with:
\$model={$className}::model()->find();
print_r(\$model);
EOD;
}
public function generateModel($source,$params)
{
list($className,$tableName)=$params;
$rules=array();
$labels=array();
$relations=array();
if(($table=$this->_schema->getTable($tableName))!==null)
{
$required=array();
$integers=array();
$numerical=array();
$length=array();
$safe=array();
foreach($table->columns as $column)
{
$label=ucwords(trim(strtolower(str_replace(array('-','_'),' ',preg_replace('/(?<![A-Z])[A-Z]/', ' \0', $column->name)))));
$label=preg_replace('/\s+/',' ',$label);
if(strcasecmp(substr($label,-3),' id')===0)
$label=substr($label,0,-3);
$labels[$column->name]=$label;
if($column->isPrimaryKey && $table->sequenceName!==null)
continue;
$r=!$column->allowNull && $column->defaultValue===null;
if($r)
$required[]=$column->name;
if($column->type==='integer')
$integers[]=$column->name;
elseif($column->type==='double')
$numerical[]=$column->name;
elseif($column->type==='string' && $column->size>0)
$length[$column->size][]=$column->name;
elseif(!$column->isPrimaryKey && !$r)
$safe[]=$column->name;
}
if($required!==array())
$rules[]="array('".implode(', ',$required)."', 'required')";
if($integers!==array())
$rules[]="array('".implode(', ',$integers)."', 'numerical', 'integerOnly'=>true)";
if($numerical!==array())
$rules[]="array('".implode(', ',$numerical)."', 'numerical')";
if($length!==array())
{
foreach($length as $len=>$cols)
$rules[]="array('".implode(', ',$cols)."', 'length', 'max'=>$len)";
}
if($safe!==array())
$rules[]="array('".implode(', ',$safe)."', 'safe')";
if(isset($this->_relations[$className]) && is_array($this->_relations[$className]))
$relations=$this->_relations[$className];
}
else
echo "Warning: the table '$tableName' does not exist in the database.\n";
if(!is_file($source)) // fall back to default ones
$source=YII_PATH.'/cli/views/shell/model/'.basename($source);
return $this->renderFile($source,array(
'className'=>$className,
'tableName'=>$this->removePrefix($tableName,true),
'columns'=>isset($table) ? $table->columns : array(),
'rules'=>$rules,
'labels'=>$labels,
'relations'=>$relations,
),true);
}
public function generateFixture($source,$table)
{
if(!is_file($source)) // fall back to default ones
$source=YII_PATH.'/cli/views/shell/model/'.basename($source);
return $this->renderFile($source, array(
'table'=>$table,
),true);
}
public function generateTest($source,$params)
{
list($className,$fixtureName)=$params;
if(!is_file($source)) // fall back to default ones
$source=YII_PATH.'/cli/views/shell/model/'.basename($source);
return $this->renderFile($source, array(
'className'=>$className,
'fixtureName'=>$fixtureName,
),true);
}
protected function removePrefix($tableName,$addBrackets=false)
{
$tablePrefix=Yii::app()->getDb()->tablePrefix;
if($tablePrefix!='' && !strncmp($tableName,$tablePrefix,strlen($tablePrefix)))
{
$tableName=substr($tableName,strlen($tablePrefix));
if($addBrackets)
$tableName='{{'.$tableName.'}}';
}
return $tableName;
}
protected function addPrefix($tableName)
{
$tablePrefix=Yii::app()->getDb()->tablePrefix;
if($tablePrefix!='' && strncmp($tableName,$tablePrefix,strlen($tablePrefix)))
$tableName=$tablePrefix.$tableName;
return $tableName;
}
}

View File

@@ -0,0 +1,93 @@
<?php
/**
* ModuleCommand class file.
*
* @author Qiang Xue <qiang.xue@gmail.com>
* @link http://www.yiiframework.com/
* @copyright 2008-2013 Yii Software LLC
* @license http://www.yiiframework.com/license/
* @version $Id: ModuleCommand.php 433 2008-12-30 22:59:17Z qiang.xue $
*/
/**
* ModuleCommand generates a controller class.
*
* @author Qiang Xue <qiang.xue@gmail.com>
* @version $Id: ModuleCommand.php 433 2008-12-30 22:59:17Z qiang.xue $
* @package system.cli.commands.shell
*/
class ModuleCommand extends CConsoleCommand
{
/**
* @var string the directory that contains templates for the module command.
* Defaults to null, meaning using 'framework/cli/views/shell/module'.
* If you set this path and some views are missing in the directory,
* the default views will be used.
*/
public $templatePath;
public function getHelp()
{
return <<<EOD
USAGE
module <module-ID>
DESCRIPTION
This command generates an application module.
PARAMETERS
* module-ID: required, module ID. It is case-sensitive.
EOD;
}
/**
* Execute the action.
* @param array $args command line parameters specific for this command
* @return integer|null non zero application exit code for help or null on success
*/
public function run($args)
{
if(!isset($args[0]))
{
echo "Error: module ID is required.\n";
echo $this->getHelp();
return 1;
}
$moduleID=$args[0];
$moduleClass=ucfirst($moduleID).'Module';
$modulePath=Yii::app()->getModulePath().DIRECTORY_SEPARATOR.$moduleID;
$sourceDir=$this->templatePath===null?YII_PATH.'/cli/views/shell/module':$this->templatePath;
$list=$this->buildFileList($sourceDir,$modulePath);
$list['module.php']['target']=$modulePath.DIRECTORY_SEPARATOR.$moduleClass.'.php';
$list['module.php']['callback']=array($this,'generateModuleClass');
$list['module.php']['params']=array(
'moduleClass'=>$moduleClass,
'moduleID'=>$moduleID,
);
$list[$moduleClass.'.php']=$list['module.php'];
unset($list['module.php']);
$this->copyFiles($list);
echo <<<EOD
Module '{$moduleID}' has been created under the following folder:
$modulePath
You may access it in the browser using the following URL:
http://hostname/path/to/index.php?r=$moduleID
Note, the module needs to be installed first by adding '{$moduleID}'
to the 'modules' property in the application configuration.
EOD;
}
public function generateModuleClass($source,$params)
{
return $this->renderFile($source,$params,true);
}
}