1<?php 2/** 3 * Zend Framework (http://framework.zend.com/) 4 * 5 * @link http://github.com/zendframework/zf2 for the canonical source repository 6 * @copyright Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com) 7 * @license http://framework.zend.com/license/new-bsd New BSD License 8 */ 9 10namespace Zend\Console\RouteMatcher; 11 12use Zend\Console\Exception; 13use Zend\Validator\ValidatorInterface; 14use Zend\Filter\FilterInterface; 15 16class DefaultRouteMatcher implements RouteMatcherInterface 17{ 18 /** 19 * Parts of the route. 20 * 21 * @var array 22 */ 23 protected $parts; 24 25 /** 26 * Default values. 27 * 28 * @var array 29 */ 30 protected $defaults; 31 32 /** 33 * Parameters' name aliases. 34 * 35 * @var array 36 */ 37 protected $aliases; 38 39 /** 40 * @var ValidatorInterface[] 41 */ 42 protected $validators = array(); 43 44 /** 45 * @var FilterInterface[] 46 */ 47 protected $filters = array(); 48 49 /** 50 * Class constructor 51 * 52 * @param string $route 53 * @param array $constraints 54 * @param array $defaults 55 * @param array $aliases 56 * @param array $filters 57 * @param ValidatorInterface[] $validators 58 * @throws Exception\InvalidArgumentException 59 */ 60 public function __construct( 61 $route, 62 array $constraints = array(), 63 array $defaults = array(), 64 array $aliases = array(), 65 array $filters = null, 66 array $validators = null 67 ) { 68 $this->defaults = $defaults; 69 $this->constraints = $constraints; 70 $this->aliases = $aliases; 71 72 if ($filters !== null) { 73 foreach ($filters as $name => $filter) { 74 if (!$filter instanceof FilterInterface) { 75 throw new Exception\InvalidArgumentException('Cannot use ' . gettype($filters) . ' as filter for ' . __CLASS__); 76 } 77 $this->filters[$name] = $filter; 78 } 79 } 80 81 if ($validators !== null) { 82 foreach ($validators as $name => $validator) { 83 if (!$validator instanceof ValidatorInterface) { 84 throw new Exception\InvalidArgumentException('Cannot use ' . gettype($validator) . ' as validator for ' . __CLASS__); 85 } 86 $this->validators[$name] = $validator; 87 } 88 } 89 90 $this->parts = $this->parseDefinition($route); 91 } 92 93 /** 94 * Parse a route definition. 95 * 96 * @param string $def 97 * @return array 98 * @throws Exception\InvalidArgumentException 99 */ 100 protected function parseDefinition($def) 101 { 102 $def = trim($def); 103 $pos = 0; 104 $length = strlen($def); 105 $parts = array(); 106 $unnamedGroupCounter = 1; 107 108 while ($pos < $length) { 109 /** 110 * Optional value param, i.e. 111 * [SOMETHING] 112 */ 113 if (preg_match('/\G\[(?P<name>[A-Z][A-Z0-9\_\-]*?)\](?: +|$)/s', $def, $m, 0, $pos)) { 114 $item = array( 115 'name' => strtolower($m['name']), 116 'literal' => false, 117 'required' => false, 118 'positional' => true, 119 'hasValue' => true, 120 ); 121 } 122 /** 123 * Mandatory value param, i.e. 124 * SOMETHING 125 */ 126 elseif (preg_match('/\G(?P<name>[A-Z][A-Z0-9\_\-]*?)(?: +|$)/s', $def, $m, 0, $pos)) { 127 $item = array( 128 'name' => strtolower($m['name']), 129 'literal' => false, 130 'required' => true, 131 'positional' => true, 132 'hasValue' => true, 133 ); 134 } 135 /** 136 * Optional literal param, i.e. 137 * [something] 138 */ 139 elseif (preg_match('/\G\[ *?(?P<name>[a-zA-Z][a-zA-Z0-9\_\-\:]*?) *?\](?: +|$)/s', $def, $m, 0, $pos)) { 140 $item = array( 141 'name' => $m['name'], 142 'literal' => true, 143 'required' => false, 144 'positional' => true, 145 'hasValue' => false, 146 ); 147 } 148 /** 149 * Optional value param, syntax 2, i.e. 150 * [<something>] 151 */ 152 elseif (preg_match('/\G\[ *\<(?P<name>[a-zA-Z][a-zA-Z0-9\_\-]*?)\> *\](?: +|$)/s', $def, $m, 0, $pos)) { 153 $item = array( 154 'name' => $m['name'], 155 'literal' => false, 156 'required' => false, 157 'positional' => true, 158 'hasValue' => true, 159 ); 160 } 161 /** 162 * Mandatory value param, i.e. 163 * <something> 164 */ 165 elseif (preg_match('/\G\< *(?P<name>[a-zA-Z][a-zA-Z0-9\_\-]*?) *\>(?: +|$)/s', $def, $m, 0, $pos)) { 166 $item = array( 167 'name' => $m['name'], 168 'literal' => false, 169 'required' => true, 170 'positional' => true, 171 'hasValue' => true, 172 ); 173 } 174 /** 175 * Mandatory literal param, i.e. 176 * something 177 */ 178 elseif (preg_match('/\G(?P<name>[a-zA-Z][a-zA-Z0-9\_\-\:]*?)(?: +|$)/s', $def, $m, 0, $pos)) { 179 $item = array( 180 'name' => $m['name'], 181 'literal' => true, 182 'required' => true, 183 'positional' => true, 184 'hasValue' => false, 185 ); 186 } 187 /** 188 * Mandatory long param 189 * --param= 190 * --param=whatever 191 */ 192 elseif (preg_match('/\G--(?P<name>[a-zA-Z0-9][a-zA-Z0-9\_\-]+)(?P<hasValue>=\S*?)?(?: +|$)/s', $def, $m, 0, $pos)) { 193 $item = array( 194 'name' => $m['name'], 195 'short' => false, 196 'literal' => false, 197 'required' => true, 198 'positional' => false, 199 'hasValue' => !empty($m['hasValue']), 200 ); 201 } 202 /** 203 * Optional long flag 204 * [--param] 205 */ 206 elseif (preg_match( 207 '/\G\[ *?--(?P<name>[a-zA-Z0-9][a-zA-Z0-9\_\-]+) *?\](?: +|$)/s', $def, $m, 0, $pos 208 )) { 209 $item = array( 210 'name' => $m['name'], 211 'short' => false, 212 'literal' => false, 213 'required' => false, 214 'positional' => false, 215 'hasValue' => false, 216 ); 217 } 218 /** 219 * Optional long param 220 * [--param=] 221 * [--param=whatever] 222 */ 223 elseif (preg_match( 224 '/\G\[ *?--(?P<name>[a-zA-Z0-9][a-zA-Z0-9\_\-]+)(?P<hasValue>=\S*?)? *?\](?: +|$)/s', $def, $m, 0, $pos 225 )) { 226 $item = array( 227 'name' => $m['name'], 228 'short' => false, 229 'literal' => false, 230 'required' => false, 231 'positional' => false, 232 'hasValue' => !empty($m['hasValue']), 233 ); 234 } 235 /** 236 * Mandatory short param 237 * -a 238 * -a=i 239 * -a=s 240 * -a=w 241 */ 242 elseif (preg_match('/\G-(?P<name>[a-zA-Z0-9])(?:=(?P<type>[ns]))?(?: +|$)/s', $def, $m, 0, $pos)) { 243 $item = array( 244 'name' => $m['name'], 245 'short' => true, 246 'literal' => false, 247 'required' => true, 248 'positional' => false, 249 'hasValue' => !empty($m['type']) ? $m['type'] : null, 250 ); 251 } 252 /** 253 * Optional short param 254 * [-a] 255 * [-a=n] 256 * [-a=s] 257 */ 258 elseif (preg_match('/\G\[ *?-(?P<name>[a-zA-Z0-9])(?:=(?P<type>[ns]))? *?\](?: +|$)/s', $def, $m, 0, $pos)) { 259 $item = array( 260 'name' => $m['name'], 261 'short' => true, 262 'literal' => false, 263 'required' => false, 264 'positional' => false, 265 'hasValue' => !empty($m['type']) ? $m['type'] : null, 266 ); 267 } 268 /** 269 * Optional literal param alternative 270 * [ something | somethingElse | anotherOne ] 271 * [ something | somethingElse | anotherOne ]:namedGroup 272 */ 273 elseif (preg_match('/ 274 \G 275 \[ 276 (?P<options> 277 (?: 278 \ *? 279 (?P<name>[a-zA-Z][a-zA-Z0-9_\-]*?) 280 \ *? 281 (?:\||(?=\])) 282 \ *? 283 )+ 284 ) 285 \] 286 (?:\:(?P<groupName>[a-zA-Z0-9]+))? 287 (?:\ +|$) 288 /sx', $def, $m, 0, $pos 289 ) 290 ) { 291 // extract available options 292 $options = preg_split('/ *\| */', trim($m['options']), 0, PREG_SPLIT_NO_EMPTY); 293 294 // remove dupes 295 array_unique($options); 296 297 // prepare item 298 $item = array( 299 'name' => isset($m['groupName']) ? $m['groupName'] : 'unnamedGroup' . $unnamedGroupCounter++, 300 'literal' => true, 301 'required' => false, 302 'positional' => true, 303 'alternatives' => $options, 304 'hasValue' => false, 305 ); 306 } 307 308 /** 309 * Required literal param alternative 310 * ( something | somethingElse | anotherOne ) 311 * ( something | somethingElse | anotherOne ):namedGroup 312 */ 313 elseif (preg_match('/ 314 \G 315 \( 316 (?P<options> 317 (?: 318 \ *? 319 (?P<name>[a-zA-Z][a-zA-Z0-9_\-]+) 320 \ *? 321 (?:\||(?=\))) 322 \ *? 323 )+ 324 ) 325 \) 326 (?:\:(?P<groupName>[a-zA-Z0-9]+))? 327 (?:\ +|$) 328 /sx', $def, $m, 0, $pos 329 )) { 330 // extract available options 331 $options = preg_split('/ *\| */', trim($m['options']), 0, PREG_SPLIT_NO_EMPTY); 332 333 // remove dupes 334 array_unique($options); 335 336 // prepare item 337 $item = array( 338 'name' => isset($m['groupName']) ? $m['groupName']:'unnamedGroupAt' . $unnamedGroupCounter++, 339 'literal' => true, 340 'required' => true, 341 'positional' => true, 342 'alternatives' => $options, 343 'hasValue' => false, 344 ); 345 } 346 /** 347 * Required long/short flag alternative 348 * ( --something | --somethingElse | --anotherOne | -s | -a ) 349 * ( --something | --somethingElse | --anotherOne | -s | -a ):namedGroup 350 */ 351 elseif (preg_match('/ 352 \G 353 \( 354 (?P<options> 355 (?: 356 \ *? 357 \-+(?P<name>[a-zA-Z0-9][a-zA-Z0-9_\-]*?) 358 \ *? 359 (?:\||(?=\))) 360 \ *? 361 )+ 362 ) 363 \) 364 (?:\:(?P<groupName>[a-zA-Z0-9]+))? 365 (?:\ +|$) 366 /sx', $def, $m, 0, $pos 367 )) { 368 // extract available options 369 $options = preg_split('/ *\| */', trim($m['options']), 0, PREG_SPLIT_NO_EMPTY); 370 371 // remove dupes 372 array_unique($options); 373 374 // remove prefix 375 array_walk($options, function (&$val) { 376 $val = ltrim($val, '-'); 377 }); 378 379 // prepare item 380 $item = array( 381 'name' => isset($m['groupName']) ? $m['groupName']:'unnamedGroupAt' . $unnamedGroupCounter++, 382 'literal' => false, 383 'required' => true, 384 'positional' => false, 385 'alternatives' => $options, 386 'hasValue' => false, 387 ); 388 } 389 /** 390 * Optional flag alternative 391 * [ --something | --somethingElse | --anotherOne | -s | -a ] 392 * [ --something | --somethingElse | --anotherOne | -s | -a ]:namedGroup 393 */ 394 elseif (preg_match('/ 395 \G 396 \[ 397 (?P<options> 398 (?: 399 \ *? 400 \-+(?P<name>[a-zA-Z0-9][a-zA-Z0-9_\-]*?) 401 \ *? 402 (?:\||(?=\])) 403 \ *? 404 )+ 405 ) 406 \] 407 (?:\:(?P<groupName>[a-zA-Z0-9]+))? 408 (?:\ +|$) 409 /sx', $def, $m, 0, $pos 410 )) { 411 // extract available options 412 $options = preg_split('/ *\| */', trim($m['options']), 0, PREG_SPLIT_NO_EMPTY); 413 414 // remove dupes 415 array_unique($options); 416 417 // remove prefix 418 array_walk($options, function (&$val) { 419 $val = ltrim($val, '-'); 420 }); 421 422 // prepare item 423 $item = array( 424 'name' => isset($m['groupName']) ? $m['groupName']:'unnamedGroupAt' . $unnamedGroupCounter++, 425 'literal' => false, 426 'required' => false, 427 'positional' => false, 428 'alternatives' => $options, 429 'hasValue' => false, 430 ); 431 } else { 432 throw new Exception\InvalidArgumentException( 433 'Cannot understand Console route at "' . substr($def, $pos) . '"' 434 ); 435 } 436 437 $pos += strlen($m[0]); 438 $parts[] = $item; 439 } 440 441 return $parts; 442 } 443 444 /** 445 * Returns list of names representing single parameter 446 * 447 * @param string $name 448 * @return string 449 */ 450 private function getAliases($name) 451 { 452 $namesToMatch = array($name); 453 foreach ($this->aliases as $alias => $canonical) { 454 if ($name == $canonical) { 455 $namesToMatch[] = $alias; 456 } 457 } 458 return $namesToMatch; 459 } 460 461 /** 462 * Returns canonical name of a parameter 463 * 464 * @param string $name 465 * @return string 466 */ 467 private function getCanonicalName($name) 468 { 469 if (isset($this->aliases[$name])) { 470 return $this->aliases[$name]; 471 } 472 return $name; 473 } 474 475 /** 476 * Match parameters against route passed to constructor 477 * 478 * @param array $params 479 * @return array|null 480 */ 481 public function match($params) 482 { 483 $matches = array(); 484 485 /* 486 * Extract positional and named parts 487 */ 488 $positional = $named = array(); 489 foreach ($this->parts as &$part) { 490 if ($part['positional']) { 491 $positional[] = &$part; 492 } else { 493 $named[] = &$part; 494 } 495 } 496 497 /* 498 * Scan for named parts inside Console params 499 */ 500 foreach ($named as &$part) { 501 /* 502 * Prepare match regex 503 */ 504 if (isset($part['alternatives'])) { 505 // an alternative of flags 506 $regex = '/^\-+(?P<name>'; 507 508 $alternativeAliases = array(); 509 foreach ($part['alternatives'] as $alternative) { 510 $alternativeAliases[] = '(?:' . implode('|', $this->getAliases($alternative)) . ')'; 511 } 512 513 $regex .= implode('|', $alternativeAliases); 514 515 if ($part['hasValue']) { 516 $regex .= ')(?:\=(?P<value>.*?)$)?$/'; 517 } else { 518 $regex .= ')$/i'; 519 } 520 } else { 521 // a single named flag 522 $name = '(?:' . implode('|', $this->getAliases($part['name'])) . ')'; 523 524 if ($part['short'] === true) { 525 // short variant 526 if ($part['hasValue']) { 527 $regex = '/^\-' . $name . '(?:\=(?P<value>.*?)$)?$/i'; 528 } else { 529 $regex = '/^\-' . $name . '$/i'; 530 } 531 } elseif ($part['short'] === false) { 532 // long variant 533 if ($part['hasValue']) { 534 $regex = '/^\-{2,}' . $name . '(?:\=(?P<value>.*?)$)?$/i'; 535 } else { 536 $regex = '/^\-{2,}' . $name . '$/i'; 537 } 538 } 539 } 540 541 /* 542 * Look for param 543 */ 544 $value = $param = null; 545 for ($x = 0, $count = count($params); $x < $count; $x++) { 546 if (preg_match($regex, $params[$x], $m)) { 547 // found param 548 $param = $params[$x]; 549 550 // prevent further scanning of this param 551 array_splice($params, $x, 1); 552 553 if (isset($m['value'])) { 554 $value = $m['value']; 555 } 556 557 if (isset($m['name'])) { 558 $matchedName = $this->getCanonicalName($m['name']); 559 } 560 561 break; 562 } 563 } 564 565 566 if (!$param) { 567 /* 568 * Drop out if that was a mandatory param 569 */ 570 if ($part['required']) { 571 return; 572 } 573 574 /* 575 * Continue to next positional param 576 */ 577 else { 578 continue; 579 } 580 } 581 582 583 /* 584 * Value for flags is always boolean 585 */ 586 if ($param && !$part['hasValue']) { 587 $value = true; 588 } 589 590 /* 591 * Try to retrieve value if it is expected 592 */ 593 if ((null === $value || "" === $value) && $part['hasValue']) { 594 if ($x < count($params)+1 && isset($params[$x])) { 595 // retrieve value from adjacent param 596 $value = $params[$x]; 597 598 // prevent further scanning of this param 599 array_splice($params, $x, 1); 600 } else { 601 // there are no more params available 602 return; 603 } 604 } 605 606 /* 607 * Validate the value against constraints 608 */ 609 if ($part['hasValue'] && isset($this->constraints[$part['name']])) { 610 if ( 611 !preg_match($this->constraints[$part['name']], $value) 612 ) { 613 // constraint failed 614 return; 615 } 616 } 617 618 /* 619 * Store the value 620 */ 621 if ($part['hasValue']) { 622 $matches[$part['name']] = $value; 623 } else { 624 $matches[$part['name']] = true; 625 } 626 627 /* 628 * If there are alternatives, fill them 629 */ 630 if (isset($part['alternatives'])) { 631 if ($part['hasValue']) { 632 foreach ($part['alternatives'] as $alt) { 633 if ($alt === $matchedName && !isset($matches[$alt])) { 634 $matches[$alt] = $value; 635 } elseif (!isset($matches[$alt])) { 636 $matches[$alt] = null; 637 } 638 } 639 } else { 640 foreach ($part['alternatives'] as $alt) { 641 if ($alt === $matchedName && !isset($matches[$alt])) { 642 $matches[$alt] = isset($this->defaults[$alt])? $this->defaults[$alt] : true; 643 } elseif (!isset($matches[$alt])) { 644 $matches[$alt] = false; 645 } 646 } 647 } 648 } 649 } 650 651 /* 652 * Scan for left-out flags that should result in a mismatch 653 */ 654 foreach ($params as $param) { 655 if (preg_match('#^\-+#', $param)) { 656 return; // there is an unrecognized flag 657 } 658 } 659 660 /* 661 * Go through all positional params 662 */ 663 $argPos = 0; 664 foreach ($positional as &$part) { 665 /* 666 * Check if param exists 667 */ 668 if (!isset($params[$argPos])) { 669 if ($part['required']) { 670 // cannot find required positional param 671 return; 672 } else { 673 // stop matching 674 break; 675 } 676 } 677 678 $value = $params[$argPos]; 679 680 /* 681 * Check if literal param matches 682 */ 683 if ($part['literal']) { 684 if ( 685 (isset($part['alternatives']) && !in_array($value, $part['alternatives'])) || 686 (!isset($part['alternatives']) && $value != $part['name']) 687 ) { 688 return; 689 } 690 } 691 692 /* 693 * Validate the value against constraints 694 */ 695 if ($part['hasValue'] && isset($this->constraints[$part['name']])) { 696 if ( 697 !preg_match($this->constraints[$part['name']], $value) 698 ) { 699 // constraint failed 700 return; 701 } 702 } 703 704 /* 705 * Store the value 706 */ 707 if ($part['hasValue']) { 708 $matches[$part['name']] = $value; 709 } elseif (isset($part['alternatives'])) { 710 // from all alternatives set matching parameter to TRUE and the rest to FALSE 711 foreach ($part['alternatives'] as $alt) { 712 if ($alt == $value) { 713 $matches[$alt] = isset($this->defaults[$alt])? $this->defaults[$alt] : true; 714 } else { 715 $matches[$alt] = false; 716 } 717 } 718 719 // set alternatives group value 720 $matches[$part['name']] = $value; 721 } elseif (!$part['required']) { 722 // set optional parameter flag 723 $name = $part['name']; 724 $matches[$name] = isset($this->defaults[$name])? $this->defaults[$name] : true; 725 } 726 727 /* 728 * Advance to next argument 729 */ 730 $argPos++; 731 } 732 733 /* 734 * Check if we have consumed all positional parameters 735 */ 736 if ($argPos < count($params)) { 737 return; // there are extraneous params that were not consumed 738 } 739 740 /* 741 * Any optional flags that were not entered have value false 742 */ 743 foreach ($this->parts as &$part) { 744 if (!$part['required'] && !$part['hasValue']) { 745 if (!isset($matches[$part['name']])) { 746 $matches[$part['name']] = false; 747 } 748 // unset alternatives also should be false 749 if (isset($part['alternatives'])) { 750 foreach ($part['alternatives'] as $alt) { 751 if (!isset($matches[$alt])) { 752 $matches[$alt] = false; 753 } 754 } 755 } 756 } 757 } 758 759 // run filters 760 foreach ($matches as $name => $value) { 761 if (isset($this->filters[$name])) { 762 $matches[$name] = $this->filters[$name]->filter($value); 763 } 764 } 765 766 // run validators 767 $valid = true; 768 foreach ($matches as $name => $value) { 769 if (isset($this->validators[$name])) { 770 $valid &= $this->validators[$name]->isValid($value); 771 } 772 } 773 774 if (!$valid) { 775 return; 776 } 777 778 return array_replace($this->defaults, $matches); 779 } 780} 781