Complex classes like DokuCLI_Options often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.
Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.
While breaking up the class, it is a good idea to analyze how other classes use DokuCLI_Options, and based on these observations, apply Extract Interface, too.
| 1 | <?php |
||
| 240 | class DokuCLI_Options { |
||
| 241 | /** @var array keeps the list of options to parse */ |
||
| 242 | protected $setup; |
||
| 243 | |||
| 244 | /** @var array store parsed options */ |
||
| 245 | protected $options = array(); |
||
| 246 | |||
| 247 | /** @var string current parsed command if any */ |
||
| 248 | protected $command = ''; |
||
| 249 | |||
| 250 | /** @var array passed non-option arguments */ |
||
| 251 | public $args = array(); |
||
| 252 | |||
| 253 | /** @var string the executed script */ |
||
| 254 | protected $bin; |
||
| 255 | |||
| 256 | /** |
||
| 257 | * Constructor |
||
| 258 | */ |
||
| 259 | public function __construct() { |
||
| 260 | $this->setup = array( |
||
| 261 | '' => array( |
||
| 262 | 'opts' => array(), |
||
| 263 | 'args' => array(), |
||
| 264 | 'help' => '' |
||
| 265 | ) |
||
| 266 | ); // default command |
||
| 267 | |||
| 268 | $this->args = $this->readPHPArgv(); |
||
| 269 | $this->bin = basename(array_shift($this->args)); |
||
| 270 | |||
| 271 | $this->options = array(); |
||
| 272 | } |
||
| 273 | |||
| 274 | /** |
||
| 275 | * Sets the help text for the tool itself |
||
| 276 | * |
||
| 277 | * @param string $help |
||
| 278 | */ |
||
| 279 | public function setHelp($help) { |
||
| 282 | |||
| 283 | /** |
||
| 284 | * Register the names of arguments for help generation and number checking |
||
| 285 | * |
||
| 286 | * This has to be called in the order arguments are expected |
||
| 287 | * |
||
| 288 | * @param string $arg argument name (just for help) |
||
| 289 | * @param string $help help text |
||
| 290 | * @param bool $required is this a required argument |
||
| 291 | * @param string $command if theses apply to a sub command only |
||
| 292 | * @throws DokuCLI_Exception |
||
| 293 | */ |
||
| 294 | public function registerArgument($arg, $help, $required = true, $command = '') { |
||
| 295 | if(!isset($this->setup[$command])) throw new DokuCLI_Exception("Command $command not registered"); |
||
| 296 | |||
| 297 | $this->setup[$command]['args'][] = array( |
||
| 298 | 'name' => $arg, |
||
| 299 | 'help' => $help, |
||
| 300 | 'required' => $required |
||
| 301 | ); |
||
| 302 | } |
||
| 303 | |||
| 304 | /** |
||
| 305 | * This registers a sub command |
||
| 306 | * |
||
| 307 | * Sub commands have their own options and use their own function (not main()). |
||
| 308 | * |
||
| 309 | * @param string $command |
||
| 310 | * @param string $help |
||
| 311 | * @throws DokuCLI_Exception |
||
| 312 | */ |
||
| 313 | public function registerCommand($command, $help) { |
||
| 314 | if(isset($this->setup[$command])) throw new DokuCLI_Exception("Command $command already registered"); |
||
| 315 | |||
| 316 | $this->setup[$command] = array( |
||
| 317 | 'opts' => array(), |
||
| 318 | 'args' => array(), |
||
| 319 | 'help' => $help |
||
| 320 | ); |
||
| 321 | |||
| 322 | } |
||
| 323 | |||
| 324 | /** |
||
| 325 | * Register an option for option parsing and help generation |
||
| 326 | * |
||
| 327 | * @param string $long multi character option (specified with --) |
||
| 328 | * @param string $help help text for this option |
||
| 329 | * @param string|null $short one character option (specified with -) |
||
| 330 | * @param bool|string $needsarg does this option require an argument? give it a name here |
||
| 331 | * @param string $command what command does this option apply to |
||
| 332 | * @throws DokuCLI_Exception |
||
| 333 | */ |
||
| 334 | public function registerOption($long, $help, $short = null, $needsarg = false, $command = '') { |
||
| 349 | |||
| 350 | /** |
||
| 351 | * Checks the actual number of arguments against the required number |
||
| 352 | * |
||
| 353 | * Throws an exception if arguments are missing. Called from parseOptions() |
||
| 354 | * |
||
| 355 | * @throws DokuCLI_Exception |
||
| 356 | */ |
||
| 357 | public function checkArguments() { |
||
| 368 | |||
| 369 | /** |
||
| 370 | * Parses the given arguments for known options and command |
||
| 371 | * |
||
| 372 | * The given $args array should NOT contain the executed file as first item anymore! The $args |
||
| 373 | * array is stripped from any options and possible command. All found otions can be accessed via the |
||
| 374 | * getOpt() function |
||
| 375 | * |
||
| 376 | * Note that command options will overwrite any global options with the same name |
||
| 377 | * |
||
| 378 | * @throws DokuCLI_Exception |
||
| 379 | */ |
||
| 380 | public function parseOptions() { |
||
| 381 | $non_opts = array(); |
||
| 382 | |||
| 383 | $argc = count($this->args); |
||
| 384 | for($i = 0; $i < $argc; $i++) { |
||
| 385 | $arg = $this->args[$i]; |
||
| 386 | |||
| 387 | // The special element '--' means explicit end of options. Treat the rest of the arguments as non-options |
||
| 388 | // and end the loop. |
||
| 389 | if($arg == '--') { |
||
| 390 | $non_opts = array_merge($non_opts, array_slice($this->args, $i + 1)); |
||
| 391 | break; |
||
| 392 | } |
||
| 393 | |||
| 394 | // '-' is stdin - a normal argument |
||
| 395 | if($arg == '-') { |
||
| 396 | $non_opts = array_merge($non_opts, array_slice($this->args, $i)); |
||
| 397 | break; |
||
| 398 | } |
||
| 399 | |||
| 400 | // first non-option |
||
| 401 | if($arg{0} != '-') { |
||
| 402 | $non_opts = array_merge($non_opts, array_slice($this->args, $i)); |
||
| 403 | break; |
||
| 404 | } |
||
| 405 | |||
| 406 | // long option |
||
| 407 | if(strlen($arg) > 1 && $arg{1} == '-') { |
||
| 408 | list($opt, $val) = explode('=', substr($arg, 2), 2); |
||
| 409 | |||
| 410 | if(!isset($this->setup[$this->command]['opts'][$opt])) { |
||
| 411 | throw new DokuCLI_Exception("No such option $arg", DokuCLI_Exception::E_UNKNOWN_OPT); |
||
| 412 | } |
||
| 413 | |||
| 414 | // argument required? |
||
| 415 | if($this->setup[$this->command]['opts'][$opt]['needsarg']) { |
||
| 416 | if(is_null($val) && $i + 1 < $argc && !preg_match('/^--?[\w]/', $this->args[$i + 1])) { |
||
| 417 | $val = $this->args[++$i]; |
||
| 418 | } |
||
| 419 | if(is_null($val)) { |
||
| 420 | throw new DokuCLI_Exception("Option $arg requires an argument", DokuCLI_Exception::E_OPT_ARG_REQUIRED); |
||
| 421 | } |
||
| 422 | $this->options[$opt] = $val; |
||
| 423 | } else { |
||
| 424 | $this->options[$opt] = true; |
||
| 425 | } |
||
| 426 | |||
| 427 | continue; |
||
| 428 | } |
||
| 429 | |||
| 430 | // short option |
||
| 431 | $opt = substr($arg, 1); |
||
| 432 | if(!isset($this->setup[$this->command]['short'][$opt])) { |
||
| 433 | throw new DokuCLI_Exception("No such option $arg", DokuCLI_Exception::E_UNKNOWN_OPT); |
||
| 434 | } else { |
||
| 435 | $opt = $this->setup[$this->command]['short'][$opt]; // store it under long name |
||
| 436 | } |
||
| 437 | |||
| 438 | // argument required? |
||
| 439 | if($this->setup[$this->command]['opts'][$opt]['needsarg']) { |
||
| 440 | $val = null; |
||
| 441 | if($i + 1 < $argc && !preg_match('/^--?[\w]/', $this->args[$i + 1])) { |
||
| 442 | $val = $this->args[++$i]; |
||
| 443 | } |
||
| 444 | if(is_null($val)) { |
||
| 445 | throw new DokuCLI_Exception("Option $arg requires an argument", DokuCLI_Exception::E_OPT_ARG_REQUIRED); |
||
| 446 | } |
||
| 447 | $this->options[$opt] = $val; |
||
| 448 | } else { |
||
| 449 | $this->options[$opt] = true; |
||
| 450 | } |
||
| 451 | } |
||
| 452 | |||
| 453 | // parsing is now done, update args array |
||
| 454 | $this->args = $non_opts; |
||
| 455 | |||
| 456 | // if not done yet, check if first argument is a command and reexecute argument parsing if it is |
||
| 457 | if(!$this->command && $this->args && isset($this->setup[$this->args[0]])) { |
||
| 458 | // it is a command! |
||
| 459 | $this->command = array_shift($this->args); |
||
| 460 | $this->parseOptions(); // second pass |
||
| 461 | } |
||
| 462 | } |
||
| 463 | |||
| 464 | /** |
||
| 465 | * Get the value of the given option |
||
| 466 | * |
||
| 467 | * Please note that all options are accessed by their long option names regardless of how they were |
||
| 468 | * specified on commandline. |
||
| 469 | * |
||
| 470 | * Can only be used after parseOptions() has been run |
||
| 471 | * |
||
| 472 | * @param string $option |
||
| 473 | * @param bool|string $default what to return if the option was not set |
||
| 474 | * @return bool|string |
||
| 475 | */ |
||
| 476 | public function getOpt($option, $default = false) { |
||
| 480 | |||
| 481 | /** |
||
| 482 | * Return the found command if any |
||
| 483 | * |
||
| 484 | * @return string |
||
| 485 | */ |
||
| 486 | public function getCmd() { |
||
| 489 | |||
| 490 | /** |
||
| 491 | * Builds a help screen from the available options. You may want to call it from -h or on error |
||
| 492 | * |
||
| 493 | * @return string |
||
| 494 | */ |
||
| 495 | public function help() { |
||
| 496 | $text = ''; |
||
| 497 | |||
| 498 | $hascommands = (count($this->setup) > 1); |
||
| 499 | foreach($this->setup as $command => $config) { |
||
| 500 | $hasopts = (bool) $this->setup[$command]['opts']; |
||
| 501 | $hasargs = (bool) $this->setup[$command]['args']; |
||
| 502 | |||
| 503 | if(!$command) { |
||
| 504 | $text .= 'USAGE: '.$this->bin; |
||
| 505 | } else { |
||
| 506 | $text .= "\n$command"; |
||
| 507 | } |
||
| 508 | |||
| 509 | if($hasopts) $text .= ' <OPTIONS>'; |
||
| 510 | |||
| 511 | foreach($this->setup[$command]['args'] as $arg) { |
||
| 512 | if($arg['required']) { |
||
| 513 | $text .= ' <'.$arg['name'].'>'; |
||
| 514 | } else { |
||
| 515 | $text .= ' [<'.$arg['name'].'>]'; |
||
| 516 | } |
||
| 517 | } |
||
| 518 | $text .= "\n"; |
||
| 519 | |||
| 520 | if($this->setup[$command]['help']) { |
||
| 521 | $text .= "\n"; |
||
| 522 | $text .= $this->tableFormat( |
||
| 523 | array(2, 72), |
||
| 524 | array('', $this->setup[$command]['help']."\n") |
||
| 525 | ); |
||
| 526 | } |
||
| 527 | |||
| 528 | if($hasopts) { |
||
| 529 | $text .= "\n OPTIONS\n\n"; |
||
| 530 | foreach($this->setup[$command]['opts'] as $long => $opt) { |
||
| 531 | |||
| 532 | $name = ''; |
||
| 533 | if($opt['short']) { |
||
| 534 | $name .= '-'.$opt['short']; |
||
| 535 | if($opt['needsarg']) $name .= ' <'.$opt['needsarg'].'>'; |
||
| 536 | $name .= ', '; |
||
| 537 | } |
||
| 538 | $name .= "--$long"; |
||
| 539 | if($opt['needsarg']) $name .= ' <'.$opt['needsarg'].'>'; |
||
| 540 | |||
| 541 | $text .= $this->tableFormat( |
||
| 542 | array(2, 20, 52), |
||
| 543 | array('', $name, $opt['help']) |
||
| 544 | ); |
||
| 545 | $text .= "\n"; |
||
| 546 | } |
||
| 547 | } |
||
| 548 | |||
| 549 | if($hasargs) { |
||
| 550 | $text .= "\n"; |
||
| 551 | foreach($this->setup[$command]['args'] as $arg) { |
||
| 552 | $name = '<'.$arg['name'].'>'; |
||
| 553 | |||
| 554 | $text .= $this->tableFormat( |
||
| 555 | array(2, 20, 52), |
||
| 556 | array('', $name, $arg['help']) |
||
| 557 | ); |
||
| 558 | } |
||
| 559 | } |
||
| 560 | |||
| 561 | if($command == '' && $hascommands) { |
||
| 562 | $text .= "\nThis tool accepts a command as first parameter as outlined below:\n"; |
||
| 563 | } |
||
| 564 | } |
||
| 565 | |||
| 566 | return $text; |
||
| 567 | } |
||
| 568 | |||
| 569 | /** |
||
| 570 | * Safely read the $argv PHP array across different PHP configurations. |
||
| 571 | * Will take care on register_globals and register_argc_argv ini directives |
||
| 572 | * |
||
| 573 | * @throws DokuCLI_Exception |
||
| 574 | * @return array the $argv PHP array or PEAR error if not registered |
||
| 575 | */ |
||
| 576 | private function readPHPArgv() { |
||
| 592 | |||
| 593 | /** |
||
| 594 | * Displays text in multiple word wrapped columns |
||
| 595 | * |
||
| 596 | * @param int[] $widths list of column widths (in characters) |
||
| 597 | * @param string[] $texts list of texts for each column |
||
| 598 | * @return string |
||
| 599 | */ |
||
| 600 | private function tableFormat($widths, $texts) { |
||
| 625 | } |
||
| 626 | |||
| 653 |
This check looks at variables that have been passed in as parameters and are passed out again to other methods.
If the outgoing method call has stricter type requirements than the method itself, an issue is raised.
An additional type check may prevent trouble.