1<?php 2/* 3 * $Id: Cli.php 2761 2007-10-07 23:42:29Z zYne $ 4 * 5 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 6 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 7 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 8 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 9 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 10 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 11 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 12 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 13 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 14 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 15 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 16 * 17 * This software consists of voluntary contributions made by many individuals 18 * and is licensed under the LGPL. For more information, see 19 * <http://www.doctrine-project.org>. 20 */ 21 22/** 23 * Command line interface class 24 * 25 * Interface for easily executing Doctrine_Task classes from a command line interface 26 * 27 * @package Doctrine 28 * @subpackage Cli 29 * @license http://www.opensource.org/licenses/lgpl-license.php LGPL 30 * @link www.doctrine-project.org 31 * @since 1.0 32 * @version $Revision: 2761 $ 33 * @author Jonathan H. Wage <jwage@mac.com> 34 */ 35class Doctrine_Cli 36{ 37 /** 38 * The name of the Doctrine Task base class 39 * 40 * @var string 41 */ 42 const TASK_BASE_CLASS = 'Doctrine_Task'; 43 44 /** 45 * @var string 46 */ 47 protected $_scriptName = null; 48 49 /** 50 * @var array 51 */ 52 private $_config; 53 54 /** 55 * @var object Doctrine_Cli_Formatter 56 */ 57 private $_formatter; 58 59 /** 60 * An array, keyed on class name, containing task instances 61 * 62 * @var array 63 */ 64 private $_registeredTask = array(); 65 66 /** 67 * @var object Doctrine_Task 68 */ 69 private $_taskInstance; 70 71 /** 72 * __construct 73 * 74 * @param array [$config=array()] 75 * @param object|null [$formatter=null] Doctrine_Cli_Formatter 76 */ 77 public function __construct(array $config = array(), Doctrine_Cli_Formatter $formatter = null) 78 { 79 $this->setConfig($config); 80 $this->setFormatter($formatter ? $formatter : new Doctrine_Cli_AnsiColorFormatter()); 81 $this->includeAndRegisterTaskClasses(); 82 } 83 84 /** 85 * @param array $config 86 */ 87 public function setConfig(array $config) 88 { 89 $this->_config = $config; 90 } 91 92 /** 93 * @return array 94 */ 95 public function getConfig() 96 { 97 return $this->_config; 98 } 99 100 /** 101 * @param object $formatter Doctrine_Cli_Formatter 102 */ 103 public function setFormatter(Doctrine_Cli_Formatter $formatter) 104 { 105 $this->_formatter = $formatter; 106 } 107 108 /** 109 * @return object Doctrine_Cli_Formatter 110 */ 111 public function getFormatter() 112 { 113 return $this->_formatter; 114 } 115 116 /** 117 * Returns the specified value from the config, or the default value, if specified 118 * 119 * @param string $name 120 * @return mixed 121 * @throws OutOfBoundsException If the element does not exist in the config 122 */ 123 public function getConfigValue($name/*, $defaultValue*/) 124 { 125 if (! isset($this->_config[$name])) { 126 if (func_num_args() > 1) { 127 return func_get_arg(1); 128 } 129 130 throw new OutOfBoundsException("The element \"{$name}\" does not exist in the config"); 131 } 132 133 return $this->_config[$name]; 134 } 135 136 /** 137 * Returns TRUE if the element in the config has the specified value, or FALSE otherwise 138 * 139 * If $value is not passed, this method will return TRUE if the specified element has _any_ value, or FALSE if the 140 * element is not set 141 * 142 * For strict checking, set $strict to TRUE - the default is FALSE 143 * 144 * @param string $name 145 * @param mixed [$value=null] 146 * @param bool [$strict=false] 147 * @return bool 148 */ 149 public function hasConfigValue($name, $value = null, $strict = false) 150 { 151 if (isset($this->_config[$name])) { 152 if (func_num_args() < 2) { 153 return true; 154 } 155 156 if ($strict) { 157 return $this->_config[$name] === $value; 158 } 159 160 return $this->_config[$name] == $value; 161 } 162 163 return false; 164 } 165 166 /** 167 * Sets the array of registered tasks 168 * 169 * @param array $registeredTask 170 */ 171 public function setRegisteredTasks(array $registeredTask) 172 { 173 $this->_registeredTask = $registeredTask; 174 } 175 176 /** 177 * Returns an array containing the registered tasks 178 * 179 * @return array 180 */ 181 public function getRegisteredTasks() 182 { 183 return $this->_registeredTask; 184 } 185 186 /** 187 * Returns TRUE if the specified Task-class is registered, or FALSE otherwise 188 * 189 * @param string $className 190 * @return bool 191 */ 192 public function taskClassIsRegistered($className) 193 { 194 return isset($this->_registeredTask[$className]); 195 } 196 197 /** 198 * Returns TRUE if a task with the specified name is registered, or FALSE otherwise 199 * 200 * If a matching task is found, $className is set with the name of the implementing class 201 * 202 * @param string $taskName 203 * @param string|null [&$className=null] 204 * @return bool 205 */ 206 public function taskNameIsRegistered($taskName, &$className = null) 207 { 208 foreach ($this->getRegisteredTasks() as $currClassName => $task) { 209 if ($task->getTaskName() == $taskName) { 210 $className = $currClassName; 211 return true; 212 } 213 } 214 215 return false; 216 } 217 218 /** 219 * @param object $task Doctrine_Task 220 */ 221 public function setTaskInstance(Doctrine_Task $task) 222 { 223 $this->_taskInstance = $task; 224 } 225 226 /** 227 * @return object Doctrine_Task 228 */ 229 public function getTaskInstance() 230 { 231 return $this->_taskInstance; 232 } 233 234 /** 235 * Called by the constructor, this method includes and registers Doctrine core Tasks and then registers all other 236 * loaded Task classes 237 * 238 * The second round of registering will pick-up loaded custom Tasks. Methods are provided that will allow users to 239 * register Tasks loaded after creating an instance of Doctrine_Cli. 240 */ 241 protected function includeAndRegisterTaskClasses() 242 { 243 $this->includeAndRegisterDoctrineTaskClasses(); 244 245 //Always autoregister custom tasks _unless_ we've been explicitly asked not to 246 if ($this->getConfigValue('autoregister_custom_tasks', true)) { 247 $this->registerIncludedTaskClasses(); 248 } 249 } 250 251 /** 252 * Includes and registers Doctrine-style tasks from the specified directory / directories 253 * 254 * If no directory is given it looks in the default Doctrine/Task folder for the core tasks 255 * 256 * @param mixed [$directories=null] Can be a string path or array of paths 257 */ 258 protected function includeAndRegisterDoctrineTaskClasses($directories = null) 259 { 260 if (is_null($directories)) { 261 $directories = Doctrine_Core::getPath() . DIRECTORY_SEPARATOR . 'Doctrine' . DIRECTORY_SEPARATOR . 'Task'; 262 } 263 264 foreach ((array) $directories as $directory) { 265 foreach ($this->includeDoctrineTaskClasses($directory) as $className) { 266 $this->registerTaskClass($className); 267 } 268 } 269 } 270 271 /** 272 * Attempts to include Doctrine-style Task-classes from the specified directory - and nothing more besides 273 * 274 * Returns an array containing the names of Task classes included 275 * 276 * This method effectively makes two assumptions: 277 * - The directory contains only _Task_ class-files 278 * - The class files, and the class in each, follow the Doctrine naming conventions 279 * 280 * This means that a file called "Foo.php", say, will be expected to contain a Task class called 281 * "Doctrine_Task_Foo". Hence the method's name, "include*Doctrine*TaskClasses". 282 * 283 * @param string $directory 284 * @return array $taskClassesIncluded 285 * @throws InvalidArgumentException If the directory does not exist 286 */ 287 protected function includeDoctrineTaskClasses($directory) 288 { 289 if (! is_dir($directory)) { 290 throw new InvalidArgumentException("The directory \"{$directory}\" does not exist"); 291 } 292 293 $taskClassesIncluded = array(); 294 295 $iterator = new RecursiveIteratorIterator( 296 new RecursiveDirectoryIterator($directory), 297 RecursiveIteratorIterator::LEAVES_ONLY 298 ); 299 300 foreach ($iterator as $file) { 301 $baseName = $file->getFileName(); 302 303 /* 304 * Class-files must start with an uppercase letter. This additional check will help prevent us 305 * accidentally running 'executable' scripts that may be mixed-in with the class files. 306 */ 307 $matched = (bool) preg_match('/^([A-Z].*?)\.php$/', $baseName, $matches); 308 309 if ( ! ($matched && (strpos($baseName, '.inc') === false))) { 310 continue; 311 } 312 313 $expectedClassName = self::TASK_BASE_CLASS . '_' . $matches[1]; 314 315 if ( ! class_exists($expectedClassName)) { 316 require_once($file->getPathName()); 317 } 318 319 //So was the expected class included, and is it a task? If so, we'll let the calling function know. 320 if (class_exists($expectedClassName, false) && $this->classIsTask($expectedClassName)) { 321 $taskClassesIncluded[] = $expectedClassName; 322 } 323 } 324 325 return $taskClassesIncluded; 326 } 327 328 /** 329 * Registers the specified _included_ task-class 330 * 331 * @param string $className 332 * @throws InvalidArgumentException If the class does not exist or the task-name is blank 333 * @throws DomainException If the class is not a Doctrine Task 334 */ 335 public function registerTaskClass($className) 336 { 337 //Simply ignore registered classes 338 if ($this->taskClassIsRegistered($className)) { 339 return; 340 } 341 342 if ( ! class_exists($className/*, false*/)) { 343 throw new InvalidArgumentException("The task class \"{$className}\" does not exist"); 344 } 345 346 if ( ! $this->classIsTask($className)) { 347 throw new DomainException("The class \"{$className}\" is not a Doctrine Task"); 348 } 349 350 $this->_registeredTask[$className] = $this->createTaskInstance($className, $this); 351 } 352 353 /** 354 * Returns TRUE if the specified class is a Task, or FALSE otherwise 355 * 356 * @param string $className 357 * @return bool 358 */ 359 protected function classIsTask($className) 360 { 361 $reflectionClass = new ReflectionClass($className); 362 return (bool) $reflectionClass->isSubclassOf(self::TASK_BASE_CLASS); 363 } 364 365 /** 366 * Creates, and returns, a new instance of the specified Task class 367 * 368 * Displays a message, and returns FALSE, if there were problems instantiating the class 369 * 370 * @param string $className 371 * @param object $cli Doctrine_Cli 372 * @return object Doctrine_Task 373 */ 374 protected function createTaskInstance($className, Doctrine_Cli $cli) 375 { 376 return new $className($cli); 377 } 378 379 /** 380 * Registers all loaded classes - by default - or the specified loaded Task classes 381 * 382 * This method will skip registered task classes, so it can be safely called many times over 383 */ 384 public function registerIncludedTaskClasses() 385 { 386 foreach (get_declared_classes() as $className) { 387 if ($this->classIsTask($className)) { 388 $this->registerTaskClass($className); 389 } 390 } 391 } 392 393 /** 394 * Notify the formatter of a message 395 * 396 * @param string $notification The notification message 397 * @param string $style Style to format the notification with(INFO, ERROR) 398 * @return void 399 */ 400 public function notify($notification = null, $style = 'HEADER') 401 { 402 $formatter = $this->getFormatter(); 403 404 echo( 405 $formatter->format($this->getTaskInstance()->getTaskName(), 'INFO') . ' - ' . 406 $formatter->format($notification, $style) . "\n" 407 ); 408 } 409 410 /** 411 * Formats, and then returns, the message in the specified exception 412 * 413 * @param Exception $exception 414 * @return string 415 */ 416 protected function formatExceptionMessage(Exception $exception) 417 { 418 $message = $exception->getMessage(); 419 420 if (Doctrine_Core::debug()) { 421 $message .= "\n" . $exception->getTraceAsString(); 422 } 423 424 return $this->getFormatter()->format($message, 'ERROR') . "\n"; 425 } 426 427 /** 428 * Notify the formatter of an exception 429 * 430 * N.B. This should really only be called by Doctrine_Cli::run(). Exceptions should be thrown when errors occur: 431 * it's up to Doctrine_Cli::run() to determine how those exceptions are reported. 432 * 433 * @param Exception $exception 434 * @return void 435 */ 436 protected function notifyException(Exception $exception) 437 { 438 echo $this->formatExceptionMessage($exception); 439 } 440 441 /** 442 * Public function to run the loaded task with the passed arguments 443 * 444 * @param array $args 445 * @return void 446 * @throws Doctrine_Cli_Exception 447 * @todo Should know more about what we're attempting to run so feedback can be improved. Continue refactoring. 448 */ 449 public function run(array $args) 450 { 451 try { 452 $this->_run($args); 453 } catch (Exception $exception) { 454 //Do not rethrow exceptions by default 455 if ($this->getConfigValue('rethrow_exceptions', false)) { 456 throw new $exception($this->formatExceptionMessage($exception)); 457 } 458 459 $this->notifyException($exception); 460 461 //User error 462 if ($exception instanceof Doctrine_Cli_Exception) { 463 $this->printTasks(); 464 } 465 } 466 } 467 468 /** 469 * Run the actual task execution with the passed arguments 470 * 471 * @param array $args Array of arguments for this task being executed 472 * @return void 473 * @throws Doctrine_Cli_Exception If the requested task has not been registered or if required arguments are missing 474 * @todo Continue refactoring for testing 475 */ 476 protected function _run(array $args) 477 { 478 $this->_scriptName = $args[0]; 479 480 $requestedTaskName = isset($args[1]) ? $args[1] : null; 481 482 if ( ! $requestedTaskName || $requestedTaskName == 'help') { 483 $this->printTasks(null, $requestedTaskName == 'help' ? true : false); 484 return; 485 } 486 487 if ($requestedTaskName && isset($args[2]) && $args[2] === 'help') { 488 $this->printTasks($requestedTaskName, true); 489 return; 490 } 491 492 if (! $this->taskNameIsRegistered($requestedTaskName, $taskClassName)) { 493 throw new Doctrine_Cli_Exception("The task \"{$requestedTaskName}\" has not been registered"); 494 } 495 496 $taskInstance = $this->createTaskInstance($taskClassName, $this); 497 $this->setTaskInstance($taskInstance); 498 $this->executeTask($taskInstance, $this->prepareArgs(array_slice($args, 2))); 499 } 500 501 /** 502 * Executes the task with the specified _prepared_ arguments 503 * 504 * @param object $task Doctrine_Task 505 * @param array $preparedArguments 506 * @throws Doctrine_Cli_Exception If required arguments are missing 507 */ 508 protected function executeTask(Doctrine_Task $task, array $preparedArguments) 509 { 510 $task->setArguments($preparedArguments); 511 512 if (! $task->validate()) { 513 throw new Doctrine_Cli_Exception('Required arguments missing'); 514 } 515 516 $task->execute(); 517 } 518 519 /** 520 * Prepare the raw arguments for execution. Combines with the required and optional argument 521 * list in order to determine a complete array of arguments for the task 522 * 523 * @param array $args Array of raw arguments 524 * @return array $prepared Array of prepared arguments 525 * @todo Continue refactoring for testing 526 */ 527 protected function prepareArgs(array $args) 528 { 529 $taskInstance = $this->getTaskInstance(); 530 531 $args = array_values($args); 532 533 // First lets load populate an array with all the possible arguments. required and optional 534 $prepared = array(); 535 536 $requiredArguments = $taskInstance->getRequiredArguments(); 537 foreach ($requiredArguments as $key => $arg) { 538 $prepared[$arg] = null; 539 } 540 541 $optionalArguments = $taskInstance->getOptionalArguments(); 542 foreach ($optionalArguments as $key => $arg) { 543 $prepared[$arg] = null; 544 } 545 546 // If we have a config array then lets try and fill some of the arguments with the config values 547 foreach ($this->getConfig() as $key => $value) { 548 if (array_key_exists($key, $prepared)) { 549 $prepared[$key] = $value; 550 } 551 } 552 553 // Now lets fill in the entered arguments to the prepared array 554 $copy = $args; 555 foreach ($prepared as $key => $value) { 556 if ( ! $value && !empty($copy)) { 557 $prepared[$key] = $copy[0]; 558 unset($copy[0]); 559 $copy = array_values($copy); 560 } 561 } 562 563 return $prepared; 564 } 565 566 /** 567 * Prints an index of all the available tasks in the CLI instance 568 * 569 * @param string|null [$taskName=null] 570 * @param bool [$full=false] 571 * @todo Continue refactoring for testing 572 */ 573 public function printTasks($taskName = null, $full = false) 574 { 575 $formatter = $this->getFormatter(); 576 $config = $this->getConfig(); 577 578 $taskIndex = $formatter->format('Doctrine Command Line Interface', 'HEADER') . "\n\n"; 579 580 foreach ($this->getRegisteredTasks() as $task) { 581 if ($taskName && (strtolower($taskName) != strtolower($task->getTaskName()))) { 582 continue; 583 } 584 585 $taskIndex .= $formatter->format($this->_scriptName . ' ' . $task->getTaskName(), 'INFO'); 586 587 if ($full) { 588 $taskIndex .= ' - ' . $task->getDescription() . "\n"; 589 590 $args = ''; 591 $args .= $this->assembleArgumentList($task->getRequiredArgumentsDescriptions(), $config, $formatter); 592 $args .= $this->assembleArgumentList($task->getOptionalArgumentsDescriptions(), $config, $formatter); 593 594 if ($args) { 595 $taskIndex .= "\n" . $formatter->format('Arguments:', 'HEADER') . "\n" . $args; 596 } 597 } 598 599 $taskIndex .= "\n"; 600 } 601 602 echo $taskIndex; 603 } 604 605 /** 606 * @param array $argumentsDescriptions 607 * @param array $config 608 * @param object $formatter Doctrine_Cli_Formatter 609 * @return string 610 */ 611 protected function assembleArgumentList(array $argumentsDescriptions, array $config, Doctrine_Cli_Formatter $formatter) 612 { 613 $argumentList = ''; 614 615 foreach ($argumentsDescriptions as $name => $description) { 616 $argumentList .= $formatter->format($name, 'ERROR') . ' - '; 617 618 if (isset($config[$name])) { 619 $argumentList .= $formatter->format($config[$name], 'COMMENT'); 620 } else { 621 $argumentList .= $description; 622 } 623 624 $argumentList .= "\n"; 625 } 626 627 return $argumentList; 628 } 629 630 /** 631 * Used by Doctrine_Cli::loadTasks() and Doctrine_Cli::getLoadedTasks() to re-create their pre-refactoring behaviour 632 * 633 * @ignore 634 * @param array $registeredTask 635 * @return array 636 */ 637 private function createOldStyleTaskList(array $registeredTask) 638 { 639 $taskNames = array(); 640 641 foreach ($registeredTask as $className => $task) { 642 $taskName = $task->getTaskName(); 643 $taskNames[$taskName] = $taskName; 644 } 645 646 return $taskNames; 647 } 648 649 /** 650 * Old method retained for backwards compatibility 651 * 652 * @deprecated 653 */ 654 public function loadTasks($directory = null) 655 { 656 $this->includeAndRegisterDoctrineTaskClasses($directory); 657 return $this->createOldStyleTaskList($this->getRegisteredTasks()); 658 } 659 660 /** 661 * Old method retained for backwards compatibility 662 * 663 * @deprecated 664 */ 665 protected function _getTaskClassFromArgs(array $args) 666 { 667 return self::TASK_BASE_CLASS . '_' . Doctrine_Inflector::classify(str_replace('-', '_', $args[1])); 668 } 669 670 /** 671 * Old method retained for backwards compatibility 672 * 673 * @deprecated 674 */ 675 public function getLoadedTasks() 676 { 677 return $this->createOldStyleTaskList($this->getRegisteredTasks()); 678 } 679}