Subversion-Projekte lars-tiefland.php_share

Revision

Details | Letzte Änderung | Log anzeigen | RSS feed

Revision Autor Zeilennr. Zeile
1 lars 1
<?php
2
 
3
/*
4
 * This file is part of the symfony package.
5
 * (c) 2004-2006 Fabien Potencier <fabien.potencier@symfony-project.com>
6
 *
7
 * For the full copyright and license information, please view the LICENSE
8
 * file that was distributed with this source code.
9
 */
10
 
11
/**
12
 * sfCommandApplication manages the lifecycle of a CLI application.
13
 *
14
 * @package    symfony
15
 * @subpackage command
16
 * @author     Fabien Potencier <fabien.potencier@symfony-project.com>
17
 * @version    SVN: $Id: sfCommandApplication.class.php 23218 2009-10-20 20:59:02Z FabianLange $
18
 */
19
abstract class sfCommandApplication
20
{
21
  protected
22
    $commandManager = null,
23
    $trace          = false,
24
    $verbose        = true,
25
    $nowrite        = false,
26
    $name           = 'UNKNOWN',
27
    $version        = 'UNKNOWN',
28
    $tasks          = array(),
29
    $currentTask    = null,
30
    $dispatcher     = null,
31
    $options        = array(),
32
    $formatter      = null;
33
 
34
  /**
35
   * Constructor.
36
   *
37
   * @param sfEventDispatcher $dispatcher   A sfEventDispatcher instance
38
   * @param sfFormatter       $formatter    A sfFormatter instance
39
   * @param array             $options      An array of options
40
   */
41
  public function __construct(sfEventDispatcher $dispatcher, sfFormatter $formatter = null, $options = array())
42
  {
43
    $this->dispatcher = $dispatcher;
44
    $this->formatter = null === $formatter ? $this->guessBestFormatter(STDOUT) : $formatter;
45
    $this->options = $options;
46
 
47
    $this->fixCgi();
48
 
49
    $argumentSet = new sfCommandArgumentSet(array(
50
      new sfCommandArgument('task', sfCommandArgument::REQUIRED, 'The task to execute'),
51
    ));
52
    $optionSet = new sfCommandOptionSet(array(
53
      new sfCommandOption('--help',    '-H', sfCommandOption::PARAMETER_NONE, 'Display this help message.'),
54
      new sfCommandOption('--quiet',   '-q', sfCommandOption::PARAMETER_NONE, 'Do not log messages to standard output.'),
55
      new sfCommandOption('--trace',   '-t', sfCommandOption::PARAMETER_NONE, 'Turn on invoke/execute tracing, enable full backtrace.'),
56
      new sfCommandOption('--version', '-V', sfCommandOption::PARAMETER_NONE, 'Display the program version.'),
57
      new sfCommandOption('--color',   '',   sfCommandOption::PARAMETER_NONE, 'Forces ANSI color output.'),
58
    ));
59
    $this->commandManager = new sfCommandManager($argumentSet, $optionSet);
60
 
61
    $this->configure();
62
 
63
    $this->registerTasks();
64
  }
65
 
66
  /**
67
   * Configures the current command application.
68
   */
69
  abstract public function configure();
70
 
71
  /**
72
   * Returns the value of a given option.
73
   *
74
   * @param  string  $name  The option name
75
   *
76
   * @return mixed  The option value
77
   */
78
  public function getOption($name)
79
  {
80
    return isset($this->options[$name]) ? $this->options[$name] : null;
81
  }
82
 
83
  /**
84
   * Returns the formatter instance.
85
   *
86
   * @return sfFormatter The formatter instance
87
   */
88
  public function getFormatter()
89
  {
90
    return $this->formatter;
91
  }
92
 
93
  /**
94
   * Sets the formatter instance.
95
   *
96
   * @param sfFormatter The formatter instance
97
   */
98
  public function setFormatter(sfFormatter $formatter)
99
  {
100
    $this->formatter = $formatter;
101
 
102
    foreach ($this->getTasks() as $task)
103
    {
104
      $task->setFormatter($formatter);
105
    }
106
  }
107
 
108
  public function clearTasks()
109
  {
110
    $this->tasks = array();
111
  }
112
 
113
  /**
114
   * Registers an array of task objects.
115
   *
116
   * If you pass null, this method will register all available tasks.
117
   *
118
   * @param array  $tasks  An array of tasks
119
   */
120
  public function registerTasks($tasks = null)
121
  {
122
    if (null === $tasks)
123
    {
124
      $tasks = $this->autodiscoverTasks();
125
    }
126
 
127
    foreach ($tasks as $task)
128
    {
129
      $this->registerTask($task);
130
    }
131
  }
132
 
133
  /**
134
   * Registers a task object.
135
   *
136
   * @param sfTask $task An sfTask object
137
   */
138
  public function registerTask(sfTask $task)
139
  {
140
    if (isset($this->tasks[$task->getFullName()]))
141
    {
142
      throw new sfCommandException(sprintf('The task named "%s" in "%s" task is already registered by the "%s" task.', $task->getFullName(), get_class($task), get_class($this->tasks[$task->getFullName()])));
143
    }
144
 
145
    $this->tasks[$task->getFullName()] = $task;
146
 
147
    foreach ($task->getAliases() as $alias)
148
    {
149
      if (isset($this->tasks[$alias]))
150
      {
151
        throw new sfCommandException(sprintf('A task named "%s" is already registered.', $alias));
152
      }
153
 
154
      $this->tasks[$alias] = $task;
155
    }
156
  }
157
 
158
  /**
159
   * Autodiscovers task classes.
160
   *
161
   * @return array An array of tasks instances
162
   */
163
  public function autodiscoverTasks()
164
  {
165
    $tasks = array();
166
    foreach (get_declared_classes() as $class)
167
    {
168
      $r = new ReflectionClass($class);
169
 
170
      if ($r->isSubclassOf('sfTask') && !$r->isAbstract())
171
      {
172
        $tasks[] = new $class($this->dispatcher, $this->formatter);
173
      }
174
    }
175
 
176
    return $tasks;
177
  }
178
 
179
  /**
180
   * Returns all registered tasks.
181
   *
182
   * @return array An array of sfTask objects
183
   */
184
  public function getTasks()
185
  {
186
    return $this->tasks;
187
  }
188
 
189
  /**
190
   * Returns a registered task by name or alias.
191
   *
192
   * @param string $name The task name or alias
193
   *
194
   * @return sfTask An sfTask object
195
   */
196
  public function getTask($name)
197
  {
198
    if (!isset($this->tasks[$name]))
199
    {
200
      throw new sfCommandException(sprintf('The task "%s" does not exist.', $name));
201
    }
202
 
203
    return $this->tasks[$name];
204
  }
205
 
206
  /**
207
   * Runs the current application.
208
   *
209
   * @param mixed $options The command line options
210
   *
211
   * @return integer 0 if everything went fine, or an error code
212
   */
213
  public function run($options = null)
214
  {
215
    $this->handleOptions($options);
216
    $arguments = $this->commandManager->getArgumentValues();
217
 
218
    $this->currentTask = $this->getTaskToExecute($arguments['task']);
219
 
220
    $ret = $this->currentTask->runFromCLI($this->commandManager, $this->commandOptions);
221
 
222
    $this->currentTask = null;
223
 
224
    return $ret;
225
  }
226
 
227
  /**
228
   * Gets the name of the application.
229
   *
230
   * @return string The application name
231
   */
232
  public function getName()
233
  {
234
    return $this->name;
235
  }
236
 
237
  /**
238
   * Sets the application name.
239
   *
240
   * @param string $name The application name
241
   */
242
  public function setName($name)
243
  {
244
    $this->name = $name;
245
  }
246
 
247
  /**
248
   * Gets the application version.
249
   *
250
   * @return string The application version
251
   */
252
  public function getVersion()
253
  {
254
    return $this->version;
255
  }
256
 
257
  /**
258
   * Sets the application version.
259
   *
260
   * @param string $version The application version
261
   */
262
  public function setVersion($version)
263
  {
264
    $this->version = $version;
265
  }
266
 
267
  /**
268
   * Returns the long version of the application.
269
   *
270
   * @return string The long application version
271
   */
272
  public function getLongVersion()
273
  {
274
    return sprintf('%s version %s', $this->getName(), $this->formatter->format($this->getVersion(), 'INFO'))."\n";
275
  }
276
 
277
  /**
278
   * Returns whether the application must be verbose.
279
   *
280
   * @return Boolean true if the application must be verbose, false otherwise
281
   */
282
  public function isVerbose()
283
  {
284
    return $this->verbose;
285
  }
286
 
287
  /**
288
   * Returns whether the application must activate the trace.
289
   *
290
   * @return Boolean true if the application must activate the trace, false otherwise
291
   */
292
  public function withTrace()
293
  {
294
    return $this->trace;
295
  }
296
 
297
  /**
298
   * Outputs a help message for the current application.
299
   */
300
  public function help()
301
  {
302
    $messages = array(
303
      $this->formatter->format('Usage:', 'COMMENT'),
304
      sprintf("  %s [options] task_name [arguments]\n", $this->getName()),
305
      $this->formatter->format('Options:', 'COMMENT'),
306
    );
307
 
308
    foreach ($this->commandManager->getOptionSet()->getOptions() as $option)
309
    {
310
      $messages[] = sprintf('  %-24s %s  %s',
311
        $this->formatter->format('--'.$option->getName(), 'INFO'),
312
        $option->getShortcut() ? $this->formatter->format('-'.$option->getShortcut(), 'INFO') : '  ',
313
        $option->getHelp()
314
      );
315
    }
316
 
317
    $this->dispatcher->notify(new sfEvent($this, 'command.log', $messages));
318
  }
319
 
320
  /**
321
   * Parses and handles command line options.
322
   *
323
   * @param mixed $options The command line options
324
   */
325
  protected function handleOptions($options = null)
326
  {
327
    $this->commandManager->process($options);
328
    $this->commandOptions = $options;
329
 
330
    // the order of option processing matters
331
 
332
    if ($this->commandManager->getOptionSet()->hasOption('color') && false !== $this->commandManager->getOptionValue('color'))
333
    {
334
      $this->setFormatter(new sfAnsiColorFormatter());
335
    }
336
 
337
    if ($this->commandManager->getOptionSet()->hasOption('quiet') && false !== $this->commandManager->getOptionValue('quiet'))
338
    {
339
      $this->verbose = false;
340
    }
341
 
342
    if ($this->commandManager->getOptionSet()->hasOption('trace') && false !== $this->commandManager->getOptionValue('trace'))
343
    {
344
      $this->verbose = true;
345
      $this->trace   = true;
346
    }
347
 
348
    if ($this->commandManager->getOptionSet()->hasOption('help') && false !== $this->commandManager->getOptionValue('help'))
349
    {
350
      $this->help();
351
      exit(0);
352
    }
353
 
354
    if ($this->commandManager->getOptionSet()->hasOption('version') && false !== $this->commandManager->getOptionValue('version'))
355
    {
356
      echo $this->getLongVersion();
357
      exit(0);
358
    }
359
  }
360
 
361
  /**
362
   * Renders an exception.
363
   *
364
   * @param Exception $e An exception object
365
   */
366
  public function renderException($e)
367
  {
368
    $title = sprintf('  [%s]  ', get_class($e));
369
    $len = $this->strlen($title);
370
    $lines = array();
371
    foreach (explode("\n", $e->getMessage()) as $line)
372
    {
373
      $lines[] = sprintf('  %s  ', $line);
374
      $len = max($this->strlen($line) + 4, $len);
375
    }
376
 
377
    $messages = array(str_repeat(' ', $len));
378
 
379
    if ($this->trace)
380
    {
381
      $messages[] = $title.str_repeat(' ', $len - $this->strlen($title));
382
    }
383
 
384
    foreach ($lines as $line)
385
    {
386
      $messages[] = $line.str_repeat(' ', $len - $this->strlen($line));
387
    }
388
 
389
    $messages[] = str_repeat(' ', $len);
390
 
391
    fwrite(STDERR, "\n");
392
    foreach ($messages as $message)
393
    {
394
      fwrite(STDERR, $this->formatter->format($message, 'ERROR', STDERR)."\n");
395
    }
396
    fwrite(STDERR, "\n");
397
 
398
    if (null !== $this->currentTask && $e instanceof sfCommandArgumentsException)
399
    {
400
      fwrite(STDERR, $this->formatter->format(sprintf($this->currentTask->getSynopsis(), $this->getName()), 'INFO', STDERR)."\n");
401
      fwrite(STDERR, "\n");
402
    }
403
 
404
    if ($this->trace)
405
    {
406
      fwrite(STDERR, $this->formatter->format("Exception trace:\n", 'COMMENT'));
407
 
408
      // exception related properties
409
      $trace = $e->getTrace();
410
      array_unshift($trace, array(
411
        'function' => '',
412
        'file'     => $e->getFile() != null ? $e->getFile() : 'n/a',
413
        'line'     => $e->getLine() != null ? $e->getLine() : 'n/a',
414
        'args'     => array(),
415
      ));
416
 
417
      for ($i = 0, $count = count($trace); $i < $count; $i++)
418
      {
419
        $class = isset($trace[$i]['class']) ? $trace[$i]['class'] : '';
420
        $type = isset($trace[$i]['type']) ? $trace[$i]['type'] : '';
421
        $function = $trace[$i]['function'];
422
        $file = isset($trace[$i]['file']) ? $trace[$i]['file'] : 'n/a';
423
        $line = isset($trace[$i]['line']) ? $trace[$i]['line'] : 'n/a';
424
 
425
        fwrite(STDERR, sprintf(" %s%s%s at %s:%s\n", $class, $type, $function, $this->formatter->format($file, 'INFO', STDERR), $this->formatter->format($line, 'INFO', STDERR)));
426
      }
427
 
428
      fwrite(STDERR, "\n");
429
    }
430
  }
431
 
432
  /**
433
   * Gets a task from a task name or a shortcut.
434
   *
435
   * @param  string  $name  The task name or a task shortcut
436
   *
437
   * @return sfTask A sfTask object
438
   */
439
  public function getTaskToExecute($name)
440
  {
441
    // namespace
442
    if (false !== $pos = strpos($name, ':'))
443
    {
444
      $namespace = substr($name, 0, $pos);
445
      $name = substr($name, $pos + 1);
446
 
447
      $namespaces = array();
448
      foreach ($this->tasks as $task)
449
      {
450
        if ($task->getNamespace() && !in_array($task->getNamespace(), $namespaces))
451
        {
452
          $namespaces[] = $task->getNamespace();
453
        }
454
      }
455
      $abbrev = $this->getAbbreviations($namespaces);
456
 
457
      if (!isset($abbrev[$namespace]))
458
      {
459
        throw new sfCommandException(sprintf('There are no tasks defined in the "%s" namespace.', $namespace));
460
      }
461
      else if (count($abbrev[$namespace]) > 1)
462
      {
463
        throw new sfCommandException(sprintf('The namespace "%s" is ambiguous (%s).', $namespace, implode(', ', $abbrev[$namespace])));
464
      }
465
      else
466
      {
467
        $namespace = $abbrev[$namespace][0];
468
      }
469
    }
470
    else
471
    {
472
      $namespace = '';
473
    }
474
 
475
    // name
476
    $tasks = array();
477
    foreach ($this->tasks as $taskName => $task)
478
    {
479
      if ($taskName == $task->getFullName() && $task->getNamespace() == $namespace)
480
      {
481
        $tasks[] = $task->getName();
482
      }
483
    }
484
 
485
    $abbrev = $this->getAbbreviations($tasks);
486
    if (isset($abbrev[$name]) && count($abbrev[$name]) == 1)
487
    {
488
      return $this->getTask($namespace ? $namespace.':'.$abbrev[$name][0] : $abbrev[$name][0]);
489
    }
490
 
491
    // aliases
492
    $aliases = array();
493
    foreach ($this->tasks as $taskName => $task)
494
    {
495
      if ($taskName == $task->getFullName())
496
      {
497
        foreach ($task->getAliases() as $alias)
498
        {
499
          $aliases[] = $alias;
500
        }
501
      }
502
    }
503
 
504
    $abbrev = $this->getAbbreviations($aliases);
505
    $fullName = $namespace ? $namespace.':'.$name : $name;
506
    if (!isset($abbrev[$fullName]))
507
    {
508
      throw new sfCommandException(sprintf('Task "%s" is not defined.', $fullName));
509
    }
510
    else if (count($abbrev[$fullName]) > 1)
511
    {
512
      throw new sfCommandException(sprintf('Task "%s" is ambiguous (%s).', $fullName, implode(', ', $abbrev[$fullName])));
513
    }
514
    else
515
    {
516
      return $this->getTask($abbrev[$fullName][0]);
517
    }
518
  }
519
 
520
  protected function strlen($string)
521
  {
522
    return function_exists('mb_strlen') ? mb_strlen($string) : strlen($string);
523
  }
524
 
525
  /**
526
   * Fixes php behavior if using cgi php.
527
   *
528
   * @see http://www.sitepoint.com/article/php-command-line-1/3
529
   */
530
  protected function fixCgi()
531
  {
532
    // handle output buffering
533
    @ob_end_flush();
534
    ob_implicit_flush(true);
535
 
536
    // PHP ini settings
537
    set_time_limit(0);
538
    ini_set('track_errors', true);
539
    ini_set('html_errors', false);
540
    ini_set('magic_quotes_runtime', false);
541
 
542
    if (false === strpos(PHP_SAPI, 'cgi'))
543
    {
544
      return;
545
    }
546
 
547
    // define stream constants
548
    define('STDIN',  fopen('php://stdin',  'r'));
549
    define('STDOUT', fopen('php://stdout', 'w'));
550
    define('STDERR', fopen('php://stderr', 'w'));
551
 
552
    // change directory
553
    if (isset($_SERVER['PWD']))
554
    {
555
      chdir($_SERVER['PWD']);
556
    }
557
 
558
    // close the streams on script termination
559
    register_shutdown_function(create_function('', 'fclose(STDIN); fclose(STDOUT); fclose(STDERR); return true;'));
560
  }
561
 
562
  /**
563
   * Returns an array of possible abbreviations given a set of names.
564
   *
565
   * @see Text::Abbrev perl module for the algorithm
566
   */
567
  protected function getAbbreviations($names)
568
  {
569
    $abbrevs = array();
570
    $table   = array();
571
 
572
    foreach ($names as $name)
573
    {
574
      for ($len = strlen($name) - 1; $len > 0; --$len)
575
      {
576
        $abbrev = substr($name, 0, $len);
577
        if (!array_key_exists($abbrev, $table))
578
        {
579
          $table[$abbrev] = 1;
580
        }
581
        else
582
        {
583
          ++$table[$abbrev];
584
        }
585
 
586
        $seen = $table[$abbrev];
587
        if ($seen == 1)
588
        {
589
          // We're the first word so far to have this abbreviation.
590
          $abbrevs[$abbrev] = array($name);
591
        }
592
        else if ($seen == 2)
593
        {
594
          // We're the second word to have this abbreviation, so we can't use it.
595
          // unset($abbrevs[$abbrev]);
596
          $abbrevs[$abbrev][] = $name;
597
        }
598
        else
599
        {
600
          // We're the third word to have this abbreviation, so skip to the next word.
601
          continue;
602
        }
603
      }
604
    }
605
 
606
    // Non-abbreviations always get entered, even if they aren't unique
607
    foreach ($names as $name)
608
    {
609
      $abbrevs[$name] = array($name);
610
    }
611
 
612
    return $abbrevs;
613
  }
614
 
615
  /**
616
   * Returns true if the stream supports colorization.
617
   *
618
   * Colorization is disabled if not supported by the stream:
619
   *
620
   *  -  windows without ansicon
621
   *  -  non tty consoles
622
   *
623
   * @param  mixed  $stream  A stream
624
   *
625
   * @return Boolean true if the stream supports colorization, false otherwise
626
   */
627
  protected function isStreamSupportsColors($stream)
628
  {
629
    if (DIRECTORY_SEPARATOR == '\\')
630
    {
631
      return false !== getenv('ANSICON');
632
    }
633
    else
634
    {
635
      return function_exists('posix_isatty') && @posix_isatty($stream);
636
    }
637
  }
638
 
639
  /**
640
   * Guesses the best formatter for the stream.
641
   *
642
   * @param  mixed       $stream  A stream
643
   *
644
   * @return sfFormatter A formatter instance
645
   */
646
  protected function guessBestFormatter($stream)
647
  {
648
    return $this->isStreamSupportsColors($stream) ? new sfAnsiColorFormatter() : new sfFormatter();
649
  }
650
}