summaryrefslogtreecommitdiff
path: root/framework/cli/commands
diff options
context:
space:
mode:
authorPatrick Seeger <pseeger@ccwn.org>2012-04-13 23:11:05 +0200
committerPatrick Seeger <pseeger@ccwn.org>2012-04-13 23:11:05 +0200
commit341cc4dd9c53ffbfb863e026dd58549c1082c7a7 (patch)
tree1bbbed20313bafb9b063b6b4d894fe580d8b000f /framework/cli/commands
yii-framework 1.1.10 hinzugefügtHEADmaster
Diffstat (limited to 'framework/cli/commands')
-rw-r--r--framework/cli/commands/MessageCommand.php211
-rw-r--r--framework/cli/commands/MigrateCommand.php561
-rw-r--r--framework/cli/commands/ShellCommand.php148
-rw-r--r--framework/cli/commands/WebAppCommand.php129
-rw-r--r--framework/cli/commands/shell/ControllerCommand.php176
-rw-r--r--framework/cli/commands/shell/CrudCommand.php327
-rw-r--r--framework/cli/commands/shell/FormCommand.php123
-rw-r--r--framework/cli/commands/shell/HelpCommand.php78
-rw-r--r--framework/cli/commands/shell/ModelCommand.php488
-rw-r--r--framework/cli/commands/shell/ModuleCommand.php92
10 files changed, 2333 insertions, 0 deletions
diff --git a/framework/cli/commands/MessageCommand.php b/framework/cli/commands/MessageCommand.php
new file mode 100644
index 0000000..410e275
--- /dev/null
+++ b/framework/cli/commands/MessageCommand.php
@@ -0,0 +1,211 @@
+<?php
+/**
+ * MessageCommand class file.
+ *
+ * @author Qiang Xue <qiang.xue@gmail.com>
+ * @link http://www.yiiframework.com/
+ * @copyright Copyright &copy; 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: MessageCommand.php 3394 2011-09-14 21:31:30Z alexander.makarow $
+ * @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.
+
+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;
+
+ $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);
+ }
+ }
+ }
+
+ 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)
+ {
+ 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)
+ $todo[$message]='@@'.$translation.'@@';
+ }
+ $merged=array_merge($todo,$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);
+ }
+} \ No newline at end of file
diff --git a/framework/cli/commands/MigrateCommand.php b/framework/cli/commands/MigrateCommand.php
new file mode 100644
index 0000000..5111b9a
--- /dev/null
+++ b/framework/cli/commands/MigrateCommand.php
@@ -0,0 +1,561 @@
+<?php
+/**
+ * MigrateCommand class file.
+ *
+ * @author Qiang Xue <qiang.xue@gmail.com>
+ * @link http://www.yiiframework.com/
+ * @copyright Copyright &copy; 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: MigrateCommand.php 3514 2011-12-27 20:28:26Z alexander.makarow $
+ * @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;
+ }
+}
diff --git a/framework/cli/commands/ShellCommand.php b/framework/cli/commands/ShellCommand.php
new file mode 100644
index 0000000..e90d9bf
--- /dev/null
+++ b/framework/cli/commands/ShellCommand.php
@@ -0,0 +1,148 @@
+<?php
+/**
+ * ShellCommand class file.
+ *
+ * @author Qiang Xue <qiang.xue@gmail.com>
+ * @link http://www.yiiframework.com/
+ * @copyright Copyright &copy; 2008-2011 Yii Software LLC
+ * @license http://www.yiiframework.com/license/
+ * @version $Id: ShellCommand.php 3477 2011-12-06 22:33:37Z alexander.makarow $
+ */
+
+/**
+ * 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: ShellCommand.php 3477 2011-12-06 22:33:37Z alexander.makarow $
+ * @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
+{
+} \ No newline at end of file
diff --git a/framework/cli/commands/WebAppCommand.php b/framework/cli/commands/WebAppCommand.php
new file mode 100644
index 0000000..f5a94f1
--- /dev/null
+++ b/framework/cli/commands/WebAppCommand.php
@@ -0,0 +1,129 @@
+<?php
+/**
+ * WebAppCommand class file.
+ *
+ * @author Qiang Xue <qiang.xue@gmail.com>
+ * @link http://www.yiiframework.com/
+ * @copyright Copyright &copy; 2008-2011 Yii Software LLC
+ * @license http://www.yiiframework.com/license/
+ * @version $Id: WebAppCommand.php 3477 2011-12-06 22:33:37Z alexander.makarow $
+ */
+
+/**
+ * WebAppCommand creates an Yii Web application at the specified location.
+ *
+ * @author Qiang Xue <qiang.xue@gmail.com>
+ * @version $Id: WebAppCommand.php 3477 2011-12-06 22:33:37Z alexander.makarow $
+ * @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).'\'';
+ }
+} \ No newline at end of file
diff --git a/framework/cli/commands/shell/ControllerCommand.php b/framework/cli/commands/shell/ControllerCommand.php
new file mode 100644
index 0000000..f7447b6
--- /dev/null
+++ b/framework/cli/commands/shell/ControllerCommand.php
@@ -0,0 +1,176 @@
+<?php
+/**
+ * ControllerCommand class file.
+ *
+ * @author Qiang Xue <qiang.xue@gmail.com>
+ * @link http://www.yiiframework.com/
+ * @copyright Copyright &copy; 2008-2011 Yii Software LLC
+ * @license http://www.yiiframework.com/license/
+ * @version $Id: ControllerCommand.php 2799 2011-01-01 19:31:13Z qiang.xue $
+ */
+
+/**
+ * ControllerCommand generates a controller class.
+ *
+ * @author Qiang Xue <qiang.xue@gmail.com>
+ * @version $Id: ControllerCommand.php 2799 2011-01-01 19:31:13Z qiang.xue $
+ * @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 command line parameters specific for this command
+ */
+ public function run($args)
+ {
+ if(!isset($args[0]))
+ {
+ echo "Error: controller name is required.\n";
+ echo $this->getHelp();
+ return;
+ }
+
+ $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);
+ }
+} \ No newline at end of file
diff --git a/framework/cli/commands/shell/CrudCommand.php b/framework/cli/commands/shell/CrudCommand.php
new file mode 100644
index 0000000..5932dea
--- /dev/null
+++ b/framework/cli/commands/shell/CrudCommand.php
@@ -0,0 +1,327 @@
+<?php
+/**
+ * CrudCommand class file.
+ *
+ * @author Qiang Xue <qiang.xue@gmail.com>
+ * @link http://www.yiiframework.com/
+ * @copyright Copyright &copy; 2008-2011 Yii Software LLC
+ * @license http://www.yiiframework.com/license/
+ * @version $Id: CrudCommand.php 2799 2011-01-01 19:31:13Z qiang.xue $
+ */
+
+/**
+ * CrudCommand generates code implementing CRUD operations.
+ *
+ * @author Qiang Xue <qiang.xue@gmail.com>
+ * @version $Id: CrudCommand.php 2799 2011-01-01 19:31:13Z qiang.xue $
+ * @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 command line parameters specific for this command
+ */
+ public function run($args)
+ {
+ if(!isset($args[0]))
+ {
+ echo "Error: data model class is required.\n";
+ echo $this->getHelp();
+ return;
+ }
+ $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())));
+ else if(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}')";
+ else if(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}')";
+ else if(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)))));
+ }
+}
diff --git a/framework/cli/commands/shell/FormCommand.php b/framework/cli/commands/shell/FormCommand.php
new file mode 100644
index 0000000..c3f064f
--- /dev/null
+++ b/framework/cli/commands/shell/FormCommand.php
@@ -0,0 +1,123 @@
+<?php
+/**
+ * FormCommand class file.
+ *
+ * @author Qiang Xue <qiang.xue@gmail.com>
+ * @link http://www.yiiframework.com/
+ * @copyright Copyright &copy; 2008-2011 Yii Software LLC
+ * @license http://www.yiiframework.com/license/
+ * @version $Id: FormCommand.php 2799 2011-01-01 19:31:13Z qiang.xue $
+ */
+
+/**
+ * FormCommand generates a form view based on a specified model.
+ *
+ * @author Qiang Xue <qiang.xue@gmail.com>
+ * @version $Id: FormCommand.php 2799 2011-01-01 19:31:13Z qiang.xue $
+ * @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 command line parameters specific for this command
+ */
+ 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;
+ }
+ $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))),'-');
+ }
+} \ No newline at end of file
diff --git a/framework/cli/commands/shell/HelpCommand.php b/framework/cli/commands/shell/HelpCommand.php
new file mode 100644
index 0000000..8007373
--- /dev/null
+++ b/framework/cli/commands/shell/HelpCommand.php
@@ -0,0 +1,78 @@
+<?php
+/**
+ * HelpCommand class file.
+ *
+ * @author Qiang Xue <qiang.xue@gmail.com>
+ * @link http://www.yiiframework.com/
+ * @copyright Copyright &copy; 2008-2011 Yii Software LLC
+ * @license http://www.yiiframework.com/license/
+ * @version $Id: HelpCommand.php 3426 2011-10-25 00:01:09Z alexander.makarow $
+ */
+
+/**
+ * HelpCommand displays help information for commands under yiic shell.
+ *
+ * @property string $help The command description.
+ *
+ * @author Qiang Xue <qiang.xue@gmail.com>
+ * @version $Id: HelpCommand.php 3426 2011-10-25 00:01:09Z alexander.makarow $
+ * @package system.cli.commands.shell
+ * @since 1.0
+ */
+class HelpCommand extends CConsoleCommand
+{
+ /**
+ * Execute the action.
+ * @param array command line parameters specific for this command
+ */
+ 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();
+ }
+
+ /**
+ * 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;
+ }
+} \ No newline at end of file
diff --git a/framework/cli/commands/shell/ModelCommand.php b/framework/cli/commands/shell/ModelCommand.php
new file mode 100644
index 0000000..cfd8c21
--- /dev/null
+++ b/framework/cli/commands/shell/ModelCommand.php
@@ -0,0 +1,488 @@
+<?php
+/**
+ * ModelCommand class file.
+ *
+ * @author Qiang Xue <qiang.xue@gmail.com>
+ * @link http://www.yiiframework.com/
+ * @copyright Copyright &copy; 2008-2011 Yii Software LLC
+ * @license http://www.yiiframework.com/license/
+ * @version $Id: ModelCommand.php 3477 2011-12-06 22:33:37Z alexander.makarow $
+ */
+
+/**
+ * ModelCommand generates a model class.
+ *
+ * @author Qiang Xue <qiang.xue@gmail.com>
+ * @version $Id: ModelCommand.php 3477 2011-12-06 22:33:37Z alexander.makarow $
+ * @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 to inspect
+ * @return boolean true if table matches description of helpter 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 foriegn 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 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 the database schema
+ * @param string 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));
+ else if(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 the name of the table to hold the relation
+ * @param string the foreign key name
+ * @param boolean whether the relation would contain multiple objects
+ */
+ 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 command line parameters specific for this command
+ */
+ public function run($args)
+ {
+ if(!isset($args[0]))
+ {
+ echo "Error: model class name is required.\n";
+ echo $this->getHelp();
+ return;
+ }
+ $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;
+ }
+
+ $db->active=true;
+ $this->_schema=$db->schema;
+
+ if(!preg_match('/^[\w\.\-\*]*(.*?)$/',$className,$matches))
+ {
+ echo "Error: model class name is invalid.\n";
+ return;
+ }
+
+ 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;
+ else if($column->type==='double')
+ $numerical[]=$column->name;
+ else if($column->type==='string' && $column->size>0)
+ $length[$column->size][]=$column->name;
+ else if(!$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;
+ }
+} \ No newline at end of file
diff --git a/framework/cli/commands/shell/ModuleCommand.php b/framework/cli/commands/shell/ModuleCommand.php
new file mode 100644
index 0000000..51a2158
--- /dev/null
+++ b/framework/cli/commands/shell/ModuleCommand.php
@@ -0,0 +1,92 @@
+<?php
+/**
+ * ModuleCommand class file.
+ *
+ * @author Qiang Xue <qiang.xue@gmail.com>
+ * @link http://www.yiiframework.com/
+ * @copyright Copyright &copy; 2008-2011 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 command line parameters specific for this command
+ */
+ public function run($args)
+ {
+ if(!isset($args[0]))
+ {
+ echo "Error: module ID is required.\n";
+ echo $this->getHelp();
+ return;
+ }
+
+ $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);
+ }
+} \ No newline at end of file