├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── composer.json └── src ├── ConfigProvider.php ├── Exception ├── DuplicateRouteException.php ├── ExceptionInterface.php ├── InvalidArgumentException.php ├── MissingDependencyException.php └── RuntimeException.php ├── Middleware ├── DispatchMiddleware.php ├── DispatchMiddlewareFactory.php ├── ImplicitHeadMiddleware.php ├── ImplicitHeadMiddlewareFactory.php ├── ImplicitOptionsMiddleware.php ├── ImplicitOptionsMiddlewareFactory.php ├── MethodNotAllowedMiddleware.php ├── MethodNotAllowedMiddlewareFactory.php ├── RouteMiddleware.php └── RouteMiddlewareFactory.php ├── Route.php ├── RouteCollector.php ├── RouteCollectorFactory.php ├── RouteResult.php ├── RouterInterface.php └── Test └── ImplicitMethodsIntegrationTest.php /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file, in reverse chronological order by release. 4 | 5 | ## 3.1.2 - TBD 6 | 7 | ### Added 8 | 9 | - Nothing. 10 | 11 | ### Changed 12 | 13 | - Nothing. 14 | 15 | ### Deprecated 16 | 17 | - Nothing. 18 | 19 | ### Removed 20 | 21 | - Nothing. 22 | 23 | ### Fixed 24 | 25 | - Nothing. 26 | 27 | ## 3.1.1 - 2019-10-16 28 | 29 | ### Added 30 | 31 | - [#80](https://github.com/zendframework/zend-expressive-router/pull/80) adds support for PHP 7.3. 32 | 33 | ### Changed 34 | 35 | - Nothing. 36 | 37 | ### Deprecated 38 | 39 | - Nothing. 40 | 41 | ### Removed 42 | 43 | - Nothing. 44 | 45 | ### Fixed 46 | 47 | - Nothing. 48 | 49 | ## 3.1.0 - 2018-06-05 50 | 51 | ### Added 52 | 53 | - Nothing. 54 | 55 | ### Changed 56 | 57 | - [#76](https://github.com/zendframework/zend-expressive-router/pull/76) modifies the `RouteMiddlewareFactory` to allow specifying a string 58 | `$routererviceName` to its constructor. This change allows having discrete 59 | factory instances for generating route middleware that use different router 60 | instances. 61 | 62 | ### Deprecated 63 | 64 | - Nothing. 65 | 66 | ### Removed 67 | 68 | - Nothing. 69 | 70 | ### Fixed 71 | 72 | - Nothing. 73 | 74 | ## 3.0.4 - TBD 75 | 76 | ### Added 77 | 78 | - Nothing. 79 | 80 | ### Changed 81 | 82 | - Nothing. 83 | 84 | ### Deprecated 85 | 86 | - Nothing. 87 | 88 | ### Removed 89 | 90 | - Nothing. 91 | 92 | ### Fixed 93 | 94 | - Nothing. 95 | 96 | ## 3.0.3 - 2018-05-10 97 | 98 | ### Added 99 | 100 | - Nothing. 101 | 102 | ### Changed 103 | 104 | - Nothing. 105 | 106 | ### Deprecated 107 | 108 | - Nothing. 109 | 110 | ### Removed 111 | 112 | - Nothing. 113 | 114 | ### Fixed 115 | 116 | - [#74](https://github.com/zendframework/zend-expressive-router/pull/74) fixes 117 | an issue with the `ImplicitHeadMiddleware` where matched route parameters 118 | were not copied into the request and would cause exceptions that would 119 | normally not happen for GET requests. 120 | 121 | ## 3.0.2 - 2018-03-21 122 | 123 | ### Added 124 | 125 | - Nothing. 126 | 127 | ### Changed 128 | 129 | - Nothing. 130 | 131 | ### Deprecated 132 | 133 | - Nothing. 134 | 135 | ### Removed 136 | 137 | - Nothing. 138 | 139 | ### Fixed 140 | 141 | - [#73](https://github.com/zendframework/zend-expressive-router/pull/73) fixes 142 | an issue with the `ImplicitOptionsMiddleware` whereby a path match failure was 143 | incorrectly being identified as a method match failure, triggering the 144 | `ImplicitOptionsMiddleware` to attempt to return a response. 145 | 146 | ## 3.0.1 - 2018-03-19 147 | 148 | ### Added 149 | 150 | - Nothing. 151 | 152 | ### Changed 153 | 154 | - Nothing. 155 | 156 | ### Deprecated 157 | 158 | - Nothing. 159 | 160 | ### Removed 161 | 162 | - Nothing. 163 | 164 | ### Fixed 165 | 166 | - [#69](https://github.com/zendframework/zend-expressive-router/pull/69) fixes 167 | the exception message emitted for missing dependencies when creating a 168 | `RouteCollector` instance to refer that class. 169 | 170 | ## 3.0.0 - 2018-03-15 171 | 172 | ### Added 173 | 174 | - [#50](https://github.com/zendframework/zend-expressive-router/pull/50) adds 175 | `Zend\Expressive\Router\ConfigProvider`, and registers it with the package. 176 | The class defines and returns the initial dependencies for the package. 177 | 178 | - [#50](https://github.com/zendframework/zend-expressive-router/pull/50) adds 179 | factory classes for all shipped middleware. In some cases 180 | (`ImplicitHeadMiddleware`, `ImplicitOptionsMiddleware`, and 181 | `MethodNotAllowedMiddleware`), these rely on additional services that you will 182 | need to configure within your application in order to work properly. See each 183 | factory for details. 184 | 185 | - [#47](https://github.com/zendframework/zend-expressive-router/pull/47), 186 | [#50](https://github.com/zendframework/zend-expressive-router/pull/50), and 187 | [#64](https://github.com/zendframework/zend-expressive-router/pull/64) add 188 | the `Zend\Expressive\Router\RouteCollector` class, 189 | which composes a `RouterInterface`, and provides methods for defining and 190 | creating path+method based routes. It exposes the following methods: 191 | 192 | - `route(string $path, MiddlewareInterface $middleware, array $methods = null, string $name = null) : Route` 193 | - `get(string $path, MiddlewareInterface $middleware, string $name = null) : Route` 194 | - `post(string $path, MiddlewareInterface $middleware, string $name = null) : Route` 195 | - `put(string $path, MiddlewareInterface $middleware, string $name = null) : Route` 196 | - `patch(string $path, MiddlewareInterface $middleware, string $name = null) : Route` 197 | - `delete(string $path, MiddlewareInterface $middleware, string $name = null) : Route` 198 | - `any(string $path, MiddlewareInterface $middleware, string $name = null) : Route` 199 | 200 | - [#48](https://github.com/zendframework/zend-expressive-router/pull/48) and 201 | [#50](https://github.com/zendframework/zend-expressive-router/pull/50) adds 202 | `Zend\Expressive\Router\Middleware\MethodNotAllowedMiddleware`. This middleware checks if 203 | the request composes a `RouteResult`, and, if so, if it is due to a method 204 | failure. If neither of those conditions is true, it delegates processing of 205 | the request to the handler. Otherwise, it uses a composed response prototype 206 | in order to create a "405 Method Not Allowed" response, with an `Allow` header 207 | containing the list of allowed request methods. 208 | 209 | - [#49](https://github.com/zendframework/zend-expressive-router/pull/49) and 210 | [#50](https://github.com/zendframework/zend-expressive-router/pull/50) add 211 | the class `Zend\Expressive\Router\Middleware\ImplicitHeadMiddleware`. This 212 | middleware will answer a `HEAD` request for a given route. If no route was 213 | matched, or the route allows `HEAD` requests, it delegates to the handler. If 214 | the route does not allow a `GET` request, it returns an empty response, as 215 | composed in the middleware. Otherwise, it issues a `GET` request to the 216 | handler, indicating the method was forwarded for a `HEAD` request, and then 217 | returns the response with an empty body. 218 | 219 | - [#49](https://github.com/zendframework/zend-expressive-router/pull/49) and 220 | [#50](https://github.com/zendframework/zend-expressive-router/pull/50) add 221 | the class `Zend\Expressive\Router\Middleware\ImplicitOptionsMiddleware`. This 222 | middleware handles `OPTIONS` requests when a route result is present and the 223 | route does not explicitly support `OPTIONS` (and otherwise delegates to the 224 | handler). In those conditions, it returns the response composed in the 225 | middleware, with an `Allow` header indicating the allowed methods. 226 | 227 | - [#39](https://github.com/zendframework/zend-expressive-router/pull/39) and 228 | [#45](https://github.com/zendframework/zend-expressive-router/pull/45) add 229 | PSR-15 `psr/http-server-middleware` support. 230 | 231 | - [#53](https://github.com/zendframework/zend-expressive-router/pull/53) and 232 | [#58](https://github.com/zendframework/zend-expressive-router/pull/58) add an 233 | abstract test case, `Zend\Expressive\Router\Test\ImplicitMethodsIntegrationTest`. 234 | Implementors of `RouterInterface` should extend this class in their own test 235 | suite to ensure that they create appropriate `RouteResult` instances for each 236 | of the following cases: 237 | 238 | - `HEAD` request called and matches one or more known routes, but the method 239 | is not defined for any of them. 240 | - `OPTIONS` request called and matches one or more known routes, but the method 241 | is not defined for any of them. 242 | - Request matches one or more known routes, but the method is not allowed. 243 | 244 | In particular, these tests ensure that implementations marshal the list of 245 | allowed HTTP methods correctly in the latter two cases. 246 | 247 | ### Changed 248 | 249 | - [#41](https://github.com/zendframework/zend-expressive-router/pull/41) updates 250 | the `Route` class to provide typehints for all arguments and return values. 251 | Typehints were generally derived from the existing annotations, with the 252 | following items of particular note: 253 | - The constructor `$middleware` argument typehints on the PSR-15 254 | `MiddlewareInterface`. 255 | - The `getMiddleware()` method now explicitly returns a PSR-15 256 | `MiddlewareInterface` instance. 257 | - `getAllowedMethods()` now returns a nullable `array`. 258 | 259 | - [#41](https://github.com/zendframework/zend-expressive-router/pull/41) and 260 | [#43](https://github.com/zendframework/zend-expressive-router/pull/43) update 261 | the `RouteResult` class to add typehints for all arguments and return values, 262 | where possible. Typehints were generally derived from the existing 263 | annotations, with the following items of particular note: 264 | - The `$methods` argument to `fromRouteFailure()` is now a nullable array 265 | (with `null` representing the fact that any method is allowed), 266 | **without a default value**. You must provide a value when creating a route 267 | failure. 268 | - `getAllowedMethods()` will now return `['*']` when any HTTP method is 269 | allowed; this will evaluate to a valid `Allows` header value, and is the 270 | recommended value when any HTTP method is allowed. 271 | 272 | - [#41](https://github.com/zendframework/zend-expressive-router/pull/41) updates 273 | the `RouteInterface` to add typehints for all arguments and return values. In 274 | particular, thse are now: 275 | - `addRoute(Route $route) : void` 276 | - `match(Psr\Http\Message\ServerRequestInterface $request) : RouteResult` 277 | - `generateUri(string $name, array $substitutions = [], array $options = []) : string` 278 | 279 | - [#47](https://github.com/zendframework/zend-expressive-router/pull/47) 280 | modifies the `RouteMiddleware::$router` property to make it `protected` 281 | visibility, allowing extensions to work with it. 282 | 283 | - [#48](https://github.com/zendframework/zend-expressive-router/pull/48) 284 | modifies `Zend\Expressive\Router\Route` to implement the PSR-15 285 | `MiddlewareInterface`. The new `process()` method proxies to the composed 286 | middleware. 287 | 288 | - [#48](https://github.com/zendframework/zend-expressive-router/pull/48) 289 | modifies `Zend\Expressive\Router\RouteResult` to implement the PSR-15 290 | `MiddlewareInterface`. The new `process()` method proxies to the composed 291 | `Route` instance in the case of a success, and otherwise delegates to the 292 | passed handler instance. 293 | 294 | - [#48](https://github.com/zendframework/zend-expressive-router/pull/48) 295 | modifies `Zend\Expressive\Router\DispatchMiddleware` to process the 296 | `RouteResult` directly, instead of pulling middleware from it. 297 | 298 | - [#50](https://github.com/zendframework/zend-expressive-router/pull/50) renames 299 | `Zend\Expressive\Router\RouteMiddleware` to 300 | `Zend\Expressive\Router\Middleware\RouteMiddleware`. 301 | 302 | - [#50](https://github.com/zendframework/zend-expressive-router/pull/50) renames 303 | `Zend\Expressive\Router\DispatchMiddleware` to 304 | `Zend\Expressive\Router\Middleware\DispatchMiddleware`. 305 | 306 | - [#58](https://github.com/zendframework/zend-expressive-router/pull/58) changes 307 | the constructor of `ImplicitHeadMiddleware` to accept a `RouterInterface` 308 | instead of a response factory. Internally, this allows it to re-match the 309 | current request using the `GET` method; the middleware never generates its own 310 | response any longer. 311 | 312 | - [#58](https://github.com/zendframework/zend-expressive-router/pull/58) changes 313 | the logic of `Route::allowsMethod()`; it no longer returns `true` for `HEAD` 314 | or `OPTIONS` requests if they are not explicitly in the list of allowed 315 | methods. 316 | 317 | - [#59](https://github.com/zendframework/zend-expressive-router/pull/59) changes 318 | the behavior of the `Route` constructor: it now raises an exception if the 319 | list of HTTP methods provided to it is empty. Routes MUST have one or more 320 | HTTP methods associated. 321 | 322 | - [#60](https://github.com/zendframework/zend-expressive-router/pull/60) changes 323 | the behavior of the `RouteResult::getAllowedMethods()` to allow a nullable 324 | return value; this will return `null` if all methods are allowed. 325 | 326 | ### Removed 327 | 328 | - [#39](https://github.com/zendframework/zend-expressive-router/pull/39) and 329 | [#41](https://github.com/zendframework/zend-expressive-router/pull/41) remove 330 | PHP 5.6 and PHP 7.0 support. 331 | 332 | - [#48](https://github.com/zendframework/zend-expressive-router/pull/48) 333 | removes the method `Zend\Expressive\Router\RouteResult::getMatchedMiddleware()`; 334 | the method is no longer necessary, as the class now implements 335 | `MiddlewareInterface` and proxies to the underlying route. 336 | 337 | - [#58](https://github.com/zendframework/zend-expressive-router/pull/58) removes 338 | the following methods from `Route`, as they are no longer used: 339 | 340 | - `implicitHead()` 341 | - `implicitOptions()` 342 | 343 | ### Fixed 344 | 345 | - [#53](https://github.com/zendframework/zend-expressive-router/pull/53) fixes 346 | logic in the `ImplicitHeadMiddleware` and `ImplicitOptionsMiddleware` classes 347 | with regards to how they determine that an implicit `HEAD` or `OPTIONS` 348 | request (respectively) has occurred. 349 | 350 | - [#66](https://github.com/zendframework/zend-expressive-router/pull/66) 351 | improves the exception message raised when a route conflict is detected to 352 | include the path, HTTP methods, and name (if available). 353 | 354 | ## 2.4.1 - 2018-03-08 355 | 356 | ### Added 357 | 358 | - Nothing. 359 | 360 | ### Changed 361 | 362 | - [#63](https://github.com/zendframework/zend-expressive-router/pull/63) 363 | improves the deprecation notice raised by the `Zend\Expressive\Router\Route` 364 | constructor when non-middleware interface implementations are passed for the 365 | `$middleware` argument. The message not contains the path, HTTP methods, and 366 | middleware type that were used to create the `Route` instance. 367 | 368 | ### Deprecated 369 | 370 | - Nothing. 371 | 372 | ### Removed 373 | 374 | - Nothing. 375 | 376 | ### Fixed 377 | 378 | - Nothing. 379 | 380 | ## 2.4.0 - 2018-03-08 381 | 382 | ### Added 383 | 384 | - [#54](https://github.com/zendframework/zend-expressive-router/pull/54) adds 385 | the middleware `Zend\Expressive\Router\Middleware\DispatchMiddleware` and 386 | `Zend\Expressive\Router\Middleware\RouteMiddleware`. These are the same as the 387 | versions shipped in 2.3.0, but under a new namespace. 388 | 389 | - [#55](https://github.com/zendframework/zend-expressive-router/pull/55) adds 390 | `Zend\Expressive\Router\Middleware\ImplicitHeadMiddleware`. It is imported 391 | from zend-expressive, and implements the same functionality. 392 | 393 | - [#55](https://github.com/zendframework/zend-expressive-router/pull/55) adds 394 | `Zend\Expressive\Router\Middleware\ImplicitOptionsMiddleware`. It is imported 395 | from zend-expressive, and implements the same functionality. 396 | 397 | - [#57](https://github.com/zendframework/zend-expressive-router/pull/57) adds 398 | the following factories for use with PSR-11 containers: 399 | 400 | - Zend\Expressive\Router\Middleware\DispatchMiddlewareFactory` 401 | - Zend\Expressive\Router\Middleware\ImplicitHeadMiddlewareFactory` 402 | - Zend\Expressive\Router\Middleware\ImplicitOptionsMiddlewareFactory` 403 | - Zend\Expressive\Router\Middleware\RouteMiddlewareFactory` 404 | 405 | - [#57](https://github.com/zendframework/zend-expressive-router/pull/57) adds 406 | `Zend\Expressive\Router\ConfigProvider`, mapping the above factories to their 407 | respective middleware, and exposing it to zend-component-installer via the 408 | package definition. 409 | 410 | ### Changed 411 | 412 | - Nothing. 413 | 414 | ### Deprecated 415 | 416 | - [#56](https://github.com/zendframework/zend-expressive-router/pull/56) 417 | deprecates the method `Zend\Expressive\RouteResult::getMatchedMiddleware()`, 418 | as it will be removed in version 3. If you need access to the middleware, 419 | use `getMatchedRoute()->getMiddleware()`. (In version 3, the `RouteResult` 420 | _is_ middleware, and will proxy to it.) 421 | 422 | - [#56](https://github.com/zendframework/zend-expressive-router/pull/56) 423 | deprecates passing non-MiddlewareInterface instances to the constructor of 424 | `Zend\Expressive\Route`. The class now triggers a deprecation notice when this 425 | occurs, indicating the changes the developer needs to make. 426 | 427 | - [#54](https://github.com/zendframework/zend-expressive-router/pull/54) 428 | deprecates the middleware `Zend\Expressive\Router\DispatchMiddleware` and 429 | `Zend\Expressive\Router\RouteMiddleware`. The final versions in the v3 release 430 | will be under the `Zend\Expressive\Router\Middleware` namespace; please use 431 | those instead. 432 | 433 | - [#55](https://github.com/zendframework/zend-expressive-router/pull/55) 434 | deprecates two methods in `Zend\Expressive\Router\Route`: 435 | 436 | - `implicitHead()` 437 | - `implicitOptions()` 438 | 439 | Starting in 3.0.0, implementations will need to return route result failures 440 | that include all allowed methods when matching `HEAD` or `OPTIONS` implicitly. 441 | 442 | ### Removed 443 | 444 | - Nothing. 445 | 446 | ### Fixed 447 | 448 | - Nothing. 449 | 450 | ## 2.3.0 - 2018-02-01 451 | 452 | ### Added 453 | 454 | - [#46](https://github.com/zendframework/zend-expressive-router/pull/46) adds 455 | two new middleware, imported from zend-expressive and re-worked for general 456 | purpose usage: 457 | 458 | - `Zend\Expressive\Router\RouteMiddleware` composes a router and a response 459 | prototype. When processed, if no match is found due to an un-matched HTTP 460 | method, it uses the response prototype to create a 405 response with an 461 | `Allow` header listing allowed methods; otherwise, it dispatches to the next 462 | middleware via the provided handler. If a match is made, the route result is 463 | stored as a request attribute using the `RouteResult` class name, and each 464 | matched parameter is also added as a request attribute before delegating 465 | request handling. 466 | 467 | - `Zend\Expressive\Router\DispatchMiddleware` checks for a `RouteResult` 468 | attribute in the request. If none is found, it delegates handling of the 469 | request to the handler. If one is found, it pulls the matched middleware and 470 | processes it. If the middleware is not http-interop middleware, it raises an 471 | exception. 472 | 473 | ### Changed 474 | 475 | - Nothing. 476 | 477 | ### Deprecated 478 | 479 | - Nothing. 480 | 481 | ### Removed 482 | 483 | - Nothing. 484 | 485 | ### Fixed 486 | 487 | - Nothing. 488 | 489 | ## 2.2.0 - 2017-10-09 490 | 491 | ### Added 492 | 493 | - [#36](https://github.com/zendframework/zend-expressive-router/pull/36) adds 494 | support for http-interop/http-middleware 0.5.0 via a polyfill provided by the 495 | package webimpress/http-middleware-compatibility. Essentially, this means you 496 | can drop this package into an application targeting either the 0.4.1 or 0.5.0 497 | versions of http-middleware, and it will "just work". 498 | 499 | ### Deprecated 500 | 501 | - Nothing. 502 | 503 | ### Removed 504 | 505 | - Nothing. 506 | 507 | ### Fixed 508 | 509 | - Nothing. 510 | 511 | ## 2.1.0 - 2017-01-24 512 | 513 | ### Added 514 | 515 | - [#32](https://github.com/zendframework/zend-expressive-router/pull/32) adds 516 | support for [http-interop/http-middleware](https://github.com/http-interop/http-middleware) 517 | server middleware in `Route` instances. 518 | 519 | ### Deprecated 520 | 521 | - Nothing. 522 | 523 | ### Removed 524 | 525 | - Nothing. 526 | 527 | ### Fixed 528 | 529 | - Nothing. 530 | 531 | ## 2.0.0 - 2017-01-06 532 | 533 | ### Added 534 | 535 | - [#6](https://github.com/zendframework/zend-expressive-router/pull/6) modifies `RouterInterface::generateUri` to 536 | support an `$options` parameter, which may pass additional configuration options to the actual router. 537 | - [#21](https://github.com/zendframework/zend-expressive-router/pull/21) makes the configured path definition 538 | accessible in the `RouteResult`. 539 | 540 | ### Deprecated 541 | 542 | - Nothing. 543 | 544 | ### Removed 545 | 546 | - Removed `RouteResultObserverInterface` and `RouteResultSubjectInterface`, as they were deprecated in 1.2.0. 547 | 548 | ### Fixed 549 | 550 | - Nothing. 551 | 552 | ## 1.3.2 - 2016-12-14 553 | 554 | ### Added 555 | 556 | - Nothing. 557 | 558 | ### Deprecated 559 | 560 | - Nothing. 561 | 562 | ### Removed 563 | 564 | - Nothing. 565 | 566 | ### Fixed 567 | 568 | - [#29](https://github.com/zendframework/zend-expressive-router/pull/29) removes 569 | the patch introduced with [#27](https://github.com/zendframework/zend-expressive-router/pull/27) 570 | and 1.3.1, as it causes `Zend\Expressive\Application` to raise exceptions 571 | regarding duplicate routes, and because some implementations, including 572 | FastRoute, also raise errors on duplication. It will be up to individual 573 | routers to determine how to handle implicit HEAD and OPTIONS support. 574 | 575 | ## 1.3.1 - 2016-12-13 576 | 577 | ### Added 578 | 579 | - Nothing. 580 | 581 | ### Deprecated 582 | 583 | - Nothing. 584 | 585 | ### Removed 586 | 587 | - Nothing. 588 | 589 | ### Fixed 590 | 591 | - [#27](https://github.com/zendframework/zend-expressive-router/pull/27) fixes 592 | the behavior of `Route` to _always_ register `HEAD` and `OPTIONS` as allowed 593 | methods; this was the original intent of [#24](https://github.com/zendframework/zend-expressive-router/pull/24). 594 | 595 | ## 1.3.0 - 2016-12-13 596 | 597 | ### Added 598 | 599 | - [#23](https://github.com/zendframework/zend-expressive-router/pull/23) adds a 600 | new static method on the `RouteResult` class, `fromRoute(Route $route, array 601 | $params = [])`, for creating a new `RouteResult` instance. It also adds 602 | `getMatchedRoute()` for retrieving the `Route` instance provided to that 603 | method. Doing so allows retrieving the list of supported HTTP methods, path, 604 | and route options from the matched route. 605 | 606 | - [#24](https://github.com/zendframework/zend-expressive-router/pull/24) adds 607 | two new methods to the `Route` class, `implicitHead()` and 608 | `implicitOptions()`. These can be used by routers or dispatchers to determine 609 | if a match based on `HEAD` or `OPTIONS` requests was due to the developer 610 | specifying the methods explicitly when creating the route (the `implicit*()` 611 | methods will return `false` if explicitly specified). 612 | 613 | ### Deprecated 614 | 615 | - [#23](https://github.com/zendframework/zend-expressive-router/pull/23) 616 | deprecates `RouteResult::fromRouteMatch()` in favor of the new `fromRoute()` 617 | method. 618 | 619 | ### Removed 620 | 621 | - Nothing. 622 | 623 | ### Fixed 624 | 625 | - Nothing. 626 | 627 | ## 1.2.0 - 2016-01-18 628 | 629 | ### Added 630 | 631 | - Nothing. 632 | 633 | ### Deprecated 634 | 635 | - [#5](https://github.com/zendframework/zend-expressive-router/pull/5) 636 | deprecates both `RouteResultObserverInterface` and 637 | `RouteResultSubjectInterface`. The changes introduced in 638 | [zend-expressive #270](https://github.com/zendframework/zend-expressive/pull/270) 639 | make the system obsolete. The interfaces will be removed in 2.0.0. 640 | 641 | ### Removed 642 | 643 | - Nothing. 644 | 645 | ### Fixed 646 | 647 | - Nothing. 648 | 649 | ## 1.1.0 - 2015-12-06 650 | 651 | ### Added 652 | 653 | - [#4](https://github.com/zendframework/zend-expressive-router/pull/4) adds 654 | `RouteResultSubjectInterface`, a complement to `RouteResultObserverInterface`, 655 | defining the following methods: 656 | - `attachRouteResultObserver(RouteResultObserverInterface $observer)` 657 | - `detachRouteResultObserver(RouteResultObserverInterface $observer)` 658 | - `notifyRouteResultObservers(RouteResult $result)` 659 | 660 | ### Deprecated 661 | 662 | - Nothing. 663 | 664 | ### Removed 665 | 666 | - [#4](https://github.com/zendframework/zend-expressive-router/pull/4) removes 667 | the deprecation notice from `RouteResultObserverInterface`. 668 | 669 | ### Fixed 670 | 671 | - Nothing. 672 | 673 | ## 1.0.1 - 2015-12-03 674 | 675 | ### Added 676 | 677 | - Nothing. 678 | 679 | ### Deprecated 680 | 681 | - [#3](https://github.com/zendframework/zend-expressive-router/pull/3) deprecates `RouteResultObserverInterface`, which 682 | [has been moved to the `Zend\Expressive` namespace and package](https://github.com/zendframework/zend-expressive/pull/206). 683 | 684 | ### Removed 685 | 686 | - Nothing. 687 | 688 | ### Fixed 689 | 690 | - [#1](https://github.com/zendframework/zend-expressive-router/pull/1) fixes the 691 | coveralls support to trigger after scripts, so the status of the check does 692 | not make the tests fail. Additionally, ensured that coveralls can receive 693 | the coverage report! 694 | 695 | ## 1.0.0 - 2015-12-02 696 | 697 | First stable release. 698 | 699 | See the [Expressive CHANGELOG](https://github.com/zendframework/zend-expressive/blob/master/CHANGELOG.md] 700 | for a history of changes prior to 1.0. 701 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015-2019, Zend Technologies USA, Inc. 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, 5 | are permitted provided that the following conditions are met: 6 | 7 | - Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | - Redistributions in binary form must reproduce the above copyright notice, this 11 | list of conditions and the following disclaimer in the documentation and/or 12 | other materials provided with the distribution. 13 | 14 | - Neither the name of Zend Technologies USA, Inc. nor the names of its 15 | contributors may be used to endorse or promote products derived from this 16 | software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 19 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 20 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 22 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 23 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 24 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 25 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 27 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # zend-expressive-router 2 | 3 | > ## Repository abandoned 2019-12-31 4 | > 5 | > This repository has moved to [mezzio/mezzio-router](https://github.com/mezzio/mezzio-router). 6 | 7 | [![Build Status](https://secure.travis-ci.org/zendframework/zend-expressive-router.svg?branch=master)](https://secure.travis-ci.org/zendframework/zend-expressive-router) 8 | [![Coverage Status](https://coveralls.io/repos/github/zendframework/zend-expressive-router/badge.svg?branch=master)](https://coveralls.io/github/zendframework/zend-expressive-router?branch=master) 9 | 10 | Router subcomponent for [Expressive](https://github.com/zendframework/zend-expressive). 11 | 12 | This package provides the following classes and interfaces: 13 | 14 | - `RouterInterface`, a generic interface to implement for providing routing 15 | capabilities around [PSR-7](http://www.php-fig.org/psr/psr-7/) 16 | `ServerRequest` messages. 17 | - `Route`, a value object describing routed middleware. 18 | - `RouteResult`, a value object describing the results of routing. 19 | 20 | ## Installation 21 | 22 | Typically, you will install this when installing Expressive. However, it can be 23 | used standalone to provide a generic way to provide routed PSR-7 middleware. To 24 | do this, use: 25 | 26 | ```bash 27 | $ composer require zendframework/zend-expressive-router 28 | ``` 29 | 30 | We currently support and provide the following routing integrations: 31 | 32 | - [Aura.Router](https://github.com/auraphp/Aura.Router): 33 | `composer require zendframework/zend-expressive-aurarouter` 34 | - [FastRoute](https://github.com/nikic/FastRoute): 35 | `composer require zendframework/zend-expressive-fastroute` 36 | - [zend-router](https://github.com/zendframework/zend-router): 37 | `composer require zendframework/zend-expressive-zendrouter` 38 | 39 | ## Documentation 40 | 41 | Expressive provides [routing documentation](https://docs.zendframework.com/zend-expressive/features/router/intro/). 42 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "zendframework/zend-expressive-router", 3 | "description": "Router subcomponent for Expressive", 4 | "license": "BSD-3-Clause", 5 | "keywords": [ 6 | "expressive", 7 | "http", 8 | "middleware", 9 | "psr", 10 | "psr-7", 11 | "zf", 12 | "zendframework", 13 | "zend-expressive" 14 | ], 15 | "support": { 16 | "issues": "https://github.com/zendframework/zend-expressive-router/issues", 17 | "source": "https://github.com/zendframework/zend-expressive-router", 18 | "rss": "https://github.com/zendframework/zend-expressive-router/releases.atom", 19 | "chat": "https://zendframework-slack.herokuapp.com", 20 | "forum": "https://discourse.zendframework.com/c/questions/expressive" 21 | }, 22 | "require": { 23 | "php": "^7.1", 24 | "fig/http-message-util": "^1.1.2", 25 | "psr/container": "^1.0", 26 | "psr/http-message": "^1.0.1", 27 | "psr/http-server-middleware": "^1.0" 28 | }, 29 | "require-dev": { 30 | "malukenho/docheader": "^0.1.6", 31 | "phpunit/phpunit": "^7.5.16 || ^8.4.1", 32 | "zendframework/zend-coding-standard": "~1.0.0" 33 | }, 34 | "suggest": { 35 | "zendframework/zend-expressive-aurarouter": "^3.0 to use the Aura.Router routing adapter", 36 | "zendframework/zend-expressive-fastroute": "^3.0 to use the FastRoute routing adapter", 37 | "zendframework/zend-expressive-zendrouter": "^3.0 to use the zend-router routing adapter" 38 | }, 39 | "autoload": { 40 | "psr-4": { 41 | "Zend\\Expressive\\Router\\": "src/" 42 | } 43 | }, 44 | "autoload-dev": { 45 | "psr-4": { 46 | "ZendTest\\Expressive\\Router\\": "test/" 47 | } 48 | }, 49 | "config": { 50 | "sort-packages": true 51 | }, 52 | "extra": { 53 | "branch-alias": { 54 | "dev-master": "3.1.x-dev", 55 | "dev-develop": "3.2.x-dev" 56 | }, 57 | "zf": { 58 | "config-provider": "Zend\\Expressive\\Router\\ConfigProvider" 59 | } 60 | }, 61 | "scripts": { 62 | "check": [ 63 | "@license-check", 64 | "@cs-check", 65 | "@test" 66 | ], 67 | "cs-check": "phpcs", 68 | "cs-fix": "phpcbf", 69 | "test": "phpunit --colors=always", 70 | "test-coverage": "phpunit --colors=always --coverage-clover clover.xml", 71 | "license-check": "docheader check src/ test/" 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/ConfigProvider.php: -------------------------------------------------------------------------------- 1 | $this->getDependencies(), 18 | ]; 19 | } 20 | 21 | public function getDependencies() : array 22 | { 23 | // @codingStandardsIgnoreStart 24 | return [ 25 | 'factories' => [ 26 | Middleware\DispatchMiddleware::class => Middleware\DispatchMiddlewareFactory::class, 27 | Middleware\ImplicitHeadMiddleware::class => Middleware\ImplicitHeadMiddlewareFactory::class, 28 | Middleware\ImplicitOptionsMiddleware::class => Middleware\ImplicitOptionsMiddlewareFactory::class, 29 | Middleware\MethodNotAllowedMiddleware::class => Middleware\MethodNotAllowedMiddlewareFactory::class, 30 | Middleware\RouteMiddleware::class => Middleware\RouteMiddlewareFactory::class, 31 | RouteCollector::class => RouteCollectorFactory::class, 32 | ] 33 | ]; 34 | // @codingStandardsIgnoreEnd 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Exception/DuplicateRouteException.php: -------------------------------------------------------------------------------- 1 | getAttribute(RouteResult::class, false); 31 | if (! $routeResult) { 32 | return $handler->handle($request); 33 | } 34 | 35 | return $routeResult->process($request, $handler); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Middleware/DispatchMiddlewareFactory.php: -------------------------------------------------------------------------------- 1 | router = $router; 67 | 68 | // Factory is wrapped in closur in order to enforce return type safety. 69 | $this->streamFactory = function () use ($streamFactory) : StreamInterface { 70 | return $streamFactory(); 71 | }; 72 | } 73 | 74 | /** 75 | * Handle an implicit HEAD request. 76 | * 77 | * If the route allows GET requests, dispatches as a GET request and 78 | * resets the response body to be empty; otherwise, creates a new empty 79 | * response. 80 | */ 81 | public function process(ServerRequestInterface $request, RequestHandlerInterface $handler) : ResponseInterface 82 | { 83 | if ($request->getMethod() !== RequestMethod::METHOD_HEAD) { 84 | return $handler->handle($request); 85 | } 86 | 87 | $result = $request->getAttribute(RouteResult::class); 88 | if (! $result) { 89 | return $handler->handle($request); 90 | } 91 | 92 | if ($result->getMatchedRoute()) { 93 | return $handler->handle($request); 94 | } 95 | 96 | $routeResult = $this->router->match($request->withMethod(RequestMethod::METHOD_GET)); 97 | if ($routeResult->isFailure()) { 98 | return $handler->handle($request); 99 | } 100 | 101 | // Copy matched parameters like RouteMiddleware does 102 | foreach ($routeResult->getMatchedParams() as $param => $value) { 103 | $request = $request->withAttribute($param, $value); 104 | } 105 | 106 | $response = $handler->handle( 107 | $request 108 | ->withAttribute(RouteResult::class, $routeResult) 109 | ->withMethod(RequestMethod::METHOD_GET) 110 | ->withAttribute(self::FORWARDED_HTTP_METHOD_ATTRIBUTE, RequestMethod::METHOD_HEAD) 111 | ); 112 | 113 | /** @var StreamInterface $body */ 114 | $body = ($this->streamFactory)(); 115 | return $response->withBody($body); 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /src/Middleware/ImplicitHeadMiddlewareFactory.php: -------------------------------------------------------------------------------- 1 | has(RouterInterface::class)) { 36 | throw MissingDependencyException::dependencyForService( 37 | RouterInterface::class, 38 | ImplicitHeadMiddleware::class 39 | ); 40 | } 41 | 42 | if (! $container->has(StreamInterface::class)) { 43 | throw MissingDependencyException::dependencyForService( 44 | StreamInterface::class, 45 | ImplicitHeadMiddleware::class 46 | ); 47 | } 48 | 49 | return new ImplicitHeadMiddleware( 50 | $container->get(RouterInterface::class), 51 | $container->get(StreamInterface::class) 52 | ); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/Middleware/ImplicitOptionsMiddleware.php: -------------------------------------------------------------------------------- 1 | responseFactory = function () use ($responseFactory) : ResponseInterface { 60 | return $responseFactory(); 61 | }; 62 | } 63 | 64 | /** 65 | * Handle an implicit OPTIONS request. 66 | */ 67 | public function process(ServerRequestInterface $request, RequestHandlerInterface $handler) : ResponseInterface 68 | { 69 | if ($request->getMethod() !== RequestMethod::METHOD_OPTIONS) { 70 | return $handler->handle($request); 71 | } 72 | 73 | $result = $request->getAttribute(RouteResult::class); 74 | if (! $result) { 75 | return $handler->handle($request); 76 | } 77 | 78 | if ($result->isFailure() && ! $result->isMethodFailure()) { 79 | return $handler->handle($request); 80 | } 81 | 82 | if ($result->getMatchedRoute()) { 83 | return $handler->handle($request); 84 | } 85 | 86 | $allowedMethods = $result->getAllowedMethods(); 87 | 88 | return ($this->responseFactory)()->withHeader('Allow', implode(',', $allowedMethods)); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/Middleware/ImplicitOptionsMiddlewareFactory.php: -------------------------------------------------------------------------------- 1 | has(ResponseInterface::class)) { 33 | throw MissingDependencyException::dependencyForService( 34 | ResponseInterface::class, 35 | ImplicitOptionsMiddleware::class 36 | ); 37 | } 38 | 39 | return new ImplicitOptionsMiddleware( 40 | $container->get(ResponseInterface::class) 41 | ); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/Middleware/MethodNotAllowedMiddleware.php: -------------------------------------------------------------------------------- 1 | responseFactory = function () use ($responseFactory) : ResponseInterface { 43 | return $responseFactory(); 44 | }; 45 | } 46 | 47 | public function process(ServerRequestInterface $request, RequestHandlerInterface $handler) : ResponseInterface 48 | { 49 | $routeResult = $request->getAttribute(RouteResult::class); 50 | if (! $routeResult || ! $routeResult->isMethodFailure()) { 51 | return $handler->handle($request); 52 | } 53 | 54 | return ($this->responseFactory)() 55 | ->withStatus(StatusCode::STATUS_METHOD_NOT_ALLOWED) 56 | ->withHeader('Allow', implode(',', $routeResult->getAllowedMethods())); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/Middleware/MethodNotAllowedMiddlewareFactory.php: -------------------------------------------------------------------------------- 1 | has(ResponseInterface::class)) { 33 | throw MissingDependencyException::dependencyForService( 34 | ResponseInterface::class, 35 | MethodNotAllowedMiddleware::class 36 | ); 37 | } 38 | 39 | return new MethodNotAllowedMiddleware( 40 | $container->get(ResponseInterface::class) 41 | ); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/Middleware/RouteMiddleware.php: -------------------------------------------------------------------------------- 1 | router = $router; 39 | } 40 | 41 | public function process(ServerRequestInterface $request, RequestHandlerInterface $handler) : ResponseInterface 42 | { 43 | $result = $this->router->match($request); 44 | 45 | // Inject the actual route result, as well as individual matched parameters. 46 | $request = $request->withAttribute(RouteResult::class, $result); 47 | 48 | if ($result->isSuccess()) { 49 | foreach ($result->getMatchedParams() as $param => $value) { 50 | $request = $request->withAttribute($param, $value); 51 | } 52 | } 53 | 54 | return $handler->handle($request); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/Middleware/RouteMiddlewareFactory.php: -------------------------------------------------------------------------------- 1 | routerServiceName = $routerServiceName; 46 | } 47 | 48 | /** 49 | * @throws MissingDependencyException if the RouterInterface service is 50 | * missing. 51 | */ 52 | public function __invoke(ContainerInterface $container) : RouteMiddleware 53 | { 54 | if (! $container->has($this->routerServiceName)) { 55 | throw MissingDependencyException::dependencyForService( 56 | $this->routerServiceName, 57 | RouteMiddleware::class 58 | ); 59 | } 60 | 61 | return new RouteMiddleware($container->get($this->routerServiceName)); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/Route.php: -------------------------------------------------------------------------------- 1 | path = $path; 84 | $this->middleware = $middleware; 85 | $this->methods = is_array($methods) ? $this->validateHttpMethods($methods) : $methods; 86 | 87 | if (! $name) { 88 | $name = $this->methods === self::HTTP_METHOD_ANY 89 | ? $path 90 | : $path . '^' . implode(self::HTTP_METHOD_SEPARATOR, $this->methods); 91 | } 92 | $this->name = $name; 93 | } 94 | 95 | /** 96 | * Proxies to the middleware composed during instantiation. 97 | */ 98 | public function process(ServerRequestInterface $request, RequestHandlerInterface $handler) : ResponseInterface 99 | { 100 | return $this->middleware->process($request, $handler); 101 | } 102 | 103 | public function getPath() : string 104 | { 105 | return $this->path; 106 | } 107 | 108 | /** 109 | * Set the route name. 110 | */ 111 | public function setName(string $name) : void 112 | { 113 | $this->name = $name; 114 | } 115 | 116 | public function getName() : string 117 | { 118 | return $this->name; 119 | } 120 | 121 | public function getMiddleware() : MiddlewareInterface 122 | { 123 | return $this->middleware; 124 | } 125 | 126 | /** 127 | * @return null|string[] Returns HTTP_METHOD_ANY or array of allowed methods. 128 | */ 129 | public function getAllowedMethods() : ?array 130 | { 131 | return $this->methods; 132 | } 133 | 134 | /** 135 | * Indicate whether the specified method is allowed by the route. 136 | * 137 | * @param string $method HTTP method to test. 138 | */ 139 | public function allowsMethod(string $method) : bool 140 | { 141 | $method = strtoupper($method); 142 | if ($this->methods === self::HTTP_METHOD_ANY 143 | || in_array($method, $this->methods, true) 144 | ) { 145 | return true; 146 | } 147 | 148 | return false; 149 | } 150 | 151 | public function setOptions(array $options) : void 152 | { 153 | $this->options = $options; 154 | } 155 | 156 | public function getOptions() : array 157 | { 158 | return $this->options; 159 | } 160 | 161 | /** 162 | * Validate the provided HTTP method names. 163 | * 164 | * Validates, and then normalizes to upper case. 165 | * 166 | * @param string[] An array of HTTP method names. 167 | * @return string[] 168 | * @throws Exception\InvalidArgumentException for any invalid method names. 169 | */ 170 | private function validateHttpMethods(array $methods) : array 171 | { 172 | if (empty($methods)) { 173 | throw new Exception\InvalidArgumentException( 174 | 'HTTP methods argument was empty; must contain at least one method' 175 | ); 176 | } 177 | 178 | if (false === array_reduce($methods, function ($valid, $method) { 179 | if (false === $valid) { 180 | return false; 181 | } 182 | 183 | if (! is_string($method)) { 184 | return false; 185 | } 186 | 187 | if (! preg_match('/^[!#$%&\'*+.^_`\|~0-9a-z-]+$/i', $method)) { 188 | return false; 189 | } 190 | 191 | return $valid; 192 | }, true)) { 193 | throw new Exception\InvalidArgumentException('One or more HTTP methods were invalid'); 194 | } 195 | 196 | return array_map('strtoupper', $methods); 197 | } 198 | } 199 | -------------------------------------------------------------------------------- /src/RouteCollector.php: -------------------------------------------------------------------------------- 1 | router = $router; 54 | } 55 | 56 | /** 57 | * Add a route for the route middleware to match. 58 | * 59 | * Accepts a combination of a path and middleware, and optionally the HTTP methods allowed. 60 | * 61 | * @param null|array $methods HTTP method to accept; null indicates any. 62 | * @param null|string $name The name of the route. 63 | * @throws Exception\DuplicateRouteException if specification represents an existing route. 64 | */ 65 | public function route( 66 | string $path, 67 | MiddlewareInterface $middleware, 68 | array $methods = null, 69 | string $name = null 70 | ) : Route { 71 | $this->checkForDuplicateRoute($path, $methods); 72 | 73 | $methods = null === $methods ? Route::HTTP_METHOD_ANY : $methods; 74 | $route = new Route($path, $middleware, $methods, $name); 75 | 76 | $this->routes[] = $route; 77 | $this->router->addRoute($route); 78 | 79 | return $route; 80 | } 81 | 82 | /** 83 | * @param null|string $name The name of the route. 84 | */ 85 | public function get(string $path, MiddlewareInterface $middleware, string $name = null) : Route 86 | { 87 | return $this->route($path, $middleware, ['GET'], $name); 88 | } 89 | 90 | /** 91 | * @param null|string $name The name of the route. 92 | */ 93 | public function post(string $path, MiddlewareInterface $middleware, string $name = null) : Route 94 | { 95 | return $this->route($path, $middleware, ['POST'], $name); 96 | } 97 | 98 | /** 99 | * @param null|string $name The name of the route. 100 | */ 101 | public function put(string $path, MiddlewareInterface $middleware, string $name = null) : Route 102 | { 103 | return $this->route($path, $middleware, ['PUT'], $name); 104 | } 105 | 106 | /** 107 | * @param null|string $name The name of the route. 108 | */ 109 | public function patch(string $path, MiddlewareInterface $middleware, string $name = null) : Route 110 | { 111 | return $this->route($path, $middleware, ['PATCH'], $name); 112 | } 113 | 114 | /** 115 | * @param null|string $name The name of the route. 116 | */ 117 | public function delete(string $path, MiddlewareInterface $middleware, string $name = null) : Route 118 | { 119 | return $this->route($path, $middleware, ['DELETE'], $name); 120 | } 121 | 122 | /** 123 | * @param null|string $name The name of the route. 124 | */ 125 | public function any(string $path, MiddlewareInterface $middleware, string $name = null) : Route 126 | { 127 | return $this->route($path, $middleware, null, $name); 128 | } 129 | 130 | /** 131 | * Retrieve all directly registered routes with the application. 132 | * 133 | * @return Route[] 134 | */ 135 | public function getRoutes() : array 136 | { 137 | return $this->routes; 138 | } 139 | 140 | /** 141 | * Determine if the route is duplicated in the current list. 142 | * 143 | * Checks if a route with the same name or path exists already in the list; 144 | * if so, and it responds to any of the $methods indicated, raises 145 | * a DuplicateRouteException indicating a duplicate route. 146 | * 147 | * @throws Exception\DuplicateRouteException on duplicate route detection. 148 | */ 149 | private function checkForDuplicateRoute(string $path, array $methods = null) : void 150 | { 151 | if (null === $methods) { 152 | $methods = Route::HTTP_METHOD_ANY; 153 | } 154 | 155 | $matches = array_filter($this->routes, function (Route $route) use ($path, $methods) { 156 | if ($path !== $route->getPath()) { 157 | return false; 158 | } 159 | 160 | if ($methods === Route::HTTP_METHOD_ANY) { 161 | return true; 162 | } 163 | 164 | return array_reduce($methods, function ($carry, $method) use ($route) { 165 | return ($carry || $route->allowsMethod($method)); 166 | }, false); 167 | }); 168 | 169 | if (! empty($matches)) { 170 | $match = reset($matches); 171 | $allowedMethods = $match->getAllowedMethods() ?: ['(any)']; 172 | $name = $match->getName(); 173 | throw new Exception\DuplicateRouteException(sprintf( 174 | 'Duplicate route detected; path "%s" answering to methods [%s]%s', 175 | $match->getPath(), 176 | implode(',', $allowedMethods), 177 | $name ? sprintf(', with name "%s"', $name) : '' 178 | )); 179 | } 180 | } 181 | } 182 | -------------------------------------------------------------------------------- /src/RouteCollectorFactory.php: -------------------------------------------------------------------------------- 1 | has(RouterInterface::class)) { 31 | throw Exception\MissingDependencyException::dependencyForService( 32 | RouterInterface::class, 33 | RouteCollector::class 34 | ); 35 | } 36 | 37 | return new RouteCollector($container->get(RouterInterface::class)); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/RouteResult.php: -------------------------------------------------------------------------------- 1 | success = true; 76 | $result->route = $route; 77 | $result->matchedParams = $params; 78 | return $result; 79 | } 80 | 81 | /** 82 | * Create an instance representing a route failure. 83 | * 84 | * @param null|array $methods HTTP methods allowed for the current URI, if any. 85 | * null is equivalent to allowing any HTTP method; empty array means none. 86 | */ 87 | public static function fromRouteFailure(?array $methods) : self 88 | { 89 | $result = new self(); 90 | $result->success = false; 91 | $result->allowedMethods = $methods; 92 | 93 | return $result; 94 | } 95 | 96 | /** 97 | * Process the result as middleware. 98 | * 99 | * If the result represents a failure, it passes handling to the handler. 100 | * 101 | * Otherwise, it processes the composed middleware using the provide request 102 | * and handler. 103 | */ 104 | public function process(ServerRequestInterface $request, RequestHandlerInterface $handler) : ResponseInterface 105 | { 106 | if ($this->isFailure()) { 107 | return $handler->handle($request); 108 | } 109 | 110 | return $this->getMatchedRoute()->process($request, $handler); 111 | } 112 | 113 | /** 114 | * Does the result represent successful routing? 115 | */ 116 | public function isSuccess() : bool 117 | { 118 | return $this->success; 119 | } 120 | 121 | /** 122 | * Retrieve the route that resulted in the route match. 123 | * 124 | * @return false|null|Route false if representing a routing failure; 125 | * null if not created via fromRoute(); Route instance otherwise. 126 | */ 127 | public function getMatchedRoute() 128 | { 129 | return $this->isFailure() ? false : $this->route; 130 | } 131 | 132 | /** 133 | * Retrieve the matched route name, if possible. 134 | * 135 | * If this result represents a failure, return false; otherwise, return the 136 | * matched route name. 137 | * 138 | * @return false|string 139 | */ 140 | public function getMatchedRouteName() 141 | { 142 | if ($this->isFailure()) { 143 | return false; 144 | } 145 | 146 | if (! $this->matchedRouteName && $this->route) { 147 | $this->matchedRouteName = $this->route->getName(); 148 | } 149 | 150 | return $this->matchedRouteName; 151 | } 152 | 153 | /** 154 | * Returns the matched params. 155 | * 156 | * Guaranted to return an array, even if it is simply empty. 157 | */ 158 | public function getMatchedParams() : array 159 | { 160 | return $this->matchedParams; 161 | } 162 | 163 | /** 164 | * Is this a routing failure result? 165 | */ 166 | public function isFailure() : bool 167 | { 168 | return (! $this->success); 169 | } 170 | 171 | /** 172 | * Does the result represent failure to route due to HTTP method? 173 | */ 174 | public function isMethodFailure() : bool 175 | { 176 | if ($this->isSuccess() || $this->allowedMethods === Route::HTTP_METHOD_ANY) { 177 | return false; 178 | } 179 | 180 | return true; 181 | } 182 | 183 | /** 184 | * Retrieve the allowed methods for the route failure. 185 | * 186 | * @return null|string[] HTTP methods allowed 187 | */ 188 | public function getAllowedMethods() : ?array 189 | { 190 | if ($this->isSuccess()) { 191 | return $this->route 192 | ? $this->route->getAllowedMethods() 193 | : []; 194 | } 195 | 196 | return $this->allowedMethods; 197 | } 198 | 199 | /** 200 | * Only allow instantiation via factory methods. 201 | */ 202 | private function __construct() 203 | { 204 | } 205 | } 206 | -------------------------------------------------------------------------------- /src/RouterInterface.php: -------------------------------------------------------------------------------- 1 | [ 79 | RequestMethod::METHOD_HEAD, 80 | [RequestMethod::METHOD_HEAD, RequestMethod::METHOD_POST], 81 | ]; 82 | 83 | yield 'HEAD: head, get' => [ 84 | RequestMethod::METHOD_HEAD, 85 | [RequestMethod::METHOD_HEAD, RequestMethod::METHOD_GET], 86 | ]; 87 | 88 | yield 'HEAD: post, head' => [ 89 | RequestMethod::METHOD_HEAD, 90 | [RequestMethod::METHOD_POST, RequestMethod::METHOD_HEAD], 91 | ]; 92 | 93 | yield 'HEAD: get, head' => [ 94 | RequestMethod::METHOD_HEAD, 95 | [RequestMethod::METHOD_GET, RequestMethod::METHOD_HEAD], 96 | ]; 97 | 98 | yield 'OPTIONS: options, post' => [ 99 | RequestMethod::METHOD_OPTIONS, 100 | [RequestMethod::METHOD_OPTIONS, RequestMethod::METHOD_POST], 101 | ]; 102 | 103 | yield 'OPTIONS: options, get' => [ 104 | RequestMethod::METHOD_OPTIONS, 105 | [RequestMethod::METHOD_OPTIONS, RequestMethod::METHOD_GET], 106 | ]; 107 | 108 | yield 'OPTIONS: post, options' => [ 109 | RequestMethod::METHOD_OPTIONS, 110 | [RequestMethod::METHOD_POST, RequestMethod::METHOD_OPTIONS], 111 | ]; 112 | 113 | yield 'OPTIONS: get, options' => [ 114 | RequestMethod::METHOD_OPTIONS, 115 | [RequestMethod::METHOD_GET, RequestMethod::METHOD_OPTIONS], 116 | ]; 117 | } 118 | 119 | /** 120 | * @dataProvider method 121 | */ 122 | public function testExplicitRequest(string $method, array $routes) 123 | { 124 | $implicitRoute = null; 125 | $router = $this->getRouter(); 126 | foreach ($routes as $routeMethod) { 127 | $route = new Route( 128 | '/api/v1/me', 129 | $this->prophesize(MiddlewareInterface::class)->reveal(), 130 | [$routeMethod] 131 | ); 132 | $router->addRoute($route); 133 | 134 | if ($routeMethod === $method) { 135 | $implicitRoute = $route; 136 | } 137 | } 138 | 139 | $pipeline = new MiddlewarePipe(); 140 | $pipeline->pipe(new RouteMiddleware($router)); 141 | $pipeline->pipe( 142 | $method === RequestMethod::METHOD_HEAD 143 | ? $this->getImplicitHeadMiddleware($router) 144 | : $this->getImplicitOptionsMiddleware() 145 | ); 146 | $pipeline->pipe(new MethodNotAllowedMiddleware($this->createInvalidResponseFactory())); 147 | 148 | $finalResponse = (new Response())->withHeader('foo-bar', 'baz'); 149 | $finalResponse->getBody()->write('FOO BAR BODY'); 150 | 151 | $finalHandler = $this->prophesize(RequestHandlerInterface::class); 152 | $finalHandler 153 | ->handle(Argument::that(function (ServerRequestInterface $request) use ($method, $implicitRoute) { 154 | Assert::assertSame($method, $request->getMethod()); 155 | Assert::assertNull($request->getAttribute(ImplicitHeadMiddleware::FORWARDED_HTTP_METHOD_ATTRIBUTE)); 156 | 157 | $routeResult = $request->getAttribute(RouteResult::class); 158 | Assert::assertInstanceOf(RouteResult::class, $routeResult); 159 | Assert::assertTrue($routeResult->isSuccess()); 160 | 161 | $matchedRoute = $routeResult->getMatchedRoute(); 162 | Assert::assertNotNull($matchedRoute); 163 | Assert::assertSame($implicitRoute, $matchedRoute); 164 | 165 | return true; 166 | })) 167 | ->willReturn($finalResponse) 168 | ->shouldBeCalledTimes(1); 169 | 170 | $request = new ServerRequest(['REQUEST_METHOD' => $method], [], '/api/v1/me', $method); 171 | 172 | $response = $pipeline->process($request, $finalHandler->reveal()); 173 | 174 | $this->assertEquals(StatusCode::STATUS_OK, $response->getStatusCode()); 175 | $this->assertSame('FOO BAR BODY', (string) $response->getBody()); 176 | $this->assertTrue($response->hasHeader('foo-bar')); 177 | $this->assertSame('baz', $response->getHeaderLine('foo-bar')); 178 | } 179 | 180 | public function withoutImplicitMiddleware() 181 | { 182 | // @codingStandardsIgnoreStart 183 | // request method, array of allowed methods for a route. 184 | yield 'HEAD: get' => [RequestMethod::METHOD_HEAD, [RequestMethod::METHOD_GET]]; 185 | yield 'HEAD: post' => [RequestMethod::METHOD_HEAD, [RequestMethod::METHOD_POST]]; 186 | yield 'HEAD: get, post' => [RequestMethod::METHOD_HEAD, [RequestMethod::METHOD_GET, RequestMethod::METHOD_POST]]; 187 | 188 | yield 'OPTIONS: get' => [RequestMethod::METHOD_OPTIONS, [RequestMethod::METHOD_GET]]; 189 | yield 'OPTIONS: post' => [RequestMethod::METHOD_OPTIONS, [RequestMethod::METHOD_POST]]; 190 | yield 'OPTIONS: get, post' => [RequestMethod::METHOD_OPTIONS, [RequestMethod::METHOD_GET, RequestMethod::METHOD_POST]]; 191 | // @codingStandardsIgnoreEnd 192 | } 193 | 194 | /** 195 | * In case we are not using Implicit*Middlewares and we don't have any route with explicit method 196 | * returned response should be 405: Method Not Allowed - handled by MethodNotAllowedMiddleware. 197 | * 198 | * @dataProvider withoutImplicitMiddleware 199 | */ 200 | public function testWithoutImplicitMiddleware(string $requestMethod, array $allowedMethods) 201 | { 202 | $router = $this->getRouter(); 203 | foreach ($allowedMethods as $routeMethod) { 204 | $route = new Route( 205 | '/api/v1/me', 206 | $this->prophesize(MiddlewareInterface::class)->reveal(), 207 | [$routeMethod] 208 | ); 209 | $router->addRoute($route); 210 | } 211 | 212 | $finalResponse = $this->prophesize(ResponseInterface::class); 213 | $finalResponse->withStatus(StatusCode::STATUS_METHOD_NOT_ALLOWED)->will([$finalResponse, 'reveal']); 214 | $finalResponse->withHeader('Allow', implode(',', $allowedMethods))->will([$finalResponse, 'reveal']); 215 | 216 | $pipeline = new MiddlewarePipe(); 217 | $pipeline->pipe(new RouteMiddleware($router)); 218 | $pipeline->pipe(new MethodNotAllowedMiddleware(function () use ($finalResponse) { 219 | return $finalResponse->reveal(); 220 | })); 221 | 222 | $finalHandler = $this->prophesize(RequestHandlerInterface::class); 223 | $finalHandler->handle(Argument::any())->shouldNotBeCalled(); 224 | 225 | $request = new ServerRequest(['REQUEST_METHOD' => $requestMethod], [], '/api/v1/me', $requestMethod); 226 | 227 | $response = $pipeline->process($request, $finalHandler->reveal()); 228 | 229 | $this->assertSame($finalResponse->reveal(), $response); 230 | } 231 | 232 | /** 233 | * Provider for the testImplicitHeadRequest method. 234 | * 235 | * Implementations must provide this method. Each test case returned 236 | * must consist of the following three elements, in order: 237 | * 238 | * - string route path (the match string) 239 | * - array route options (if any/required) 240 | * - string request path (the path in the ServerRequest instance) 241 | * - array params (expected route parameters matched) 242 | */ 243 | abstract public function implicitRoutesAndRequests() : Generator; 244 | 245 | /** 246 | * @dataProvider implicitRoutesAndRequests 247 | */ 248 | public function testImplicitHeadRequest( 249 | string $routePath, 250 | array $routeOptions, 251 | string $requestPath, 252 | array $expectedParams 253 | ) { 254 | $finalResponse = (new Response())->withHeader('foo-bar', 'baz'); 255 | $finalResponse->getBody()->write('FOO BAR BODY'); 256 | 257 | $middleware1 = $this->prophesize(MiddlewareInterface::class); 258 | $middleware2 = $this->prophesize(MiddlewareInterface::class); 259 | $middleware2->process(Argument::any(), Argument::any())->shouldNotBeCalled(); 260 | 261 | $route1 = new Route($routePath, $middleware1->reveal(), [RequestMethod::METHOD_GET]); 262 | $route1->setOptions($routeOptions); 263 | $middleware1 264 | ->process( 265 | Argument::that(function (ServerRequestInterface $request) use ($route1, $expectedParams) { 266 | Assert::assertSame(RequestMethod::METHOD_GET, $request->getMethod()); 267 | Assert::assertSame( 268 | RequestMethod::METHOD_HEAD, 269 | $request->getAttribute(ImplicitHeadMiddleware::FORWARDED_HTTP_METHOD_ATTRIBUTE) 270 | ); 271 | 272 | $routeResult = $request->getAttribute(RouteResult::class); 273 | Assert::assertInstanceOf(RouteResult::class, $routeResult); 274 | Assert::assertTrue($routeResult->isSuccess()); 275 | 276 | // Some implementations include more in the matched params than what we expect; 277 | // e.g., zend-router will include the middleware as well. 278 | $matchedParams = $routeResult->getMatchedParams(); 279 | foreach ($expectedParams as $key => $value) { 280 | Assert::assertArrayHasKey($key, $matchedParams); 281 | Assert::assertSame($value, $matchedParams[$key]); 282 | } 283 | 284 | $matchedRoute = $routeResult->getMatchedRoute(); 285 | Assert::assertNotNull($matchedRoute); 286 | Assert::assertSame($route1, $matchedRoute); 287 | 288 | return true; 289 | }), 290 | Argument::type(RequestHandlerInterface::class) 291 | ) 292 | ->willReturn($finalResponse); 293 | 294 | $route2 = new Route($routePath, $middleware2->reveal(), [RequestMethod::METHOD_POST]); 295 | $route2->setOptions($routeOptions); 296 | 297 | $router = $this->getRouter(); 298 | $router->addRoute($route1); 299 | $router->addRoute($route2); 300 | 301 | $finalHandler = $this->prophesize(RequestHandlerInterface::class); 302 | $finalHandler->handle(Argument::any())->shouldNotBeCalled(); 303 | 304 | $pipeline = new MiddlewarePipe(); 305 | $pipeline->pipe(new RouteMiddleware($router)); 306 | $pipeline->pipe($this->getImplicitHeadMiddleware($router)); 307 | $pipeline->pipe(new MethodNotAllowedMiddleware($this->createInvalidResponseFactory())); 308 | $pipeline->pipe(new DispatchMiddleware()); 309 | 310 | $request = new ServerRequest( 311 | ['REQUEST_METHOD' => RequestMethod::METHOD_HEAD], 312 | [], 313 | $requestPath, 314 | RequestMethod::METHOD_HEAD 315 | ); 316 | 317 | $response = $pipeline->process($request, $finalHandler->reveal()); 318 | 319 | $this->assertEquals(StatusCode::STATUS_OK, $response->getStatusCode()); 320 | $this->assertEmpty((string) $response->getBody()); 321 | $this->assertTrue($response->hasHeader('foo-bar')); 322 | $this->assertSame('baz', $response->getHeaderLine('foo-bar')); 323 | } 324 | 325 | /** 326 | * @dataProvider implicitRoutesAndRequests 327 | */ 328 | public function testImplicitOptionsRequest( 329 | string $routePath, 330 | array $routeOptions, 331 | string $requestPath 332 | ) { 333 | $middleware1 = $this->prophesize(MiddlewareInterface::class)->reveal(); 334 | $middleware2 = $this->prophesize(MiddlewareInterface::class)->reveal(); 335 | $route1 = new Route($routePath, $middleware1, [RequestMethod::METHOD_GET]); 336 | $route1->setOptions($routeOptions); 337 | $route2 = new Route($routePath, $middleware2, [RequestMethod::METHOD_POST]); 338 | $route2->setOptions($routeOptions); 339 | 340 | $router = $this->getRouter(); 341 | $router->addRoute($route1); 342 | $router->addRoute($route2); 343 | 344 | $finalResponse = $this->prophesize(ResponseInterface::class); 345 | $finalResponse->withHeader('Allow', 'GET,POST')->will([$finalResponse, 'reveal']); 346 | 347 | $pipeline = new MiddlewarePipe(); 348 | $pipeline->pipe(new RouteMiddleware($router)); 349 | $pipeline->pipe($this->getImplicitOptionsMiddleware($finalResponse->reveal())); 350 | $pipeline->pipe(new MethodNotAllowedMiddleware($this->createInvalidResponseFactory())); 351 | 352 | $request = new ServerRequest( 353 | ['REQUEST_METHOD' => RequestMethod::METHOD_OPTIONS], 354 | [], 355 | $requestPath, 356 | RequestMethod::METHOD_OPTIONS 357 | ); 358 | 359 | $finalHandler = $this->prophesize(RequestHandlerInterface::class); 360 | $finalHandler->handle()->shouldNotBeCalled(); 361 | 362 | $response = $pipeline->process($request, $finalHandler->reveal()); 363 | 364 | $this->assertSame($finalResponse->reveal(), $response); 365 | } 366 | 367 | public function testImplicitOptionsRequestRouteNotFound() 368 | { 369 | $router = $this->getRouter(); 370 | 371 | $pipeline = new MiddlewarePipe(); 372 | $pipeline->pipe(new RouteMiddleware($router)); 373 | $pipeline->pipe($this->getImplicitOptionsMiddleware()); 374 | $pipeline->pipe(new MethodNotAllowedMiddleware($this->createInvalidResponseFactory())); 375 | $pipeline->pipe(new DispatchMiddleware()); 376 | 377 | $finalResponse = (new Response()) 378 | ->withStatus(StatusCode::STATUS_IM_A_TEAPOT) 379 | ->withHeader('foo-bar', 'baz'); 380 | $finalResponse->getBody()->write('FOO BAR BODY'); 381 | 382 | $request = new ServerRequest( 383 | ['REQUEST_METHOD' => RequestMethod::METHOD_OPTIONS], 384 | [], 385 | '/not-found', 386 | RequestMethod::METHOD_OPTIONS 387 | ); 388 | 389 | $finalHandler = $this->prophesize(RequestHandlerInterface::class); 390 | $finalHandler 391 | ->handle(Argument::that(function (ServerRequestInterface $request) { 392 | Assert::assertSame(RequestMethod::METHOD_OPTIONS, $request->getMethod()); 393 | 394 | $routeResult = $request->getAttribute(RouteResult::class); 395 | Assert::assertInstanceOf(RouteResult::class, $routeResult); 396 | Assert::assertTrue($routeResult->isFailure()); 397 | Assert::assertFalse($routeResult->isSuccess()); 398 | Assert::assertFalse($routeResult->isMethodFailure()); 399 | Assert::assertFalse($routeResult->getMatchedRoute()); 400 | 401 | return true; 402 | })) 403 | ->willReturn($finalResponse) 404 | ->shouldBeCalledTimes(1); 405 | 406 | $response = $pipeline->process($request, $finalHandler->reveal()); 407 | 408 | $this->assertEquals(StatusCode::STATUS_IM_A_TEAPOT, $response->getStatusCode()); 409 | $this->assertSame('FOO BAR BODY', (string) $response->getBody()); 410 | $this->assertTrue($response->hasHeader('foo-bar')); 411 | $this->assertSame('baz', $response->getHeaderLine('foo-bar')); 412 | } 413 | } 414 | --------------------------------------------------------------------------------