Subversion-Projekte lars-tiefland.php_share

Revision

Details | Letzte Änderung | Log anzeigen | RSS feed

Revision Autor Zeilennr. Zeile
1 lars 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
 */
35
class 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
}