diff options
| author | Patrick Seeger <pseeger@ccwn.org> | 2012-04-13 23:11:05 +0200 |
|---|---|---|
| committer | Patrick Seeger <pseeger@ccwn.org> | 2012-04-13 23:11:05 +0200 |
| commit | 341cc4dd9c53ffbfb863e026dd58549c1082c7a7 (patch) | |
| tree | 1bbbed20313bafb9b063b6b4d894fe580d8b000f /framework/cli/commands | |
Diffstat (limited to 'framework/cli/commands')
| -rw-r--r-- | framework/cli/commands/MessageCommand.php | 211 | ||||
| -rw-r--r-- | framework/cli/commands/MigrateCommand.php | 561 | ||||
| -rw-r--r-- | framework/cli/commands/ShellCommand.php | 148 | ||||
| -rw-r--r-- | framework/cli/commands/WebAppCommand.php | 129 | ||||
| -rw-r--r-- | framework/cli/commands/shell/ControllerCommand.php | 176 | ||||
| -rw-r--r-- | framework/cli/commands/shell/CrudCommand.php | 327 | ||||
| -rw-r--r-- | framework/cli/commands/shell/FormCommand.php | 123 | ||||
| -rw-r--r-- | framework/cli/commands/shell/HelpCommand.php | 78 | ||||
| -rw-r--r-- | framework/cli/commands/shell/ModelCommand.php | 488 | ||||
| -rw-r--r-- | framework/cli/commands/shell/ModuleCommand.php | 92 |
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 © 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 © 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 © 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 © 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 © 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 © 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 © 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 © 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 © 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 © 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 |
