Compare commits
355 Commits
v2.3.0-alp
...
master
| Author | SHA1 | Date |
|---|---|---|
|
|
ca3bb2cb0a | |
|
|
c5f49214b5 | |
|
|
c153bc0028 | |
|
|
e22d5889b0 | |
|
|
fb2b2ab527 | |
|
|
00ea49ded6 | |
|
|
4c4b6104e4 | |
|
|
6b31620314 | |
|
|
e6bb814a76 | |
|
|
19c03b9b8b | |
|
|
b94b01453a | |
|
|
9db267c427 | |
|
|
8379dd69a0 | |
|
|
9a4b78b84a | |
|
|
2c80da2f79 | |
|
|
a15b5396e9 | |
|
|
aeb9d733cc | |
|
|
c74a468f3e | |
|
|
84b4bce04f | |
|
|
337baa2e26 | |
|
|
bba0602122 | |
|
|
66ff6d2fc1 | |
|
|
198ebb60f7 | |
|
|
8f2206a65f | |
|
|
8b467193d7 | |
|
|
95c3be85c9 | |
|
|
9243dd7663 | |
|
|
20012dc671 | |
|
|
79d23e37a4 | |
|
|
5ba8771e93 | |
|
|
fe0f1ff8f9 | |
|
|
36df1f33c1 | |
|
|
c137a2066a | |
|
|
1d71f06e71 | |
|
|
997582f8d7 | |
|
|
56503da35e | |
|
|
79c4799a7b | |
|
|
fec5a4d405 | |
|
|
4eec56b582 | |
|
|
c75168afae | |
|
|
288705b77a | |
|
|
f542aaf3a9 | |
|
|
2d7db1ed83 | |
|
|
4796e1d5c5 | |
|
|
8649090774 | |
|
|
d8294d3ac3 | |
|
|
899ebb2492 | |
|
|
83c2290a2f | |
|
|
4a3545cd3c | |
|
|
2e3475b882 | |
|
|
168867206e | |
|
|
cd2e4448e2 | |
|
|
e6d1398bb1 | |
|
|
ff28f3c6eb | |
|
|
002bdb7541 | |
|
|
fb18d2ee1e | |
|
|
a73ad17ddd | |
|
|
d98789ebfd | |
|
|
09dd1d7a32 | |
|
|
98014d8c59 | |
|
|
ca204a07e7 | |
|
|
967b6ac2a4 | |
|
|
c339512f01 | |
|
|
7ade042b4b | |
|
|
bdc5ac40d9 | |
|
|
ecc077a1be | |
|
|
e9fb474eb7 | |
|
|
a7b08ad8a3 | |
|
|
fe780e6b92 | |
|
|
29cfa34f17 | |
|
|
08ddb0aa2f | |
|
|
2cf65def5c | |
|
|
4485675c11 | |
|
|
fbd1c10ebe | |
|
|
e320e7e6c3 | |
|
|
c7b2c335a6 | |
|
|
0bea30f434 | |
|
|
d1c7076929 | |
|
|
d78537809b | |
|
|
a1a0dc0f45 | |
|
|
17c58ae362 | |
|
|
f6a273dbb5 | |
|
|
2eaa8c8697 | |
|
|
645bcf227c | |
|
|
e4cc02dc8a | |
|
|
e558d613ab | |
|
|
e676a17cac | |
|
|
677cdb4d7d | |
|
|
0a0d3c3bc9 | |
|
|
36b03b6ca2 | |
|
|
de46c8e089 | |
|
|
be3d007961 | |
|
|
64628c4065 | |
|
|
ac8bdce037 | |
|
|
73b6e4ab83 | |
|
|
9b29f2a09e | |
|
|
72d5df244d | |
|
|
b82ebf6d95 | |
|
|
5e9e7f154b | |
|
|
9aab0d780e | |
|
|
29cad3687e | |
|
|
f016b74c38 | |
|
|
6f247bccfa | |
|
|
9ce784c897 | |
|
|
04c7b100db | |
|
|
cd9cc09afe | |
|
|
6849c9456f | |
|
|
d5f9dfa37b | |
|
|
473d103739 | |
|
|
cd5f25ba5e | |
|
|
ac9f40be5f | |
|
|
6395a6177c | |
|
|
af1bb538dd | |
|
|
7caf5343d4 | |
|
|
6ddcb03fe8 | |
|
|
a9ba30fa79 | |
|
|
e531af0da5 | |
|
|
4c40db8ecc | |
|
|
1dd9bf0f9c | |
|
|
af3eef4657 | |
|
|
4b1ec94e3f | |
|
|
b8b87a8032 | |
|
|
50f1004be5 | |
|
|
3a77d99e00 | |
|
|
76d952b076 | |
|
|
83381bf5d5 | |
|
|
36a170bcff | |
|
|
353b48394b | |
|
|
54d1aecda3 | |
|
|
6772bd1ae0 | |
|
|
409ffe9371 | |
|
|
887b885eb9 | |
|
|
92294a2e67 | |
|
|
a294a7eaf5 | |
|
|
e0b5c836db | |
|
|
4fb7bf6050 | |
|
|
b3dc82e744 | |
|
|
91249d885f | |
|
|
f9ab311b79 | |
|
|
4eb0b2641e | |
|
|
36bb00dc1a | |
|
|
929f8ffd97 | |
|
|
d3e924485c | |
|
|
f48b3c5fd1 | |
|
|
344b4bb4b9 | |
|
|
e9a18ba224 | |
|
|
0c61641376 | |
|
|
db7aaa2688 | |
|
|
977f89c50e | |
|
|
fa6fb124ad | |
|
|
45379ab241 | |
|
|
180608ac1a | |
|
|
e6205b7ee7 | |
|
|
2fe3575e69 | |
|
|
0cbcd6cbfc | |
|
|
acc5b48314 | |
|
|
1945f63ca1 | |
|
|
6a1f0c2915 | |
|
|
6f33eab90b | |
|
|
4429d4280b | |
|
|
18d6e5fc6a | |
|
|
a8b3ce9829 | |
|
|
375ba819ef | |
|
|
badf9cad95 | |
|
|
eaff062895 | |
|
|
aa8c7b2afe | |
|
|
6abc4044f1 | |
|
|
d08b1cda63 | |
|
|
139e3c43da | |
|
|
753e9ff33a | |
|
|
4ba6763126 | |
|
|
41336d9387 | |
|
|
559a08dc8d | |
|
|
8db2babd44 | |
|
|
5dcd119952 | |
|
|
6b3f2dded1 | |
|
|
9dcb2502b7 | |
|
|
5a56bdebbe | |
|
|
9d9d5e3a1b | |
|
|
97807d8735 | |
|
|
2179446433 | |
|
|
6dcd3251d4 | |
|
|
6acd7c44a1 | |
|
|
c8ddfaae37 | |
|
|
19e72f7040 | |
|
|
4ec0694351 | |
|
|
ab05ca0b40 | |
|
|
dedec4ec4e | |
|
|
0387255676 | |
|
|
ac2ed4a24a | |
|
|
a825654336 | |
|
|
474d8da61c | |
|
|
b06abc0df2 | |
|
|
15da2ab805 | |
|
|
c8bbd6d2b8 | |
|
|
15602d8e97 | |
|
|
74369f5b0b | |
|
|
6dda878dd7 | |
|
|
1953acf25d | |
|
|
3d4a263beb | |
|
|
3f5e2321d9 | |
|
|
1be4ff7691 | |
|
|
3b18d1dcdb | |
|
|
6232f67b9c | |
|
|
61fd0f3354 | |
|
|
1bb93434b2 | |
|
|
22a17e42bb | |
|
|
297e985e84 | |
|
|
26a6a25d3b | |
|
|
e3083609db | |
|
|
75ddf6fa9c | |
|
|
14a7a1bd17 | |
|
|
9cc08bb875 | |
|
|
64eb5aecdd | |
|
|
0f9c5079f9 | |
|
|
67d562b3bc | |
|
|
b198e83d55 | |
|
|
7874484c53 | |
|
|
f849a6ff89 | |
|
|
3811b9085f | |
|
|
3786cfaade | |
|
|
6507028dd3 | |
|
|
37af085ec5 | |
|
|
87caa09b61 | |
|
|
94d6cc23b2 | |
|
|
ec091b34c4 | |
|
|
8071b0b5db | |
|
|
bbb138996a | |
|
|
560b1e8ff0 | |
|
|
2adcbd8636 | |
|
|
b0db3cbcdd | |
|
|
9470f90ee2 | |
|
|
c1a104af4f | |
|
|
06f694154c | |
|
|
5a01d20f8e | |
|
|
36263ba3de | |
|
|
72767b74e8 | |
|
|
d8352e71d9 | |
|
|
a0e4ace6a5 | |
|
|
8874827524 | |
|
|
1d30fcbbba | |
|
|
09ea17d349 | |
|
|
8f4165cdb6 | |
|
|
cfcc3b9690 | |
|
|
86d36e8c15 | |
|
|
58b5107289 | |
|
|
7a53a02c5f | |
|
|
1a49a4ac6c | |
|
|
d5eb044169 | |
|
|
66319218cb | |
|
|
9915dffcfc | |
|
|
ccbe8bb2e0 | |
|
|
7cbbe6d7c5 | |
|
|
ec7dceac98 | |
|
|
9083f2a444 | |
|
|
121b8be044 | |
|
|
b523f2e79d | |
|
|
147ddd0539 | |
|
|
559044a82f | |
|
|
e1058a4132 | |
|
|
8462e2effc | |
|
|
f98ee59e4a | |
|
|
81055c3bd9 | |
|
|
a93b37a548 | |
|
|
257f2b7610 | |
|
|
b76883c9e9 | |
|
|
adf8def961 | |
|
|
086b09db4f | |
|
|
2e7783d19d | |
|
|
5eb30ccafb | |
|
|
dc2aecf3ff | |
|
|
af9fbf9c50 | |
|
|
cee55cada0 | |
|
|
4d5430e589 | |
|
|
1b0fccfe0e | |
|
|
4f667f1dda | |
|
|
dce4bdf572 | |
|
|
534bd43d9b | |
|
|
212bb6871e | |
|
|
f706d47c6d | |
|
|
43c050ec2e | |
|
|
3686e3b1b2 | |
|
|
a254c69607 | |
|
|
26d71bd792 | |
|
|
2e2b9d57c0 | |
|
|
0fabbc5cb1 | |
|
|
7dfa3facc1 | |
|
|
8c4b59c525 | |
|
|
408d82fb73 | |
|
|
6b20d1ea96 | |
|
|
4a75f4e3a6 | |
|
|
b14641d2f4 | |
|
|
9e1c049c38 | |
|
|
5ef74f8b89 | |
|
|
4dba068f3d | |
|
|
79be20c826 | |
|
|
963e1acd58 | |
|
|
b0a0f5262e | |
|
|
4096295421 | |
|
|
6e83b6b050 | |
|
|
90b9503c72 | |
|
|
df8e274f26 | |
|
|
15ddaa1dd2 | |
|
|
9c768793db | |
|
|
dea577fdb4 | |
|
|
cbeadbda53 | |
|
|
5cc259944e | |
|
|
d269970210 | |
|
|
45b13691a2 | |
|
|
cb87660548 | |
|
|
6d9adfc7ee | |
|
|
d66ba80ec9 | |
|
|
0d204d9279 | |
|
|
decf712354 | |
|
|
57271fa19f | |
|
|
f788d9a2f3 | |
|
|
918e33bd0a | |
|
|
e4ef1a8cb3 | |
|
|
c82acfa380 | |
|
|
d367f1de79 | |
|
|
506c37ffdd | |
|
|
bd5902415a | |
|
|
4502df5c1c | |
|
|
a5cb481d79 | |
|
|
a6b8a11cde | |
|
|
64e5786537 | |
|
|
f3e5cddf4a | |
|
|
d95498bcae | |
|
|
51e1be92fd | |
|
|
7cb6304037 | |
|
|
d696727cb1 | |
|
|
166fc66117 | |
|
|
fcbdd1ebfb | |
|
|
fe93ab13c1 | |
|
|
51f057b300 | |
|
|
513db2def1 | |
|
|
734c87188f | |
|
|
60a0913daf | |
|
|
292e213c0a | |
|
|
9da0780875 | |
|
|
197ea3000a | |
|
|
60b309a3d1 | |
|
|
b6df67afd0 | |
|
|
2575bc743e | |
|
|
a2ae6fff7d | |
|
|
5f676cb79f | |
|
|
465425f01f | |
|
|
de9d75fdfc | |
|
|
9d7030faa0 | |
|
|
3ab7c55257 | |
|
|
ba26379fdc | |
|
|
16ed00a841 | |
|
|
f39e820287 | |
|
|
b318e26076 | |
|
|
fcc5474114 |
|
|
@ -0,0 +1,15 @@
|
|||
/.env export-ignore
|
||||
/.gitattributes export-ignore
|
||||
/.gitignore export-ignore
|
||||
/.php_cs* export-ignore
|
||||
/.travis.yml export-ignore
|
||||
/composer.lock export-ignore
|
||||
/coverage export-ignore
|
||||
/docker export-ignore
|
||||
/docker-compose* export-ignore
|
||||
/docs export-ignore
|
||||
/tests export-ignore
|
||||
/phpunit.xml* export-ignore
|
||||
/psalm.xml export-ignore
|
||||
/public export-ignore
|
||||
/vendor export-ignore
|
||||
|
|
@ -5,25 +5,26 @@ vendor/
|
|||
phpdoc/
|
||||
|
||||
# Code coverage report
|
||||
coverage/
|
||||
report/
|
||||
|
||||
# Cache
|
||||
.php_cs.cache
|
||||
|
||||
# Sphinx Documentation
|
||||
docs/build
|
||||
|
||||
# Previewing the README.md
|
||||
README.html
|
||||
preview
|
||||
|
||||
# OSX
|
||||
.DS_Store
|
||||
.AppleDouble
|
||||
|
||||
# IDE
|
||||
.idea/
|
||||
|
||||
# PhpStorm
|
||||
workspace.xml
|
||||
|
||||
# Vagrant
|
||||
.vagrant/
|
||||
# Local scratch files
|
||||
notes
|
||||
|
||||
# Vagrant sandbox site files.
|
||||
/htdocs/
|
||||
/autoload/
|
||||
# Local overrides
|
||||
.env
|
||||
docker-compose.override.yml
|
||||
phpunit.xml
|
||||
12
.travis.yml
12
.travis.yml
|
|
@ -1,12 +0,0 @@
|
|||
language: php
|
||||
php:
|
||||
- "5.6"
|
||||
- "5.5"
|
||||
- "5.4"
|
||||
|
||||
before_script:
|
||||
- composer selfupdate
|
||||
- composer install --prefer-source
|
||||
|
||||
script:
|
||||
- vendor/bin/phpunit
|
||||
454
README.md
454
README.md
|
|
@ -1,440 +1,128 @@
|
|||
WellRESTed
|
||||
==========
|
||||
|
||||
[](https://travis-ci.org/pjdietz/wellrested)
|
||||
[](https://php.net/)
|
||||
[](http://wellrested.readthedocs.org/en/latest/)
|
||||
|
||||
WellRESTed is a micro-framework for creating RESTful APIs in PHP. It provides a lightweight yet powerful routing system and classes to make working with HTTP requests and responses clean and easy.
|
||||
WellRESTed is a library for creating RESTful APIs and websites in PHP that provides abstraction for HTTP messages, a powerful handler and middleware system, and a flexible router.
|
||||
|
||||
Requirements
|
||||
------------
|
||||
This fork (basemaster/wellrested) is back to php 7.2 release.
|
||||
|
||||
- PHP 5.3
|
||||
- [PHP cURL](http://php.net/manual/en/book.curl.php) for making requests with the [`Client`](src/pjdietz/WellRESTed/Client.php) class (Optional)
|
||||
### Features
|
||||
|
||||
- Uses [PSR-7](https://www.php-fig.org/psr/psr-7/) interfaces for requests, responses, and streams. This lets you use other PSR-7 compatable libraries seamlessly with WellRESTed.
|
||||
- Uses [PSR-15](https://www.php-fig.org/psr/psr-15/) interfaces for handlers and middleware to allow sharing and reusing code
|
||||
- Router allows you to match paths with variables such as `/foo/{bar}/{baz}`.
|
||||
- Middleware system provides a way to compose your application from discrete, modular components.
|
||||
- Lazy-loaded handlers and middleware don't instantiate unless they're needed.
|
||||
|
||||
Install
|
||||
-------
|
||||
|
||||
Add an entry for "pjdietz/wellrested" to your composer.json file's `require` property. If you are not already using Composer, create a file in your project called "composer.json" with the following content:
|
||||
Add an entry for "wellrested/wellrested" to your composer.json file's `require` property.
|
||||
|
||||
```json
|
||||
{
|
||||
"require": {
|
||||
"pjdietz/wellrested": "2.*"
|
||||
"wellrested/wellrested": "^5"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Use Composer to download and install WellRESTed. Run these commands from the directory containing the **composer.json** file.
|
||||
Documentation
|
||||
-------------
|
||||
|
||||
```bash
|
||||
$ curl -s https://getcomposer.org/installer | php
|
||||
$ php composer.phar install
|
||||
```
|
||||
See [the documentation](https://wellrested.readthedocs.org/en/latest/) to get started.
|
||||
|
||||
You can now use WellRESTed by including the `vendor/autoload.php` file generated by Composer.
|
||||
|
||||
|
||||
Overview
|
||||
--------
|
||||
|
||||
### Routing and Routes
|
||||
|
||||
WellRESTed's primary goal is to facilitate mapping of URIs to classes that will provide or accept representations. To do this, create a [`Router`](src/pjdietz/WellRESTed/Router.php) instance and load it up with some routes.
|
||||
Example
|
||||
-------
|
||||
|
||||
```php
|
||||
// Build the router.
|
||||
$router = new Router();
|
||||
<?php
|
||||
|
||||
$router->add(
|
||||
["/cats/", $catHandler],
|
||||
["/dogs/*", $dogHandler],
|
||||
["/hamsters/{id}", $hamsterHandler],
|
||||
["~/rabbits/([0-9]+)~", $rabbitHandler]
|
||||
);
|
||||
$router->respond();
|
||||
```
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Psr\Http\Server\RequestHandlerInterface;
|
||||
use WellRESTed\Message\Response;
|
||||
use WellRESTed\Message\Stream;
|
||||
use WellRESTed\Server;
|
||||
|
||||
You can use `Router::add` to one route or multiple routes. When you add routes one-at-a-time, the first argument is the path pattern to match and the second argument is the handler to dispatch. (More on handlers below.) When you add mutple routes, pass an array for route where the first item is the path pattern and the second is the handler.
|
||||
|
||||
Here's the same router added one route at a time.
|
||||
|
||||
```php
|
||||
// Build the router.
|
||||
$router = new Router();
|
||||
|
||||
// Match requests to the exact path "/cats/"
|
||||
$router->add("/cats/", $catHandler);
|
||||
|
||||
// Match requests that begin with "/dogs/", including "/dogs/", "/dogs/flat-coated-retriever", etc.
|
||||
$router->add("/dogs/*", $dogHandler);
|
||||
|
||||
// Match using a URI template and forward the variable to the handler.
|
||||
// "/hamsters/teddy-bear" will provide the handler with an array containing an "id" element with the value "teddy-bear".
|
||||
$router->add("/hamsters/{id}", $hamsterHandler
|
||||
|
||||
// Match using a regular expression and forward the captures to the handler.
|
||||
// "/rabbit/56" will provide the handler with an array containing "56".
|
||||
$router->add("~/rabbits/([0-9]+)~", $rabbitHandler);
|
||||
|
||||
// Dispatch the requset sent to the web server through the router and output the response.
|
||||
$router->respond();
|
||||
```
|
||||
|
||||
#### Route Types
|
||||
|
||||
`Router::add()` scans the path pattern and figures out the best type of route to use. Here's a brief descripton of the types.
|
||||
|
||||
Route Type | Example | Matches
|
||||
--------------- | --------------------- | --------------------------------------
|
||||
`StaticRoute` | `/cats/` | Exact paths only
|
||||
`PrefixRoute` | `/dogs/*` | Paths beginning with a prefix
|
||||
`TemplateRoute` | `/hamsters/{id}` | URI template with variables
|
||||
`RegexRoute` | `~/rabbits/([0-9]+)~` | Regular expression
|
||||
|
||||
Template and Regex routes will forward variables or captures to their handlers. Template routes can also be customized to restrict variables to match specific patterns.
|
||||
|
||||
#### Route Order
|
||||
|
||||
##### Static Routes (Exact Matches)
|
||||
|
||||
When the router evalutes its routes to find the best match, it first checks for a route that is an exact match to the path.
|
||||
The example router above will match a request to `/cats/` immediately.
|
||||
|
||||
##### Prefix Routes
|
||||
|
||||
If no static routes match, the router next tries prefix routes. If the router above were to get a request for `/dogs/border-collies`, it would first check for any static routes to that path, then move on to prefix routes, where it would match and dispatch `$dogHandler`.
|
||||
|
||||
When a router could match a request to multiple prefix routes, it always uses the longest match.
|
||||
|
||||
Give the following router, a request for `/animals/cats/calicos/` will be matched by the second route because it is longer.
|
||||
|
||||
```
|
||||
$myRouter->add(
|
||||
["/animals/*", $animalsHandler],
|
||||
["/animals/cats/*", $catsHandler]
|
||||
);
|
||||
```
|
||||
|
||||
#### Other Routes
|
||||
|
||||
Once the router tries and fails to find a matching static or prefix route, it will try all other routes in series until it find route that matches. The router will always test them in the order you add the routes.
|
||||
|
||||
#### Special Notes
|
||||
|
||||
To prevent the route from interpreting your regular expression as a really weird path, a character other than `/` as the regular expression [delimiter](http://php.net/manual/en/regexp.reference.delimiters.php). For example, `~` or `#`.
|
||||
|
||||
Template Routes can take extra parameters to indicate the characters allows in the variables. See the [wiki page about Routes](https://github.com/pjdietz/wellrested/wiki/Routes) for details.
|
||||
|
||||
|
||||
### Handlers
|
||||
|
||||
Matching the path is the first part of a routes job. The second part is dispatching the request to a handler.
|
||||
|
||||
#### Callables
|
||||
|
||||
WellRESTed provides a number of ways to create and use handlers. The simplest way to use them is to provide a callable that gets called when the route matches.
|
||||
|
||||
```php
|
||||
$router = new Router();
|
||||
$router->add("/hello", function () {
|
||||
header("Content-type: text/plain");
|
||||
echo "Hello, world";
|
||||
return true; // Returning anything non-null signals the router to stop.
|
||||
});
|
||||
$router->respond();
|
||||
```
|
||||
|
||||
This is okay, but a better a approach is to return an instance that implements [`ResponseInterface`](src/pjdietz/WellRESTed/Interfaces/ResponseInterface.php).
|
||||
|
||||
```php
|
||||
$router = new Router();
|
||||
$router->add("/hello", function () {
|
||||
$response = new Response();
|
||||
$response->setStatusCode(200);
|
||||
$response->setHeader("Content-type", "text/plain");
|
||||
$response->setBody("Hello, world");
|
||||
return $response;
|
||||
});
|
||||
$router->respond();
|
||||
```
|
||||
|
||||
Building and returning a response object allows you to provide all of the information for your response such as the status code, headers, and body. It also makes your code more testable since you can send a mock request through the router and inspect the response it returns.
|
||||
|
||||
##### Callable Arguments
|
||||
|
||||
The callable will receive to arguments when it is called. The first is an object representing the request. See [`RequestInterface`](src/pjdietz/WellRESTed/Interfaces/RequestInterface.php).
|
||||
|
||||
```php
|
||||
$router = new Router();
|
||||
$router->add("/cats/", function ($rqst, $args) {
|
||||
|
||||
// Create the response and set defaults.
|
||||
$response = new Response();
|
||||
$response->setHeader("Content-type", "application/json");
|
||||
|
||||
// Determine how to respond based on the request's HTTP method.
|
||||
$method = $rqst->getMethod();
|
||||
if ($method === "GET") {
|
||||
// ...Lookup the current list of cats here...
|
||||
$response->setStatusCode(200);
|
||||
$response->setBody(json_encode($catsList);
|
||||
} elseif ($method === "POST") {
|
||||
// Read from the request and store a new cat.
|
||||
$cat = json_decode($request->getBody());
|
||||
// ...store $cat to the database...
|
||||
$response->setStatusCode(201);
|
||||
$response->setBody(json_encode($newCat);
|
||||
} else {
|
||||
$response->setStatusCode(405);
|
||||
}
|
||||
return $response;
|
||||
});
|
||||
$router->respond();
|
||||
```
|
||||
|
||||
The second argument is an array of extra data. The extra data will contain things such as variables from `TemplateRoutes` or captures from `RegexRoutes`.
|
||||
|
||||
With this router, a request to `/cats/molly` will respond with "Hello, molly".
|
||||
|
||||
```php
|
||||
$router = new Router();
|
||||
$router->add("/cats/{name}", function ($rqst, $args) {
|
||||
$response = new Response();
|
||||
$response->setStatusCode(200);
|
||||
$response->setHeader("Content-type", "text/plain");
|
||||
$response->setBody("Hello, " . $args["name"]);
|
||||
return $response;
|
||||
});
|
||||
$router->respond();
|
||||
```
|
||||
|
||||
You can also pass in your own custom variables by passing an array to `Router::respond` (or `Router::getResponse`).
|
||||
|
||||
```php
|
||||
$router = new Router();
|
||||
$router->add("/", function ($rqst, $args) {
|
||||
$response = new Response();
|
||||
$response->setStatusCode(200);
|
||||
$response->setHeader("Content-type", "text/plain");
|
||||
$response->setBody("Hello, " . $args["name"]);
|
||||
return $response;
|
||||
});
|
||||
$router->respond(["name" => "molly"]);
|
||||
```
|
||||
|
||||
This is one approach to dependency injection. You can pass your dependency containter in as one of the array elements.
|
||||
|
||||
```php
|
||||
$container = new MySuperCoolDependencyContainer();
|
||||
$router = new Router();
|
||||
// ... Add routes ...
|
||||
$router->respond(["container" => container]);
|
||||
```
|
||||
|
||||
The router will make the dependency container available to the handlers it dispatches as the "container" element of the $args array.
|
||||
|
||||
|
||||
#### HandlerInterface
|
||||
|
||||
The callables are handy, but once you need to do something a little more significant, they start to get a bit unmanageble. Rather than returning a [`ResponseInterface`](src/pjdietz/WellRESTed/Interfaces/ResponseInterface.php), you can return a [`HandlerInterface`](src/pjdietz/WellRESTed/Interfaces/HandlerInterface.php).
|
||||
|
||||
The [`HandlerInterface`](src/pjdietz/WellRESTed/Interfaces/HandlerInterface.php) is very simple and only has one method which looks like this:
|
||||
|
||||
```php
|
||||
/**
|
||||
* Return the handled response.
|
||||
*
|
||||
* @param RequestInterface $request The request to respond to.
|
||||
* @param array|null $args Optional additional arguments.
|
||||
* @return ResponseInterface The handled response.
|
||||
*/
|
||||
public function getResponse(RequestInterface $request, array $args = null);
|
||||
```
|
||||
|
||||
Does it look familiar? It should! It's the same signature as the callables we've used to return responses.
|
||||
|
||||
Let's refactor one of our callables to use a proper handler.
|
||||
|
||||
```php
|
||||
Class CatHandler implements HandlerInterface
|
||||
// Create a handler using the PSR-15 RequestHandlerInterface
|
||||
class HomePageHandler implements RequestHandlerInterface
|
||||
{
|
||||
public function getResponse(RequestInterface $request, array $args = null)
|
||||
public function handle(ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
// Create the response and set defaults.
|
||||
$response = new Response();
|
||||
$response->setHeader("Content-type", "application/json");
|
||||
|
||||
// Determine how to respond based on the request's HTTP method.
|
||||
$method = $rqst->getMethod();
|
||||
if ($method === "GET") {
|
||||
// ...Lookup the current list of cats here...
|
||||
$response->setStatusCode(200);
|
||||
$response->setBody(json_encode($catsList);
|
||||
} elseif ($method === "POST") {
|
||||
// Read from the request and store a new cat.
|
||||
$cat = json_decode($request->getBody());
|
||||
// ...store $cat to the database...
|
||||
$response->setStatusCode(201);
|
||||
$response->setBody(json_encode($newCat);
|
||||
} else {
|
||||
$response->setStatusCode(405);
|
||||
}
|
||||
// Create and return new Response object to return with status code,
|
||||
// headers, and body.
|
||||
$response = (new Response(200))
|
||||
->withHeader('Content-type', 'text/html')
|
||||
->withBody(new Stream('<h1>Hello, world!</h1>'));
|
||||
return $response;
|
||||
}
|
||||
}
|
||||
|
||||
$router = new Router();
|
||||
$router->add("/cats/", function () {
|
||||
return new CatHandler();
|
||||
});
|
||||
$router->respond();
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
// Create a new Server instance.
|
||||
$server = new Server();
|
||||
// Add a router to the server to map methods and endpoints to handlers.
|
||||
$router = $server->createRouter();
|
||||
// Register the route GET / with an anonymous function that provides a handler.
|
||||
$router->register("GET", "/", function () { return new HomePageHandler(); });
|
||||
// Add the router to the server.
|
||||
$server->add($router);
|
||||
// Read the request from the client, dispatch a handler, and output.
|
||||
$server->respond();
|
||||
```
|
||||
|
||||
We've pushed all the code for our handler out to a seperate class, allowing the router to just describe the actual routing. The `CatHandler` can also now have its own private methods, etc.
|
||||
Development
|
||||
-----------
|
||||
|
||||
This also gives us another chance for dependency injection. The CatHandler could be modified to take a reference to the dependency continer in its constructor. Our router could then look like this:
|
||||
Use Docker to run unit tests, manage Composer dependencies, and render a preview of the documentation site.
|
||||
|
||||
```php
|
||||
$container = new MySuperCoolDependencyContainer();
|
||||
To get started, run:
|
||||
|
||||
$router = new Router();
|
||||
$router->add("/cats/", function () use ($container) {
|
||||
return new CatHandler($container);
|
||||
});
|
||||
$router->respond();
|
||||
```bash
|
||||
docker-compose build
|
||||
docker-compose run --rm php composer install
|
||||
```
|
||||
|
||||
For extra fun (and more readable code), you could store the callable that provides the handler in the container. Here's an example using [Pimple](http://pimple.sensiolabs.org/).
|
||||
To run PHPUnit tests, use the `php` service:
|
||||
|
||||
```php
|
||||
$c = new Pimple\Container();
|
||||
$c["catHandler"] = $c->protect(function () use ($c) {
|
||||
return new CatHandler($c);
|
||||
});
|
||||
|
||||
$router = new Router();
|
||||
$router->add("/cats/", $c["catHandler"]);
|
||||
$router->respond();
|
||||
```bash
|
||||
docker-compose run --rm php phpunit
|
||||
```
|
||||
|
||||
#### String Handlers
|
||||
To run Psalm for static analysis:
|
||||
|
||||
An addional way to add handlers (previously the only way) is to provide a string containing the fully qualified name of a handler class.
|
||||
|
||||
For example, if `CatHandler` were in the the namespace `MyApi\Handlers', the router would look like this
|
||||
|
||||
```php
|
||||
$router = new Router();
|
||||
$router->add("/cats/", '\MyApi\Handlers\CatHandler');
|
||||
$router->respond();
|
||||
```bash
|
||||
docker-compose run --rm php psalm
|
||||
```
|
||||
|
||||
The router take care of instantiating the handler for you, but it won't do it unless it is needed. In other words, a router with 100 handlers registered as strings will only ever autoload and instantiate the one handler that matches the request.
|
||||
To run PHP Coding Standards Fixer:
|
||||
|
||||
#### Handler Instances
|
||||
|
||||
The final approach to registering handlers is to just pass in a `HandlerInterface` instance.
|
||||
|
||||
```php
|
||||
$catHandler = new CatHandler();
|
||||
|
||||
$router = new Router();
|
||||
$router->add("/cats/", $catHandler);
|
||||
$router->respond();
|
||||
```bash
|
||||
docker-compose run --rm php php-cs-fixer fix
|
||||
```
|
||||
|
||||
This is not usually a good approach for anything other than testing becauce you need to have mutiple handlers instantiated and taking up memory even though only one will be dispatched. Prefer the callable or string approaches unless you really have a good reason.
|
||||
To generate documentation, use the `docs` service:
|
||||
|
||||
#### Handler Class
|
||||
|
||||
WellRESTed provides the abstract class [`Handler`](src/pjdietz/WellRESTed/Handler.php) which you can subclass for your handlers. This class provides methods for responding based on HTTP method. When you create your [`Handler`](src/pjdietz/WellRESTed/Handler.php) subclass, you will implement a method for each HTTP verb you would like the endpoint to support. For example, if `/cats/` should support `GET`, you would override the `get()` method. For `POST`, `post()`, etc.
|
||||
|
||||
Here's another version of the `CatHandler` that inherits from `Handler`.
|
||||
|
||||
```php
|
||||
class CatsCollectionHandler extends \pjdietz\WellRESTed\Handler
|
||||
{
|
||||
protected function get()
|
||||
{
|
||||
// ...Lookup the current list of cats here...
|
||||
$this->response->setStatusCode(200);
|
||||
$this->response->setHeader("Content-type", "application/json");
|
||||
$this->response->setBody(json_encode($catsList);
|
||||
}
|
||||
|
||||
protected function post()
|
||||
{
|
||||
// Read from the instance's request member and store a new cat.
|
||||
$cat = json_decode($this->request->getBody());
|
||||
// ...store $cat to the database...
|
||||
|
||||
// Build a response to send to the client.
|
||||
$this->response->setStatusCode(201);
|
||||
$this->response->setHeader("Content-type", "application/json");
|
||||
$this->response->setBody(json_encode($cat));
|
||||
}
|
||||
}
|
||||
```bash
|
||||
# Generate
|
||||
docker-compose run --rm docs
|
||||
# Clean
|
||||
docker-compose run --rm docs make clean -C docs
|
||||
```
|
||||
|
||||
See [Handlers](https://github.com/pjdietz/wellrested/wiki/Handlers) to learn about the subclassing the [`Handler`](src/pjdietz/WellRESTed/Handler.php) class.
|
||||
See [HandlerInteface](https://github.com/pjdietz/wellrested/wiki/HandlerInterface) to learn about more ways build completely custom classes.
|
||||
To run a local playground site, use:
|
||||
|
||||
### Responses
|
||||
|
||||
You've already seen a [`Response`](src/pjdietz/WellRESTed/Response.php) used inside a [`Handler`](src/pjdietz/WellRESTed/Handler.php) in the examples above. You can also create a [`Response`](src/pjdietz/WellRESTed/Response.php) outside of [`Handler`](src/pjdietz/WellRESTed/Handler.php). Let's take a look at creating a new [`Response`](src/pjdietz/WellRESTed/Response.php), setting a header, supplying the body, and outputting.
|
||||
|
||||
```php
|
||||
$resp = new \pjdietz\WellRESTed\Response();
|
||||
$resp->setStatusCode(200);
|
||||
$resp->setHeader("Content-type", "text/plain");
|
||||
$resp->setBody("Hello world!");
|
||||
$resp->respond();
|
||||
exit;
|
||||
```bash
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
This will output nice response, complete with status code, headers, body.
|
||||
|
||||
### Requests
|
||||
|
||||
From outside the context of a [`Handler`](src/pjdietz/WellRESTed/Handler.php), you can also use the [`Request`](src/pjdietz/WellRESTed/Request.php) class to read info for the request sent to the server by using the static method `Request::getRequest()`.
|
||||
|
||||
```php
|
||||
// Call the static method Request::getRequest() to get the request made to the server.
|
||||
$rqst = \pjdietz\WellRESTed\Request::getRequest();
|
||||
|
||||
if ($rqst->getMethod() === 'PUT') {
|
||||
$obj = json_decode($rqst->getBody());
|
||||
// Do something with the JSON sent as the message body.
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
### HTTP Client
|
||||
|
||||
The [`Client`](src/pjdietz/WellRESTed/Client.php) class allows you to make an HTTP request using cURL.
|
||||
|
||||
(This feature requires [PHP cURL](http://php.net/manual/en/book.curl.php).)
|
||||
|
||||
```php
|
||||
// Prepare a request.
|
||||
$rqst = new \pjdietz\WellRESTed\Request();
|
||||
$rqst->setUri('http://my.api.local/resources/');
|
||||
$rqst->setMethod('POST');
|
||||
$rqst->setBody(json_encode($newResource));
|
||||
|
||||
// Use a Client to get a Response.
|
||||
$client = new Client();
|
||||
$resp = $client->request($rqst);
|
||||
|
||||
// Read the response.
|
||||
if ($resp->getStatusCode() === 201) {
|
||||
// The new resource was created.
|
||||
$createdResource = json_decode($resp->getBody());
|
||||
}
|
||||
```
|
||||
The runs a site you can access at [http://localhost:8080](http://localhost:8080). You can use this site to browser the [documentation](http://localhost:8080/docs/) or [code coverage report](http://localhost:8080/coverage/).
|
||||
|
||||
Copyright and License
|
||||
---------------------
|
||||
Copyright © 2015 by PJ Dietz
|
||||
Copyright © 2020 by PJ Dietz
|
||||
Licensed under the [MIT license](http://opensource.org/licenses/MIT)
|
||||
|
|
|
|||
|
|
@ -1,8 +0,0 @@
|
|||
port = ENV["HOST_PORT"] || 8080
|
||||
|
||||
Vagrant.configure("2") do |config|
|
||||
# Ubuntu 14.04 LTS
|
||||
config.vm.box = "ubuntu/trusty64"
|
||||
config.vm.network "forwarded_port", guest: 80, host: port
|
||||
config.vm.provision "shell", path: "vagrant/provision.sh"
|
||||
end
|
||||
|
|
@ -1,26 +1,29 @@
|
|||
{
|
||||
"name": "pjdietz/wellrested",
|
||||
"description": "Simple PHP Library for RESTful APIs",
|
||||
"keywords": ["rest", "restful", "api", "curl", "http"],
|
||||
"homepage": "https://github.com/pjdietz/wellrested",
|
||||
"name": "basemaster/wellrested",
|
||||
"description": "Clone for Simple PHP Library for RESTful APIs (wellrested.org)",
|
||||
"keywords": ["rest", "restful", "api", "http", "psr7", "psr-7", "psr15", "psr-15", "psr17", "psr-17"],
|
||||
"license": "MIT",
|
||||
"type": "library",
|
||||
"authors": [
|
||||
{
|
||||
"name": "PJ Dietz",
|
||||
"email": "pj@pjdietz.com"
|
||||
"email": "pjdietz@gmail.com"
|
||||
}
|
||||
],
|
||||
"require": {
|
||||
"php": ">=5.3.0"
|
||||
"php": ">=7.2",
|
||||
"psr/http-factory": "~1.0",
|
||||
"psr/http-message": "~1.0",
|
||||
"psr/http-server-handler": "~1.0",
|
||||
"psr/http-server-middleware": "~1.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"fzaninotto/faker": "1.5.*@dev",
|
||||
"phpunit/phpunit": "~4.5",
|
||||
"pjdietz/shamserver": "dev-master"
|
||||
"provide": {
|
||||
"psr/http-message-implementation": "1.0",
|
||||
"psr/http-factory-implementation": "1.0"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-0": { "pjdietz\\WellRESTed": "src/" },
|
||||
"classmap": ["src/pjdietz/WellRESTed/Exceptions/HttpExceptions.php"]
|
||||
"psr-4": {
|
||||
"WellRESTed\\": "src/"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,177 @@
|
|||
# Makefile for Sphinx documentation
|
||||
#
|
||||
|
||||
# You can set these variables from the command line.
|
||||
SPHINXOPTS =
|
||||
SPHINXBUILD = sphinx-build
|
||||
PAPER =
|
||||
BUILDDIR = build
|
||||
|
||||
# User-friendly check for sphinx-build
|
||||
ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1)
|
||||
$(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/)
|
||||
endif
|
||||
|
||||
# Internal variables.
|
||||
PAPEROPT_a4 = -D latex_paper_size=a4
|
||||
PAPEROPT_letter = -D latex_paper_size=letter
|
||||
ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source
|
||||
# the i18n builder cannot share the environment and doctrees with the others
|
||||
I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source
|
||||
|
||||
.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext
|
||||
|
||||
help:
|
||||
@echo "Please use \`make <target>' where <target> is one of"
|
||||
@echo " html to make standalone HTML files"
|
||||
@echo " dirhtml to make HTML files named index.html in directories"
|
||||
@echo " singlehtml to make a single large HTML file"
|
||||
@echo " pickle to make pickle files"
|
||||
@echo " json to make JSON files"
|
||||
@echo " htmlhelp to make HTML files and a HTML help project"
|
||||
@echo " qthelp to make HTML files and a qthelp project"
|
||||
@echo " devhelp to make HTML files and a Devhelp project"
|
||||
@echo " epub to make an epub"
|
||||
@echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
|
||||
@echo " latexpdf to make LaTeX files and run them through pdflatex"
|
||||
@echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx"
|
||||
@echo " text to make text files"
|
||||
@echo " man to make manual pages"
|
||||
@echo " texinfo to make Texinfo files"
|
||||
@echo " info to make Texinfo files and run them through makeinfo"
|
||||
@echo " gettext to make PO message catalogs"
|
||||
@echo " changes to make an overview of all changed/added/deprecated items"
|
||||
@echo " xml to make Docutils-native XML files"
|
||||
@echo " pseudoxml to make pseudoxml-XML files for display purposes"
|
||||
@echo " linkcheck to check all external links for integrity"
|
||||
@echo " doctest to run all doctests embedded in the documentation (if enabled)"
|
||||
|
||||
clean:
|
||||
rm -rf $(BUILDDIR)/*
|
||||
|
||||
html:
|
||||
$(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
|
||||
@echo
|
||||
@echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
|
||||
|
||||
dirhtml:
|
||||
$(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
|
||||
@echo
|
||||
@echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
|
||||
|
||||
singlehtml:
|
||||
$(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml
|
||||
@echo
|
||||
@echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml."
|
||||
|
||||
pickle:
|
||||
$(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
|
||||
@echo
|
||||
@echo "Build finished; now you can process the pickle files."
|
||||
|
||||
json:
|
||||
$(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
|
||||
@echo
|
||||
@echo "Build finished; now you can process the JSON files."
|
||||
|
||||
htmlhelp:
|
||||
$(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
|
||||
@echo
|
||||
@echo "Build finished; now you can run HTML Help Workshop with the" \
|
||||
".hhp project file in $(BUILDDIR)/htmlhelp."
|
||||
|
||||
qthelp:
|
||||
$(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
|
||||
@echo
|
||||
@echo "Build finished; now you can run "qcollectiongenerator" with the" \
|
||||
".qhcp project file in $(BUILDDIR)/qthelp, like this:"
|
||||
@echo "# qcollectiongenerator $(BUILDDIR)/qthelp/WellRESTed.qhcp"
|
||||
@echo "To view the help file:"
|
||||
@echo "# assistant -collectionFile $(BUILDDIR)/qthelp/WellRESTed.qhc"
|
||||
|
||||
devhelp:
|
||||
$(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp
|
||||
@echo
|
||||
@echo "Build finished."
|
||||
@echo "To view the help file:"
|
||||
@echo "# mkdir -p $$HOME/.local/share/devhelp/WellRESTed"
|
||||
@echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/WellRESTed"
|
||||
@echo "# devhelp"
|
||||
|
||||
epub:
|
||||
$(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub
|
||||
@echo
|
||||
@echo "Build finished. The epub file is in $(BUILDDIR)/epub."
|
||||
|
||||
latex:
|
||||
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
|
||||
@echo
|
||||
@echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
|
||||
@echo "Run \`make' in that directory to run these through (pdf)latex" \
|
||||
"(use \`make latexpdf' here to do that automatically)."
|
||||
|
||||
latexpdf:
|
||||
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
|
||||
@echo "Running LaTeX files through pdflatex..."
|
||||
$(MAKE) -C $(BUILDDIR)/latex all-pdf
|
||||
@echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
|
||||
|
||||
latexpdfja:
|
||||
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
|
||||
@echo "Running LaTeX files through platex and dvipdfmx..."
|
||||
$(MAKE) -C $(BUILDDIR)/latex all-pdf-ja
|
||||
@echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
|
||||
|
||||
text:
|
||||
$(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text
|
||||
@echo
|
||||
@echo "Build finished. The text files are in $(BUILDDIR)/text."
|
||||
|
||||
man:
|
||||
$(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man
|
||||
@echo
|
||||
@echo "Build finished. The manual pages are in $(BUILDDIR)/man."
|
||||
|
||||
texinfo:
|
||||
$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
|
||||
@echo
|
||||
@echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo."
|
||||
@echo "Run \`make' in that directory to run these through makeinfo" \
|
||||
"(use \`make info' here to do that automatically)."
|
||||
|
||||
info:
|
||||
$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
|
||||
@echo "Running Texinfo files through makeinfo..."
|
||||
make -C $(BUILDDIR)/texinfo info
|
||||
@echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo."
|
||||
|
||||
gettext:
|
||||
$(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale
|
||||
@echo
|
||||
@echo "Build finished. The message catalogs are in $(BUILDDIR)/locale."
|
||||
|
||||
changes:
|
||||
$(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
|
||||
@echo
|
||||
@echo "The overview file is in $(BUILDDIR)/changes."
|
||||
|
||||
linkcheck:
|
||||
$(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck
|
||||
@echo
|
||||
@echo "Link check complete; look for any errors in the above output " \
|
||||
"or in $(BUILDDIR)/linkcheck/output.txt."
|
||||
|
||||
doctest:
|
||||
$(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
|
||||
@echo "Testing of doctests in the sources finished, look at the " \
|
||||
"results in $(BUILDDIR)/doctest/output.txt."
|
||||
|
||||
xml:
|
||||
$(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml
|
||||
@echo
|
||||
@echo "Build finished. The XML files are in $(BUILDDIR)/xml."
|
||||
|
||||
pseudoxml:
|
||||
$(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml
|
||||
@echo
|
||||
@echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml."
|
||||
|
|
@ -0,0 +1,242 @@
|
|||
@ECHO OFF
|
||||
|
||||
REM Command file for Sphinx documentation
|
||||
|
||||
if "%SPHINXBUILD%" == "" (
|
||||
set SPHINXBUILD=sphinx-build
|
||||
)
|
||||
set BUILDDIR=build
|
||||
set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% source
|
||||
set I18NSPHINXOPTS=%SPHINXOPTS% source
|
||||
if NOT "%PAPER%" == "" (
|
||||
set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS%
|
||||
set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS%
|
||||
)
|
||||
|
||||
if "%1" == "" goto help
|
||||
|
||||
if "%1" == "help" (
|
||||
:help
|
||||
echo.Please use `make ^<target^>` where ^<target^> is one of
|
||||
echo. html to make standalone HTML files
|
||||
echo. dirhtml to make HTML files named index.html in directories
|
||||
echo. singlehtml to make a single large HTML file
|
||||
echo. pickle to make pickle files
|
||||
echo. json to make JSON files
|
||||
echo. htmlhelp to make HTML files and a HTML help project
|
||||
echo. qthelp to make HTML files and a qthelp project
|
||||
echo. devhelp to make HTML files and a Devhelp project
|
||||
echo. epub to make an epub
|
||||
echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter
|
||||
echo. text to make text files
|
||||
echo. man to make manual pages
|
||||
echo. texinfo to make Texinfo files
|
||||
echo. gettext to make PO message catalogs
|
||||
echo. changes to make an overview over all changed/added/deprecated items
|
||||
echo. xml to make Docutils-native XML files
|
||||
echo. pseudoxml to make pseudoxml-XML files for display purposes
|
||||
echo. linkcheck to check all external links for integrity
|
||||
echo. doctest to run all doctests embedded in the documentation if enabled
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "clean" (
|
||||
for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i
|
||||
del /q /s %BUILDDIR%\*
|
||||
goto end
|
||||
)
|
||||
|
||||
|
||||
%SPHINXBUILD% 2> nul
|
||||
if errorlevel 9009 (
|
||||
echo.
|
||||
echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
|
||||
echo.installed, then set the SPHINXBUILD environment variable to point
|
||||
echo.to the full path of the 'sphinx-build' executable. Alternatively you
|
||||
echo.may add the Sphinx directory to PATH.
|
||||
echo.
|
||||
echo.If you don't have Sphinx installed, grab it from
|
||||
echo.http://sphinx-doc.org/
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
if "%1" == "html" (
|
||||
%SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.Build finished. The HTML pages are in %BUILDDIR%/html.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "dirhtml" (
|
||||
%SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "singlehtml" (
|
||||
%SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "pickle" (
|
||||
%SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.Build finished; now you can process the pickle files.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "json" (
|
||||
%SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.Build finished; now you can process the JSON files.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "htmlhelp" (
|
||||
%SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.Build finished; now you can run HTML Help Workshop with the ^
|
||||
.hhp project file in %BUILDDIR%/htmlhelp.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "qthelp" (
|
||||
%SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.Build finished; now you can run "qcollectiongenerator" with the ^
|
||||
.qhcp project file in %BUILDDIR%/qthelp, like this:
|
||||
echo.^> qcollectiongenerator %BUILDDIR%\qthelp\WellRESTed.qhcp
|
||||
echo.To view the help file:
|
||||
echo.^> assistant -collectionFile %BUILDDIR%\qthelp\WellRESTed.ghc
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "devhelp" (
|
||||
%SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.Build finished.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "epub" (
|
||||
%SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.Build finished. The epub file is in %BUILDDIR%/epub.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "latex" (
|
||||
%SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.Build finished; the LaTeX files are in %BUILDDIR%/latex.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "latexpdf" (
|
||||
%SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex
|
||||
cd %BUILDDIR%/latex
|
||||
make all-pdf
|
||||
cd %BUILDDIR%/..
|
||||
echo.
|
||||
echo.Build finished; the PDF files are in %BUILDDIR%/latex.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "latexpdfja" (
|
||||
%SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex
|
||||
cd %BUILDDIR%/latex
|
||||
make all-pdf-ja
|
||||
cd %BUILDDIR%/..
|
||||
echo.
|
||||
echo.Build finished; the PDF files are in %BUILDDIR%/latex.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "text" (
|
||||
%SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.Build finished. The text files are in %BUILDDIR%/text.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "man" (
|
||||
%SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.Build finished. The manual pages are in %BUILDDIR%/man.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "texinfo" (
|
||||
%SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "gettext" (
|
||||
%SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.Build finished. The message catalogs are in %BUILDDIR%/locale.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "changes" (
|
||||
%SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.The overview file is in %BUILDDIR%/changes.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "linkcheck" (
|
||||
%SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.Link check complete; look for any errors in the above output ^
|
||||
or in %BUILDDIR%/linkcheck/output.txt.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "doctest" (
|
||||
%SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.Testing of doctests in the sources finished, look at the ^
|
||||
results in %BUILDDIR%/doctest/output.txt.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "xml" (
|
||||
%SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.Build finished. The XML files are in %BUILDDIR%/xml.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "pseudoxml" (
|
||||
%SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml.
|
||||
goto end
|
||||
)
|
||||
|
||||
:end
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
Additional Components
|
||||
=====================
|
||||
|
||||
The core WellRESTed library is designed to be very small and limited in scope. It should do only what's needed, and no more. One of WellRESTed's main goals is to stay small, and not force anything on consumers.
|
||||
|
||||
That being said, there are a number or situations that come up that warrant solutions. For that, WellRESTed also provides a (growing) number of companion packages that you may find useful, depending on the project.
|
||||
|
||||
`HTTP Exceptions`_
|
||||
A collection of Exception classes that correspond to common HTTP error status codes.
|
||||
|
||||
`Error Handling`_
|
||||
Classes to facilitate error handling including
|
||||
|
||||
`Test Components`_
|
||||
Test cases and doubles for use with WellRESTed
|
||||
|
||||
Or, see WellRESTed_ on GitHub.
|
||||
|
||||
.. _HTTP Exceptions: https://github.com/wellrestedphp/http-exceptions
|
||||
.. _Error Handling: https://github.com/wellrestedphp/error-handling
|
||||
.. _Test Components: https://github.com/wellrestedphp/test
|
||||
.. _WellRESTed: https://github.com/wellrestedphp
|
||||
|
|
@ -0,0 +1,195 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
import sys
|
||||
import os
|
||||
from sphinx.highlighting import lexers
|
||||
from pygments.lexers.web import PhpLexer
|
||||
|
||||
lexers['php'] = PhpLexer(startinline=True, linenos=1)
|
||||
lexers['php-annotations'] = PhpLexer(startinline=True, linenos=1)
|
||||
|
||||
# Add any Sphinx extension module names here, as strings. They can be
|
||||
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
|
||||
# ones.
|
||||
extensions = [
|
||||
'sphinx.ext.autodoc',
|
||||
]
|
||||
|
||||
# Add any paths that contain templates here, relative to this directory.
|
||||
templates_path = ['_templates']
|
||||
|
||||
# The suffix of source filenames.
|
||||
source_suffix = '.rst'
|
||||
|
||||
# The master toctree document.
|
||||
master_doc = 'index'
|
||||
|
||||
# General information about the project.
|
||||
project = u'WellRESTed'
|
||||
copyright = u'2021, PJ Dietz'
|
||||
version = '5.0.0'
|
||||
release = '5.0.0'
|
||||
|
||||
# List of patterns, relative to source directory, that match files and
|
||||
# directories to ignore when looking for source files.
|
||||
exclude_patterns = []
|
||||
|
||||
# The name of the Pygments (syntax highlighting) style to use.
|
||||
pygments_style = 'sphinx'
|
||||
|
||||
# -- Options for HTML output ----------------------------------------------
|
||||
|
||||
# The theme to use for HTML and HTML Help pages. See the documentation for
|
||||
# a list of builtin themes.
|
||||
html_theme = 'default'
|
||||
|
||||
# Theme options are theme-specific and customize the look and feel of a theme
|
||||
# further. For a list of options available for each theme, see the
|
||||
# documentation.
|
||||
#html_theme_options = {}
|
||||
|
||||
# Add any paths that contain custom themes here, relative to this directory.
|
||||
#html_theme_path = []
|
||||
|
||||
# The name for this set of Sphinx documents. If None, it defaults to
|
||||
# "<project> v<release> documentation".
|
||||
#html_title = None
|
||||
|
||||
# A shorter title for the navigation bar. Default is the same as html_title.
|
||||
#html_short_title = None
|
||||
|
||||
# The name of an image file (relative to this directory) to place at the top
|
||||
# of the sidebar.
|
||||
#html_logo = None
|
||||
|
||||
# The name of an image file (within the static path) to use as favicon of the
|
||||
# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
|
||||
# pixels large.
|
||||
#html_favicon = None
|
||||
|
||||
# Add any paths that contain custom static files (such as style sheets) here,
|
||||
# relative to this directory. They are copied after the builtin static files,
|
||||
# so a file named "default.css" will overwrite the builtin "default.css".
|
||||
html_static_path = ['_static']
|
||||
|
||||
# Add any extra paths that contain custom files (such as robots.txt or
|
||||
# .htaccess) here, relative to this directory. These files are copied
|
||||
# directly to the root of the documentation.
|
||||
#html_extra_path = []
|
||||
|
||||
# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
|
||||
# using the given strftime format.
|
||||
#html_last_updated_fmt = '%b %d, %Y'
|
||||
|
||||
# If true, SmartyPants will be used to convert quotes and dashes to
|
||||
# typographically correct entities.
|
||||
#html_use_smartypants = True
|
||||
|
||||
# Custom sidebar templates, maps document names to template names.
|
||||
#html_sidebars = {}
|
||||
|
||||
# Additional templates that should be rendered to pages, maps page names to
|
||||
# template names.
|
||||
#html_additional_pages = {}
|
||||
|
||||
# If false, no module index is generated.
|
||||
#html_domain_indices = True
|
||||
|
||||
# If false, no index is generated.
|
||||
#html_use_index = True
|
||||
|
||||
# If true, the index is split into individual pages for each letter.
|
||||
#html_split_index = False
|
||||
|
||||
# If true, links to the reST sources are added to the pages.
|
||||
#html_show_sourcelink = True
|
||||
|
||||
# If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
|
||||
#html_show_sphinx = True
|
||||
|
||||
# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
|
||||
#html_show_copyright = True
|
||||
|
||||
# If true, an OpenSearch description file will be output, and all pages will
|
||||
# contain a <link> tag referring to it. The value of this option must be the
|
||||
# base URL from which the finished HTML is served.
|
||||
#html_use_opensearch = ''
|
||||
|
||||
# This is the file name suffix for HTML files (e.g. ".xhtml").
|
||||
#html_file_suffix = None
|
||||
|
||||
# Output file base name for HTML help builder.
|
||||
htmlhelp_basename = 'WellRESTeddoc'
|
||||
|
||||
|
||||
# -- Options for LaTeX output ---------------------------------------------
|
||||
|
||||
latex_elements = {
|
||||
# The paper size ('letterpaper' or 'a4paper').
|
||||
#'papersize': 'letterpaper',
|
||||
|
||||
# The font size ('10pt', '11pt' or '12pt').
|
||||
#'pointsize': '10pt',
|
||||
|
||||
# Additional stuff for the LaTeX preamble.
|
||||
#'preamble': '',
|
||||
}
|
||||
|
||||
# Grouping the document tree into LaTeX files. List of tuples
|
||||
# (source start file, target name, title,
|
||||
# author, documentclass [howto, manual, or own class]).
|
||||
latex_documents = [
|
||||
('index', 'WellRESTed.tex', u'WellRESTed Documentation',
|
||||
u'PJ Dietz', 'manual'),
|
||||
]
|
||||
|
||||
# The name of an image file (relative to this directory) to place at the top of
|
||||
# the title page.
|
||||
#latex_logo = None
|
||||
|
||||
# For "manual" documents, if this is true, then toplevel headings are parts,
|
||||
# not chapters.
|
||||
#latex_use_parts = False
|
||||
|
||||
# If true, show page references after internal links.
|
||||
#latex_show_pagerefs = False
|
||||
|
||||
# If true, show URL addresses after external links.
|
||||
#latex_show_urls = False
|
||||
|
||||
# Documents to append as an appendix to all manuals.
|
||||
#latex_appendices = []
|
||||
|
||||
# If false, no module index is generated.
|
||||
#latex_domain_indices = True
|
||||
|
||||
|
||||
# -- Options for manual page output ---------------------------------------
|
||||
|
||||
# One entry per manual page. List of tuples
|
||||
# (source start file, name, description, authors, manual section).
|
||||
man_pages = [
|
||||
('index', 'wellrested', u'WellRESTed Documentation',
|
||||
[u'PJ Dietz'], 1)
|
||||
]
|
||||
|
||||
# If true, show URL addresses after external links.
|
||||
#man_show_urls = False
|
||||
|
||||
|
||||
# -- Options for Texinfo output -------------------------------------------
|
||||
|
||||
# Grouping the document tree into Texinfo files. List of tuples
|
||||
# (source start file, target name, title, author,
|
||||
# dir menu entry, description, category)
|
||||
texinfo_documents = [
|
||||
('index', 'WellRESTed', u'WellRESTed Documentation',
|
||||
u'PJ Dietz', 'WellRESTed', 'One line description of project.',
|
||||
'Miscellaneous'),
|
||||
]
|
||||
|
||||
on_rtd = os.environ.get('READTHEDOCS', None) == 'True'
|
||||
|
||||
if not on_rtd: # only import and set the theme if we're building docs locally
|
||||
import sphinx_rtd_theme
|
||||
html_theme = 'sphinx_rtd_theme'
|
||||
html_theme_path = [sphinx_rtd_theme.get_html_theme_path()]
|
||||
|
|
@ -0,0 +1,63 @@
|
|||
Dependency Injection
|
||||
====================
|
||||
|
||||
WellRESTed strives to play nicely with other code and not force developers into using any specific libraries or frameworks. As such, WellRESTed does not provide a dependency injection container, nor does it require you to use a specific container (or any).
|
||||
|
||||
This section describes the recommended way of using WellRESTed with Pimple_, a common dependency injection container for PHP.
|
||||
|
||||
Imaging we have a ``FooHandler`` that depends on a ``BarInterface``, and ``BazInterface``. Our handler looks something like this:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
class FooHandler implements RequestHandlerInterface
|
||||
{
|
||||
private $bar;
|
||||
private $baz;
|
||||
|
||||
public function __construct(BarInterface $bar, BazInterface $baz)
|
||||
{
|
||||
$this->bar = $bar;
|
||||
$this->baz = $baz;
|
||||
}
|
||||
|
||||
public function handle(ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
// Do something with the bar and baz and return a response...
|
||||
// ...
|
||||
}
|
||||
}
|
||||
|
||||
We can register the handler and these dependencies in a Pimple_ service provider.
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
class MyServiceProvider implements ServiceProviderInterface
|
||||
{
|
||||
public function register(Container $c)
|
||||
{
|
||||
// Register the Bar and Baz as services.
|
||||
$c['bar'] = function ($c) {
|
||||
return new Bar();
|
||||
};
|
||||
$c['baz'] = function ($c) {
|
||||
return new Baz();
|
||||
};
|
||||
|
||||
// Register the Handler as a protected function. When you use
|
||||
// protect, Pimple returns the function itself instead of the return
|
||||
// value of the function.
|
||||
$c['fooHandler'] = $c->protect(function () use ($c) {
|
||||
return new FooHandler($c['bar'], $c['baz']);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
To register this handler with a router, we can pass the service:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
$router->register('GET', '/foo', $c['fooHandler']);
|
||||
|
||||
By "protecting" the ``fooHandler`` service, we are delaying the instantiation of the ``FooHandler``, the ``Bar``, and the ``Baz`` until the handler needs to be dispatched. This works because we're not passing instance of ``FooHandler`` when we register this with a router, we're passing a function to it that does the instantiation on demand.
|
||||
|
||||
.. _Pimple: https://pimple.symfony.com/
|
||||
|
|
@ -0,0 +1,102 @@
|
|||
Extending and Customizing
|
||||
=========================
|
||||
|
||||
WellRESTed is designed with customization in mind. This section describes some common scenarios for customization, starting with using a handler that implements a different interface.
|
||||
|
||||
Custom Handlers and Middleware
|
||||
------------------------------
|
||||
|
||||
Imagine you found a handler class from a third party that does exactly what you need. The only problem is that it implements a different interface.
|
||||
|
||||
Here's the interface for the third-party handler:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
interface OtherHandlerInterface
|
||||
{
|
||||
/**
|
||||
* @param ServerRequestInterface $request
|
||||
* @return ResponseInterface
|
||||
*/
|
||||
public function run(ResponseInterface $response);
|
||||
}
|
||||
|
||||
Wrapping
|
||||
^^^^^^^^
|
||||
|
||||
One solution is to wrap an instance of this handler inside of a ``Psr\Http\Server\RequestHandlerInterface`` instance.
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
/**
|
||||
* Wraps an instance of OtherHandlerInterface
|
||||
*/
|
||||
class OtherHandlerWrapper implements RequestHandlerInterface
|
||||
{
|
||||
private $handler;
|
||||
|
||||
public function __construct(OtherHandlerInterface $handler)
|
||||
{
|
||||
$this->handler = $handler;
|
||||
}
|
||||
|
||||
public function handle(ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
return $this->handler->run($request);
|
||||
}
|
||||
}
|
||||
|
||||
Custom Dispatcher
|
||||
^^^^^^^^^^^^^^^^^
|
||||
|
||||
Wrapping works well when you have one or two handlers implementing a third-party interface. If you want to integrate a lot of classes that implement a given third-party interface, you're might consider customizing the dispatcher.
|
||||
|
||||
The dispatcher is an instance that unpacks your handlers and middleware and sends the request and response through it. A default dispatcher is created for you when you use your ``WellRESTed\Server``.
|
||||
|
||||
If you need the ability to dispatch other types of middleware, you can create your own by implementing ``WellRESTed\Dispatching\DispatcherInterface``. The easiest way to do this is to subclass ``WellRESTed\Dispatching\Dispatcher``. Here's an example that extends ``Dispatcher`` and adds support for ``OtherHandlerInterface``:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
/**
|
||||
* Dispatcher with support for OtherHandlerInterface
|
||||
*/
|
||||
class CustomDispatcher extends \WellRESTed\Dispatching\Dispatcher
|
||||
{
|
||||
public function dispatch(
|
||||
$dispatchable,
|
||||
ServerRequestInterface $request,
|
||||
ResponseInterface $response,
|
||||
$next
|
||||
) {
|
||||
try {
|
||||
// Use the dispatch method in the parent class first.
|
||||
$response = parent::dispatch($dispatchable, $request, $response, $next);
|
||||
} catch (\WellRESTed\Dispatching\DispatchException $e) {
|
||||
// If there's a problem, check if the handler or middleware
|
||||
// (the "dispatchable") implements OtherHandlerInterface.
|
||||
// Dispatch it if it does.
|
||||
if ($dispatchable instanceof OtherHandlerInterface) {
|
||||
$response = $dispatchable->run($request);
|
||||
} else {
|
||||
// Otherwise, re-throw the exception.
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
return $response;
|
||||
}
|
||||
}
|
||||
|
||||
To use this dispatcher, create an instance implementing ``WellRESTed\Dispatching\DispatcherInterface`` and pass it to the server's ``setDispatcher`` method.
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
$server = new WellRESTed\Server();
|
||||
$server->setDispatcher(new MyApi\CustomDispatcher());
|
||||
|
||||
.. warning::
|
||||
|
||||
When you supply a custom Dispatcher, be sure to call ``Server::setDispatcher`` before you create any routers with ``Server::createRouter`` to allow the ``Server`` to pass you customer ``Dispatcher`` on to the newly created ``Router``.
|
||||
|
||||
.. _PSR-7: https://www.php-fig.org/psr/psr-7/
|
||||
.. _Handlers and Middleware: handlers-and-middleware.html
|
||||
.. _Request Attributes: messages.html#attributes
|
||||
|
|
@ -0,0 +1,169 @@
|
|||
Getting Started
|
||||
===============
|
||||
|
||||
This page provides a brief introduction to WellRESTed. We'll take a tour of some of the features of WellRESTed without getting into too much depth.
|
||||
|
||||
To start, we'll make a "Hello, world!" to demonstrate the concepts of handlers and routing and show how to read variables from the request path.
|
||||
|
||||
Hello, World!
|
||||
^^^^^^^^^^^^^
|
||||
|
||||
Let's start with a very basic "Hello, world!" Here, we will create a server. A ``WellRESTed\Server`` reads the incoming request from the client, dispatches a handler, and transmits a response back to the client.
|
||||
|
||||
Our handler will create and return a response with the status code set to ``200`` and the body set to "Hello, world!".
|
||||
|
||||
.. _`Example 1`:
|
||||
.. rubric:: Example 1: Simple "Hello, world!"
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
|
||||
use Psr\Http\Server\RequestHandlerInterface;
|
||||
use WellRESTed\Message\Response;
|
||||
use WellRESTed\Message\Stream;
|
||||
use WellRESTed\Server;
|
||||
|
||||
require_once 'vendor/autoload.php';
|
||||
|
||||
// Define a handler implementing the PSR-15 RequestHandlerInterface interface.
|
||||
class HelloHandler implements RequestHandlerInterface
|
||||
{
|
||||
public function handle(ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
$response = (new Response(200))
|
||||
->withHeader('Content-type', 'text/plain')
|
||||
->withBody(new Stream('Hello, world!'));
|
||||
return $response;
|
||||
}
|
||||
}
|
||||
|
||||
// Create a new server.
|
||||
$server = new Server();
|
||||
|
||||
// Add this handler to the server.
|
||||
$server->add(new HelloHandler());
|
||||
|
||||
// Read the request sent to the server and use it to output a response.
|
||||
$server->respond();
|
||||
|
||||
.. note::
|
||||
|
||||
The handler in this example provides a ``Stream`` as the body instead of a string. This is a feature or PSR-7 where HTTP message bodies are always represented by streams. This allows you to work with very large bodies without having to store the entire contents in memory.
|
||||
|
||||
WellRESTed provides ``Stream`` and ``NullStream``, but you can use any implementation of ``Psr\Http\Message\StreamInterface``.
|
||||
|
||||
Routing by Path
|
||||
^^^^^^^^^^^^^^^
|
||||
|
||||
This is a good start, but it provides the same response to every request. Let's provide this response only when a client sends a request to ``/hello``.
|
||||
|
||||
For this, we need a router_. A router_ examines the request and sends the request through to the handler that matches the request's HTTP method and path.
|
||||
|
||||
.. _`Example 2`:
|
||||
.. rubric:: Example 2: Routed "Hello, world!"
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
// Create a new server.
|
||||
$server = new Server();
|
||||
|
||||
// Create a router to map methods and endpoints to handlers.
|
||||
$router = $server->createRouter();
|
||||
$router->register('GET', '/hello', new HelloHandler());
|
||||
$server->add($router);
|
||||
|
||||
// Read the request sent to the server and use it to output a response.
|
||||
$server->respond();
|
||||
|
||||
Reading Path Variables
|
||||
^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
Routes can be static (like the one above that matches only ``/hello``), or they can be dynamic. Here's an example that uses a dynamic route to read a portion from the path to use as the greeting. For example, a request to ``/hello/Molly`` will respond "Hello, Molly", while a request to ``/hello/Oscar`` will respond "Hello, Oscar!"
|
||||
|
||||
.. _`Example 3`:
|
||||
.. rubric:: Example 3: Personalized "Hello, world!"
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
class HelloHandler implements RequestHandlerInterface
|
||||
{
|
||||
public function handle(ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
// Check for a "name" attribute which may have been provided as a
|
||||
// path variable. Use "world" as a default.
|
||||
$name = $request->getAttribute("name", "world");
|
||||
|
||||
// Set the response body to the greeting and the status code to 200 OK.
|
||||
$response = (new Response(200))
|
||||
->withHeader("Content-type", "text/plain")
|
||||
->withBody(new Stream("Hello, $name!"));
|
||||
|
||||
// Return the response.
|
||||
return $response;
|
||||
}
|
||||
}
|
||||
|
||||
// Create the server and router.
|
||||
$server = new Server();
|
||||
$router = $server->createRouter();
|
||||
|
||||
// Register the middleware for an exact match to /hello
|
||||
$router->register("GET", "/hello", $hello);
|
||||
// Register to match a pattern with a variable.
|
||||
$router->register("GET", "/hello/{name}", $hello);
|
||||
$server->add($router);
|
||||
|
||||
$server->respond();
|
||||
|
||||
Middleware
|
||||
^^^^^^^^^^
|
||||
|
||||
In addition to handlers, which provide responses directly, WellRESTed also supports middleware to act on the requests and then pass them on for other middleware or handlers to work with.
|
||||
|
||||
Middleware allows you to compose your application in multiple pieces. In the example, we'll use middleware to add a header to every response, regardless of which handler is called.
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
// This middleware will add a custom header to every response.
|
||||
class CustomHeaderMiddleware implements MiddlewareInterface
|
||||
{
|
||||
public function process(
|
||||
ServerRequestInterface $request,
|
||||
RequestHandlerInterface $handler
|
||||
): ResponseInterface {
|
||||
|
||||
// Delegate to the next handler in the chain to obtain a response.
|
||||
$response = $handler->handle($request);
|
||||
|
||||
// Add the header to the response we got back from upstream.
|
||||
$response = $response->withHeader("X-example", "hello world");
|
||||
|
||||
// Return the altered response.
|
||||
return $response;
|
||||
}
|
||||
}
|
||||
|
||||
// Create a server
|
||||
$server = new Server();
|
||||
|
||||
// Add the header-adding middleware to the server first so that it will
|
||||
// forward requests on to the router.
|
||||
$server->add(new CustomHeaderMiddleware());
|
||||
|
||||
// Create a router to map methods and endpoints to handlers.
|
||||
$router = $server->createRouter();
|
||||
|
||||
$handler = new HelloHandler();
|
||||
// Register a route to the handler without a variable in the path.
|
||||
$router->register('GET', '/hello', $handler);
|
||||
// Register a route that reads a "name" from the path.
|
||||
// This will make the "name" request attribute available to the handler.
|
||||
$router->register('GET', '/hello/{name}', $handler);
|
||||
$server->add($router);
|
||||
|
||||
// Read the request from the client, dispatch, and output.
|
||||
$server->respond();
|
||||
|
||||
.. _middleware: middleware.html
|
||||
.. _router: router.html
|
||||
|
|
@ -0,0 +1,236 @@
|
|||
Handlers and Middleware
|
||||
=======================
|
||||
|
||||
WellRESTed allows you to define and use your handlers and middleware in a number of ways.
|
||||
|
||||
Defining Handlers and Middleware
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
PSR-15 Interfaces
|
||||
-----------------
|
||||
|
||||
The preferred method is to use the interfaces standardized by PSR-15_. This standard includes two interfaces, ``Psr\Http\Server\RequestHandlerInterface`` and ``Psr\Http\Server\MiddlewareInterface``.
|
||||
|
||||
Use ``RequestHandlerInterface`` for individual components that generate and return responses.
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
class HelloHandler implements RequestHandlerInterface
|
||||
{
|
||||
public function handle(ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
// Check for a "name" attribute which may have been provided as a
|
||||
// path variable. Use "world" as a default.
|
||||
$name = $request->getAttribute("name", "world");
|
||||
|
||||
// Set the response body to the greeting and the status code to 200 OK.
|
||||
$response = (new Response(200))
|
||||
->withHeader("Content-type", "text/plain")
|
||||
->withBody(new Stream("Hello, $name!"));
|
||||
|
||||
// Return the response.
|
||||
return $response;
|
||||
}
|
||||
}
|
||||
|
||||
Use ``MiddlewareInterface`` for classes that interact with other middleware and handlers. For example, you may have middleware that attempts to retrieve a cached response and delegates to other handlers on a cache miss.
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
class CacheMiddleware implements MiddlewareInterface
|
||||
{
|
||||
public function process(
|
||||
ServerRequestInterface $request,
|
||||
RequestHandlerInterface $handler
|
||||
): ResponseInterface
|
||||
{
|
||||
|
||||
// Inspect the request to see if there is a representation on hand.
|
||||
$representation = $this->getCachedRepresentation($request);
|
||||
if ($representation !== null) {
|
||||
// There is already a cached representation.
|
||||
// Return it without delegating to the next handler.
|
||||
return (new Response())
|
||||
->withStatus(200)
|
||||
->withBody($representation);
|
||||
}
|
||||
|
||||
// No representation exists. Delegate to the next handler.
|
||||
$response = $handler->handle($request);
|
||||
|
||||
// Attempt to store the response to the cache.
|
||||
$this->storeRepresentationToCache($response);
|
||||
|
||||
return $response
|
||||
}
|
||||
|
||||
private function getCachedRepresentation(ServerRequestInterface $request)
|
||||
{
|
||||
// Look for a cached representation. Return null if not found.
|
||||
// ...
|
||||
}
|
||||
|
||||
private function storeRepresentationToCache(ResponseInterface $response)
|
||||
{
|
||||
// Ensure the response contains a success code, a valid body,
|
||||
// headers that allow caching, etc. and store the representation.
|
||||
// ...
|
||||
}
|
||||
}
|
||||
|
||||
Legacy Middleware Interface
|
||||
---------------------------
|
||||
|
||||
Prior to PSR-15, WellRESTed's recommended handler interface was ``WellRESTed\MiddlewareInterface``. This interface is still supported for backwards compatibility.
|
||||
|
||||
This interface serves for both handlers and middleware. It differs from the ``Psr\Http\Server\MiddlewareInterface`` in that is expects an incoming ``$response`` parameter which you may use to generate the returned response. It also expected a ``$next`` parameter which is a ``callable`` with this signature:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
function next($request, $response): ResponseInterface
|
||||
|
||||
Call ``$next`` and pass ``$request`` and ``$response`` to forward the request to the next handler. ``$next`` will return the response from the handler. Here's the cache example above as a ``WellRESTed\MiddlewareInterface``.
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
class CacheMiddleware implements WellRESTed\MiddlewareInterface
|
||||
{
|
||||
public function __invoke(
|
||||
ServerRequestInterface $request,
|
||||
ResponseInterface $response,
|
||||
$next
|
||||
) {
|
||||
|
||||
// Inspect the request to see if there is a representation on hand.
|
||||
$representation = $this->getCachedRepresentation($request);
|
||||
if ($representation !== null) {
|
||||
// There is already a cached representation.
|
||||
// Return it without delegating to the next handler.
|
||||
return $response
|
||||
->withStatus(200)
|
||||
->withBody($representation);
|
||||
}
|
||||
|
||||
// No representation exists. Delegate to the next handler.
|
||||
$response = $next($request, $response);
|
||||
|
||||
// Attempt to store the response to the cache.
|
||||
$this->storeRepresentationToCache($response);
|
||||
|
||||
return $response
|
||||
}
|
||||
|
||||
private function getCachedRepresentation(ServerRequestInterface $request)
|
||||
{
|
||||
// Look for a cached representation. Return null if not found.
|
||||
// ...
|
||||
}
|
||||
|
||||
private function storeRepresentationToCache(ResponseInterface $response)
|
||||
{
|
||||
// Ensure the response contains a success code, a valid body,
|
||||
// headers that allow caching, etc. and store the representation.
|
||||
// ...
|
||||
}
|
||||
}
|
||||
|
||||
Callables
|
||||
---------
|
||||
|
||||
You may also use a ``callable`` similar to the legacy ``WellRESTed\MiddlewareInterface``. The signature of the callable matches the signature of ``WellRESTed\MiddlewareInterface::__invoke``.
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
$handler = function ($request, $response, $next) {
|
||||
|
||||
// Delegate to the next handler.
|
||||
$response = $next($request, $response);
|
||||
|
||||
return $response
|
||||
->withHeader("Content-type", "text/plain")
|
||||
->withBody(new Stream("Hello, $name!"));
|
||||
}
|
||||
|
||||
Using Handlers and Middleware
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
Methods that accept handlers and middleware (e.g., ``Server::add``, ``Router::register``) allow you to provide them in a number of ways. For example, you can provide an instance, a ``callable`` that returns an instance, or an ``array`` of middleware to use in sequence. The following examples will demonstrate all of the ways you can register handlers and middleware.
|
||||
|
||||
Factory Functions
|
||||
-----------------
|
||||
|
||||
The best method is to use a function that returns an instance of your handler. The main benefit of this approach is that no handlers are instantiated until they are needed.
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
$router->register("GET,PUT,DELETE", "/widgets/{id}",
|
||||
function () { return new App\WidgetHandler() }
|
||||
);
|
||||
|
||||
If you're using ``Pimple``, a popular `dependency injection`_ container for PHP, you may have code that looks like this:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
// Create a DI container.
|
||||
$c = new Container();
|
||||
// Store a function to the container that will create and return the handler.
|
||||
$c['widgetHandler'] = $c->protect(function () use ($c) {
|
||||
return new App\WidgetHandler();
|
||||
});
|
||||
|
||||
$router->register("GET,PUT,DELETE", "/widgets/{id}", $c['widgetHandler']);
|
||||
|
||||
Instance
|
||||
--------
|
||||
|
||||
WellRESTed also allows you to pass an instance of a handler directly. This may be useful for smaller handlers that don't require many dependencies, although the factory function approach is better in most cases.
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
$widgetHandler = new App\WidgetHandler();
|
||||
|
||||
$router->register("GET,PUT,DELETE", "/widgets/{id}", $widgetHandler);
|
||||
|
||||
.. warning::
|
||||
|
||||
This is simple, but has a significant disadvantage over the other options because each middleware used this way will be loaded and instantiated, even if it's not needed for a given request-response cycle. You may find this approach useful for testing, but avoid if for production code.
|
||||
|
||||
Fully Qualified Class Name (FQCN)
|
||||
---------------------------------
|
||||
|
||||
For handlers that do not require any arguments passed to the constructor, you may pass the fully qualified class name of your handler as a ``string``. You can do that like this:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
$router->register('GET,PUT,DELETE', '/widgets/{id}', App\WidgetHandler::class);
|
||||
// ... or ...
|
||||
$router->register('GET,PUT,DELETE', '/widgets/{id}', 'App\\WidgetHandler');
|
||||
|
||||
The class is not loaded, and no instances are created, until the route is matched and dispatched. However, the drawback to this approach is the there is no way to pass any arguments to the constructor.
|
||||
|
||||
Array
|
||||
-----
|
||||
|
||||
The final approach is to provide a sequence of middleware and a handler as an ``array``.
|
||||
|
||||
For example, imagine if we had a Pimple_ container with these services:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
$c['authMiddleware'] // Ensures the user is logged in
|
||||
$c['cacheMiddleware'] // Provides a cached response if able
|
||||
$c['widgetHandler'] // Provides a widget representation
|
||||
|
||||
We could provide these as a sequence by using an ``array``.
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
$router->register('GET', '/widgets/{id}', [
|
||||
$c['authMiddleware'],
|
||||
$c['cacheMiddleware'],
|
||||
$c['widgetHandler']
|
||||
]);
|
||||
|
||||
.. _Dependency Injection: dependency-injection.html
|
||||
.. _Pimple: https://pimple.symfony.com/
|
||||
.. _PSR-15: https://www.php-fig.org/psr/psr-15/
|
||||
|
|
@ -0,0 +1,147 @@
|
|||
WellRESTed
|
||||
==========
|
||||
|
||||
WellRESTed is a library for creating RESTful APIs and websites in PHP that provides abstraction for HTTP messages, a powerful handler and middleware system, and a flexible router.
|
||||
|
||||
Features
|
||||
--------
|
||||
|
||||
PSR-7 HTTP Messages
|
||||
^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
Request and response messages are built to the interfaces standardized by PSR-7_ making it easy to share code and use components from other libraries and frameworks.
|
||||
|
||||
The message abstractions facilitate working with message headers, status codes, variables extracted from the path, message bodies, and all the other aspects of requests and responses.
|
||||
|
||||
PSR-15 Handler Interfaces
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
WellRESTed can use handlers and middleware using the interfaces defined by the PSR-15_ standard.
|
||||
|
||||
Router
|
||||
^^^^^^
|
||||
|
||||
The router_ allows you to define your endpoints using `URI Templates`_ like ``/foo/{bar}/{baz}`` that match patterns of paths and provide captured variables. You can also match exact paths for extra speed or regular expressions for extra flexibility.
|
||||
|
||||
WellRESTed's router automates responding to ``OPTIONS`` requests for each endpoint based on the methods you assign. ``405 Method Not Allowed`` responses come free of charge as well for any methods you have not implemented on a given endpoint.
|
||||
|
||||
Middleware
|
||||
^^^^^^^^^^
|
||||
|
||||
The middleware_ system allows you to build your Web service out of discrete, modular pieces. These pieces can be run in sequences where each has an opportunity to work with the request before handing it off to the next. For example, an authenticator can validate a request and forward it to a cache; the cache can check for a stored representation and forward to another middleware if no cached representation is found, etc. All of this happens without any one middleware needing to know anything about where it is in the chain or which middleware comes before or after.
|
||||
|
||||
Lazy Loading
|
||||
^^^^^^^^^^^^
|
||||
|
||||
Handlers and middleware can be registered using `factory functions`_ so that they are only instantiated if needed. This way, a Web service with hundreds of handlers and middleware only creates instances required for the current request-response cycle.
|
||||
|
||||
Extensible
|
||||
^^^^^^^^^^
|
||||
|
||||
Most classes are coded to interfaces to allow you to provide your own implementations and use them in place of the built-in classes. For example, if your Web service needs to be able to dispatch middleware that implements a third-party interface, you can provide your own custom ``DispatcherInterface`` implementation.
|
||||
|
||||
Example
|
||||
-------
|
||||
|
||||
Here's a customary "Hello, world!" example. This site will respond to requests for ``GET /hello`` with "Hello, world!" and provide custom responses for other paths (e.g., ``GET /hello/Molly`` will respond "Hello, Molly!").
|
||||
|
||||
The site will also provide an ``X-example: hello world`` using dedicated middleware, just to illustrate how middleware propagates.
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Psr\Http\Server\MiddlewareInterface;
|
||||
use Psr\Http\Server\RequestHandlerInterface;
|
||||
use WellRESTed\Message\Response;
|
||||
use WellRESTed\Message\Stream;
|
||||
use WellRESTed\Server;
|
||||
|
||||
require_once 'vendor/autoload.php';
|
||||
|
||||
// Create a handler that will construct and return a response. We'll
|
||||
// register this handler with a server and router below.
|
||||
class HelloHandler implements RequestHandlerInterface
|
||||
{
|
||||
public function handle(ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
// Check for a "name" attribute which may have been provided as a
|
||||
// path variable. Use "world" as a default.
|
||||
$name = $request->getAttribute("name", "world");
|
||||
|
||||
// Set the response body to the greeting and the status code to 200 OK.
|
||||
$response = (new Response(200))
|
||||
->withHeader("Content-type", "text/plain")
|
||||
->withBody(new Stream("Hello, $name!"));
|
||||
|
||||
// Return the response.
|
||||
return $response;
|
||||
}
|
||||
}
|
||||
|
||||
// Create middleware that will add a custom header to every response.
|
||||
class CustomerHeaderMiddleware implements MiddlewareInterface
|
||||
{
|
||||
public function process(
|
||||
ServerRequestInterface $request,
|
||||
RequestHandlerInterface $handler
|
||||
): ResponseInterface {
|
||||
|
||||
// Delegate to the next handler in the chain to obtain a response.
|
||||
$response = $handler->handle($request);
|
||||
|
||||
// Add the header.
|
||||
$response = $response->withHeader("X-example", "hello world");
|
||||
|
||||
// Return the altered response.
|
||||
return $response;
|
||||
}
|
||||
}
|
||||
|
||||
// Create a server
|
||||
$server = new Server();
|
||||
|
||||
// Add the header-adding middleware to the server first so that it will
|
||||
// forward requests on to the router.
|
||||
$server->add(new CustomerHeaderMiddleware());
|
||||
|
||||
// Create a router to map methods and endpoints to handlers.
|
||||
$router = $server->createRouter();
|
||||
|
||||
$handler = new HelloHandler();
|
||||
// Register a route to the handler without a variable in the path.
|
||||
$router->register('GET', '/hello', $handler);
|
||||
// Register a route that reads a "name" from the path.
|
||||
// This will make the "name" request attribute available to the handler.
|
||||
$router->register('GET', '/hello/{name}', $handler);
|
||||
$server->add($router);
|
||||
|
||||
// Read the request from the client, dispatch, and output.
|
||||
$server->respond();
|
||||
|
||||
Contents
|
||||
--------
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 4
|
||||
|
||||
overview
|
||||
getting-started
|
||||
messages
|
||||
handlers-and-middleware
|
||||
router
|
||||
uri-templates
|
||||
uri-templates-advanced
|
||||
extending
|
||||
dependency-injection
|
||||
additional
|
||||
web-server-configuration
|
||||
|
||||
.. _PSR-7: https://www.php-fig.org/psr/psr-7/
|
||||
.. _PSR-15: https://www.php-fig.org/psr/psr-15/
|
||||
.. _factory functions: handlers-and-middleware.html#factory-functions
|
||||
.. _middleware: handlers-and-middleware.html
|
||||
.. _router: router.html
|
||||
.. _URI Templates: uri-templates.html
|
||||
|
|
@ -0,0 +1,443 @@
|
|||
Messages and PSR-7
|
||||
==================
|
||||
|
||||
WellRESTed uses the PSR-7_ interfaces for HTTP messages. This section provides an introduction to working with these interfaces and the implementations provided with WellRESTed. For more information, please read about PSR-7_.
|
||||
|
||||
Requests
|
||||
--------
|
||||
|
||||
The ``$request`` variable passed to handlers and middleware represents the request message sent by the client. You can inspect this variable to read information such as the request path, method, query, headers, and body.
|
||||
|
||||
Let's start with a very simple GET request to the path ``/cats/?color=orange``.
|
||||
|
||||
.. code-block:: http
|
||||
|
||||
GET /cats/?color=orange HTTP/1.1
|
||||
Host: example.com
|
||||
Cache-control: no-cache
|
||||
|
||||
You can read information from the request in your handler like this:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
class MyHandler implements RequestHandlerInterface
|
||||
{
|
||||
public function handle(ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
$path = $request->getRequestTarget();
|
||||
// "/cats/?color=orange"
|
||||
|
||||
$method = $request->getMethod();
|
||||
// "GET"
|
||||
|
||||
$query = $request->getQueryParams();
|
||||
/*
|
||||
Array
|
||||
(
|
||||
[color] => orange
|
||||
)
|
||||
*/
|
||||
}
|
||||
}
|
||||
|
||||
This example shows that you can use:
|
||||
|
||||
- ``getRequestTarget()`` to read the path and query string for the request
|
||||
- ``getMethod()`` to read the HTTP verb (e.g., GET, POST, OPTIONS, DELETE)
|
||||
- ``getQueryParams()`` to read the query as an associative array
|
||||
|
||||
Headers
|
||||
^^^^^^^
|
||||
|
||||
The request above also included a ``Cache-control: no-cache`` header. You can read this header a number of ways. The simplest way is with the ``getHeaderLine($name)`` method.
|
||||
|
||||
Call ``getHeaderLine($name)`` and pass the case-insensitive name of a header. The method will return the value for the header, or an empty string if the header is not present.
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
class MyHandler implements RequestHandlerInterface
|
||||
{
|
||||
public function handle(ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
// This message contains a "Cache-control: no-cache" header.
|
||||
$cacheControl = $request->getHeaderLine("cache-control");
|
||||
// "no-cache"
|
||||
|
||||
// This message does not contain any authorization headers.
|
||||
$authorization = $request->getHeaderLine("authorization");
|
||||
// ""
|
||||
}
|
||||
}
|
||||
|
||||
.. note::
|
||||
|
||||
All methods relating to headers treat header field names case insensitively.
|
||||
|
||||
|
||||
Because HTTP messages may contain multiple headers with the same field name, ``getHeaderLine($name)`` has one other feature: If multiple headers with the same field name are present in the message, ``getHeaderLine($name)`` returns a string containing all of the values for that field, concatenated by commas. This is more common with responses, particularly with the ``Set-cookie`` header, but is still possible for requests.
|
||||
|
||||
You may also use ``hasHeader($name)`` to test if a header exists, ``getHeader($name)`` to receive an array of values for this field name, and ``getHeaders()`` to receive an associative array of headers where each key is a field name and each value is an array of field values.
|
||||
|
||||
|
||||
Body
|
||||
^^^^
|
||||
|
||||
PSR-7_ provides access to the body of the request as a stream and—when possible—as a parsed object or array. Let's start by looking at a request with form fields made available as an array.
|
||||
|
||||
Parsed Body
|
||||
~~~~~~~~~~~
|
||||
|
||||
For POST requests for forms (i.e., the ``Content-type`` header is either ``application/x-www-form-urlencoded`` or ``multipart/form-data``), the request makes the form fields available via the ``getParsedBody`` method. This provides access to the fields without needing to rely on the ``$_POST`` superglobal.
|
||||
|
||||
Given this request:
|
||||
|
||||
.. code-block:: http
|
||||
|
||||
POST /cats/ HTTP/1.1
|
||||
Host: example.com
|
||||
Content-type: application/x-www-form-urlencoded
|
||||
Content-length: 23
|
||||
|
||||
name=Molly&color=Calico
|
||||
|
||||
We can read the parsed body like this:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
class MyHandler implements RequestHandlerInterface
|
||||
{
|
||||
public function handle(ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
$cat = $request->getParsedBody();
|
||||
/*
|
||||
Array
|
||||
(
|
||||
[name] => Molly
|
||||
[color] => calico
|
||||
)
|
||||
*/
|
||||
}
|
||||
}
|
||||
|
||||
Body Stream
|
||||
~~~~~~~~~~~
|
||||
|
||||
For other content types, use the ``getBody`` method to get a stream containing the contents of request entity body.
|
||||
|
||||
Using a JSON representation of our cat, we can make a request like this:
|
||||
|
||||
.. code-block:: http
|
||||
|
||||
POST /cats/ HTTP/1.1
|
||||
Host: example.com
|
||||
Content-type: application/json
|
||||
Content-length: 46
|
||||
|
||||
{
|
||||
"name": "Molly",
|
||||
"color": "Calico"
|
||||
}
|
||||
|
||||
We can read and parse the JSON body, and even provide it as the parsedBody for later middleware or handlers like this:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
class JsonParser implements MiddlewareInterface
|
||||
{
|
||||
public function process(
|
||||
ServerRequestInterface $request,
|
||||
RequestHandlerInterface $handler
|
||||
): ResponseInterface
|
||||
{
|
||||
// Parse the body.
|
||||
$cat = json_decode((string) $request->getBody());
|
||||
/*
|
||||
stdClass Object
|
||||
(
|
||||
[name] => Molly
|
||||
[color] => calico
|
||||
)
|
||||
*/
|
||||
// Add the parsed JSON to the request.
|
||||
$request = $request->withParsedBody($cat);
|
||||
// Send the request to the next handler.
|
||||
return $handler->handle($request);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Because the entity body of a request or response can be very large, PSR-7_ represents bodies as streams using the ``Psr\Http\Message\StreamInterface`` (see `PSR-7 Section 1.3`_).
|
||||
|
||||
The JSON example casts the stream to a string, but we can also do things like copy the stream to a local file:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
// Store the body to a temp file.
|
||||
$chunkSize = 2048; // Number of bytes to read at once.
|
||||
$localPath = tempnam(sys_get_temp_dir(), "body");
|
||||
$h = fopen($localPath, "wb");
|
||||
$body = $request->getBody();
|
||||
while (!$body->eof()) {
|
||||
fwrite($h, $body->read($chunkSize));
|
||||
}
|
||||
fclose($h);
|
||||
|
||||
Parameters
|
||||
^^^^^^^^^^
|
||||
|
||||
PSR-7_ eliminates the need to read from many of the superglobals. We already saw how ``getParsedBody`` takes the place of reading directly from ``$_POST`` and ``getQueryParams`` replaces reading from ``$_GET``. Here are some other ``ServerRequestInterface`` methods with brief descriptions. Please see PSR-7_ for full details, particularly for ``getUploadedFiles``.
|
||||
|
||||
.. list-table::
|
||||
:header-rows: 1
|
||||
|
||||
* - Method
|
||||
- Replaces
|
||||
- Note
|
||||
* - getServerParams
|
||||
- $_SERVER
|
||||
- Data related to the request environment
|
||||
* - getCookieParams
|
||||
- $_COOKIE
|
||||
- Compatible with the structure of $_COOKIE
|
||||
* - getQueryParams
|
||||
- $_GET
|
||||
- Deserialized query string arguments, if any
|
||||
* - getParsedBody
|
||||
- $_POST
|
||||
- Request body as an object or array
|
||||
* - getUploadedFiles
|
||||
- $_FILES
|
||||
- Normalized tree of file upload data
|
||||
|
||||
Attributes
|
||||
^^^^^^^^^^
|
||||
|
||||
``ServerRequestInterface`` provides another useful feature called "attributes". Attributes are key-value pairs associated with the request that can be, well, pretty much anything.
|
||||
|
||||
The primary use for attributes in WellRESTed is to provide access to path variables when using `template routes`_ or `regex routes`_.
|
||||
|
||||
For example, the template route ``/cats/{name}`` matches routes such as ``/cats/Molly`` and ``/cats/Oscar``. When the route is dispatched, the router takes the portion of the actual request path matched by ``{name}`` and provides it as an attribute.
|
||||
|
||||
For a request to ``/cats/Rufus``:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
$name = $request->getAttribute("name");
|
||||
// "Rufus"
|
||||
|
||||
When calling ``getAttribute``, you can optionally provide a default value as the second argument. The value of this argument will be returned if the request has no attribute with that name.
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
// Request has no attribute "dog"
|
||||
$name = $request->getAttribute("dog", "Bear");
|
||||
// "Bear"
|
||||
|
||||
Middleware can also use attributes as a way to provide extra information to subsequent handlers. For example, an authorization middleware could obtain an object representing a user and store is as the "user" attribute which later middleware could read.
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
class AuthorizationMiddleware implements MiddlewareInterface
|
||||
{
|
||||
public function process(
|
||||
ServerRequestInterface $request,
|
||||
RequestHandlerInterface $handler
|
||||
): ResponseInterface
|
||||
|
||||
try {
|
||||
$user = $this->readUserFromCredentials($request);
|
||||
} catch (NoCredentialsSupplied $e) {
|
||||
return $response->withStatus(401);
|
||||
} catch (UserNotAllowedHere $e) {
|
||||
return $response->withStatus(403);
|
||||
}
|
||||
|
||||
// Store this as an attribute.
|
||||
$request = $request->withAttribute("user", $user);
|
||||
|
||||
// Delegate to the handler, passing the request with the "user" attribute.
|
||||
return $handler->handle($request);
|
||||
}
|
||||
};
|
||||
|
||||
class SecureHandler implements RequestHandlerInterface
|
||||
{
|
||||
public function handle(ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
// Read the "user" attribute added by a previous middleware.
|
||||
$user = $request->getAttribute("user");
|
||||
|
||||
// Do something with $user ...
|
||||
}
|
||||
}
|
||||
|
||||
$server = new \WellRESTed\Server();
|
||||
$server->add(new AuthorizationMiddleware());
|
||||
$server->add(new SecureHandler()); // Must be added AFTER authorization to get "user"
|
||||
$server->respond();
|
||||
|
||||
Responses
|
||||
---------
|
||||
|
||||
PSR-7_ messages are immutable, so you will not be able to alter values of response properties. Instead, ``with*`` methods provide ways to get a copy of the current message with updated properties. For example, ``ResponseInterface::withStatus`` returns a copy of the original response with the status changed.
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
// The original response has a 500 status code.
|
||||
$response->getStatusCode();
|
||||
// 500
|
||||
|
||||
// Replace this instance with a new instance with the status updated.
|
||||
$response = $response->withStatus(200);
|
||||
$response->getStatusCode();
|
||||
// 200
|
||||
|
||||
.. note::
|
||||
|
||||
PSR-7_ requests are immutable as well, and we used ``withAttribute`` and ``withParsedBody`` in a few of the examples in the Requests section.
|
||||
|
||||
Chain multiple ``with`` methods together fluently:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
// Get a new response with updated status, headers, and body.
|
||||
$response = (new Response())
|
||||
->withStatus(200)
|
||||
->withHeader("Content-type", "text/plain")
|
||||
->withBody(new \WellRESTed\Message\Stream("Hello, world!);
|
||||
|
||||
Status
|
||||
^^^^^^
|
||||
|
||||
Provide the status code for your response with the ``withStatus`` method. When you pass a standard status code to this method, the WellRESTed response implementation will provide an appropriate reason phrase for you. For a list of reason phrases provided by WellRESTed, see the IANA `HTTP Status Code Registry`_.
|
||||
|
||||
.. note::
|
||||
|
||||
The "reason phrase" is the text description of the status that appears in the status line of the response. The "status line" is the very first line in the response that appears before the first header.
|
||||
|
||||
Although the PSR-7_ ``ResponseInterface::withStatus`` method accepts the reason phrase as an optional second parameter, you generally shouldn't pass anything unless you are using a non-standard status code. (And you probably shouldn't be using a non-standard status code.)
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
// Set the status and view the reason phrase provided.
|
||||
|
||||
$response = $response->withStatus(200);
|
||||
$response->getReasonPhrase();
|
||||
// "OK"
|
||||
|
||||
$response = $response->withStatus(404);
|
||||
$response->getReasonPhrase();
|
||||
// "Not Found"
|
||||
|
||||
Headers
|
||||
^^^^^^^
|
||||
|
||||
Use the ``withHeader`` method to add a header to a response. ``withHeader`` will add the header if not already set, or replace the value of an existing header with the same name.
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
// Add a "Content-type" header.
|
||||
$response = $response->withHeader("Content-type", "text/plain");
|
||||
$response->getHeaderLine("Content-type");
|
||||
// "text/plain"
|
||||
|
||||
// Calling withHeader a second time updates the value.
|
||||
$response = $response->withHeader("Content-type", "text/html");
|
||||
$response->getHeaderLine("Content-type");
|
||||
// "text/html"
|
||||
|
||||
To set multiple values for a given header field name (e.g., for ``Set-cookie`` headers), call ``withAddedHeader``. ``withAddedHeader`` adds the new header without altering existing headers with the same name.
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
$response = $response
|
||||
->withHeader("Set-cookie", "cat=Molly; Path=/cats; Expires=Wed, 13 Jan 2021 22:23:01 GMT;")
|
||||
->withAddedHeader("Set-cookie", "dog=Bear; Domain=.foo.com; Path=/; Expires=Wed, 13 Jan 2021 22:23:01 GMT;")
|
||||
->withAddedHeader("Set-cookie", "hamster=Fizzgig; Domain=.foo.com; Path=/; Expires=Wed, 13 Jan 2021 22:23:01 GMT;");
|
||||
|
||||
To check if a header exists or to remove a header, use ``hasHeader`` and ``withoutHeader``.
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
// Check if a header exists.
|
||||
$response->hasHeader("Content-type");
|
||||
// true
|
||||
|
||||
// Clone this response without the "Content-type" header.
|
||||
$response = $response->withoutHeader("Content-type");
|
||||
|
||||
// Check if a header exists.
|
||||
$response->hasHeader("Content-type");
|
||||
// false
|
||||
|
||||
Body
|
||||
^^^^
|
||||
|
||||
To set the body for the response, pass an instance implementing ``Psr\Http\Message\Stream`` to the ``withBody`` method.
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
$stream = new \WellRESTed\Message\Stream("Hello, world!");
|
||||
$response = $response->withBody($stream);
|
||||
|
||||
WellRESTed provides two ``Psr\Http\Message\Stream`` implementations. You can use these, or any other implementation.
|
||||
|
||||
Stream
|
||||
~~~~~~
|
||||
|
||||
``WellRESTed\Message\Stream`` wraps a file pointer resource and is useful for responding with a string or file.
|
||||
|
||||
When you pass a string to the constructor, the Stream instance uses `php://temp`_ as the file pointer resource. The string passed to the constructor is automatically stored to ``php://temp``, and you can write more content to it using the ``StreamInterface::write`` method.
|
||||
|
||||
.. note::
|
||||
|
||||
``php://temp`` stores the contents to memory, but switches to a temporary file once the amount of data stored hits a predefined limit (the default is 2 MB).
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
// Pass the beginning of the contents to the constructor as a string.
|
||||
$body = new \WellRESTed\Message\Stream("Hello ");
|
||||
|
||||
// Append more contents.
|
||||
$body->write("world!");
|
||||
|
||||
// Set the body and status code.
|
||||
$response = (new Response())
|
||||
->withStatus(200)
|
||||
->withBody($body);
|
||||
|
||||
To respond with the contents of an existing file, use ``fopen`` to open the file with read access and pass the pointer to the constructor.
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
// Open the file with read access.
|
||||
$resource = fopen("/home/user/some/file", "rb");
|
||||
|
||||
// Pass the file pointer resource to the constructor.
|
||||
$body = new \WellRESTed\Message\Stream($resource);
|
||||
|
||||
// Set the body and status code.
|
||||
$response = (new Response())
|
||||
->withStatus(200)
|
||||
->withBody($body);
|
||||
|
||||
NullStream
|
||||
~~~~~~~~~~
|
||||
|
||||
Each PSR-7_ message MUST have a body, so there's no ``withoutBody`` method. You also cannot pass ``null`` to ``withBody``. Instead, use a ``WellRESTed\Messages\NullStream`` to provide a very simple, zero-length, no-content body.
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
$response = (new Response())
|
||||
->withStatus(200)
|
||||
->withBody(new \WellRESTed\Message\NullStream());
|
||||
|
||||
.. _HTTP Status Code Registry: https://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml
|
||||
.. _PSR-7: http://www.php-fig.org/psr/psr-7/
|
||||
.. _PSR-7 Section 1.3: https://www.php-fig.org/psr/psr-7/#13-streams
|
||||
.. _Getting Started: getting-started.html
|
||||
.. _Middleware: middleware.html
|
||||
.. _template routes: router.html#template-routes
|
||||
.. _regex routes: router.html#regex-routes
|
||||
.. _dependency injection: dependency-injection.html
|
||||
.. _`php://temp`: https://php.net/manual/ro/wrappers.php.php
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
Overview
|
||||
========
|
||||
|
||||
Installation
|
||||
^^^^^^^^^^^^
|
||||
|
||||
The recommended method for installing WellRESTed is to use Composer_. Add an entry for WellRESTed in your project's ``composer.json`` file.
|
||||
|
||||
.. code-block:: js
|
||||
|
||||
{
|
||||
"require": {
|
||||
"wellrested/wellrested": "^5"
|
||||
}
|
||||
}
|
||||
|
||||
Requirements
|
||||
^^^^^^^^^^^^
|
||||
|
||||
- PHP 7.3
|
||||
|
||||
License
|
||||
^^^^^^^
|
||||
|
||||
Licensed using the `MIT license <http://opensource.org/licenses/MIT>`_.
|
||||
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2021 PJ Dietz
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
|
||||
.. _Composer: https://getcomposer.org/
|
||||
|
|
@ -0,0 +1,358 @@
|
|||
Router
|
||||
======
|
||||
|
||||
A router is a type of middleware that organizes the components of a site by associating HTTP methods and paths with handlers and middleware. When the router receives a request, it examines the path components of the request's URI, determines which "route" matches, and dispatches the associated handler. The dispatched handler is then responsible for reacting to the request and providing a response.
|
||||
|
||||
Basic Usage
|
||||
^^^^^^^^^^^
|
||||
|
||||
Typically, you will want to use the ``WellRESTed\Server::createRouter`` method to create a ``Router``.
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
$server = new WellRESTed\Server();
|
||||
$router = $server->createRouter();
|
||||
|
||||
Suppose ``$catHandler`` is a handler that you want to dispatch whenever a client makes a ``GET`` request to the path ``/cats/``. Use the ``register`` method map it to that path and method.
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
$router->register("GET", "/cats/", $catHandler);
|
||||
|
||||
The ``register`` method is fluent, so you can add multiple routes in either of these styles:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
$router->register("GET", "/cats/", $catReader);
|
||||
$router->register("POST", "/cats/", $catWriter);
|
||||
$router->register("GET", "/cats/{id}", $catItemReader);
|
||||
$router->register("PUT,DELETE", "/cats/{id}", $catItemWriter);
|
||||
|
||||
...Or...
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
$router
|
||||
->register("GET", "/cats/", $catReader)
|
||||
->register("POST", "/cats/", $catWriter)
|
||||
->register("GET", "/cats/{id}", $catItemReader)
|
||||
->register("PUT,DELETE", "/cats/{id}", $catItemWriter);
|
||||
|
||||
Paths
|
||||
^^^^^
|
||||
|
||||
A router can map a handler to an exact path, or to a pattern of paths.
|
||||
|
||||
Static Routes
|
||||
-------------
|
||||
|
||||
The simplest type of route is called a "static route". It maps a handler to an exact path.
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
$router->register("GET", "/cats/", $catHandler);
|
||||
|
||||
This route will map a request to ``/cats/`` and only ``/cats/``. It will **not** match requests to ``/cats`` or ``/cats/molly``.
|
||||
|
||||
Prefix Routes
|
||||
-------------
|
||||
|
||||
The next simplest type of route is a "prefix route". A prefix route matches requests by the beginning of the path.
|
||||
|
||||
To create a "prefix handler", include ``*`` at the end of the path. For example, this route will match any request that begins with ``/cats/``.
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
$router->register("GET", "/cats/*", $catHandler);
|
||||
|
||||
Template Routes
|
||||
---------------
|
||||
|
||||
Template routes allow you to provide patterns for paths with one or more variables (sections surrounded by curly braces) that will be extracted.
|
||||
|
||||
For example, this template will match requests to ``/cats/12``, ``/cats/molly``, etc.,
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
$router->register("GET", "/cats/{cat}", $catHandler);
|
||||
|
||||
When the router dispatches a route matched by a template route, it provides the extracted variables as request attributes. To access a variable, call the request object's ``getAttribute`` method and pass the variable's name.
|
||||
|
||||
For a request to ``/cats/molly``:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
$name = $request->getAttribute("cat");
|
||||
// "molly"
|
||||
|
||||
Template routes are very powerful, and this only scratches the surface. See `URI Templates`_ for a full explanation of the syntax supported.
|
||||
|
||||
Regex Routes
|
||||
------------
|
||||
|
||||
You can also use regular expressions to describe route paths.
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
$router->register("GET", "~cats/(?<name>[a-z]+)-(?<number>[0-9]+)~", $catHandler);
|
||||
|
||||
When using regular expression routes, the attributes will contain the captures from preg_match_.
|
||||
|
||||
For a request to ``/cats/molly-90``:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
$vars = $request->getAttributes();
|
||||
/*
|
||||
Array
|
||||
(
|
||||
[0] => cats/molly-12
|
||||
[name] => molly
|
||||
[1] => molly
|
||||
[number] => 12
|
||||
[2] => 12
|
||||
)
|
||||
*/
|
||||
|
||||
Route Priority
|
||||
--------------
|
||||
|
||||
A router will often contain many routes, and sometimes more than one route will match for a given request. When the router looks for a matching route, it performs these checks in order.
|
||||
|
||||
#. If there is a static route with exact match to path, dispatch it.
|
||||
#. If one prefix route matches the beginning of the path, dispatch it.
|
||||
#. If multiple prefix routes match, dispatch the longest matching prefix route.
|
||||
#. Inspect each pattern route (template and regular expression) in the order in which they were added to the router. Dispatch the first route that matches.
|
||||
#. If no pattern routes match, return a response with a ``404 Not Found`` status. (**Note:** This is the default behavior. To configure a router to delegate to the next middleware when no route matches, call the router's ``continueOnNotFound()`` method.)
|
||||
|
||||
Static vs. Prefix
|
||||
~~~~~~~~~~~~~~~~~
|
||||
|
||||
Consider these routes:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
$router
|
||||
->register("GET", "/cats/", $static);
|
||||
->register("GET", "/cats/*", $prefix);
|
||||
|
||||
The router will dispatch a request for ``/cats/`` to ``$static`` because the static route ``/cats/`` has priority over the prefix route ``/cats/*``.
|
||||
|
||||
The router will dispatch a request to ``/cats/maine-coon`` to ``$prefix`` because it is not an exact match for ``/cats/``, but it does begin with ``/cats/``.
|
||||
|
||||
Prefix vs. Prefix
|
||||
~~~~~~~~~~~~~~~~~
|
||||
|
||||
Given these routes:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
$router
|
||||
->register("GET", "/dogs/*", $short);
|
||||
->register("GET", "/dogs/sporting/*", $long);
|
||||
|
||||
A request to ``/dogs/herding/australian-shepherd`` will be dispatched to ``$short`` because it matches ``/dogs/*``, but does not match ``/dogs/sporting/*``
|
||||
|
||||
A request to ``/dogs/sporing/flat-coated-retriever`` will be dispatched to ``$long`` because it matches both routes, but ``/dogs/sporting`` is longer.
|
||||
|
||||
Prefix vs. Pattern
|
||||
~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Given these routes:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
$router
|
||||
->register("GET", "/dogs/*", $prefix);
|
||||
->register("GET", "/dogs/{group}/{breed}", $pattern);
|
||||
|
||||
``$pattern`` will **never** be dispatched because any route that matches ``/dogs/{group}/{breed}`` also matches ``/dogs/*``, and prefix routes have priority over pattern routes.
|
||||
|
||||
Pattern vs. Pattern
|
||||
~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
When multiple pattern routes match a path, the first one that was added to the router will be the one dispatched. **Be careful to add the specific routes before the general routes.** For example, say you want to send traffic to two similar looking URIs to different handlers based whether the variables were supplied as numbers or letters—``/dogs/102/132`` should be dispatched to ``$numbers``, while ``/dogs/herding/australian-shepherd`` should be dispatched to ``$letters``.
|
||||
|
||||
This will work:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
// Matches only when the variables are digits.
|
||||
$router->register("GET", "~/dogs/([0-9]+)/([0-9]+)", $numbers);
|
||||
// Matches variables with any unreserved characters.
|
||||
$router->register("GET", "/dogs/{group}/{breed}", $letters);
|
||||
|
||||
This will **NOT** work:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
// Matches variables with any unreserved characters.
|
||||
$router->register("GET", "/dogs/{group}/{breed}", $letters);
|
||||
// Matches only when the variables are digits.
|
||||
$router->register("GET", "~/dogs/([0-9]+)/([0-9]+)", $numbers);
|
||||
|
||||
This is because ``/dogs/{group}/{breed}`` will match both ``/dogs/102/132`` **and** ``/dogs/herding/australian-shepherd``. If it is added to the router before the route for ``$numbers``, it will be dispatched before the route for ``$numbers`` is ever evaluated.
|
||||
|
||||
Methods
|
||||
^^^^^^^
|
||||
|
||||
When you register a route, you can provide a specific method, a list of methods, or a wildcard to indicate any method.
|
||||
|
||||
Registering by Method
|
||||
---------------------
|
||||
|
||||
Specify a specific handler for a path and method by including the method as the first parameter.
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
// Dispatch $dogCollectionReader for GET requests to /dogs/
|
||||
$router->register("GET", "/dogs/", $dogCollectionReader);
|
||||
|
||||
// Dispatch $dogCollectionWriter for POST requests to /dogs/
|
||||
$router->register("POST", "/dogs/", $dogCollectionWriter);
|
||||
|
||||
Registering by Method List
|
||||
--------------------------
|
||||
|
||||
Specify the same handler for multiple methods for a given path by proving a comma-separated list of methods as the first parameter.
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
// Dispatch $catCollectionHandler for GET and POST requests to /cats/
|
||||
$router->register("GET,POST", "/cats/", $catCollectionHandler);
|
||||
|
||||
// Dispatch $catItemReader for GET requests to /cats/12, /cats/12, etc.
|
||||
$router->register("GET", "/cats/{id}", $catItemReader);
|
||||
|
||||
// Dispatch $catItemWriter for PUT, and DELETE requests to /cats/12, /cats/12, etc.
|
||||
$router->register("PUT,DELETE", "/cats/{id}", $catItemWriter);
|
||||
|
||||
Registering by Wildcard
|
||||
-----------------------
|
||||
|
||||
Specify a handler for all methods for a given path by proving a ``*`` wildcard.
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
// Dispatch $guineaPigHandler for all requests to /guinea-pigs/, regardless of method.
|
||||
$router->register("*", "/guinea-pigs/", $guineaPigHandler);
|
||||
|
||||
// Use $hamstersHandler by default for requests to /hamsters/
|
||||
$router->register("*", "/hamsters/", $hamstersHandler);
|
||||
|
||||
// Provide a specific handler for POST /hamsters/
|
||||
$router->register("POST", "/hamsters/", $hamstersPostOnly);
|
||||
|
||||
.. note::
|
||||
|
||||
The wildcard ``*`` can be useful, but be aware that the associated middleware will need to manage ``HEAD`` and ``OPTIONS`` requests, whereas this is done automatically for non-wildcard routes.
|
||||
|
||||
HEAD
|
||||
----
|
||||
|
||||
Any route that supports ``GET`` requests will automatically support ``HEAD``. You don't need to provide any specific middleware for ``HEAD``, and you usually shouldn't. (Although you can if you want.)
|
||||
|
||||
For most cases, just implement ``GET``, and the webserver will manage suppressing the response body for you.
|
||||
|
||||
OPTIONS, 405 Responses, and Allow Headers
|
||||
-----------------------------------------
|
||||
|
||||
When you add routes to a router by method, the router automatically provides responses for ``OPTIONS`` requests. For example, given this route:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
// Dispatch $catItemReader for GET requests to /cats/12, /cats/12, etc.
|
||||
$router->register("GET", "/cats/{id}", $catItemReader);
|
||||
|
||||
// Dispatch $catItemWriter for PUT, and DELETE requests to /cats/12, /cats/12, etc.
|
||||
$router->register("PUT,DELETE", "/cats/{id}", $catItemWriter);
|
||||
|
||||
An ``OPTIONS`` request to ``/cats/12`` will provide a response like:
|
||||
|
||||
.. code-block:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Allow: GET,PUT,DELETE,HEAD,OPTIONS
|
||||
|
||||
Likewise, a request to an unsupported method will return a ``405 Method Not Allowed`` response with a descriptive ``Allow`` header.
|
||||
|
||||
A ``POST`` request to ``/cats/12`` will provide:
|
||||
|
||||
.. code-block:: http
|
||||
|
||||
HTTP/1.1 405 Method Not Allowed
|
||||
Allow: GET,PUT,DELETE,HEAD,OPTIONS
|
||||
|
||||
Error Responses
|
||||
^^^^^^^^^^^^^^^
|
||||
|
||||
Then a router is able to locate a route that matches the path, but that route doesn't support the request's method, the router will respond ``405 Method Not Allowed``.
|
||||
|
||||
When a router is unable to match the route, it will delegate to the next middleware.
|
||||
|
||||
.. note::
|
||||
|
||||
When no route matches, the Router will delegate to the next middleware in the server. This is a change from previous versions of WellRESTed where there Router would return a 404 Not Found response. This new behaviour allows a servers to have multiple routers.
|
||||
|
||||
Router-specific Middleware
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
WellRESTed allows a Router to have a set of middleware to dispatch whenever it finds a route that matches. This middleware runs before the handler for the matched route, and only when a route matches.
|
||||
|
||||
This feature allows you to build a site where some sections use certain middleware and other do not. For example, suppose your site has a public section that does not require authentication and a private section that does. We can use a different router for each section, and provide authentication middleware on only the router for the private area.
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
$server = new Server();
|
||||
|
||||
// Add the "public" router.
|
||||
$public = $server->createRouter();
|
||||
$public->register('GET', '/', $homeHandler);
|
||||
$public->register('GET', '/about', $homeHandler);
|
||||
// Set the router call the next middleware when no route matches.
|
||||
$public->continueOnNotFound();
|
||||
$server->add($public);
|
||||
|
||||
// Add the "private" router.
|
||||
$private = $server->createRouter();
|
||||
// Authorization middleware checks for an Authorization header and
|
||||
// responds 401 when the header is missing or invalid.
|
||||
$private->add($authorizationMiddleware);
|
||||
$private->register('GET', '/secret', $secretHandler);
|
||||
$private->register('GET', '/members-only', $otherHandler);
|
||||
$server->add($private);
|
||||
|
||||
$server->respond();
|
||||
|
||||
Nested Routers
|
||||
^^^^^^^^^^^^^^
|
||||
|
||||
For large Web services with large numbers of endpoints, a single, monolithic router may not to optimal. To avoid having each request test every pattern-based route, you can break up a router into a hierarchy of routers.
|
||||
|
||||
Here's an example where all of the traffic beginning with ``/cats/`` is sent to one router, and all the traffic for endpoints beginning with ``/dogs/`` is sent to another.
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
$server = new Server();
|
||||
|
||||
$catRouter = $server->createRouter()
|
||||
->register("GET", "/cats/", $catReader)
|
||||
->register("POST", "/cats/", $catWriter)
|
||||
// ... many more endpoints starting with /cats/
|
||||
->register("POST", "/cats/{cat}/photo/{gallery}/{width}x{height}.{extension}", $catImageHandler);
|
||||
|
||||
$dogRouter = $server->createRouter()
|
||||
->register("GET,POST", "/dogs/", $dogHandler)
|
||||
// ... many more endpoints starting with /dogs/
|
||||
->register("POST", "/dogs/{dog}/photo/{gallery}/{width}x{height}.{extension}", $dogImageHandler);
|
||||
|
||||
$server->add($server->createRouter()
|
||||
->register("*", "/cats/*", $catRouter)
|
||||
->register("*", "/dogs/*", $dogRouter)
|
||||
);
|
||||
|
||||
$server->respond();
|
||||
|
||||
.. _preg_match: https://php.net/manual/en/function.preg-match.php
|
||||
.. _URI Template: `URI Templates`_s
|
||||
.. _URI Templates: uri-templates.html
|
||||
|
|
@ -0,0 +1,144 @@
|
|||
URI Templates (Advanced)
|
||||
========================
|
||||
|
||||
In `URI Templates`_, we looked at the most common ways to use URI Templates. Here, we'll look at some of the extended syntaxes that URI Templates provide.
|
||||
|
||||
Path Components
|
||||
^^^^^^^^^^^^^^^
|
||||
|
||||
To match a path component, include a slash ``/`` at the beginning of the variable expression. This instructs the template to match the variable if it:
|
||||
|
||||
- Begins with ``/``
|
||||
- Contains only unreserved and percent-encoded characters
|
||||
|
||||
You may also use the explode (``*``) modifier to match a variable number of path components and provide them as an array. When using the explode (``*``) modifier to match paths components, the ``/`` character serves as the delimiter instead of a comma.
|
||||
|
||||
.. list-table:: Matching path components
|
||||
:header-rows: 1
|
||||
|
||||
* - Template
|
||||
- Path
|
||||
- Match?
|
||||
- Attributes
|
||||
* - {/path}
|
||||
- /hello.html
|
||||
- Yes
|
||||
- :path: ``"hello.html"``
|
||||
* - {/path}
|
||||
- /too/many/parts.jpg
|
||||
- No
|
||||
-
|
||||
* - {/one}{/two}{/three}
|
||||
- /just/enough/parts.jpg
|
||||
- Yes
|
||||
- :one: ``"just"``
|
||||
:two: ``"enough"``
|
||||
:three: ``"parts.jpg"``
|
||||
* - {/path*}
|
||||
- /any/number/of/parts.jpg
|
||||
- Yes
|
||||
- :path: ``["any", "number", "of", "parts.jpg"]``
|
||||
* - /image{/image*}.jpg
|
||||
- /image/with/any/path.jpg
|
||||
- Yes
|
||||
- :image: ``["with", "any", "path"]``
|
||||
|
||||
.. note::
|
||||
|
||||
The template ``{/path}`` fails to match the path ``/too/many/parts.jpg``. Although the path does begin with a slash, the subsequent slashes are reserved characters, and therefore the match fails. To match a variable number of path components, use the explode ``*`` modifier (e.g, ``{/paths*}``), or use the reserved (``+``) operator (e.g., ``/{+paths}``).
|
||||
|
||||
Dot Prefixes
|
||||
^^^^^^^^^^^^
|
||||
|
||||
Dot prefixes work similarly to matching path components, but a dot ``.`` is the prefix character in place of a slash. This may be useful for file extensions, etc.
|
||||
|
||||
Including a dot ``.`` at the beginning of the variable expression instructs the template to match the variable if it:
|
||||
|
||||
- Begins with ``.``
|
||||
- Contains only unreserved (including ``.``) and percent-encoded characters
|
||||
|
||||
You may also use the explode (``*``) modifier to match a variable number of dot-prefixed segments and store them to an array. When using the explode (``*``) modifier to match paths components, the ``.`` character serves as the delimiter instead of a comma.
|
||||
|
||||
.. list-table:: Matching dot prefixes
|
||||
:header-rows: 1
|
||||
|
||||
* - Template
|
||||
- Path
|
||||
- Match?
|
||||
- Attributes
|
||||
* - /file{.ext}
|
||||
- /file.jpg
|
||||
- Yes
|
||||
- :ext: ``"jpg"``
|
||||
* - /file{.ext}
|
||||
- /file.tar.gz
|
||||
- Yes
|
||||
- :ext: ``"tar.gz"``
|
||||
* - /file{.ext1}{.ext2}
|
||||
- /file.tar.gz
|
||||
- Yes
|
||||
- :ext1: ``"tar"``
|
||||
:ext2: ``"gz"``
|
||||
* - /file{.ext*}
|
||||
- /file.tar.gz
|
||||
- Yes
|
||||
- :ext: ``["tar", "gz"]``
|
||||
|
||||
.. note::
|
||||
|
||||
Because ``.`` is an unreserved character, the template ``/file{.ext}`` matches the path ``/file.tar.gz`` and provides the value ``"tar.gz"``. This is different from the behavior of the slash prefix, where an unexpected slash causes the match to fail.
|
||||
|
||||
Multiple-variable Expressions
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
An expression in a URI template may contain more than one variable. For example, the template ``/aliases/{one},{two},{three}`` can be written as ``/aliases/{one,two,three}``.
|
||||
|
||||
The delimiter between the matched variables is the same as when matching with the explode (``*``) modifier:
|
||||
|
||||
.. list-table::
|
||||
:header-rows: 1
|
||||
|
||||
* - Type
|
||||
- Delimiter
|
||||
* - Simple String
|
||||
- Comma ``,``
|
||||
* - Reserved
|
||||
- Comma ``,``
|
||||
* - Path Components
|
||||
- Slash ``/``
|
||||
* - Dot Prefix
|
||||
- Dot ``.``
|
||||
|
||||
.. list-table:: Multiple-variable expressions
|
||||
:header-rows: 1
|
||||
|
||||
* - Template
|
||||
- Path
|
||||
- Attributes
|
||||
* - /{one,two,three}
|
||||
- /fry,leela,bender
|
||||
- :one: ``"fry"``
|
||||
:two: ``"leela"``
|
||||
:three: ``"bender"``
|
||||
* - /{one,two,three}
|
||||
- /fry,leela,Nixon%27s%20head
|
||||
- :one: ``"fry"``
|
||||
:two: ``"leela"``
|
||||
:three: ``"Nixon's head"``
|
||||
* - /{+one,two,three}
|
||||
- /fry,leela,Nixon's+head
|
||||
- :one: ``"fry"``
|
||||
:two: ``"leela"``
|
||||
:three: ``"Nixon's head"``
|
||||
* - /{/one,two,three}
|
||||
- /fry/leela/bender
|
||||
- :one: ``"fry"``
|
||||
:two: ``"leela"``
|
||||
:three: ``"bender"``
|
||||
* - /file{.one,two,three}
|
||||
- /file.fry.leela.bender
|
||||
- :one: ``"fry"``
|
||||
:two: ``"leela"``
|
||||
:three: ``"bender"``
|
||||
|
||||
.. _URI Templates: uri-templates.html
|
||||
|
|
@ -0,0 +1,139 @@
|
|||
URI Templates
|
||||
=============
|
||||
|
||||
WellRESTed allows you to register handlers with a router using URI Templates, based on the URI Templates defined in `RFC 6570`_. These templates include variables (enclosed in curly braces) which are extracted and made available to the dispatched middleware.
|
||||
|
||||
Reading Variables
|
||||
^^^^^^^^^^^^^^^^^
|
||||
|
||||
Basic Usage
|
||||
-----------
|
||||
|
||||
Register a handler with a URI Template by providing a path that include at least one section enclosed in curly braces. The curly braces define variables for the template.
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
$router->register("GET", "/widgets/{id}", $widgetHandler);
|
||||
|
||||
The router will match requests for paths like ``/widgets/12`` and ``/widgets/mega-widget`` and dispatch ``$widgetHandler`` with the extracted variables made available as request attributes.
|
||||
|
||||
To read a path variable, router inspects the request attribute named ``"id"``, since ``id`` is what appears inside curly braces in the URI template.
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
// For a request to /widgets/12
|
||||
$id = $request->getAttribute("id");
|
||||
// "12"
|
||||
|
||||
// For a request to /widgets/mega-widget
|
||||
$id = $request->getAttribute("id");
|
||||
// "mega-widget"
|
||||
|
||||
.. note::
|
||||
|
||||
Request attributes are a feature of the ``ServerRequestInterface`` provided by PSR-7_.
|
||||
|
||||
Multiple Variables
|
||||
------------------
|
||||
|
||||
The example above included one variable, but URI Templates may include multiple. Each variable will be provided as a request attribute, so be sure to give your variables unique names.
|
||||
|
||||
Here's an example with a handful of variables. Suppose we have a template describing the path for a user's avatar image. The image is identified by a username and the image dimensions.
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
$router->register("GET", "/avatars/{username}-{width}x{height}.jpg", $avatarHandlers);
|
||||
|
||||
A request for ``GET /avatars/zoidberg-100x150.jpg`` will provide these request attributes:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
// Read the variables extracted form the path.
|
||||
$username = $request->getAttribute("username");
|
||||
// "zoidberg"
|
||||
$width = $request->getAttribute("width");
|
||||
// "100"
|
||||
$height = $request->getAttribute("height");
|
||||
// "150"
|
||||
|
||||
Arrays
|
||||
------
|
||||
|
||||
You may also match a comma-separated series of values as an array using a URI Template by providing a ``*`` at the end of the variable name.
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
$router->register("GET", "/favorite-colors/{colors*}", $colorsHandler);
|
||||
|
||||
A request for ``GET /favorite-colors/red,green,blue`` will provide an array as the value for the ``"colors"`` request attribute.
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
$colorsHandler = function ($request, $response, $next) {
|
||||
// Read the variable extracted form the path.
|
||||
$colorsList = $request->getAttribute("colors");
|
||||
/* Array
|
||||
(
|
||||
[0] => red
|
||||
[1] => green
|
||||
[2] => blue
|
||||
)
|
||||
*/
|
||||
};
|
||||
|
||||
Matching Characters
|
||||
^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
Unreserved Characters
|
||||
---------------------
|
||||
|
||||
By default, URI Template variables will match only "unreserved" characters. `RFC 3968 Section 2.3`_ defines unreserved characters as alphanumeric characters, ``-``, ``.``, ``_``, and ``~``. All other characters must be percent encoded to be matched by a default template variable.
|
||||
|
||||
.. note::
|
||||
|
||||
Percent-encoded characters matched by template variables are automatically decoded when provided as request attributes.
|
||||
|
||||
Given the template ``/users/{user}``, the following paths provide these values for ``getAttribute("user")``:
|
||||
|
||||
.. list-table:: Paths and Values for the Template ``/users/{user}``
|
||||
:header-rows: 1
|
||||
|
||||
* - Path
|
||||
- Value
|
||||
* - /users/123
|
||||
- "123"
|
||||
* - /users/zoidberg
|
||||
- "zoidberg"
|
||||
* - /users/zoidberg%40planetexpress.com
|
||||
- "zoidberg@planetexpress.com"
|
||||
|
||||
A request for ``GET /uses/zoidberg@planetexpress.com`` will **not** match this template, because ``@`` is a reserved character and is not percent encoded.
|
||||
|
||||
Reserved Characters
|
||||
-------------------
|
||||
|
||||
If you need to match a non-percent-encoded reserved character like ``@`` or ``/``, use the ``+`` operator at the beginning of the variable name.
|
||||
|
||||
Using the template ``/users/{+user}``, we can match all of the paths above, plus ``/users/zoidberg@planetexpress.com``.
|
||||
|
||||
Reserved matching also allows matching unencoded slashes (``/``). For example, given this template:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
$router->register("GET", "/my-favorite-path{+path}", $pathHandler);
|
||||
|
||||
The router will dispatch ``$pathHandler`` with for a request to ``GET /my-favorite-path/has/a/few/slashes.jpg``
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
$path = $request->getAttribute("path");
|
||||
// "/has/a/few/slashes.jpg"
|
||||
|
||||
.. note::
|
||||
|
||||
Combine the ``+`` operator and ``*`` modifier to match reserved characters as an array. For example, the template ``/{+vars*}`` will match the path ``/c@t,d*g``, providing the array ``["c@t", "d*g"]``.
|
||||
|
||||
.. _RFC 3968 Section 2.3: https://tools.ietf.org/html/rfc3986#section-2.3
|
||||
.. _PSR-7: https://www.php-fig.org/psr/psr-7/
|
||||
.. _RFC 6570: https://tools.ietf.org/html/rfc6570
|
||||
.. _RFC 6570 Section 3.2.7: https://tools.ietf.org/html/rfc6570#section-3.2.7
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
Web Server Configuration
|
||||
========================
|
||||
|
||||
You will typically want to have all traffic on your site directed to a single script that creates a ``WellRESTed\Server`` and calls ``respond``. Here are basic setups for doing this in Nginx_ and Apache_.
|
||||
|
||||
Nginx
|
||||
^^^^^
|
||||
|
||||
.. code-block:: nginx
|
||||
|
||||
server {
|
||||
|
||||
listen 80;
|
||||
server_name your.hostname.here;
|
||||
root /your/sites/document/root;
|
||||
index index.php index.html;
|
||||
charset utf-8;
|
||||
|
||||
# Attempt to serve actual files first.
|
||||
# If no file exists, send to /index.php
|
||||
location / {
|
||||
try_files $uri $uri/ /index.php?$args;
|
||||
}
|
||||
|
||||
location ~ \.php$ {
|
||||
try_files $uri =404;
|
||||
fastcgi_pass unix:/var/run/php5-fpm.sock;
|
||||
fastcgi_index index.php;
|
||||
include fastcgi_params;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Apache
|
||||
^^^^^^
|
||||
|
||||
.. code-block:: apache
|
||||
|
||||
RewriteEngine on
|
||||
RewriteBase /
|
||||
|
||||
# Send all requests to non-regular files and directories to index.php
|
||||
RewriteCond %{REQUEST_FILENAME} !-f
|
||||
RewriteCond %{REQUEST_FILENAME} !-d
|
||||
RewriteRule ^.+$ index.php [L,QSA]
|
||||
|
|
@ -1,21 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<phpdoc>
|
||||
<title>WellRESTed</title>
|
||||
<parser>
|
||||
<target>docs</target>
|
||||
<markers>
|
||||
<item>TODO</item>
|
||||
<item>FIXME</item>
|
||||
</markers>
|
||||
</parser>
|
||||
<transformer>
|
||||
<target>docs</target>
|
||||
</transformer>
|
||||
<transformations>
|
||||
<template name="responsive" />
|
||||
</transformations>
|
||||
<files>
|
||||
<directory>src</directory>
|
||||
<ignore>test/*</ignore>
|
||||
</files>
|
||||
</phpdoc>
|
||||
|
|
@ -1,38 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<phpunit
|
||||
bootstrap="vendor/autoload.php"
|
||||
colors="true"
|
||||
convertErrorsToExceptions="true"
|
||||
convertNoticesToExceptions="true"
|
||||
convertWarningsToExceptions="true"
|
||||
processIsolation="false"
|
||||
stopOnFailure="false"
|
||||
syntaxCheck="false"
|
||||
verbose="true"
|
||||
>
|
||||
<php>
|
||||
<!-- Port for testing the Client class. Set this to a port that is not used on the system.
|
||||
Use a range (e.g., 2000-9000) to randomly pick a port within the boundaries.
|
||||
To override, "export PORT=8081"
|
||||
-->
|
||||
<env name="PORT" value="2000-9000" />
|
||||
<!-- Port for testing Client class failuers. Set this to a port that is not used on the system. -->
|
||||
<env name="FAIL_PORT" value="9001-9100" />
|
||||
</php>
|
||||
<testsuites>
|
||||
<testsuite name="unit">
|
||||
<directory>./test/tests/unit/</directory>
|
||||
</testsuite>
|
||||
<testsuite name="integration">
|
||||
<directory>./test/tests/integration/</directory>
|
||||
</testsuite>
|
||||
</testsuites>
|
||||
<filter>
|
||||
<whitelist>
|
||||
<directory suffix=".php">./src/</directory>
|
||||
</whitelist>
|
||||
</filter>
|
||||
<logging>
|
||||
<log type="coverage-html" target="./report" charset="UTF-8" hightlight="true" />
|
||||
</logging>
|
||||
</phpunit>
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
<?php
|
||||
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Psr\Http\Server\RequestHandlerInterface;
|
||||
use WellRESTed\Message\Response;
|
||||
use WellRESTed\Message\Stream;
|
||||
use WellRESTed\Server;
|
||||
|
||||
ini_set('display_errors', 1);
|
||||
ini_set('display_startup_errors', 1);
|
||||
error_reporting(E_ALL);
|
||||
|
||||
require_once __DIR__ . '/../vendor/autoload.php';
|
||||
|
||||
// Create a handler using the PSR-15 RequestHandlerInterface
|
||||
class HomePageHandler implements RequestHandlerInterface
|
||||
{
|
||||
public function handle(ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
$view = <<<HTML
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>WellRESTed Development Site</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>WellRESTed Development Site</h1>
|
||||
|
||||
<p>To run unit tests, run:</p>
|
||||
<code>docker-compose run --rm php phpunit</code>
|
||||
<p>View the <a href="/coverage/">code coverage report</a>.</p>
|
||||
|
||||
<p>To generate documentation, run:</p>
|
||||
<code>docker-compose run --rm docs</code>
|
||||
<p>View <a href="/docs/"> documentation</a>.</p>
|
||||
</body>
|
||||
</html>
|
||||
HTML;
|
||||
|
||||
return (new Response(200))
|
||||
->withHeader('Content-type', 'text/html')
|
||||
->withBody(new Stream($view));
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
// Create a new Server instance.
|
||||
$server = new Server();
|
||||
// Add a router to the server to map methods and endpoints to handlers.
|
||||
$router = $server->createRouter();
|
||||
// Register the route GET / with an anonymous function that provides a handler.
|
||||
$router->register("GET", "/", function () { return new HomePageHandler(); });
|
||||
// Add the router to the server.
|
||||
$server->add($router);
|
||||
// Read the request from the client, dispatch a handler, and output.
|
||||
$server->respond();
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
<?php
|
||||
|
||||
namespace WellRESTed\Dispatching;
|
||||
|
||||
use InvalidArgumentException;
|
||||
|
||||
class DispatchException extends InvalidArgumentException
|
||||
{
|
||||
}
|
||||
|
|
@ -0,0 +1,103 @@
|
|||
<?php
|
||||
|
||||
namespace WellRESTed\Dispatching;
|
||||
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
|
||||
/**
|
||||
* Dispatches an ordered sequence of middleware.
|
||||
*/
|
||||
class DispatchStack implements DispatchStackInterface
|
||||
{
|
||||
/** @var mixed[] */
|
||||
private $stack;
|
||||
/** @var DispatcherInterface */
|
||||
private $dispatcher;
|
||||
|
||||
public function __construct(DispatcherInterface $dispatcher)
|
||||
{
|
||||
$this->dispatcher = $dispatcher;
|
||||
$this->stack = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Push a new middleware onto the stack.
|
||||
*
|
||||
* @param mixed $middleware Middleware to dispatch in sequence
|
||||
* @return static
|
||||
*/
|
||||
public function add($middleware)
|
||||
{
|
||||
$this->stack[] = $middleware;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatch the contained middleware in the order in which they were added.
|
||||
*
|
||||
* The first middleware that was added is dispatched first.
|
||||
*
|
||||
* Each middleware, when dispatched, receives a $next callable that, when
|
||||
* called, will dispatch the following middleware in the sequence.
|
||||
*
|
||||
* When the stack is dispatched empty, or when all middleware in the stack
|
||||
* call the $next argument they were passed, this method will call the
|
||||
* $next it received.
|
||||
*
|
||||
* When any middleware in the stack returns a response without calling its
|
||||
* $next, the stack will not call the $next it received.
|
||||
*
|
||||
* @param ServerRequestInterface $request
|
||||
* @param ResponseInterface $response
|
||||
* @param callable $next
|
||||
* @return ResponseInterface
|
||||
*/
|
||||
public function __invoke(
|
||||
ServerRequestInterface $request,
|
||||
ResponseInterface $response,
|
||||
$next
|
||||
) {
|
||||
$dispatcher = $this->dispatcher;
|
||||
|
||||
// This flag will be set to true when the last middleware calls $next.
|
||||
$stackCompleted = false;
|
||||
|
||||
// The final middleware's $next returns $response unchanged and sets
|
||||
// the $stackCompleted flag to indicate the stack has completed.
|
||||
$chain = function (
|
||||
ServerRequestInterface $request,
|
||||
ResponseInterface $response
|
||||
) use (&$stackCompleted): ResponseInterface {
|
||||
$stackCompleted = true;
|
||||
return $response;
|
||||
};
|
||||
|
||||
// Create a chain of callables.
|
||||
//
|
||||
// Each callable will take $request and $response parameters, and will
|
||||
// contain a dispatcher, the associated middleware, and a $next function
|
||||
// that serves as the link to the next middleware in the chain.
|
||||
foreach (array_reverse($this->stack) as $middleware) {
|
||||
$chain = function (
|
||||
ServerRequestInterface $request,
|
||||
ResponseInterface $response
|
||||
) use ($dispatcher, $middleware, $chain): ResponseInterface {
|
||||
return $dispatcher->dispatch(
|
||||
$middleware,
|
||||
$request,
|
||||
$response,
|
||||
$chain
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
$response = $chain($request, $response);
|
||||
|
||||
if ($stackCompleted) {
|
||||
return $next($request, $response);
|
||||
} else {
|
||||
return $response;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,56 @@
|
|||
<?php
|
||||
|
||||
namespace WellRESTed\Dispatching;
|
||||
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use WellRESTed\MiddlewareInterface;
|
||||
|
||||
/**
|
||||
* Dispatches an ordered sequence of middleware
|
||||
*/
|
||||
interface DispatchStackInterface extends MiddlewareInterface
|
||||
{
|
||||
/**
|
||||
* Push a new middleware onto the stack.
|
||||
*
|
||||
* This method MUST preserve the order in which middleware are added.
|
||||
*
|
||||
* @param mixed $middleware Middleware to dispatch in sequence
|
||||
* @return static
|
||||
*/
|
||||
public function add($middleware);
|
||||
|
||||
/**
|
||||
* Dispatch the contained middleware in the order in which they were added.
|
||||
*
|
||||
* The first middleware added to the stack MUST be dispatched first.
|
||||
*
|
||||
* Each middleware, when dispatched, MUST receive a $next callable that
|
||||
* dispatches the middleware that follows it, unless it is the last
|
||||
* middleware. The last middleware MUST receive a $next callable that
|
||||
* returns the response unchanged.
|
||||
*
|
||||
* When any middleware returns a response without calling the $next
|
||||
* argument it received, the stack instance MUST stop propagating and MUST
|
||||
* return a response without calling the $next argument passed to __invoke.
|
||||
*
|
||||
* This method MUST call the passed $next argument when:
|
||||
* - The stack is empty (i.e., there is no middleware to dispatch)
|
||||
* - Each middleware called the $next that it received.
|
||||
*
|
||||
* This method MUST NOT call the passed $next argument when the stack is
|
||||
* not empty and any middleware returns a response without calling the
|
||||
* $next it received.
|
||||
*
|
||||
* @param ServerRequestInterface $request
|
||||
* @param ResponseInterface $response
|
||||
* @param callable $next
|
||||
* @return ResponseInterface
|
||||
*/
|
||||
public function __invoke(
|
||||
ServerRequestInterface $request,
|
||||
ResponseInterface $response,
|
||||
$next
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,82 @@
|
|||
<?php
|
||||
|
||||
namespace WellRESTed\Dispatching;
|
||||
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Psr\Http\Server\MiddlewareInterface;
|
||||
use Psr\Http\Server\RequestHandlerInterface;
|
||||
|
||||
/**
|
||||
* Runs a handler or middleware with a request and return the response.
|
||||
*/
|
||||
class Dispatcher implements DispatcherInterface
|
||||
{
|
||||
/**
|
||||
* Run a handler or middleware with a request and return the response.
|
||||
*
|
||||
* Dispatcher can dispatch any of the following:
|
||||
* - An instance implementing one of these interfaces:
|
||||
* - Psr\Http\Server\RequestHandlerInterface
|
||||
* - Psr\Http\Server\MiddlewareInterface
|
||||
* - WellRESTed\MiddlewareInterface
|
||||
* - Psr\Http\Message\ResponseInterface
|
||||
* - A string containing the fully qualified class name of a class
|
||||
* implementing one of the interfaces listed above.
|
||||
* - A callable that returns an instance implementing one of the
|
||||
* interfaces listed above.
|
||||
* - A callable with a signature matching the signature of
|
||||
* WellRESTed\MiddlewareInterface::__invoke
|
||||
* - An array containing any of the items in this list.
|
||||
*
|
||||
* When Dispatcher receives a $dispatchable that is not of a type it
|
||||
* can dispatch, it throws a DispatchException.
|
||||
*
|
||||
* @param mixed $dispatchable
|
||||
* @param ServerRequestInterface $request
|
||||
* @param ResponseInterface $response
|
||||
* @param callable $next
|
||||
* @return ResponseInterface
|
||||
* @throws DispatchException Unable to dispatch $middleware
|
||||
*/
|
||||
public function dispatch(
|
||||
$dispatchable,
|
||||
ServerRequestInterface $request,
|
||||
ResponseInterface $response,
|
||||
$next
|
||||
) {
|
||||
if (is_callable($dispatchable)) {
|
||||
$dispatchable = $dispatchable($request, $response, $next);
|
||||
} elseif (is_string($dispatchable)) {
|
||||
$dispatchable = new $dispatchable();
|
||||
} elseif (is_array($dispatchable)) {
|
||||
$dispatchable = $this->getDispatchStack($dispatchable);
|
||||
}
|
||||
|
||||
if (is_callable($dispatchable)) {
|
||||
return $dispatchable($request, $response, $next);
|
||||
} elseif ($dispatchable instanceof RequestHandlerInterface) {
|
||||
return $dispatchable->handle($request);
|
||||
} elseif ($dispatchable instanceof MiddlewareInterface) {
|
||||
$delegate = new DispatcherDelegate($response, $next);
|
||||
return $dispatchable->process($request, $delegate);
|
||||
} elseif ($dispatchable instanceof ResponseInterface) {
|
||||
return $dispatchable;
|
||||
} else {
|
||||
throw new DispatchException('Unable to dispatch handler.');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed[] $dispatchables
|
||||
* @return DispatchStack
|
||||
*/
|
||||
private function getDispatchStack($dispatchables)
|
||||
{
|
||||
$stack = new DispatchStack($this);
|
||||
foreach ($dispatchables as $dispatchable) {
|
||||
$stack->add($dispatchable);
|
||||
}
|
||||
return $stack;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
<?php
|
||||
|
||||
namespace WellRESTed\Dispatching;
|
||||
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Psr\Http\Server\RequestHandlerInterface;
|
||||
|
||||
/**
|
||||
* Adapter to allow use of PSR-15 Middleware with double pass implementations.
|
||||
*/
|
||||
class DispatcherDelegate implements RequestHandlerInterface
|
||||
{
|
||||
/** @var ResponseInterface */
|
||||
private $response;
|
||||
/** @var callable */
|
||||
private $next;
|
||||
|
||||
public function __construct(ResponseInterface $response, callable $next)
|
||||
{
|
||||
$this->response = $response;
|
||||
$this->next = $next;
|
||||
}
|
||||
|
||||
public function handle(ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
return call_user_func($this->next, $request, $this->response);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
<?php
|
||||
|
||||
namespace WellRESTed\Dispatching;
|
||||
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
|
||||
/**
|
||||
* Runs a handler or middleware with a request and return the response.
|
||||
*/
|
||||
interface DispatcherInterface
|
||||
{
|
||||
/**
|
||||
* Run a handler or middleware with a request and return the response.
|
||||
*
|
||||
* Dispatchables (middleware and handlers) comes in a number of varieties
|
||||
* (e.g., instance, string, callable). DispatcherInterface interface unpacks
|
||||
* the dispatchable and dispatches it.
|
||||
*
|
||||
* Implementations MUST be able to dispatch the following:
|
||||
* - An instance implementing one of these interfaces:
|
||||
* - Psr\Http\Server\RequestHandlerInterface
|
||||
* - Psr\Http\Server\MiddlewareInterface
|
||||
* - WellRESTed\MiddlewareInterface
|
||||
* - Psr\Http\Message\ResponseInterface
|
||||
* - A string containing the fully qualified class name of a class
|
||||
* implementing one of the interfaces listed above.
|
||||
* - A callable that returns an instance implementing one of the
|
||||
* interfaces listed above.
|
||||
* - A callable with a signature matching the signature of
|
||||
* WellRESTed\MiddlewareInterface::__invoke
|
||||
* - An array containing any of the items in this list.
|
||||
*
|
||||
* Implementation MAY dispatch other types of middleware.
|
||||
*
|
||||
* When an implementation receives a $dispatchable that is not of a type it
|
||||
* can dispatch, it MUST throw a DispatchException.
|
||||
*
|
||||
* @param mixed $dispatchable
|
||||
* @param ServerRequestInterface $request
|
||||
* @param ResponseInterface $response
|
||||
* @param callable $next
|
||||
* @return ResponseInterface
|
||||
* @throws DispatchException Unable to dispatch $middleware
|
||||
*/
|
||||
public function dispatch(
|
||||
$dispatchable,
|
||||
ServerRequestInterface $request,
|
||||
ResponseInterface $response,
|
||||
$next
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,137 @@
|
|||
<?php
|
||||
|
||||
namespace WellRESTed\Message;
|
||||
|
||||
use ArrayAccess;
|
||||
use Iterator;
|
||||
|
||||
/**
|
||||
* HeaderCollection provides case-insensitive access to lists of header values.
|
||||
*
|
||||
* HeaderCollection preserves the cases of keys as they are set, but treats key
|
||||
* access case insensitively.
|
||||
*
|
||||
* Any values added to HeaderCollection are added to list arrays. Subsequent
|
||||
* calls to add a value for a given key will append the new value to the list
|
||||
* array of values for that key.
|
||||
*
|
||||
* @internal This class is an internal class used by Message and is not intended
|
||||
* for direct use by consumers.
|
||||
*/
|
||||
class HeaderCollection implements ArrayAccess, Iterator
|
||||
{
|
||||
/**
|
||||
* Hash array mapping lowercase header names to original case header names.
|
||||
*
|
||||
* @var array<string, string>
|
||||
*/
|
||||
private $fields = [];
|
||||
|
||||
/**
|
||||
* Hash array mapping lowercase header names to values as string[]
|
||||
*
|
||||
* @var array<string, string[]>
|
||||
*/
|
||||
private $values = [];
|
||||
|
||||
/**
|
||||
* List array of lowercase header names.
|
||||
*
|
||||
* @var string[]
|
||||
*/
|
||||
private $keys = [];
|
||||
|
||||
/** @var int */
|
||||
private $position = 0;
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// ArrayAccess
|
||||
|
||||
/**
|
||||
* @param string $offset
|
||||
* @return bool
|
||||
*/
|
||||
public function offsetExists($offset): bool
|
||||
{
|
||||
return isset($this->values[strtolower($offset)]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $offset
|
||||
* @return string[]
|
||||
*/
|
||||
public function offsetGet($offset): array
|
||||
{
|
||||
return $this->values[strtolower($offset)];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $offset
|
||||
* @param string $value
|
||||
*/
|
||||
public function offsetSet($offset, $value): void
|
||||
{
|
||||
$normalized = strtolower($offset);
|
||||
|
||||
// Add the normalized key to the list of keys, if not already set.
|
||||
if (!in_array($normalized, $this->keys)) {
|
||||
$this->keys[] = $normalized;
|
||||
}
|
||||
|
||||
// Add or update the preserved case key.
|
||||
$this->fields[$normalized] = $offset;
|
||||
|
||||
// Store the value.
|
||||
if (isset($this->values[$normalized])) {
|
||||
$this->values[$normalized][] = $value;
|
||||
} else {
|
||||
$this->values[$normalized] = [$value];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $offset
|
||||
*/
|
||||
public function offsetUnset($offset): void
|
||||
{
|
||||
$normalized = strtolower($offset);
|
||||
unset($this->fields[$normalized]);
|
||||
unset($this->values[$normalized]);
|
||||
// Remove and normalize the list of keys.
|
||||
if (($key = array_search($normalized, $this->keys)) !== false) {
|
||||
unset($this->keys[$key]);
|
||||
$this->keys = array_values($this->keys);
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Iterator
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
public function current(): array
|
||||
{
|
||||
return $this->values[$this->keys[$this->position]];
|
||||
}
|
||||
|
||||
public function next(): void
|
||||
{
|
||||
++$this->position;
|
||||
}
|
||||
|
||||
public function key(): string
|
||||
{
|
||||
return $this->fields[$this->keys[$this->position]];
|
||||
}
|
||||
|
||||
public function valid(): bool
|
||||
{
|
||||
return isset($this->keys[$this->position]);
|
||||
}
|
||||
|
||||
public function rewind(): void
|
||||
{
|
||||
$this->position = 0;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,297 @@
|
|||
<?php
|
||||
|
||||
namespace WellRESTed\Message;
|
||||
|
||||
use InvalidArgumentException;
|
||||
use Psr\Http\Message\MessageInterface;
|
||||
use Psr\Http\Message\StreamInterface;
|
||||
|
||||
/**
|
||||
* Message defines core functionality for classes that represent HTTP messages.
|
||||
*/
|
||||
abstract class Message implements MessageInterface
|
||||
{
|
||||
/** @var HeaderCollection */
|
||||
protected $headers;
|
||||
/** @var StreamInterface */
|
||||
protected $body;
|
||||
/** @var string */
|
||||
protected $protocolVersion = '1.1';
|
||||
|
||||
/**
|
||||
* Create a new Message, optionally with headers and a body.
|
||||
*
|
||||
* $headers is an optional associative array with header field names as
|
||||
* string keys and values as either string or string[].
|
||||
*
|
||||
* If no StreamInterface is provided for $body, the instance will create
|
||||
* a NullStream instance for the message body.
|
||||
*
|
||||
* @param array $headers Associative array with header field names as
|
||||
* keys and values as string|string[]
|
||||
* @param StreamInterface|null $body A stream representation of the message
|
||||
* entity body
|
||||
*/
|
||||
public function __construct(
|
||||
array $headers = [],
|
||||
?StreamInterface $body = null
|
||||
) {
|
||||
$this->headers = new HeaderCollection();
|
||||
|
||||
foreach ($headers as $name => $values) {
|
||||
if (is_string($values)) {
|
||||
$values = [$values];
|
||||
}
|
||||
foreach ($values as $value) {
|
||||
$this->headers[$name] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
$this->body = $body ?? new Stream('');
|
||||
}
|
||||
|
||||
public function __clone()
|
||||
{
|
||||
$this->headers = clone $this->headers;
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Psr\Http\Message\MessageInterface
|
||||
|
||||
/**
|
||||
* Retrieves the HTTP protocol version as a string.
|
||||
*
|
||||
* @return string HTTP protocol version.
|
||||
*/
|
||||
public function getProtocolVersion()
|
||||
{
|
||||
return $this->protocolVersion;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new instance with the specified HTTP protocol version.
|
||||
*
|
||||
* @param string $version HTTP protocol version
|
||||
* @return static
|
||||
*/
|
||||
public function withProtocolVersion($version)
|
||||
{
|
||||
$message = clone $this;
|
||||
$message->protocolVersion = $version;
|
||||
return $message;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve all message headers.
|
||||
*
|
||||
* The keys represent the header name as it will be sent over the wire, and
|
||||
* each value is an array of strings associated with the header.
|
||||
*
|
||||
* // Represent the headers as a string
|
||||
* foreach ($message->getHeaders() as $name => $values) {
|
||||
* echo $name . ': ' . implode(', ', $values);
|
||||
* }
|
||||
*
|
||||
* // Emit headers iteratively:
|
||||
* foreach ($message->getHeaders() as $name => $values) {
|
||||
* foreach ($values as $value) {
|
||||
* header(sprintf('%s: %s', $name, $value), false);
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* While header names are not case-sensitive, getHeaders() will preserve the
|
||||
* exact case in which headers were originally specified.
|
||||
*
|
||||
* @return string[][] Returns an associative array of the message's headers.
|
||||
*/
|
||||
public function getHeaders()
|
||||
{
|
||||
$headers = [];
|
||||
foreach ($this->headers as $key => $value) {
|
||||
$headers[$key] = $value;
|
||||
}
|
||||
return $headers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a header exists by the given case-insensitive name.
|
||||
*
|
||||
* @param string $name Case-insensitive header field name.
|
||||
* @return bool Returns true if any header names match the given header
|
||||
* name using a case-insensitive string comparison. Returns false if
|
||||
* no matching header name is found in the message.
|
||||
*/
|
||||
public function hasHeader($name)
|
||||
{
|
||||
return isset($this->headers[$name]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a message header value by the given case-insensitive name.
|
||||
*
|
||||
* This method returns an array of all the header values of the given
|
||||
* case-insensitive header name.
|
||||
*
|
||||
* If the header does not appear in the message, this method returns an
|
||||
* empty array.
|
||||
*
|
||||
* @param string $name Case-insensitive header field name.
|
||||
* @return string[] An array of string values as provided for the given
|
||||
* header. If the header does not appear in the message, this method
|
||||
* returns an empty array.
|
||||
*/
|
||||
public function getHeader($name)
|
||||
{
|
||||
if (isset($this->headers[$name])) {
|
||||
return $this->headers[$name];
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the line for a single header, with the header values as a
|
||||
* comma-separated string.
|
||||
*
|
||||
* This method returns all of the header values of the given
|
||||
* case-insensitive header name as a string concatenated together using
|
||||
* a comma.
|
||||
*
|
||||
* NOTE: Not all header values may be appropriately represented using
|
||||
* comma concatenation. For such headers, use getHeader() instead
|
||||
* and supply your own delimiter when concatenating.
|
||||
*
|
||||
* If the header does not appear in the message, this method returns an
|
||||
* empty string.
|
||||
*
|
||||
* @param string $name Case-insensitive header field name.
|
||||
* @return string A string of values as provided for the given header
|
||||
* concatenated together using a comma. If the header does not appear in
|
||||
* the message, this method returns an empty string.
|
||||
*/
|
||||
public function getHeaderLine($name)
|
||||
{
|
||||
if (isset($this->headers[$name])) {
|
||||
return join(', ', $this->headers[$name]);
|
||||
} else {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new instance with the provided header, replacing any existing
|
||||
* values of any headers with the same case-insensitive name.
|
||||
*
|
||||
* While header names are case-insensitive, the casing of the header will
|
||||
* be preserved by this function, and returned from getHeaders().
|
||||
*
|
||||
* @param string $name Case-insensitive header field name.
|
||||
* @param string|string[] $value Header value(s).
|
||||
* @return static
|
||||
* @throws InvalidArgumentException for invalid header names or values.
|
||||
*/
|
||||
public function withHeader($name, $value)
|
||||
{
|
||||
$values = $this->getValidatedHeaders($name, $value);
|
||||
$message = clone $this;
|
||||
unset($message->headers[$name]);
|
||||
foreach ($values as $value) {
|
||||
$message->headers[$name] = (string) $value;
|
||||
}
|
||||
return $message;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new instance, with the specified header appended with the
|
||||
* given value.
|
||||
*
|
||||
* Existing values for the specified header will be maintained. The new
|
||||
* value(s) will be appended to the existing list. If the header did not
|
||||
* exist previously, it will be added.
|
||||
*
|
||||
* @param string $name Case-insensitive header field name to add.
|
||||
* @param string|string[] $value Header value(s).
|
||||
* @return static
|
||||
* @throws InvalidArgumentException for invalid header names or values.
|
||||
*/
|
||||
public function withAddedHeader($name, $value)
|
||||
{
|
||||
$values = $this->getValidatedHeaders($name, $value);
|
||||
|
||||
$message = clone $this;
|
||||
foreach ($values as $value) {
|
||||
$message->headers[$name] = (string) $value;
|
||||
}
|
||||
return $message;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new instance, without the specified header.
|
||||
*
|
||||
* @param string $name Case-insensitive header field name to remove.
|
||||
* @return static
|
||||
*/
|
||||
public function withoutHeader($name)
|
||||
{
|
||||
$message = clone $this;
|
||||
unset($message->headers[$name]);
|
||||
return $message;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the body of the message.
|
||||
*
|
||||
* @return StreamInterface Returns the body as a stream.
|
||||
*/
|
||||
public function getBody()
|
||||
{
|
||||
return $this->body;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new instance, with the specified message body.
|
||||
*
|
||||
* The body MUST be a StreamInterface object.
|
||||
*
|
||||
* @param StreamInterface $body Body.
|
||||
* @return static
|
||||
* @throws InvalidArgumentException When the body is not valid.
|
||||
*/
|
||||
public function withBody(StreamInterface $body)
|
||||
{
|
||||
$message = clone $this;
|
||||
$message->body = $body;
|
||||
return $message;
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @param mixed $name
|
||||
* @param mixed|mixed[] $values
|
||||
* @return string[]
|
||||
* @throws InvalidArgumentException Name is not a string or value is not
|
||||
* a string or array of strings
|
||||
*/
|
||||
private function getValidatedHeaders($name, $values)
|
||||
{
|
||||
if (!is_string($name)) {
|
||||
throw new InvalidArgumentException('Header name must be a string');
|
||||
}
|
||||
|
||||
if (!is_array($values)) {
|
||||
$values = [$values];
|
||||
}
|
||||
|
||||
$isNotStringOrNumber = function ($item): bool {
|
||||
return !(is_string($item) || is_numeric($item));
|
||||
};
|
||||
|
||||
$invalid = array_filter($values, $isNotStringOrNumber);
|
||||
if ($invalid) {
|
||||
throw new InvalidArgumentException('Header values must be a string or string[]');
|
||||
}
|
||||
|
||||
return array_map('strval', $values);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,188 @@
|
|||
<?php
|
||||
|
||||
namespace WellRESTed\Message;
|
||||
|
||||
use Psr\Http\Message\StreamInterface;
|
||||
use RuntimeException;
|
||||
|
||||
/**
|
||||
* NullStream is a minimal, always-empty, non-writable stream.
|
||||
*
|
||||
* Use this for messages with no body.
|
||||
*/
|
||||
class NullStream implements StreamInterface
|
||||
{
|
||||
/**
|
||||
* Returns an empty string
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function __toString()
|
||||
{
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* No-op
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function close()
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* No-op
|
||||
*
|
||||
* @return resource|null Underlying PHP stream, if any
|
||||
*/
|
||||
public function detach()
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns 0
|
||||
*
|
||||
* @return int|null Returns the size in bytes if known, or null if unknown.
|
||||
*/
|
||||
public function getSize()
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current position of the file read/write pointer
|
||||
*
|
||||
* @return int Position of the file pointer
|
||||
* @throws RuntimeException on error.
|
||||
*/
|
||||
public function tell()
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function eof()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns false
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isSeekable()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Always throws exception
|
||||
*
|
||||
* @link http://www.php.net/manual/en/function.fseek.php
|
||||
* @param int $offset Stream offset
|
||||
* @param int $whence Specifies how the cursor position will be calculated
|
||||
* based on the seek offset. Valid values are identical to the built-in
|
||||
* PHP $whence values for `fseek()`. SEEK_SET: Set position equal to
|
||||
* offset bytes SEEK_CUR: Set position to current location plus offset
|
||||
* SEEK_END: Set position to end-of-stream plus offset.
|
||||
* @return void
|
||||
* @throws RuntimeException on failure.
|
||||
*/
|
||||
public function seek($offset, $whence = SEEK_SET)
|
||||
{
|
||||
throw new RuntimeException('Unable to seek to position.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Always throws exception
|
||||
*
|
||||
* @see seek()
|
||||
* @link http://www.php.net/manual/en/function.fseek.php
|
||||
* @return void
|
||||
* @throws RuntimeException on failure.
|
||||
*/
|
||||
public function rewind()
|
||||
{
|
||||
throw new RuntimeException('Unable to rewind stream.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns false.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isWritable()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Always throws exception
|
||||
*
|
||||
* @param string $string The string that is to be written.
|
||||
* @return int Returns the number of bytes written to the stream.
|
||||
* @throws RuntimeException on failure.
|
||||
*/
|
||||
public function write($string)
|
||||
{
|
||||
throw new RuntimeException('Unable to write to stream.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isReadable()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an empty string
|
||||
*
|
||||
* @param int $length Read up to $length bytes from the object and return
|
||||
* them. Fewer than $length bytes may be returned if underlying stream
|
||||
* call returns fewer bytes.
|
||||
* @return string Returns the data read from the stream, or an empty string
|
||||
* if no bytes are available.
|
||||
* @throws RuntimeException if an error occurs.
|
||||
*/
|
||||
public function read($length)
|
||||
{
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the remaining contents in a string
|
||||
*
|
||||
* @return string
|
||||
* @throws RuntimeException if unable to read or an error occurs while
|
||||
* reading.
|
||||
*/
|
||||
public function getContents()
|
||||
{
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns null
|
||||
*
|
||||
* @link http://php.net/manual/en/function.stream-get-meta-data.php
|
||||
* @param string $key Specific metadata to retrieve.
|
||||
* @return array|mixed|null Returns an associative array if no key is
|
||||
* provided. Returns a specific key value if a key is provided and the
|
||||
* value is found, or null if the key is not found.
|
||||
*/
|
||||
public function getMetadata($key = null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,235 @@
|
|||
<?php
|
||||
|
||||
namespace WellRESTed\Message;
|
||||
|
||||
use InvalidArgumentException;
|
||||
use Psr\Http\Message\RequestInterface;
|
||||
use Psr\Http\Message\StreamInterface;
|
||||
use Psr\Http\Message\UriInterface;
|
||||
|
||||
/**
|
||||
* Representation of an outgoing, client-side request.
|
||||
*
|
||||
* Per the HTTP specification, this interface includes properties for
|
||||
* each of the following:
|
||||
*
|
||||
* - Protocol version
|
||||
* - HTTP method
|
||||
* - URI
|
||||
* - Headers
|
||||
* - Message body
|
||||
*/
|
||||
class Request extends Message implements RequestInterface
|
||||
{
|
||||
/** @var string */
|
||||
protected $method;
|
||||
/** @var string|null */
|
||||
protected $requestTarget;
|
||||
/** @var UriInterface */
|
||||
protected $uri;
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Create a new Request.
|
||||
*
|
||||
* $headers is an optional associative array with header field names as
|
||||
* string keys and values as either string or string[].
|
||||
*
|
||||
* If no StreamInterface is provided for $body, the instance will create
|
||||
* a NullStream instance for the message body.
|
||||
*
|
||||
* @param string $method
|
||||
* @param string|UriInterface $uri
|
||||
* @param array $headers Associative array with header field names as
|
||||
* keys and values as string|string[]
|
||||
* @param StreamInterface|null $body A stream representation of the message
|
||||
* entity body
|
||||
*/
|
||||
public function __construct(
|
||||
string $method = 'GET',
|
||||
$uri = '',
|
||||
array $headers = [],
|
||||
?StreamInterface $body = null
|
||||
) {
|
||||
parent::__construct($headers, $body);
|
||||
$this->method = $method;
|
||||
if (!($uri instanceof UriInterface)) {
|
||||
$uri = new Uri($uri);
|
||||
}
|
||||
$this->uri = $uri;
|
||||
$this->requestTarget = null;
|
||||
}
|
||||
|
||||
public function __clone()
|
||||
{
|
||||
$this->uri = clone $this->uri;
|
||||
parent::__clone();
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Psr\Http\Message\RequestInterface
|
||||
|
||||
/**
|
||||
* Retrieves the message's request target.
|
||||
*
|
||||
* Retrieves the message's request-target either as it will appear (for
|
||||
* clients), as it appeared at request (for servers), or as it was
|
||||
* specified for the instance (see withRequestTarget()).
|
||||
*
|
||||
* In most cases, this will be the origin-form of the composed URI,
|
||||
* unless a value was provided to the concrete implementation (see
|
||||
* withRequestTarget() below).
|
||||
*
|
||||
* If no URI is available, and no request-target has been specifically
|
||||
* provided, this method will return the string "/".
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getRequestTarget()
|
||||
{
|
||||
// Use the explicitly set request target first.
|
||||
if ($this->requestTarget !== null) {
|
||||
return $this->requestTarget;
|
||||
}
|
||||
|
||||
// Build the origin form from the composed URI.
|
||||
$target = $this->uri->getPath();
|
||||
$query = $this->uri->getQuery();
|
||||
if ($query) {
|
||||
$target .= '?' . $query;
|
||||
}
|
||||
|
||||
// Return "/" if the origin form is empty.
|
||||
return $target ?: '/';
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new instance with a specific request-target.
|
||||
*
|
||||
* If the request needs a non-origin-form request-target — e.g., for
|
||||
* specifying an absolute-form, authority-form, or asterisk-form —
|
||||
* this method may be used to create an instance with the specified
|
||||
* request-target, verbatim.
|
||||
*
|
||||
* @link http://tools.ietf.org/html/rfc7230#section-2.7 (for the various
|
||||
* request-target forms allowed in request messages)
|
||||
* @param mixed $requestTarget
|
||||
* @return static
|
||||
*/
|
||||
public function withRequestTarget($requestTarget)
|
||||
{
|
||||
$request = clone $this;
|
||||
$request->requestTarget = $requestTarget;
|
||||
return $request;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the HTTP method of the request.
|
||||
*
|
||||
* @return string Returns the request method.
|
||||
*/
|
||||
public function getMethod()
|
||||
{
|
||||
return $this->method;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new instance with the provided HTTP method.
|
||||
*
|
||||
* While HTTP method names are typically all uppercase characters, HTTP
|
||||
* method names are case-sensitive. Therefore, this method will not
|
||||
* modify the given string.
|
||||
*
|
||||
* @param string $method Case-insensitive method.
|
||||
* @return static
|
||||
* @throws InvalidArgumentException for invalid HTTP methods.
|
||||
*/
|
||||
public function withMethod($method)
|
||||
{
|
||||
$request = clone $this;
|
||||
$request->method = $this->getValidatedMethod($method);
|
||||
return $request;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the URI instance.
|
||||
*
|
||||
* @link http://tools.ietf.org/html/rfc3986#section-4.3
|
||||
* @return UriInterface Returns a UriInterface instance
|
||||
* representing the URI of the request.
|
||||
*/
|
||||
public function getUri()
|
||||
{
|
||||
return $this->uri;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an instance with the provided URI.
|
||||
*
|
||||
* This method updates the Host header of the returned request by
|
||||
* default if the URI contains a host component. If the URI does not
|
||||
* contain a host component, any pre-existing Host header will be carried
|
||||
* over to the returned request.
|
||||
*
|
||||
* You can opt-in to preserving the original state of the Host header by
|
||||
* setting `$preserveHost` to `true`. When `$preserveHost` is set to
|
||||
* `true`, this method interacts with the Host header in the following ways:
|
||||
*
|
||||
* - If the the Host header is missing or empty, and the new URI contains
|
||||
* a host component, this method will update the Host header in the returned
|
||||
* request.
|
||||
* - If the Host header is missing or empty, and the new URI does not contain a
|
||||
* host component, this method will not update the Host header in the returned
|
||||
* request.
|
||||
* - If a Host header is present and non-empty, this method will not update
|
||||
* the Host header in the returned request.
|
||||
*
|
||||
* @link http://tools.ietf.org/html/rfc3986#section-4.3
|
||||
* @param UriInterface $uri New request URI to use.
|
||||
* @param bool $preserveHost Preserve the original state of the Host header.
|
||||
* @return static
|
||||
*/
|
||||
public function withUri(UriInterface $uri, $preserveHost = false)
|
||||
{
|
||||
$request = clone $this;
|
||||
|
||||
$newHost = $uri->getHost();
|
||||
$oldHost = $request->headers['Host'] ?? '';
|
||||
|
||||
if ($preserveHost === false) {
|
||||
// Update Host
|
||||
if ($newHost && $newHost !== $oldHost) {
|
||||
unset($request->headers['Host']);
|
||||
$request->headers['Host'] = $newHost;
|
||||
}
|
||||
} else {
|
||||
// Preserve Host
|
||||
if (!$oldHost && $newHost) {
|
||||
$request->headers['Host'] = $newHost;
|
||||
}
|
||||
}
|
||||
|
||||
$request->uri = $uri;
|
||||
return $request;
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @param mixed $method
|
||||
* @return string
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
private function getValidatedMethod($method)
|
||||
{
|
||||
if (!is_string($method)) {
|
||||
throw new InvalidArgumentException('Method must be a string.');
|
||||
}
|
||||
$method = trim($method);
|
||||
if (strpos($method, ' ') !== false) {
|
||||
throw new InvalidArgumentException('Method cannot contain spaces.');
|
||||
}
|
||||
return $method;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
<?php
|
||||
|
||||
namespace WellRESTed\Message;
|
||||
|
||||
use Psr\Http\Message\RequestFactoryInterface;
|
||||
use Psr\Http\Message\RequestInterface;
|
||||
use Psr\Http\Message\UriInterface;
|
||||
|
||||
class RequestFactory implements RequestFactoryInterface
|
||||
{
|
||||
/**
|
||||
* Create a new request.
|
||||
*
|
||||
* @param string $method The HTTP method associated with the request.
|
||||
* @param UriInterface|string $uri The URI associated with the request. If
|
||||
* the value is a string, the factory MUST create a UriInterface
|
||||
* instance based on it.
|
||||
*
|
||||
* @return RequestInterface
|
||||
*/
|
||||
public function createRequest(string $method, $uri): RequestInterface
|
||||
{
|
||||
return new Request($method, $uri);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,177 @@
|
|||
<?php
|
||||
|
||||
namespace WellRESTed\Message;
|
||||
|
||||
use InvalidArgumentException;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\StreamInterface;
|
||||
|
||||
/**
|
||||
* Representation of an outgoing, server-side response.
|
||||
*
|
||||
* Per the HTTP specification, this interface includes properties for
|
||||
* each of the following:
|
||||
*
|
||||
* - Protocol version
|
||||
* - Status code and reason phrase
|
||||
* - Headers
|
||||
* - Message body
|
||||
*/
|
||||
class Response extends Message implements ResponseInterface
|
||||
{
|
||||
/** @var string Text explanation of the HTTP Status Code. */
|
||||
private $reasonPhrase;
|
||||
/** @var int HTTP status code */
|
||||
private $statusCode;
|
||||
|
||||
/**
|
||||
* Create a new Response, optionally with status code, headers, and a body.
|
||||
*
|
||||
* If provided, $headers MUST by an associative array with header field
|
||||
* names as (string) keys and lists of header field values (string[])
|
||||
* as values.
|
||||
*
|
||||
* If no StreamInterface is provided for $body, the instance will create
|
||||
* a NullStream instance for the message body.
|
||||
*
|
||||
* @see \WellRESTed\Message\Message
|
||||
*
|
||||
* @param int $statusCode
|
||||
* @param array $headers
|
||||
* @param StreamInterface|null $body
|
||||
*/
|
||||
public function __construct(
|
||||
int $statusCode = 500,
|
||||
array $headers = [],
|
||||
?StreamInterface $body = null
|
||||
) {
|
||||
parent::__construct($headers, $body);
|
||||
$this->statusCode = $statusCode;
|
||||
$this->reasonPhrase = $this->getDefaultReasonPhraseForStatusCode($statusCode);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Psr\Http\Message\ResponseInterface
|
||||
|
||||
/**
|
||||
* Gets the response status code.
|
||||
*
|
||||
* The status code is a 3-digit integer result code of the server's attempt
|
||||
* to understand and satisfy the request.
|
||||
*
|
||||
* @return int Status code.
|
||||
*/
|
||||
public function getStatusCode()
|
||||
{
|
||||
return $this->statusCode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new instance with the specified status code, and optionally
|
||||
* reason phrase, for the response.
|
||||
*
|
||||
* If no reason phrase is specified, this method will provide a standard
|
||||
* reason phrase, if possible.
|
||||
*
|
||||
* @link http://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml
|
||||
* @param int $code The 3-digit integer result code to set.
|
||||
* @param string $reasonPhrase The reason phrase to use with the
|
||||
* provided status code; if none is provided, implementations MAY
|
||||
* use the defaults as suggested in the HTTP specification.
|
||||
* @return static
|
||||
* @throws InvalidArgumentException For invalid status code arguments.
|
||||
*/
|
||||
public function withStatus($code, $reasonPhrase = '')
|
||||
{
|
||||
$response = clone $this;
|
||||
$response->statusCode = $code;
|
||||
if (!$reasonPhrase) {
|
||||
$reasonPhrase = $this->getDefaultReasonPhraseForStatusCode($code);
|
||||
}
|
||||
$response->reasonPhrase = $reasonPhrase;
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the response reason phrase, a short textual description of the status code.
|
||||
*
|
||||
* The reason phrase is not required and may be an empty string.
|
||||
*
|
||||
* @link http://tools.ietf.org/html/rfc7231#section-6
|
||||
* @link http://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml
|
||||
* @return string Reason phrase, or an empty string if unknown.
|
||||
*/
|
||||
public function getReasonPhrase()
|
||||
{
|
||||
return $this->reasonPhrase;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $statusCode
|
||||
* @return string
|
||||
*/
|
||||
private function getDefaultReasonPhraseForStatusCode($statusCode)
|
||||
{
|
||||
$reasonPhraseLookup = [
|
||||
100 => 'Continue',
|
||||
101 => 'Switching Protocols',
|
||||
102 => 'Processing',
|
||||
200 => 'OK',
|
||||
201 => 'Created',
|
||||
202 => 'Accepted',
|
||||
203 => 'Non-Authoritative Information',
|
||||
204 => 'No Content',
|
||||
205 => 'Reset Content',
|
||||
206 => 'Partial Content',
|
||||
207 => 'Multi-Status',
|
||||
208 => 'Already Reported',
|
||||
226 => 'IM Used',
|
||||
300 => 'Multiple Choices',
|
||||
301 => 'Moved Permanently',
|
||||
302 => 'Found',
|
||||
303 => 'See Other',
|
||||
304 => 'Not Modified',
|
||||
305 => 'Use Proxy',
|
||||
307 => 'Temporary Redirect',
|
||||
308 => 'Permanent Redirect',
|
||||
400 => 'Bad Request',
|
||||
401 => 'Unauthorized',
|
||||
402 => 'Payment Required',
|
||||
403 => 'Forbidden',
|
||||
404 => 'Not Found',
|
||||
405 => 'Method Not Allowed',
|
||||
406 => 'Not Acceptable',
|
||||
407 => 'Proxy Authentication Required',
|
||||
408 => 'Request Timeout',
|
||||
409 => 'Conflict',
|
||||
410 => 'Gone',
|
||||
411 => 'Length Required',
|
||||
412 => 'Precondition Failed',
|
||||
413 => 'Payload Too Large',
|
||||
414 => 'URI Too Long',
|
||||
415 => 'Unsupported Media Type',
|
||||
416 => 'Range Not Satisfiable',
|
||||
417 => 'Expectation Failed',
|
||||
421 => 'Misdirected Request',
|
||||
422 => 'Unprocessable Entity',
|
||||
423 => 'Locked',
|
||||
424 => 'Failed Dependency',
|
||||
426 => 'Upgrade Required',
|
||||
428 => 'Precondition Required',
|
||||
429 => 'Too Many Requests',
|
||||
431 => 'Request Header Fields Too Large',
|
||||
500 => 'Internal Server Error',
|
||||
501 => 'Not Implemented',
|
||||
502 => 'Bad Gateway',
|
||||
503 => 'Service Unavailable',
|
||||
504 => 'Gateway Timeout',
|
||||
505 => 'HTTP Version Not Supported',
|
||||
506 => 'Variant Also Negotiates',
|
||||
507 => 'Insufficient Storage',
|
||||
508 => 'Loop Detected',
|
||||
510 => 'Not Extended',
|
||||
511 => 'Network Authentication Required'
|
||||
];
|
||||
return $reasonPhraseLookup[$statusCode] ?? '';
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
<?php
|
||||
|
||||
namespace WellRESTed\Message;
|
||||
|
||||
use Psr\Http\Message\ResponseFactoryInterface;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
|
||||
class ResponseFactory implements ResponseFactoryInterface
|
||||
{
|
||||
/**
|
||||
* Create a new response.
|
||||
*
|
||||
* @param int $code HTTP status code; defaults to 200
|
||||
* @param string $reasonPhrase Reason phrase to associate with status code
|
||||
* in generated response; if none is provided implementations MAY use
|
||||
* the defaults as suggested in the HTTP specification.
|
||||
*
|
||||
* @return ResponseInterface
|
||||
*/
|
||||
public function createResponse(
|
||||
int $code = 200,
|
||||
string $reasonPhrase = ''
|
||||
): ResponseInterface {
|
||||
return (new Response())
|
||||
->withStatus($code, $reasonPhrase);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,400 @@
|
|||
<?php
|
||||
|
||||
namespace WellRESTed\Message;
|
||||
|
||||
use InvalidArgumentException;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Psr\Http\Message\StreamInterface;
|
||||
use Psr\Http\Message\UploadedFileInterface;
|
||||
use Psr\Http\Message\UriInterface;
|
||||
|
||||
/**
|
||||
* Representation of an incoming, server-side HTTP request.
|
||||
*
|
||||
* Per the HTTP specification, this interface includes properties for
|
||||
* each of the following:
|
||||
*
|
||||
* - Protocol version
|
||||
* - HTTP method
|
||||
* - URI
|
||||
* - Headers
|
||||
* - Message body
|
||||
*
|
||||
* Additionally, it encapsulates all data as it has arrived to the
|
||||
* application from the CGI and/or PHP environment, including:
|
||||
*
|
||||
* - The values represented in $_SERVER.
|
||||
* - Any cookies provided (generally via $_COOKIE)
|
||||
* - Query string arguments (generally via $_GET, or as parsed via parse_str())
|
||||
* - Upload files, if any (as represented by $_FILES)
|
||||
* - Deserialized body parameters (generally from $_POST)
|
||||
*
|
||||
* $_SERVER values MUST be treated as immutable, as they represent application
|
||||
* state at the time of request; as such, no methods are provided to allow
|
||||
* modification of those values. The other values provide such methods, as they
|
||||
* can be restored from $_SERVER or the request body, and may need treatment
|
||||
* during the application (e.g., body parameters may be deserialized based on
|
||||
* content type).
|
||||
*/
|
||||
class ServerRequest extends Request implements ServerRequestInterface
|
||||
{
|
||||
/** @var array */
|
||||
private $attributes;
|
||||
/** @var array */
|
||||
private $cookieParams;
|
||||
/** @var mixed */
|
||||
private $parsedBody;
|
||||
/** @var array */
|
||||
private $queryParams;
|
||||
/** @var array */
|
||||
private $serverParams;
|
||||
/** @var array */
|
||||
private $uploadedFiles;
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Create a new ServerRequest.
|
||||
*
|
||||
* $headers is an optional associative array with header field names as
|
||||
* string keys and values as either string or string[].
|
||||
*
|
||||
* If no StreamInterface is provided for $body, the instance will create
|
||||
* a NullStream instance for the message body.
|
||||
*
|
||||
* @param string $method
|
||||
* @param string|UriInterface $uri
|
||||
* @param array $headers Associative array with header field names as
|
||||
* keys and values as string|string[]
|
||||
* @param StreamInterface|null $body A stream representation of the message
|
||||
* entity body
|
||||
* @param array $serverParams An array of Server API (SAPI) parameters
|
||||
*/
|
||||
public function __construct(
|
||||
string $method = 'GET',
|
||||
$uri = '',
|
||||
array $headers = [],
|
||||
?StreamInterface $body = null,
|
||||
array $serverParams = []
|
||||
) {
|
||||
parent::__construct($method, $uri, $headers, $body);
|
||||
$this->serverParams = $serverParams;
|
||||
$this->cookieParams = [];
|
||||
$this->queryParams = [];
|
||||
$this->attributes = [];
|
||||
$this->uploadedFiles = [];
|
||||
}
|
||||
|
||||
public function __clone()
|
||||
{
|
||||
if (is_object($this->parsedBody)) {
|
||||
$this->parsedBody = clone $this->parsedBody;
|
||||
}
|
||||
parent::__clone();
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Psr\Http\Message\ServerRequestInterface
|
||||
|
||||
/**
|
||||
* Retrieve server parameters.
|
||||
*
|
||||
* Retrieves data related to the incoming request environment,
|
||||
* typically derived from PHP's $_SERVER superglobal. The data IS NOT
|
||||
* REQUIRED to originate from $_SERVER.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getServerParams()
|
||||
{
|
||||
return $this->serverParams;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve cookies.
|
||||
*
|
||||
* Retrieves cookies sent by the client to the server.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getCookieParams()
|
||||
{
|
||||
return $this->cookieParams;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new instance with the specified cookies.
|
||||
*
|
||||
* The data IS NOT REQUIRED to come from the $_COOKIE superglobal, but MUST
|
||||
* be compatible with the structure of $_COOKIE. Typically, this data will
|
||||
* be injected at instantiation.
|
||||
*
|
||||
* @param array $cookies Array of key/value pairs representing cookies.
|
||||
* @return static
|
||||
*/
|
||||
public function withCookieParams(array $cookies)
|
||||
{
|
||||
$request = clone $this;
|
||||
$request->cookieParams = $cookies;
|
||||
return $request;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve query string arguments.
|
||||
*
|
||||
* Retrieves the deserialized query string arguments, if any.
|
||||
*
|
||||
* Note: the query params might not be in sync with the URI or server
|
||||
* params. If you need to ensure you are only getting the original
|
||||
* values, you may need to parse the query string from `getUri()->getQuery()`
|
||||
* or from the `QUERY_STRING` server param.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getQueryParams()
|
||||
{
|
||||
return $this->queryParams;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new instance with the specified query string arguments.
|
||||
*
|
||||
* These values SHOULD remain immutable over the course of the incoming
|
||||
* request. They MAY be injected during instantiation, such as from PHP's
|
||||
* $_GET superglobal, or MAY be derived from some other value such as the
|
||||
* URI. In cases where the arguments are parsed from the URI, the data
|
||||
* MUST be compatible with what PHP's parse_str() would return for
|
||||
* purposes of how duplicate query parameters are handled, and how nested
|
||||
* sets are handled.
|
||||
*
|
||||
* Setting query string arguments MUST NOT change the URL stored by the
|
||||
* request, nor the values in the server params.
|
||||
*
|
||||
* @param array $query Array of query string arguments, typically from
|
||||
* $_GET.
|
||||
* @return static
|
||||
*/
|
||||
public function withQueryParams(array $query)
|
||||
{
|
||||
$request = clone $this;
|
||||
$request->queryParams = $query;
|
||||
return $request;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve normalized file upload data.
|
||||
*
|
||||
* This method returns upload metadata in a normalized tree, with each leaf
|
||||
* an instance of Psr\Http\Message\UploadedFileInterface.
|
||||
*
|
||||
* These values MAY be prepared from $_FILES or the message body during
|
||||
* instantiation, or MAY be injected via withUploadedFiles().
|
||||
*
|
||||
* @return array An array tree of UploadedFileInterface instances; an empty
|
||||
* array will be returned if no data is present.
|
||||
*/
|
||||
public function getUploadedFiles()
|
||||
{
|
||||
return $this->uploadedFiles;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new instance with the specified uploaded files.
|
||||
*
|
||||
* @param array $uploadedFiles An array tree of UploadedFileInterface instances.
|
||||
* @return static
|
||||
* @throws InvalidArgumentException if an invalid structure is provided.
|
||||
*/
|
||||
public function withUploadedFiles(array $uploadedFiles)
|
||||
{
|
||||
if (!$this->isValidUploadedFilesTree($uploadedFiles)) {
|
||||
throw new InvalidArgumentException(
|
||||
'withUploadedFiles expects an array tree with UploadedFileInterface leaves.'
|
||||
);
|
||||
}
|
||||
|
||||
$request = clone $this;
|
||||
$request->uploadedFiles = $uploadedFiles;
|
||||
return $request;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve any parameters provided in the request body.
|
||||
*
|
||||
* If the request Content-Type is either application/x-www-form-urlencoded
|
||||
* or multipart/form-data, and the request method is POST, this method will
|
||||
* return the contents of $_POST.
|
||||
*
|
||||
* Otherwise, this method may return any results of deserializing
|
||||
* the request body content; as parsing returns structured content, the
|
||||
* potential types MUST be arrays or objects only. A null value indicates
|
||||
* the absence of body content.
|
||||
*
|
||||
* @return null|array|object The deserialized body parameters, if any.
|
||||
* These will typically be an array or object.
|
||||
*/
|
||||
public function getParsedBody()
|
||||
{
|
||||
return $this->parsedBody;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new instance with the specified body parameters.
|
||||
*
|
||||
* These MAY be injected during instantiation.
|
||||
*
|
||||
* If the request Content-Type is either application/x-www-form-urlencoded
|
||||
* or multipart/form-data, and the request method is POST, use this method
|
||||
* ONLY to inject the contents of $_POST.
|
||||
*
|
||||
* The data IS NOT REQUIRED to come from $_POST, but MUST be the results of
|
||||
* deserializing the request body content. Deserialization/parsing returns
|
||||
* structured data, and, as such, this method ONLY accepts arrays or objects,
|
||||
* or a null value if nothing was available to parse.
|
||||
*
|
||||
* As an example, if content negotiation determines that the request data
|
||||
* is a JSON payload, this method could be used to create a request
|
||||
* instance with the deserialized parameters.
|
||||
*
|
||||
* @param null|array|object $data The deserialized body data. This will
|
||||
* typically be in an array or object.
|
||||
* @return static
|
||||
*/
|
||||
public function withParsedBody($data)
|
||||
{
|
||||
if (!(is_null($data) || is_array($data) || is_object($data))) {
|
||||
throw new InvalidArgumentException('Parsed body must be null, array, or object.');
|
||||
}
|
||||
|
||||
$request = clone $this;
|
||||
$request->parsedBody = $data;
|
||||
return $request;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve attributes derived from the request.
|
||||
*
|
||||
* The request "attributes" may be used to allow injection of any
|
||||
* parameters derived from the request: e.g., the results of path
|
||||
* match operations; the results of decrypting cookies; the results of
|
||||
* deserializing non-form-encoded message bodies; etc. Attributes
|
||||
* will be application and request specific, and CAN be mutable.
|
||||
*
|
||||
* @return array Attributes derived from the request.
|
||||
*/
|
||||
public function getAttributes()
|
||||
{
|
||||
return $this->attributes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve a single derived request attribute.
|
||||
*
|
||||
* Retrieves a single derived request attribute as described in
|
||||
* getAttributes(). If the attribute has not been previously set, returns
|
||||
* the default value as provided.
|
||||
*
|
||||
* This method obviates the need for a hasAttribute() method, as it allows
|
||||
* specifying a default value to return if the attribute is not found.
|
||||
*
|
||||
* @see getAttributes()
|
||||
* @param string $name The attribute name.
|
||||
* @param mixed $default Default value to return if the attribute does not exist.
|
||||
* @return mixed
|
||||
*/
|
||||
public function getAttribute($name, $default = null)
|
||||
{
|
||||
if (isset($this->attributes[$name])) {
|
||||
return $this->attributes[$name];
|
||||
}
|
||||
return $default;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new instance with the specified derived request attribute.
|
||||
*
|
||||
* This method allows setting a single derived request attribute as
|
||||
* described in getAttributes().
|
||||
*
|
||||
* @see getAttributes()
|
||||
* @param string $name The attribute name.
|
||||
* @param mixed $value The value of the attribute.
|
||||
* @return static
|
||||
*/
|
||||
public function withAttribute($name, $value)
|
||||
{
|
||||
$request = clone $this;
|
||||
$request->attributes[$name] = $value;
|
||||
return $request;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new instance that removes the specified derived request
|
||||
* attribute.
|
||||
*
|
||||
* This method allows removing a single derived request attribute as
|
||||
* described in getAttributes().
|
||||
*
|
||||
* This method MUST be implemented in such a way as to retain the
|
||||
* immutability of the message, and MUST return a new instance that removes
|
||||
* the attribute.
|
||||
*
|
||||
* @see getAttributes()
|
||||
* @param string $name The attribute name.
|
||||
* @return static
|
||||
*/
|
||||
public function withoutAttribute($name)
|
||||
{
|
||||
$request = clone $this;
|
||||
unset($request->attributes[$name]);
|
||||
return $request;
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @param array $root
|
||||
* @return bool
|
||||
*/
|
||||
private function isValidUploadedFilesTree(array $root)
|
||||
{
|
||||
// Allow empty array.
|
||||
if (count($root) === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// If not empty, the array MUST have all string keys.
|
||||
$keys = array_keys($root);
|
||||
if (count($keys) !== count(array_filter($keys, 'is_string'))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Valid if each child branch is valid.
|
||||
foreach ($root as $branch) {
|
||||
if (!$this->isValidUploadedFilesBranch($branch)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param UploadedFileInterface|array $branch
|
||||
* @return bool
|
||||
*/
|
||||
private function isValidUploadedFilesBranch($branch): bool
|
||||
{
|
||||
if (is_array($branch)) {
|
||||
// Branch.
|
||||
foreach ($branch as $child) {
|
||||
if (!$this->isValidUploadedFilesBranch($child)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
} else {
|
||||
// Leaf. Valid only if this is an UploadedFileInterface.
|
||||
return $branch instanceof UploadedFileInterface;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,191 @@
|
|||
<?php
|
||||
|
||||
namespace WellRESTed\Message;
|
||||
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Psr\Http\Message\StreamInterface;
|
||||
use Psr\Http\Message\UriInterface;
|
||||
|
||||
class ServerRequestMarshaller
|
||||
{
|
||||
/**
|
||||
* Read the request as sent from the client and construct a ServerRequest
|
||||
* representation.
|
||||
*
|
||||
* @return ServerRequestInterface
|
||||
* @internal
|
||||
*/
|
||||
public function getServerRequest(): ServerRequestInterface
|
||||
{
|
||||
$method = self::parseMethod($_SERVER);
|
||||
$uri = self::readUri($_SERVER);
|
||||
$headers = self::parseHeaders($_SERVER);
|
||||
$body = self::readBody();
|
||||
|
||||
$request = (new ServerRequest($method, $uri, $headers, $body, $_SERVER))
|
||||
->withProtocolVersion(self::parseProtocolVersion($_SERVER))
|
||||
->withUploadedFiles(self::readUploadedFiles($_FILES))
|
||||
->withCookieParams($_COOKIE)
|
||||
->withQueryParams(self::parseQuery($_SERVER));
|
||||
|
||||
if (self::isForm($request)) {
|
||||
$request = $request->withParsedBody($_POST);
|
||||
}
|
||||
|
||||
return $request;
|
||||
}
|
||||
|
||||
private static function parseQuery(array $serverParams): array
|
||||
{
|
||||
$queryParams = [];
|
||||
if (isset($serverParams['QUERY_STRING'])) {
|
||||
parse_str($serverParams['QUERY_STRING'], $queryParams);
|
||||
}
|
||||
return $queryParams;
|
||||
}
|
||||
|
||||
private static function parseProtocolVersion(array $serverParams): string
|
||||
{
|
||||
if (isset($serverParams['SERVER_PROTOCOL'])
|
||||
&& $serverParams['SERVER_PROTOCOL'] === 'HTTP/1.0') {
|
||||
return '1.0';
|
||||
}
|
||||
return '1.1';
|
||||
}
|
||||
|
||||
private static function parseHeaders(array $serverParams): array
|
||||
{
|
||||
// http://www.php.net/manual/en/function.getallheaders.php#84262
|
||||
$headers = [];
|
||||
foreach ($serverParams as $name => $value) {
|
||||
if (substr($name, 0, 5) === 'HTTP_') {
|
||||
$name = self::normalizeHeaderName(substr($name, 5));
|
||||
$headers[$name] = trim($value);
|
||||
} elseif (self::isContentHeader($name) && !empty(trim($value))) {
|
||||
$name = self::normalizeHeaderName($name);
|
||||
$headers[$name] = trim($value);
|
||||
}
|
||||
}
|
||||
return $headers;
|
||||
}
|
||||
|
||||
private static function normalizeHeaderName(string $name): string
|
||||
{
|
||||
$name = ucwords(strtolower(str_replace('_', ' ', $name)));
|
||||
return str_replace(' ', '-', $name);
|
||||
}
|
||||
|
||||
private static function isContentHeader(string $name): bool
|
||||
{
|
||||
return $name === 'CONTENT_LENGTH' || $name === 'CONTENT_TYPE';
|
||||
}
|
||||
|
||||
private static function parseMethod(array $serverParams): string
|
||||
{
|
||||
return $serverParams['REQUEST_METHOD'] ?? 'GET';
|
||||
}
|
||||
|
||||
private static function readBody(): StreamInterface
|
||||
{
|
||||
$input = fopen('php://input', 'rb');
|
||||
$temp = fopen('php://temp', 'wb+');
|
||||
stream_copy_to_stream($input, $temp);
|
||||
rewind($temp);
|
||||
return new Stream($temp);
|
||||
}
|
||||
|
||||
private static function readUri(array $serverParams): UriInterface
|
||||
{
|
||||
$uri = '';
|
||||
|
||||
$scheme = 'http';
|
||||
if (isset($serverParams['HTTPS']) && $serverParams['HTTPS'] && $serverParams['HTTPS'] !== 'off') {
|
||||
$scheme = 'https';
|
||||
}
|
||||
|
||||
if (isset($serverParams['HTTP_HOST'])) {
|
||||
$authority = $serverParams['HTTP_HOST'];
|
||||
$uri .= "$scheme://$authority";
|
||||
}
|
||||
|
||||
// Path and query string
|
||||
if (isset($serverParams['REQUEST_URI'])) {
|
||||
$uri .= $serverParams['REQUEST_URI'];
|
||||
}
|
||||
|
||||
return new Uri($uri);
|
||||
}
|
||||
|
||||
private static function isForm(ServerRequestInterface $request): bool
|
||||
{
|
||||
$contentType = $request->getHeaderLine('Content-type');
|
||||
return (strpos($contentType, 'application/x-www-form-urlencoded') !== false)
|
||||
|| (strpos($contentType, 'multipart/form-data') !== false);
|
||||
}
|
||||
|
||||
private static function readUploadedFiles(array $input): array
|
||||
{
|
||||
$uploadedFiles = [];
|
||||
foreach ($input as $name => $value) {
|
||||
self::addUploadedFilesToBranch($uploadedFiles, $name, $value);
|
||||
}
|
||||
return $uploadedFiles;
|
||||
}
|
||||
|
||||
private static function addUploadedFilesToBranch(
|
||||
array &$branch,
|
||||
string $name,
|
||||
array $value
|
||||
): void {
|
||||
if (self::isUploadedFile($value)) {
|
||||
if (self::isUploadedFileList($value)) {
|
||||
$files = [];
|
||||
$keys = array_keys($value['name']);
|
||||
foreach ($keys as $key) {
|
||||
$files[$key] = new UploadedFile(
|
||||
$value['name'][$key],
|
||||
$value['type'][$key],
|
||||
$value['size'][$key],
|
||||
$value['tmp_name'][$key],
|
||||
$value['error'][$key]
|
||||
);
|
||||
}
|
||||
$branch[$name] = $files;
|
||||
} else {
|
||||
// Single uploaded file
|
||||
$uploadedFile = new UploadedFile(
|
||||
$value['name'],
|
||||
$value['type'],
|
||||
$value['size'],
|
||||
$value['tmp_name'],
|
||||
$value['error']
|
||||
);
|
||||
$branch[$name] = $uploadedFile;
|
||||
}
|
||||
} else {
|
||||
// Add another branch
|
||||
$nextBranch = [];
|
||||
foreach ($value as $nextName => $nextValue) {
|
||||
self::addUploadedFilesToBranch($nextBranch, $nextName, $nextValue);
|
||||
}
|
||||
$branch[$name] = $nextBranch;
|
||||
}
|
||||
}
|
||||
|
||||
private static function isUploadedFile(array $value): bool
|
||||
{
|
||||
// Check for each of the expected keys. If all are present, this is a
|
||||
// a file. It may be a single file, or a list of files.
|
||||
return isset($value['name'], $value['type'], $value['tmp_name'], $value['error'], $value['size']);
|
||||
}
|
||||
|
||||
private static function isUploadedFileList(array $value): bool
|
||||
{
|
||||
// When each item is an array, this is a list of uploaded files.
|
||||
return is_array($value['name'])
|
||||
&& is_array($value['type'])
|
||||
&& is_array($value['tmp_name'])
|
||||
&& is_array($value['error'])
|
||||
&& is_array($value['size']);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,353 @@
|
|||
<?php
|
||||
|
||||
namespace WellRESTed\Message;
|
||||
|
||||
use Exception;
|
||||
use InvalidArgumentException;
|
||||
use Psr\Http\Message\StreamInterface;
|
||||
use RuntimeException;
|
||||
|
||||
class Stream implements StreamInterface
|
||||
{
|
||||
private const READABLE_MODES = ['r', 'r+', 'w+', 'a+', 'x+', 'c+'];
|
||||
private const WRITABLE_MODES = ['r+', 'w', 'w+', 'a', 'a+', 'x', 'x+', 'c', 'c+'];
|
||||
|
||||
/** @var resource|null */
|
||||
private $resource;
|
||||
|
||||
/**
|
||||
* Create a new Stream by passing either a stream resource handle (e.g.,
|
||||
* from fopen) or a string.
|
||||
*
|
||||
* If $resource is a string, the Stream will open a php://temp stream,
|
||||
* write the string to the stream, and use that temp resource.
|
||||
*
|
||||
* @param resource|string $resource A file system pointer resource or
|
||||
* string
|
||||
*/
|
||||
public function __construct($resource = '')
|
||||
{
|
||||
if (is_resource($resource) && get_resource_type($resource) === 'stream') {
|
||||
$this->resource = $resource;
|
||||
} elseif (is_string($resource)) {
|
||||
$this->resource = fopen('php://temp', 'wb+');
|
||||
if ($resource !== '') {
|
||||
$this->write($resource);
|
||||
}
|
||||
} else {
|
||||
throw new InvalidArgumentException('Expected resource or string.');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads all data from the stream into a string, from the beginning to end.
|
||||
*
|
||||
* This method will attempt to seek to the beginning of the stream before
|
||||
* reading data and read the stream until the end is reached.
|
||||
*
|
||||
* Warning: This could attempt to load a large amount of data into memory.
|
||||
*
|
||||
* @see http://php.net/manual/en/language.oop5.magic.php#object.tostring
|
||||
* @return string
|
||||
*/
|
||||
public function __toString()
|
||||
{
|
||||
try {
|
||||
if ($this->isSeekable()) {
|
||||
$this->rewind();
|
||||
}
|
||||
return $this->getContents();
|
||||
} catch (Exception $e) {
|
||||
// Silence exceptions in order to conform with PHP's string casting
|
||||
// operations.
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes the stream and any underlying resources.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function close()
|
||||
{
|
||||
if ($this->resource === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
$resource = $this->resource;
|
||||
fclose($resource);
|
||||
$this->resource = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Separates any underlying resources from the stream.
|
||||
*
|
||||
* After the stream has been detached, the stream is in an unusable state.
|
||||
*
|
||||
* @return resource|null Underlying file-pointer handler
|
||||
*/
|
||||
public function detach()
|
||||
{
|
||||
$resource = $this->resource;
|
||||
$this->resource = null;
|
||||
return $resource;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the size of the stream if known
|
||||
*
|
||||
* @return int|null Returns the size in bytes if known, or null if unknown.
|
||||
*/
|
||||
public function getSize()
|
||||
{
|
||||
if ($this->resource === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$statistics = fstat($this->resource);
|
||||
if ($statistics && $statistics['size']) {
|
||||
return $statistics['size'];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current position of the file read/write pointer
|
||||
*
|
||||
* @return int Position of the file pointer
|
||||
* @throws RuntimeException on error.
|
||||
*/
|
||||
public function tell()
|
||||
{
|
||||
if ($this->resource === null) {
|
||||
throw new RuntimeException('Unable to retrieve current position of detached stream.');
|
||||
}
|
||||
|
||||
$position = ftell($this->resource);
|
||||
if ($position === false) {
|
||||
throw new RuntimeException('Unable to retrieve current position of file pointer.');
|
||||
}
|
||||
return $position;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the stream is at the end of the stream.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function eof()
|
||||
{
|
||||
if ($this->resource === null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return feof($this->resource);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether or not the stream is seekable.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isSeekable()
|
||||
{
|
||||
if ($this->resource === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $this->getMetadata('seekable') == 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Seek to a position in the stream.
|
||||
*
|
||||
* @link http://www.php.net/manual/en/function.fseek.php
|
||||
* @param int $offset Stream offset
|
||||
* @param int $whence Specifies how the cursor position will be calculated
|
||||
* based on the seek offset. Valid values are identical to the built-in
|
||||
* PHP $whence values for `fseek()`. SEEK_SET: Set position equal to
|
||||
* offset bytes SEEK_CUR: Set position to current location plus offset
|
||||
* SEEK_END: Set position to end-of-stream plus offset.
|
||||
* @return void
|
||||
* @throws RuntimeException on failure.
|
||||
*/
|
||||
public function seek($offset, $whence = SEEK_SET)
|
||||
{
|
||||
if ($this->resource === null) {
|
||||
throw new RuntimeException('Unable to seek detached stream.');
|
||||
}
|
||||
|
||||
$result = -1;
|
||||
if ($this->isSeekable()) {
|
||||
$result = fseek($this->resource, $offset, $whence);
|
||||
}
|
||||
if ($result === -1) {
|
||||
throw new RuntimeException('Unable to seek to position.');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Seek to the beginning of the stream.
|
||||
*
|
||||
* If the stream is not seekable, this method will raise an exception;
|
||||
* otherwise, it will perform a seek(0).
|
||||
*
|
||||
* @see seek()
|
||||
* @link http://www.php.net/manual/en/function.fseek.php
|
||||
* @return void
|
||||
* @throws RuntimeException on failure.
|
||||
*/
|
||||
public function rewind()
|
||||
{
|
||||
if ($this->resource === null) {
|
||||
throw new RuntimeException('Unable to seek detached stream.');
|
||||
}
|
||||
|
||||
$result = false;
|
||||
if ($this->isSeekable()) {
|
||||
$result = rewind($this->resource);
|
||||
}
|
||||
if ($result === false) {
|
||||
throw new RuntimeException('Unable to rewind.');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether or not the stream is writable.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isWritable()
|
||||
{
|
||||
if ($this->resource === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$mode = $this->getBasicMode();
|
||||
return in_array($mode, self::WRITABLE_MODES);
|
||||
}
|
||||
|
||||
/**
|
||||
* Write data to the stream.
|
||||
*
|
||||
* @param string $string The string that is to be written.
|
||||
* @return int Returns the number of bytes written to the stream.
|
||||
* @throws RuntimeException on failure.
|
||||
*/
|
||||
public function write($string)
|
||||
{
|
||||
if ($this->resource === null) {
|
||||
throw new RuntimeException('Unable to write to detached stream.');
|
||||
}
|
||||
|
||||
$result = false;
|
||||
if ($this->isWritable()) {
|
||||
$result = fwrite($this->resource, $string);
|
||||
}
|
||||
if ($result === false) {
|
||||
throw new RuntimeException('Unable to write to stream.');
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether or not the stream is readable.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isReadable()
|
||||
{
|
||||
if ($this->resource === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$mode = $this->getBasicMode();
|
||||
return in_array($mode, self::READABLE_MODES);
|
||||
}
|
||||
|
||||
/**
|
||||
* Read data from the stream.
|
||||
*
|
||||
* @param int $length Read up to $length bytes from the object and return
|
||||
* them. Fewer than $length bytes may be returned if underlying stream
|
||||
* call returns fewer bytes.
|
||||
* @return string Returns the data read from the stream, or an empty string
|
||||
* if no bytes are available.
|
||||
* @throws RuntimeException if an error occurs.
|
||||
*/
|
||||
public function read($length)
|
||||
{
|
||||
if ($this->resource === null) {
|
||||
throw new RuntimeException('Unable to read to detached stream.');
|
||||
}
|
||||
|
||||
$result = false;
|
||||
if ($this->isReadable()) {
|
||||
$result = fread($this->resource, $length);
|
||||
}
|
||||
if ($result === false) {
|
||||
throw new RuntimeException('Unable to read from stream.');
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the remaining contents in a string
|
||||
*
|
||||
* @return string
|
||||
* @throws RuntimeException if unable to read or an error occurs while
|
||||
* reading.
|
||||
*/
|
||||
public function getContents()
|
||||
{
|
||||
if ($this->resource === null) {
|
||||
throw new RuntimeException('Unable to read to detached stream.');
|
||||
}
|
||||
|
||||
$result = false;
|
||||
if ($this->isReadable()) {
|
||||
$result = stream_get_contents($this->resource);
|
||||
}
|
||||
if ($result === false) {
|
||||
throw new RuntimeException('Unable to read from stream.');
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get stream metadata as an associative array or retrieve a specific key.
|
||||
*
|
||||
* The keys returned are identical to the keys returned from PHP's
|
||||
* stream_get_meta_data() function.
|
||||
*
|
||||
* @link http://php.net/manual/en/function.stream-get-meta-data.php
|
||||
* @param string|null $key Specific metadata to retrieve.
|
||||
* @return array|mixed|null Returns an associative array if no key is
|
||||
* provided. Returns a specific key value if a key is provided and the
|
||||
* value is found, or null if the key is not found.
|
||||
*/
|
||||
public function getMetadata($key = null)
|
||||
{
|
||||
if ($this->resource === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$metadata = stream_get_meta_data($this->resource);
|
||||
if ($key === null) {
|
||||
return $metadata;
|
||||
} else {
|
||||
return $metadata[$key];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string Mode for the resource reduced to only the characters
|
||||
* r, w, a, x, c, and + needed to determine readable and writeable status.
|
||||
*/
|
||||
private function getBasicMode()
|
||||
{
|
||||
$mode = $this->getMetadata('mode') ?? '';
|
||||
return preg_replace('/[^rwaxc+]/', '', $mode);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
<?php
|
||||
|
||||
namespace WellRESTed\Message;
|
||||
|
||||
use Psr\Http\Message\StreamFactoryInterface;
|
||||
use Psr\Http\Message\StreamInterface;
|
||||
use RuntimeException;
|
||||
|
||||
class StreamFactory implements StreamFactoryInterface
|
||||
{
|
||||
/**
|
||||
* Create a new stream from a string.
|
||||
*
|
||||
* @param string $content String content with which to populate the stream.
|
||||
* @return StreamInterface
|
||||
*/
|
||||
public function createStream(string $content = ''): StreamInterface
|
||||
{
|
||||
return new Stream($content);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a stream from an existing file.
|
||||
*
|
||||
* @param string $filename Filename or stream URI to use as basis of stream.
|
||||
* @param string $mode Mode with which to open the underlying file/stream.
|
||||
*
|
||||
* @return StreamInterface
|
||||
* @throws RuntimeException If the file cannot be opened.
|
||||
*/
|
||||
public function createStreamFromFile(
|
||||
string $filename,
|
||||
string $mode = 'r'
|
||||
): StreamInterface {
|
||||
$f = fopen($filename, $mode);
|
||||
if ($f === false) {
|
||||
throw new RuntimeException();
|
||||
}
|
||||
return new Stream($f);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new stream from an existing resource.
|
||||
*
|
||||
* @param resource $resource PHP resource to use as basis of stream.
|
||||
*
|
||||
* @return StreamInterface
|
||||
*/
|
||||
public function createStreamFromResource($resource): StreamInterface
|
||||
{
|
||||
return new Stream($resource);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,194 @@
|
|||
<?php
|
||||
|
||||
namespace WellRESTed\Message;
|
||||
|
||||
use InvalidArgumentException;
|
||||
use Psr\Http\Message\StreamInterface;
|
||||
use Psr\Http\Message\UploadedFileInterface;
|
||||
use RuntimeException;
|
||||
|
||||
/**
|
||||
* Value object representing a file uploaded through an HTTP request.
|
||||
*/
|
||||
class UploadedFile implements UploadedFileInterface
|
||||
{
|
||||
/** @var string */
|
||||
private $clientFilename;
|
||||
/** @var string */
|
||||
private $clientMediaType;
|
||||
/** @var int */
|
||||
private $error;
|
||||
/** @var bool */
|
||||
private $moved = false;
|
||||
/** @var int */
|
||||
private $size;
|
||||
/** @var StreamInterface */
|
||||
private $stream;
|
||||
/** @var string|null */
|
||||
private $tmpName;
|
||||
|
||||
/**
|
||||
* Create a new UploadedFile. The arguments correspond with keys from arrays
|
||||
* provided by $_FILES. For example, given this structure for $_FILES:
|
||||
*
|
||||
* array(
|
||||
* 'avatar' => array(
|
||||
* 'name' => 'my-avatar.png',
|
||||
* 'type' => 'image/png',
|
||||
* 'size' => 90996,
|
||||
* 'tmp_name' => 'phpUxcOty',
|
||||
* 'error' => 0
|
||||
* )
|
||||
* )
|
||||
*
|
||||
* ...use this call:
|
||||
*
|
||||
* new UploadedFile(
|
||||
* $_FILES['avatar']['name'],
|
||||
* $_FILES['avatar']['type'],
|
||||
* $_FILES['avatar']['size'],
|
||||
* $_FILES['avatar']['tmp_name'],
|
||||
* $_FILES['avatar']['error']
|
||||
* );
|
||||
*
|
||||
* @param string $name Name of the file; provided by the client
|
||||
* @param string $type Media type of the file; provided by the client
|
||||
* @param int $size The file size in bytes.
|
||||
* @param string $tmpName Local filesystem name of the file
|
||||
* @param int $error One of PHP's UPLOAD_ERR_XXX constants.
|
||||
* @see http://php.net/manual/en/features.file-upload.errors.php
|
||||
*/
|
||||
public function __construct($name, $type, $size, $tmpName, $error)
|
||||
{
|
||||
$this->clientFilename = $name;
|
||||
$this->error = $error;
|
||||
$this->clientMediaType = $type;
|
||||
$this->size = $size;
|
||||
|
||||
if (file_exists($tmpName)) {
|
||||
$this->stream = new Stream(fopen($tmpName, 'rb'));
|
||||
$this->tmpName = $tmpName;
|
||||
} else {
|
||||
$this->stream = new NullStream();
|
||||
$this->tmpName = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve a stream representing the uploaded file.
|
||||
*
|
||||
* This method returns a StreamInterface instance, representing the
|
||||
* uploaded file. The purpose of this method is to allow using native PHP
|
||||
* stream functionality to manipulate the file upload, such as
|
||||
* stream_copy_to_stream() (though the result will need to be decorated in
|
||||
* a native PHP stream wrapper to work with such functions).
|
||||
*
|
||||
* If the moveTo() method has been called previously, this method will
|
||||
* raise an exception.
|
||||
*
|
||||
* @return StreamInterface Stream representation of the uploaded file.
|
||||
* @throws RuntimeException in cases when no stream is available or can
|
||||
* be created.
|
||||
*/
|
||||
public function getStream()
|
||||
{
|
||||
if ($this->tmpName === null) {
|
||||
throw new RuntimeException('Unable to read uploaded file.');
|
||||
}
|
||||
if ($this->moved) {
|
||||
throw new RuntimeException('File has already been moved.');
|
||||
}
|
||||
if (php_sapi_name() !== 'cli' && !is_uploaded_file($this->tmpName)) {
|
||||
throw new RuntimeException('File is not an uploaded file.');
|
||||
}
|
||||
return $this->stream;
|
||||
}
|
||||
|
||||
/**
|
||||
* Move the uploaded file to a new location.
|
||||
*
|
||||
* Use this method as an alternative to move_uploaded_file(). This method
|
||||
* is guaranteed to work in both SAPI and non-SAPI environments.
|
||||
*
|
||||
* The original file or stream will be removed on completion.
|
||||
*
|
||||
* If this method is called more than once, any subsequent calls will raise
|
||||
* an exception.
|
||||
*
|
||||
* @see http://php.net/is_uploaded_file
|
||||
* @see http://php.net/move_uploaded_file
|
||||
* @param string $targetPath Path to which to move the uploaded file.
|
||||
* @return void
|
||||
* @throws InvalidArgumentException if the $path specified is invalid.
|
||||
* @throws RuntimeException on any error during the move operation, or on
|
||||
* the second or subsequent call to the method.
|
||||
*/
|
||||
public function moveTo($targetPath)
|
||||
{
|
||||
if ($this->tmpName === null || !file_exists($this->tmpName)) {
|
||||
throw new RuntimeException("File {$this->tmpName} does not exist.");
|
||||
}
|
||||
if (php_sapi_name() === 'cli') {
|
||||
rename($this->tmpName, $targetPath);
|
||||
} else {
|
||||
move_uploaded_file($this->tmpName, $targetPath);
|
||||
}
|
||||
$this->moved = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the file size.
|
||||
*
|
||||
* @return int|null The file size in bytes or null if unknown.
|
||||
*/
|
||||
public function getSize()
|
||||
{
|
||||
return $this->size;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the error associated with the uploaded file.
|
||||
*
|
||||
* The return value will be one of PHP's UPLOAD_ERR_XXX constants.
|
||||
*
|
||||
* If the file was uploaded successfully, this method will return
|
||||
* UPLOAD_ERR_OK.
|
||||
*
|
||||
* @see http://php.net/manual/en/features.file-upload.errors.php
|
||||
* @return int One of PHP's UPLOAD_ERR_XXX constants.
|
||||
*/
|
||||
public function getError()
|
||||
{
|
||||
return $this->error;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the filename sent by the client.
|
||||
*
|
||||
* Do not trust the value returned by this method. A client could send
|
||||
* a malicious filename with the intention to corrupt or hack your
|
||||
* application.
|
||||
*
|
||||
* @return string|null The filename sent by the client or null if none
|
||||
* was provided.
|
||||
*/
|
||||
public function getClientFilename()
|
||||
{
|
||||
return $this->clientFilename;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the media type sent by the client.
|
||||
*
|
||||
* Do not trust the value returned by this method. A client could send
|
||||
* a malicious media type with the intention to corrupt or hack your
|
||||
* application.
|
||||
*
|
||||
* @return string|null The media type sent by the client or null if none
|
||||
* was provided.
|
||||
*/
|
||||
public function getClientMediaType()
|
||||
{
|
||||
return $this->clientMediaType;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,533 @@
|
|||
<?php
|
||||
|
||||
namespace WellRESTed\Message;
|
||||
|
||||
use InvalidArgumentException;
|
||||
use Psr\Http\Message\UriInterface;
|
||||
|
||||
/**
|
||||
* Value object representing a URI.
|
||||
*
|
||||
* This interface is meant to represent URIs according to RFC 3986 and to
|
||||
* provide methods for most common operations. Additional functionality for
|
||||
* working with URIs can be provided on top of the interface or externally.
|
||||
* Its primary use is for HTTP requests, but may also be used in other
|
||||
* contexts.
|
||||
*
|
||||
* @link http://tools.ietf.org/html/rfc3986 (the URI specification)
|
||||
*/
|
||||
class Uri implements UriInterface
|
||||
{
|
||||
private const MIN_PORT = 0;
|
||||
private const MAX_PORT = 65535;
|
||||
|
||||
/** @var string */
|
||||
private $scheme;
|
||||
/** @var string */
|
||||
private $user;
|
||||
/** @var string */
|
||||
private $password;
|
||||
/** @var string */
|
||||
private $host;
|
||||
/** @var int|null */
|
||||
private $port;
|
||||
/** @var string */
|
||||
private $path;
|
||||
/** @var string */
|
||||
private $query;
|
||||
/** @var string */
|
||||
private $fragment;
|
||||
|
||||
/**
|
||||
* @param string $uri A string representation of a URI.
|
||||
*/
|
||||
public function __construct(string $uri = '')
|
||||
{
|
||||
$parsed = parse_url($uri);
|
||||
|
||||
$this->scheme = $parsed['scheme'] ?? '';
|
||||
$this->user = $parsed['user'] ?? '';
|
||||
$this->password = $parsed['pass'] ?? '';
|
||||
$this->host = strtolower($parsed['host'] ?? '');
|
||||
$this->port = $parsed['port'] ?? null;
|
||||
$this->path = $parsed['path'] ?? '';
|
||||
$this->query = $parsed['query'] ?? '';
|
||||
$this->fragment = $parsed['fragment'] ?? '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the scheme component of the URI.
|
||||
*
|
||||
* If no scheme is present, this method will return an empty string.
|
||||
*
|
||||
* The value returned will be normalized to lowercase, per RFC 3986
|
||||
* Section 3.1.
|
||||
*
|
||||
* The trailing ":" character is not part of the scheme and will not be
|
||||
* added.
|
||||
*
|
||||
* @see https://tools.ietf.org/html/rfc3986#section-3.1
|
||||
* @return string The URI scheme.
|
||||
*/
|
||||
public function getScheme()
|
||||
{
|
||||
return $this->scheme;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the authority component of the URI.
|
||||
*
|
||||
* If no authority information is present, this method will return an empty
|
||||
* string.
|
||||
*
|
||||
* The authority syntax of the URI is:
|
||||
*
|
||||
* <pre>
|
||||
* [user-info@]host[:port]
|
||||
* </pre>
|
||||
*
|
||||
* If the port component is not set or is the standard port for the current
|
||||
* scheme, it will not be included.
|
||||
*
|
||||
* @see https://tools.ietf.org/html/rfc3986#section-3.2
|
||||
* @return string The URI authority, in "[user-info@]host[:port]" format.
|
||||
*/
|
||||
public function getAuthority()
|
||||
{
|
||||
$host = $this->getHost();
|
||||
if (!$host) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$authority = '';
|
||||
|
||||
// User Info
|
||||
$userInfo = $this->getUserInfo();
|
||||
if ($userInfo) {
|
||||
$authority .= $userInfo . '@';
|
||||
}
|
||||
|
||||
// Host
|
||||
$authority .= $host;
|
||||
|
||||
// Port: Include only if non-standard
|
||||
if ($this->nonStandardPort()) {
|
||||
$authority .= ':' . $this->getPort();
|
||||
}
|
||||
|
||||
return $authority;
|
||||
}
|
||||
|
||||
private function nonStandardPort(): bool
|
||||
{
|
||||
$port = $this->getPort();
|
||||
$scheme = $this->getScheme();
|
||||
return $scheme === 'http' && $port !== 80
|
||||
|| $scheme === 'https' && $port !== 443;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the user information component of the URI.
|
||||
*
|
||||
* If no user information is present, this method will return an empty
|
||||
* string.
|
||||
*
|
||||
* If a user is present in the URI, this will return that value;
|
||||
* additionally, if the password is also present, it will be appended to the
|
||||
* user value, with a colon (":") separating the values.
|
||||
*
|
||||
* The trailing "@" character is not part of the user information and will
|
||||
* not be added.
|
||||
*
|
||||
* @return string The URI user information, in "username[:password]" format.
|
||||
*/
|
||||
public function getUserInfo()
|
||||
{
|
||||
$userInfo = $this->user;
|
||||
if ($userInfo && $this->password) {
|
||||
$userInfo .= ':' . $this->password;
|
||||
}
|
||||
return $userInfo;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the host component of the URI.
|
||||
*
|
||||
* If no host is present, this method will return an empty string.
|
||||
*
|
||||
* The value returned will be normalized to lowercase, per RFC 3986
|
||||
* Section 3.2.2.
|
||||
*
|
||||
* @see http://tools.ietf.org/html/rfc3986#section-3.2.2
|
||||
* @return string The URI host.
|
||||
*/
|
||||
public function getHost()
|
||||
{
|
||||
return $this->host;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the port component of the URI.
|
||||
*
|
||||
* If a port is present, and it is non-standard for the current scheme,
|
||||
* this method MUST return it as an integer. If the port is the standard port
|
||||
* used with the current scheme, this method SHOULD return null.
|
||||
*
|
||||
* If no port is present, and no scheme is present, this method MUST return
|
||||
* a null value.
|
||||
*
|
||||
* If no port is present, but a scheme is present, this method MAY return
|
||||
* the standard port for that scheme, but SHOULD return null.
|
||||
*
|
||||
* @return null|int The URI port.
|
||||
*/
|
||||
public function getPort()
|
||||
{
|
||||
if ($this->port === null) {
|
||||
switch ($this->scheme) {
|
||||
case 'http':
|
||||
return 80;
|
||||
case 'https':
|
||||
return 443;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return $this->port;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the path component of the URI.
|
||||
*
|
||||
* The path can either be empty or absolute (starting with a slash) or
|
||||
* rootless (not starting with a slash). Implementations MUST support all
|
||||
* three syntaxes.
|
||||
*
|
||||
* Normally, the empty path "" and absolute path "/" are considered equal as
|
||||
* defined in RFC 7230 Section 2.7.3. But this method MUST NOT automatically
|
||||
* do this normalization because in contexts with a trimmed base path, e.g.
|
||||
* the front controller, this difference becomes significant. It's the task
|
||||
* of the user to handle both "" and "/".
|
||||
*
|
||||
* The value returned MUST be percent-encoded, but MUST NOT double-encode
|
||||
* any characters. To determine what characters to encode, please refer to
|
||||
* RFC 3986, Sections 2 and 3.3.
|
||||
*
|
||||
* As an example, if the value should include a slash ("/") not intended as
|
||||
* delimiter between path segments, that value MUST be passed in encoded
|
||||
* form (e.g., "%2F") to the instance.
|
||||
*
|
||||
* @see https://tools.ietf.org/html/rfc3986#section-2
|
||||
* @see https://tools.ietf.org/html/rfc3986#section-3.3
|
||||
* @return string The URI path.
|
||||
*/
|
||||
public function getPath()
|
||||
{
|
||||
if ($this->path === '*') {
|
||||
return $this->path;
|
||||
}
|
||||
return $this->percentEncode($this->path);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the query string of the URI.
|
||||
*
|
||||
* If no query string is present, this method MUST return an empty string.
|
||||
*
|
||||
* The leading "?" character is not part of the query and MUST NOT be
|
||||
* added.
|
||||
*
|
||||
* The value returned MUST be percent-encoded, but MUST NOT double-encode
|
||||
* any characters. To determine what characters to encode, please refer to
|
||||
* RFC 3986, Sections 2 and 3.4.
|
||||
*
|
||||
* As an example, if a value in a key/value pair of the query string should
|
||||
* include an ampersand ("&") not intended as a delimiter between values,
|
||||
* that value MUST be passed in encoded form (e.g., "%26") to the instance.
|
||||
*
|
||||
* @see https://tools.ietf.org/html/rfc3986#section-2
|
||||
* @see https://tools.ietf.org/html/rfc3986#section-3.4
|
||||
* @return string The URI query string.
|
||||
*/
|
||||
public function getQuery()
|
||||
{
|
||||
return $this->percentEncode($this->query);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the fragment component of the URI.
|
||||
*
|
||||
* If no fragment is present, this method MUST return an empty string.
|
||||
*
|
||||
* The leading "#" character is not part of the fragment and MUST NOT be
|
||||
* added.
|
||||
*
|
||||
* The value returned MUST be percent-encoded, but MUST NOT double-encode
|
||||
* any characters. To determine what characters to encode, please refer to
|
||||
* RFC 3986, Sections 2 and 3.5.
|
||||
*
|
||||
* @see https://tools.ietf.org/html/rfc3986#section-2
|
||||
* @see https://tools.ietf.org/html/rfc3986#section-3.5
|
||||
* @return string The URI fragment.
|
||||
*/
|
||||
public function getFragment()
|
||||
{
|
||||
return $this->percentEncode($this->fragment);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return an instance with the specified scheme.
|
||||
*
|
||||
* This method MUST retain the state of the current instance, and return
|
||||
* an instance that contains the specified scheme.
|
||||
*
|
||||
* Implementations MUST support the schemes "http" and "https" case
|
||||
* insensitively, and MAY accommodate other schemes if required.
|
||||
*
|
||||
* An empty scheme is equivalent to removing the scheme.
|
||||
*
|
||||
* @param string $scheme The scheme to use with the new instance.
|
||||
* @return static A new instance with the specified scheme.
|
||||
* @throws InvalidArgumentException for invalid or unsupported schemes.
|
||||
*/
|
||||
public function withScheme($scheme)
|
||||
{
|
||||
$scheme = strtolower($scheme ?? '');
|
||||
if (!in_array($scheme, ['', 'http', 'https'])) {
|
||||
throw new InvalidArgumentException('Scheme must be http, https, or empty.');
|
||||
}
|
||||
$uri = clone $this;
|
||||
$uri->scheme = $scheme;
|
||||
return $uri;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return an instance with the specified user information.
|
||||
*
|
||||
* This method MUST retain the state of the current instance, and return
|
||||
* an instance that contains the specified user information.
|
||||
*
|
||||
* Password is optional, but the user information MUST include the
|
||||
* user; an empty string for the user is equivalent to removing user
|
||||
* information.
|
||||
*
|
||||
* @param string $user The user name to use for authority.
|
||||
* @param null|string $password The password associated with $user.
|
||||
* @return static A new instance with the specified user information.
|
||||
*/
|
||||
public function withUserInfo($user, $password = null)
|
||||
{
|
||||
$uri = clone $this;
|
||||
$uri->user = $user;
|
||||
$uri->password = $password ?? '';
|
||||
return $uri;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return an instance with the specified host.
|
||||
*
|
||||
* This method MUST retain the state of the current instance, and return
|
||||
* an instance that contains the specified host.
|
||||
*
|
||||
* An empty host value is equivalent to removing the host.
|
||||
*
|
||||
* @param string $host The hostname to use with the new instance.
|
||||
* @return static A new instance with the specified host.
|
||||
* @throws InvalidArgumentException for invalid hostnames.
|
||||
*/
|
||||
public function withHost($host)
|
||||
{
|
||||
if (!is_string($host)) {
|
||||
throw new InvalidArgumentException('Host must be a string.');
|
||||
}
|
||||
|
||||
$uri = clone $this;
|
||||
$uri->host = strtolower($host);
|
||||
return $uri;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return an instance with the specified port.
|
||||
*
|
||||
* This method MUST retain the state of the current instance, and return
|
||||
* an instance that contains the specified port.
|
||||
*
|
||||
* Implementations MUST raise an exception for ports outside the
|
||||
* established TCP and UDP port ranges.
|
||||
*
|
||||
* A null value provided for the port is equivalent to removing the port
|
||||
* information.
|
||||
*
|
||||
* @param null|int $port The port to use with the new instance; a null value
|
||||
* removes the port information.
|
||||
* @return static A new instance with the specified port.
|
||||
* @throws InvalidArgumentException for invalid ports.
|
||||
*/
|
||||
public function withPort($port)
|
||||
{
|
||||
if (is_numeric($port)) {
|
||||
if ($port < self::MIN_PORT || $port > self::MAX_PORT) {
|
||||
$message = sprintf('Port must be between %s and %s.', self::MIN_PORT, self::MAX_PORT);
|
||||
throw new InvalidArgumentException($message);
|
||||
}
|
||||
$port = (int) $port;
|
||||
} elseif ($port !== null) {
|
||||
throw new InvalidArgumentException('Port must be an int or null.');
|
||||
}
|
||||
|
||||
$uri = clone $this;
|
||||
$uri->port = $port;
|
||||
return $uri;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return an instance with the specified path.
|
||||
*
|
||||
* This method MUST retain the state of the current instance, and return
|
||||
* an instance that contains the specified path.
|
||||
*
|
||||
* The path can either be empty or absolute (starting with a slash) or
|
||||
* rootless (not starting with a slash). Implementations MUST support all
|
||||
* three syntaxes.
|
||||
*
|
||||
* Users can provide both encoded and decoded path characters.
|
||||
* Implementations ensure the correct encoding as outlined in getPath().
|
||||
*
|
||||
* @param string $path The path to use with the new instance.
|
||||
* @return static A new instance with the specified path.
|
||||
* @throws InvalidArgumentException for invalid paths.
|
||||
*/
|
||||
public function withPath($path)
|
||||
{
|
||||
if (!is_string($path)) {
|
||||
throw new InvalidArgumentException('Path must be a string');
|
||||
}
|
||||
$uri = clone $this;
|
||||
$uri->path = $path;
|
||||
return $uri;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return an instance with the specified query string.
|
||||
*
|
||||
* This method MUST retain the state of the current instance, and return
|
||||
* an instance that contains the specified query string.
|
||||
*
|
||||
* Users can provide both encoded and decoded query characters.
|
||||
* Implementations ensure the correct encoding as outlined in getQuery().
|
||||
*
|
||||
* An empty query string value is equivalent to removing the query string.
|
||||
*
|
||||
* @param string $query The query string to use with the new instance.
|
||||
* @return static A new instance with the specified query string.
|
||||
* @throws InvalidArgumentException for invalid query strings.
|
||||
*/
|
||||
public function withQuery($query)
|
||||
{
|
||||
$uri = clone $this;
|
||||
$uri->query = $query;
|
||||
return $uri;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return an instance with the specified URI fragment.
|
||||
*
|
||||
* This method MUST retain the state of the current instance, and return
|
||||
* an instance that contains the specified URI fragment.
|
||||
*
|
||||
* Users can provide both encoded and decoded fragment characters.
|
||||
* Implementations ensure the correct encoding as outlined in getFragment().
|
||||
*
|
||||
* An empty fragment value is equivalent to removing the fragment.
|
||||
*
|
||||
* @param string $fragment The fragment to use with the new instance.
|
||||
* @return static A new instance with the specified fragment.
|
||||
*/
|
||||
public function withFragment($fragment)
|
||||
{
|
||||
$uri = clone $this;
|
||||
$uri->fragment = $fragment ?? '';
|
||||
return $uri;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the string representation as a URI reference.
|
||||
*
|
||||
* Depending on which components of the URI are present, the resulting
|
||||
* string is either a full URI or relative reference according to RFC 3985,
|
||||
* Section 4.1. The method concatenates the various components of the URI,
|
||||
* using the appropriate delimiters:
|
||||
*
|
||||
* - If a scheme is present, it MUST be suffixed by ":".
|
||||
* - If an authority is present, it MUST be prefixed by "//".
|
||||
* - The path can be concatenated without delimiters. But there are two
|
||||
* cases where the path has to be adjusted to make the URI reference
|
||||
* valid as PHP does not allow to throw an exception in __toString():
|
||||
* - If the path is rootless and an authority is present, the path MUST
|
||||
* be prefixed by "/".
|
||||
* - If the path is starting with more than one "/" and no authority is
|
||||
* present, the starting slashes MUST be reduced to one.
|
||||
* - If a query is present, it MUST be prefixed by "?".
|
||||
* - If a fragment is present, it MUST be prefixed by "#".
|
||||
*
|
||||
* @see http://tools.ietf.org/html/rfc3986#section-4.1
|
||||
* @return string
|
||||
*/
|
||||
public function __toString()
|
||||
{
|
||||
$string = '';
|
||||
|
||||
$authority = $this->getAuthority();
|
||||
if ($authority !== '') {
|
||||
$scheme = $this->getScheme();
|
||||
if ($scheme !== '') {
|
||||
$string = $scheme . ':';
|
||||
}
|
||||
$string .= "//$authority";
|
||||
}
|
||||
|
||||
$path = $this->getPath();
|
||||
if ($path !== '') {
|
||||
$string .= $path;
|
||||
}
|
||||
|
||||
$query = $this->getQuery();
|
||||
if ($query !== '') {
|
||||
$string .= "?$query";
|
||||
}
|
||||
|
||||
$fragment = $this->getFragment();
|
||||
if ($fragment !== '') {
|
||||
$string .= "#$fragment";
|
||||
}
|
||||
|
||||
return $string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a percent-encoded string.
|
||||
*
|
||||
* This method encode each character that is not:
|
||||
* - A percent sign ("%") that is followed by a hex character (0-9, a-f, A-F)
|
||||
* - An "unreserved character" per RFC 3986 (see below)
|
||||
* - A "reserved character" per RFC 3986 (see below)
|
||||
*
|
||||
* unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~"
|
||||
* reserved = gen-delims / sub-delims
|
||||
* gen-delims = ":" / "/" / "?" / "#" / "[" / "]" / "@"
|
||||
* sub-delims = "!" / "$" / "&" / "'" / "(" / ")" / "*" / "+" / "," / ";" / "="
|
||||
*
|
||||
* @param string $subject
|
||||
* @return string
|
||||
*/
|
||||
private function percentEncode(string $subject)
|
||||
{
|
||||
$reserved = ':/?#[]@!$&\'()*+,;=';
|
||||
$reserved = preg_quote($reserved);
|
||||
$pattern = '~(?:%(?![a-fA-F0-9]{2}))|(?:[^%a-zA-Z0-9\-\.\_\~' . $reserved . ']{1})~';
|
||||
$callback = function (array $matches): string {
|
||||
return urlencode($matches[0]);
|
||||
};
|
||||
return preg_replace_callback($pattern, $callback, $subject);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
<?php
|
||||
|
||||
namespace WellRESTed;
|
||||
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
|
||||
interface MiddlewareInterface
|
||||
{
|
||||
/**
|
||||
* Accepts a request and response and returns a modified response.
|
||||
*
|
||||
* $request represents the request issued by the client.
|
||||
*
|
||||
* $response represents the current state of the response.
|
||||
*
|
||||
* $next is a callable that expects a request and response as parameters
|
||||
* and returns a response. Calling $next forwards a request and response
|
||||
* to the next middleware in the sequence (if any) and continues
|
||||
* propagation; returning a response without calling $next halts
|
||||
* propagation and prevents subsequent middleware from running.
|
||||
*
|
||||
* Implementations SHOULD call $next to allow subsequent middleware to act
|
||||
* on the request and response. Implementations MAY further alter the
|
||||
* response returned by $next before returning it.
|
||||
*
|
||||
* Implementations MAY return a response without calling $next to prevent
|
||||
* propagation (e.g., for error conditions).
|
||||
*
|
||||
* Implementations SHOULD NOT call $next and disregard the response by
|
||||
* returning an entirely unrelated response.
|
||||
*
|
||||
* @param ServerRequestInterface $request
|
||||
* @param ResponseInterface $response
|
||||
* @param callable $next
|
||||
* @return ResponseInterface
|
||||
*/
|
||||
public function __invoke(ServerRequestInterface $request, ResponseInterface $response, $next);
|
||||
}
|
||||
|
|
@ -0,0 +1,134 @@
|
|||
<?php
|
||||
|
||||
namespace WellRESTed\Routing\Route;
|
||||
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use WellRESTed\Dispatching\DispatcherInterface;
|
||||
use WellRESTed\MiddlewareInterface;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
class MethodMap implements MiddlewareInterface
|
||||
{
|
||||
/** @var DispatcherInterface */
|
||||
private $dispatcher;
|
||||
/** @var array */
|
||||
private $map;
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
public function __construct(DispatcherInterface $dispatcher)
|
||||
{
|
||||
$this->map = [];
|
||||
$this->dispatcher = $dispatcher;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a dispatchable (e.g.m handler or middleware) with a method.
|
||||
*
|
||||
* $method may be:
|
||||
* - A single verb ("GET"),
|
||||
* - A comma-separated list of verbs ("GET,PUT,DELETE")
|
||||
* - "*" to indicate any method.
|
||||
*
|
||||
* $dispatchable may be anything a Dispatcher can dispatch.
|
||||
* @see DispatcherInterface::dispatch
|
||||
*
|
||||
* $dispatchable may also be null, in which case any previously set
|
||||
* handlers and middle for that method or methods will be unset.
|
||||
*
|
||||
* @param string $method
|
||||
* @param mixed $dispatchable
|
||||
*/
|
||||
public function register(string $method, $dispatchable): void
|
||||
{
|
||||
$methods = explode(',', $method);
|
||||
$methods = array_map('trim', $methods);
|
||||
foreach ($methods as $method) {
|
||||
$this->map[$method] = $dispatchable;
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// MiddlewareInterface
|
||||
|
||||
/**
|
||||
* @param ServerRequestInterface $request
|
||||
* @param ResponseInterface $response
|
||||
* @param callable $next
|
||||
* @return ResponseInterface
|
||||
*/
|
||||
public function __invoke(
|
||||
ServerRequestInterface $request,
|
||||
ResponseInterface $response,
|
||||
$next
|
||||
) {
|
||||
$method = $request->getMethod();
|
||||
// Dispatch middleware registered with the explicitly matching method.
|
||||
if (isset($this->map[$method])) {
|
||||
$middleware = $this->map[$method];
|
||||
return $this->dispatchMiddleware($middleware, $request, $response, $next);
|
||||
}
|
||||
// For HEAD, dispatch GET by default.
|
||||
if ($method === 'HEAD' && isset($this->map['GET'])) {
|
||||
$middleware = $this->map['GET'];
|
||||
return $this->dispatchMiddleware($middleware, $request, $response, $next);
|
||||
}
|
||||
// Dispatch * middleware, if registered.
|
||||
if (isset($this->map['*'])) {
|
||||
$middleware = $this->map['*'];
|
||||
return $this->dispatchMiddleware($middleware, $request, $response, $next);
|
||||
}
|
||||
// Respond describing the allowed methods, either as a 405 response or
|
||||
// in response to an OPTIONS request.
|
||||
if ($method === 'OPTIONS') {
|
||||
$response = $response->withStatus(200);
|
||||
} else {
|
||||
$response = $response->withStatus(405);
|
||||
}
|
||||
return $this->addAllowHeader($response);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
private function addAllowHeader(ResponseInterface $response): ResponseInterface
|
||||
{
|
||||
$methods = join(',', $this->getAllowedMethods());
|
||||
return $response->withHeader('Allow', $methods);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
private function getAllowedMethods(): array
|
||||
{
|
||||
$methods = array_keys($this->map);
|
||||
// Add HEAD if GET is allowed and HEAD is not present.
|
||||
if (in_array('GET', $methods) && !in_array('HEAD', $methods)) {
|
||||
$methods[] = 'HEAD';
|
||||
}
|
||||
// Add OPTIONS if not already present.
|
||||
if (!in_array('OPTIONS', $methods)) {
|
||||
$methods[] = 'OPTIONS';
|
||||
}
|
||||
return $methods;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $middleware
|
||||
* @param ServerRequestInterface $request
|
||||
* @param ResponseInterface $response
|
||||
* @param callable $next
|
||||
* @return ResponseInterface
|
||||
*/
|
||||
private function dispatchMiddleware(
|
||||
$middleware,
|
||||
ServerRequestInterface $request,
|
||||
ResponseInterface $response,
|
||||
$next
|
||||
) {
|
||||
return $this->dispatcher->dispatch($middleware, $request, $response, $next);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
<?php
|
||||
|
||||
namespace WellRESTed\Routing\Route;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
class PrefixRoute extends Route
|
||||
{
|
||||
public function __construct(string $target, MethodMap $methodMap)
|
||||
{
|
||||
parent::__construct(rtrim($target, '*'), $methodMap);
|
||||
}
|
||||
|
||||
public function getType(): int
|
||||
{
|
||||
return Route::TYPE_PREFIX;
|
||||
}
|
||||
|
||||
/**
|
||||
* Examines a request target to see if it is a match for the route.
|
||||
*
|
||||
* @param string $requestTarget
|
||||
* @return bool
|
||||
*/
|
||||
public function matchesRequestTarget(string $requestTarget): bool
|
||||
{
|
||||
return strrpos($requestTarget, $this->target, -strlen($requestTarget)) !== false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Always returns an empty array.
|
||||
*/
|
||||
public function getPathVariables(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
<?php
|
||||
|
||||
namespace WellRESTed\Routing\Route;
|
||||
|
||||
use RuntimeException;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
class RegexRoute extends Route
|
||||
{
|
||||
/** @var array */
|
||||
private $captures = [];
|
||||
|
||||
public function getType(): int
|
||||
{
|
||||
return Route::TYPE_PATTERN;
|
||||
}
|
||||
|
||||
/**
|
||||
* Examines a request target to see if it is a match for the route.
|
||||
*
|
||||
* @param string $requestTarget
|
||||
* @return bool
|
||||
*/
|
||||
public function matchesRequestTarget(string $requestTarget): bool
|
||||
{
|
||||
$this->captures = [];
|
||||
$matched = preg_match($this->getTarget(), $requestTarget, $captures);
|
||||
if ($matched) {
|
||||
$this->captures = $captures;
|
||||
return true;
|
||||
} elseif ($matched === false) {
|
||||
throw new RuntimeException('Invalid regular expression: ' . $this->getTarget());
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array of matches from the last call to matchesRequestTarget.
|
||||
*
|
||||
* @see \preg_match
|
||||
* @return array
|
||||
*/
|
||||
public function getPathVariables(): array
|
||||
{
|
||||
return $this->captures;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,112 @@
|
|||
<?php
|
||||
|
||||
namespace WellRESTed\Routing\Route;
|
||||
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use RuntimeException;
|
||||
use WellRESTed\MiddlewareInterface;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
abstract class Route implements MiddlewareInterface
|
||||
{
|
||||
/** Matches when request path is an exact match to entire target */
|
||||
public const TYPE_STATIC = 0;
|
||||
/** Matches when request path is an exact match to start of target */
|
||||
public const TYPE_PREFIX = 1;
|
||||
/** Matches by request path by pattern and may extract matched varialbes */
|
||||
public const TYPE_PATTERN = 2;
|
||||
|
||||
/** @var string */
|
||||
protected $target;
|
||||
/** @var MethodMap */
|
||||
protected $methodMap;
|
||||
|
||||
public function __construct(string $target, MethodMap $methodMap)
|
||||
{
|
||||
$this->target = $target;
|
||||
$this->methodMap = $methodMap;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the Route::TYPE_ constants that identifies the type.
|
||||
*
|
||||
* TYPE_STATIC indicates the route MUST match only when the path is an
|
||||
* exact match to the route's entire target. This route type SHOULD NOT
|
||||
* provide path variables.
|
||||
*
|
||||
* TYPE_PREFIX indicates the route MUST match when the route's target
|
||||
* appears in its entirety at the beginning of the path.
|
||||
*
|
||||
* TYPE_PATTERN indicates that matchesRequestTarget MUST be used
|
||||
* to determine a match against a given path. This route type SHOULD
|
||||
* provide path variables.
|
||||
*
|
||||
* @return int One of the Route::TYPE_ constants.
|
||||
*/
|
||||
abstract public function getType(): int;
|
||||
|
||||
/**
|
||||
* Return an array of variables extracted from the path most recently
|
||||
* passed to matchesRequestTarget.
|
||||
*
|
||||
* If the path does not contain variables, or if matchesRequestTarget
|
||||
* has not yet been called, this method MUST return an empty array.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
abstract public function getPathVariables(): array;
|
||||
|
||||
/**
|
||||
* Examines a request target to see if it is a match for the route.
|
||||
*
|
||||
* @param string $requestTarget
|
||||
* @return bool
|
||||
* @throws RuntimeException Error occurred testing the target such as an
|
||||
* invalid regular expression
|
||||
*/
|
||||
abstract public function matchesRequestTarget(string $requestTarget): bool;
|
||||
|
||||
/**
|
||||
* Path, partial path, or pattern to match request paths against.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getTarget(): string
|
||||
{
|
||||
return $this->target;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a dispatchable (handler or middleware) with a method.
|
||||
*
|
||||
* $method may be:
|
||||
* - A single verb ("GET"),
|
||||
* - A comma-separated list of verbs ("GET,PUT,DELETE")
|
||||
* - "*" to indicate any method.
|
||||
*
|
||||
* $dispatchable may be anything a Dispatcher can dispatch.
|
||||
* @see DispatcherInterface::dispatch
|
||||
*
|
||||
* @param string $method
|
||||
* @param mixed $dispatchable
|
||||
*/
|
||||
public function register(string $method, $dispatchable): void
|
||||
{
|
||||
$this->methodMap->register($method, $dispatchable);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ServerRequestInterface $request
|
||||
* @param ResponseInterface $response
|
||||
* @param callable $next
|
||||
* @return ResponseInterface
|
||||
*/
|
||||
public function __invoke(ServerRequestInterface $request, ResponseInterface $response, $next)
|
||||
{
|
||||
$map = $this->methodMap;
|
||||
return $map($request, $response, $next);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
<?php
|
||||
|
||||
namespace WellRESTed\Routing\Route;
|
||||
|
||||
use WellRESTed\Dispatching\DispatcherInterface;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
class RouteFactory
|
||||
{
|
||||
private $dispatcher;
|
||||
|
||||
public function __construct(DispatcherInterface $dispatcher)
|
||||
{
|
||||
$this->dispatcher = $dispatcher;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a route for the given target.
|
||||
*
|
||||
* - Target with no special characters will create StaticRoutes
|
||||
* - Target ending with * will create PrefixRoutes
|
||||
* - Target containing URI variables (e.g., {id}) will create TemplateRoutes
|
||||
* - Regular expressions will create RegexRoutes
|
||||
*
|
||||
* @param string $target Route target or target pattern
|
||||
* @return Route
|
||||
*/
|
||||
public function create(string $target): Route
|
||||
{
|
||||
if ($target[0] === '/') {
|
||||
|
||||
// Possible static, prefix, or template
|
||||
|
||||
// PrefixRoutes end with *
|
||||
if (substr($target, -1) === '*') {
|
||||
return new PrefixRoute($target, new MethodMap($this->dispatcher));
|
||||
}
|
||||
|
||||
// TemplateRoutes contain {variable}
|
||||
if (preg_match(TemplateRoute::URI_TEMPLATE_EXPRESSION_RE, $target)) {
|
||||
return new TemplateRoute($target, new MethodMap($this->dispatcher));
|
||||
}
|
||||
|
||||
// StaticRoute
|
||||
return new StaticRoute($target, new MethodMap($this->dispatcher));
|
||||
}
|
||||
|
||||
// Regex
|
||||
return new RegexRoute($target, new MethodMap($this->dispatcher));
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
<?php
|
||||
|
||||
namespace WellRESTed\Routing\Route;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
class StaticRoute extends Route
|
||||
{
|
||||
public function getType(): int
|
||||
{
|
||||
return Route::TYPE_STATIC;
|
||||
}
|
||||
|
||||
/**
|
||||
* Examines a request target to see if it is a match for the route.
|
||||
*
|
||||
* @param string $requestTarget
|
||||
* @return bool
|
||||
*/
|
||||
public function matchesRequestTarget(string $requestTarget): bool
|
||||
{
|
||||
return $requestTarget === $this->getTarget();
|
||||
}
|
||||
|
||||
/**
|
||||
* Always returns an empty array.
|
||||
*/
|
||||
public function getPathVariables(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,175 @@
|
|||
<?php
|
||||
|
||||
namespace WellRESTed\Routing\Route;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
class TemplateRoute extends Route
|
||||
{
|
||||
/** Regular expression matching a URI template variable (e.g., {id}) */
|
||||
public const URI_TEMPLATE_EXPRESSION_RE = '/{([+.\/]?[a-zA-Z0-9_,]+\*?)}/';
|
||||
/**
|
||||
* Regular expression matching 1 or more unreserved characters.
|
||||
* ALPHA / DIGIT / "-" / "." / "_" / "~"
|
||||
*/
|
||||
private const RE_UNRESERVED = '[0-9a-zA-Z\-._\~%]*';
|
||||
|
||||
/** @var array */
|
||||
private $pathVariables = [];
|
||||
/** @var array */
|
||||
private $explosions = [];
|
||||
|
||||
public function getType(): int
|
||||
{
|
||||
return Route::TYPE_PATTERN;
|
||||
}
|
||||
|
||||
public function getPathVariables(): array
|
||||
{
|
||||
return $this->pathVariables;
|
||||
}
|
||||
|
||||
/**
|
||||
* Examines a request target to see if it is a match for the route.
|
||||
*
|
||||
* @param string $requestTarget
|
||||
* @return bool
|
||||
*/
|
||||
public function matchesRequestTarget(string $requestTarget): bool
|
||||
{
|
||||
$this->pathVariables = [];
|
||||
$this->explosions = [];
|
||||
|
||||
if (!$this->matchesStartOfRequestTarget($requestTarget)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$matchingPattern = $this->getMatchingPattern();
|
||||
|
||||
if (preg_match($matchingPattern, $requestTarget, $captures)) {
|
||||
$this->pathVariables = $this->processMatches($captures);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private function matchesStartOfRequestTarget(string $requestTarget): bool
|
||||
{
|
||||
$firstVarPos = strpos($this->target, '{');
|
||||
if ($firstVarPos === false) {
|
||||
return $requestTarget === $this->target;
|
||||
}
|
||||
return substr($requestTarget, 0, $firstVarPos) === substr($this->target, 0, $firstVarPos);
|
||||
}
|
||||
|
||||
private function processMatches(array $matches): array
|
||||
{
|
||||
$variables = [];
|
||||
|
||||
// Isolate the named captures.
|
||||
$keys = array_filter(array_keys($matches), 'is_string');
|
||||
|
||||
// Store named captures to the variables.
|
||||
foreach ($keys as $key) {
|
||||
$value = $matches[$key];
|
||||
|
||||
if (isset($this->explosions[$key])) {
|
||||
$values = explode($this->explosions[$key], $value);
|
||||
$variables[$key] = array_map('urldecode', $values);
|
||||
} else {
|
||||
$value = urldecode($value);
|
||||
$variables[$key] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
return $variables;
|
||||
}
|
||||
|
||||
private function getMatchingPattern(): string
|
||||
{
|
||||
// Convert the template into the pattern
|
||||
$pattern = $this->target;
|
||||
|
||||
// Escape allowable characters with regex meaning.
|
||||
$escape = [
|
||||
'.' => '\\.',
|
||||
'-' => '\\-',
|
||||
'+' => '\\+',
|
||||
'*' => '\\*'
|
||||
];
|
||||
$pattern = str_replace(array_keys($escape), array_values($escape), $pattern);
|
||||
$unescape = [
|
||||
'{\\+' => '{+',
|
||||
'{\\.' => '{.',
|
||||
'\\*}' => '*}'
|
||||
];
|
||||
$pattern = str_replace(array_keys($unescape), array_values($unescape), $pattern);
|
||||
|
||||
// Surround the pattern with delimiters.
|
||||
$pattern = "~^{$pattern}$~";
|
||||
|
||||
$pattern = preg_replace_callback(
|
||||
self::URI_TEMPLATE_EXPRESSION_RE,
|
||||
[$this, 'uriVariableReplacementCallback'],
|
||||
$pattern
|
||||
);
|
||||
|
||||
return $pattern;
|
||||
}
|
||||
|
||||
private function uriVariableReplacementCallback(array $matches): string
|
||||
{
|
||||
$name = $matches[1];
|
||||
$pattern = self::RE_UNRESERVED;
|
||||
|
||||
$prefix = '';
|
||||
$delimiter = ',';
|
||||
$explodeDelimiter = ',';
|
||||
|
||||
// Read the first character as an operator. This determines which
|
||||
// characters to allow in the match.
|
||||
$operator = $name[0];
|
||||
|
||||
// Read the last character as the modifier.
|
||||
$explosion = (substr($name, -1, 1) === '*');
|
||||
|
||||
switch ($operator) {
|
||||
case '+':
|
||||
$name = substr($name, 1);
|
||||
$pattern = '.*';
|
||||
break;
|
||||
case '.':
|
||||
$name = substr($name, 1);
|
||||
$prefix = '\\.';
|
||||
$delimiter = '\\.';
|
||||
$explodeDelimiter = '.';
|
||||
break;
|
||||
case '/':
|
||||
$name = substr($name, 1);
|
||||
$prefix = '\\/';
|
||||
$delimiter = '\\/';
|
||||
if ($explosion) {
|
||||
$pattern = '[0-9a-zA-Z\-._\~%,\/]*'; // Unreserved + "," and "/"
|
||||
$explodeDelimiter = '/';
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// Explosion
|
||||
if ($explosion) {
|
||||
$name = substr($name, 0, -1);
|
||||
if ($pattern === self::RE_UNRESERVED) {
|
||||
$pattern = '[0-9a-zA-Z\-._\~%,]*'; // Unreserved + ","
|
||||
}
|
||||
$this->explosions[$name] = $explodeDelimiter;
|
||||
}
|
||||
|
||||
$names = explode(',', $name);
|
||||
$results = [];
|
||||
foreach ($names as $name) {
|
||||
$results[] = "(?<{$name}>{$pattern})";
|
||||
}
|
||||
return $prefix . join($delimiter, $results);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,290 @@
|
|||
<?php
|
||||
|
||||
namespace WellRESTed\Routing;
|
||||
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use WellRESTed\Dispatching\Dispatcher;
|
||||
use WellRESTed\Dispatching\DispatcherInterface;
|
||||
use WellRESTed\MiddlewareInterface;
|
||||
use WellRESTed\Routing\Route\Route;
|
||||
use WellRESTed\Routing\Route\RouteFactory;
|
||||
|
||||
class Router implements MiddlewareInterface
|
||||
{
|
||||
/** @var string|null Attribute name for matched path variables */
|
||||
private $pathVariablesAttributeName;
|
||||
/** @var DispatcherInterface */
|
||||
private $dispatcher;
|
||||
/** @var RouteFactory */
|
||||
private $factory;
|
||||
/** @var Route[] Array of Route objects */
|
||||
private $routes;
|
||||
/** @var Route[] Hash array mapping exact paths to routes */
|
||||
private $staticRoutes;
|
||||
/** @var Route[] Hash array mapping path prefixes to routes */
|
||||
private $prefixRoutes;
|
||||
/** @var Route[] Hash array mapping path prefixes to routes */
|
||||
private $patternRoutes;
|
||||
/** @var mixed[] List array of middleware */
|
||||
private $stack;
|
||||
/** @var bool Call the next middleware when no route matches */
|
||||
private $continueOnNotFound = false;
|
||||
|
||||
/**
|
||||
* Create a new Router.
|
||||
*
|
||||
* By default, when a route containing path variables matches, the path
|
||||
* variables are stored individually as attributes on the
|
||||
* ServerRequestInterface.
|
||||
*
|
||||
* When $pathVariablesAttributeName is set, a single attribute will be
|
||||
* stored with the name. The value will be an array containing all of the
|
||||
* path variables.
|
||||
*
|
||||
* Use Server->createRouter to instantiate a new Router rather than calling
|
||||
* this constructor manually.
|
||||
*
|
||||
* @param string|null $pathVariablesAttributeName
|
||||
* Attribute name for matched path variables. A null value sets
|
||||
* attributes directly.
|
||||
* @param DispatcherInterface|null $dispatcher
|
||||
* Instance to use for dispatching middleware and handlers.
|
||||
* @param RouteFactory|null $routeFactory
|
||||
*/
|
||||
public function __construct(
|
||||
?string $pathVariablesAttributeName = null,
|
||||
?DispatcherInterface $dispatcher = null,
|
||||
?RouteFactory $routeFactory = null
|
||||
) {
|
||||
$this->pathVariablesAttributeName = $pathVariablesAttributeName;
|
||||
$this->dispatcher = $dispatcher ?? new Dispatcher();
|
||||
$this->factory = $routeFactory ?? new RouteFactory($this->dispatcher);
|
||||
$this->routes = [];
|
||||
$this->staticRoutes = [];
|
||||
$this->prefixRoutes = [];
|
||||
$this->patternRoutes = [];
|
||||
$this->stack = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ServerRequestInterface $request
|
||||
* @param ResponseInterface $response
|
||||
* @param callable $next
|
||||
* @return ResponseInterface
|
||||
*/
|
||||
public function __invoke(
|
||||
ServerRequestInterface $request,
|
||||
ResponseInterface $response,
|
||||
$next
|
||||
): ResponseInterface {
|
||||
$path = $this->getPath($request->getRequestTarget());
|
||||
|
||||
$route = $this->getStaticRoute($path);
|
||||
if ($route) {
|
||||
return $this->dispatch($route, $request, $response, $next);
|
||||
}
|
||||
|
||||
$route = $this->getPrefixRoute($path);
|
||||
if ($route) {
|
||||
return $this->dispatch($route, $request, $response, $next);
|
||||
}
|
||||
|
||||
// Try each of the routes.
|
||||
foreach ($this->patternRoutes as $route) {
|
||||
if ($route->matchesRequestTarget($path)) {
|
||||
$pathVariables = $route->getPathVariables();
|
||||
if ($this->pathVariablesAttributeName) {
|
||||
$request = $request->withAttribute($this->pathVariablesAttributeName, $pathVariables);
|
||||
} else {
|
||||
foreach ($pathVariables as $name => $value) {
|
||||
$request = $request->withAttribute($name, $value);
|
||||
}
|
||||
}
|
||||
return $this->dispatch($route, $request, $response, $next);
|
||||
}
|
||||
}
|
||||
|
||||
if (!$this->continueOnNotFound) {
|
||||
return $response->withStatus(404);
|
||||
}
|
||||
|
||||
return $next($request, $response);
|
||||
}
|
||||
|
||||
private function getPath(string $requestTarget): string
|
||||
{
|
||||
$queryStart = strpos($requestTarget, '?');
|
||||
if ($queryStart === false) {
|
||||
return $requestTarget;
|
||||
}
|
||||
return substr($requestTarget, 0, $queryStart);
|
||||
}
|
||||
|
||||
private function dispatch(
|
||||
callable $route,
|
||||
ServerRequestInterface $request,
|
||||
ResponseInterface $response,
|
||||
callable $next
|
||||
): ResponseInterface {
|
||||
if (!$this->stack) {
|
||||
return $route($request, $response, $next);
|
||||
}
|
||||
$stack = array_merge($this->stack, [$route]);
|
||||
return $this->dispatcher->dispatch(
|
||||
$stack,
|
||||
$request,
|
||||
$response,
|
||||
$next
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Register handlers and middleware with the router for a given path and
|
||||
* method.
|
||||
*
|
||||
* $method may be:
|
||||
* - A single verb ("GET"),
|
||||
* - A comma-separated list of verbs ("GET,PUT,DELETE")
|
||||
* - "*" to indicate any method.
|
||||
*
|
||||
* $target may be:
|
||||
* - An exact path (e.g., "/path/")
|
||||
* - A prefix path ending with "*"" ("/path/*"")
|
||||
* - A URI template with variables enclosed in "{}" ("/path/{id}")
|
||||
* - A regular expression ("~/cat/([0-9]+)~")
|
||||
*
|
||||
* $dispatchable may be:
|
||||
* - An instance implementing one of these interfaces:
|
||||
* - Psr\Http\Server\RequestHandlerInterface
|
||||
* - Psr\Http\Server\MiddlewareInterface
|
||||
* - WellRESTed\MiddlewareInterface
|
||||
* - Psr\Http\Message\ResponseInterface
|
||||
* - A string containing the fully qualified class name of a class
|
||||
* implementing one of the interfaces listed above.
|
||||
* - A callable that returns an instance implementing one of the
|
||||
* interfaces listed above.
|
||||
* - A callable with a signature matching the signature of
|
||||
* WellRESTed\MiddlewareInterface::__invoke
|
||||
* - An array containing any of the items in this list.
|
||||
* @see DispatchedInterface::dispatch
|
||||
*
|
||||
* @param string $method HTTP method(s) to match
|
||||
* @param string $target Request target or pattern to match
|
||||
* @param mixed $dispatchable Handler or middleware to dispatch
|
||||
* @return static
|
||||
*/
|
||||
public function register(string $method, string $target, $dispatchable): Router
|
||||
{
|
||||
$route = $this->getRouteForTarget($target);
|
||||
$route->register($method, $dispatchable);
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Push a new middleware onto the stack.
|
||||
*
|
||||
* Middleware for a router runs before the middleware and handler for the
|
||||
* matched route and runs only when a route matched.
|
||||
*
|
||||
* $middleware may be:
|
||||
* - An instance implementing MiddlewareInterface
|
||||
* - A string containing the fully qualified class name of a class
|
||||
* implementing MiddlewareInterface
|
||||
* - A callable that returns an instance implementing MiddleInterface
|
||||
* - A callable matching the signature of MiddlewareInterface::dispatch
|
||||
* @see DispatchedInterface::dispatch
|
||||
*
|
||||
* @param mixed $middleware Middleware to dispatch in sequence
|
||||
* @return static
|
||||
*/
|
||||
public function add($middleware): Router
|
||||
{
|
||||
$this->stack[] = $middleware;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure the instance to delegate to the next middleware when no route
|
||||
* matches.
|
||||
*
|
||||
* @return static
|
||||
*/
|
||||
public function continueOnNotFound(): Router
|
||||
{
|
||||
$this->continueOnNotFound = true;
|
||||
return $this;
|
||||
}
|
||||
|
||||
private function getRouteForTarget(string $target): Route
|
||||
{
|
||||
if (isset($this->routes[$target])) {
|
||||
$route = $this->routes[$target];
|
||||
} else {
|
||||
$route = $this->factory->create($target);
|
||||
$this->registerRouteForTarget($route, $target);
|
||||
}
|
||||
return $route;
|
||||
}
|
||||
|
||||
private function registerRouteForTarget(Route $route, string $target): void
|
||||
{
|
||||
// Store the route to the hash indexed by original target.
|
||||
$this->routes[$target] = $route;
|
||||
|
||||
// Store the route to the array of routes for its type.
|
||||
switch ($route->getType()) {
|
||||
case Route::TYPE_STATIC:
|
||||
$this->staticRoutes[$route->getTarget()] = $route;
|
||||
break;
|
||||
case Route::TYPE_PREFIX:
|
||||
$this->prefixRoutes[rtrim($route->getTarget(), '*')] = $route;
|
||||
break;
|
||||
case Route::TYPE_PATTERN:
|
||||
$this->patternRoutes[] = $route;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private function getStaticRoute(string $requestTarget): ?Route
|
||||
{
|
||||
if (isset($this->staticRoutes[$requestTarget])) {
|
||||
return $this->staticRoutes[$requestTarget];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private function getPrefixRoute(string $requestTarget): ?Route
|
||||
{
|
||||
// Find all prefixes that match the start of this path.
|
||||
$prefixes = array_keys($this->prefixRoutes);
|
||||
$matches = array_filter(
|
||||
$prefixes,
|
||||
function ($prefix) use ($requestTarget) {
|
||||
return $this->startsWith($requestTarget, $prefix);
|
||||
}
|
||||
);
|
||||
|
||||
if (!$matches) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// If there are multiple matches, sort them to find the one with the
|
||||
// longest string length.
|
||||
if (count($matches) > 1) {
|
||||
$compareByLength = function (string $a, string $b): int {
|
||||
return strlen($b) - strlen($a);
|
||||
};
|
||||
usort($matches, $compareByLength);
|
||||
}
|
||||
|
||||
$bestMatch = array_values($matches)[0];
|
||||
return $this->prefixRoutes[$bestMatch];
|
||||
}
|
||||
|
||||
private function startsWith(string $haystack, string $needle): bool
|
||||
{
|
||||
$length = strlen($needle);
|
||||
return substr($haystack, 0, $length) === $needle;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,171 @@
|
|||
<?php
|
||||
|
||||
namespace WellRESTed;
|
||||
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use WellRESTed\Dispatching\Dispatcher;
|
||||
use WellRESTed\Dispatching\DispatcherInterface;
|
||||
use WellRESTed\Message\Response;
|
||||
use WellRESTed\Message\ServerRequestMarshaller;
|
||||
use WellRESTed\Routing\Router;
|
||||
use WellRESTed\Transmission\Transmitter;
|
||||
use WellRESTed\Transmission\TransmitterInterface;
|
||||
|
||||
class Server
|
||||
{
|
||||
/** @var mixed[] */
|
||||
private $attributes = [];
|
||||
/** @var DispatcherInterface */
|
||||
private $dispatcher;
|
||||
/** @var string|null attribute name for matched path variables */
|
||||
private $pathVariablesAttributeName = null;
|
||||
/** @var ServerRequestInterface|null */
|
||||
private $request = null;
|
||||
/** @var ResponseInterface */
|
||||
private $response;
|
||||
/** @var TransmitterInterface */
|
||||
private $transmitter;
|
||||
/** @var mixed[] List array of middleware */
|
||||
private $stack;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->stack = [];
|
||||
$this->response = new Response();
|
||||
$this->dispatcher = new Dispatcher();
|
||||
$this->transmitter = new Transmitter();
|
||||
}
|
||||
|
||||
/**
|
||||
* Push a new middleware onto the stack.
|
||||
*
|
||||
* @param mixed $middleware Middleware to dispatch in sequence
|
||||
* @return Server
|
||||
*/
|
||||
public function add($middleware): Server
|
||||
{
|
||||
$this->stack[] = $middleware;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a new Router that uses the server's configuration.
|
||||
*
|
||||
* @return Router
|
||||
*/
|
||||
public function createRouter(): Router
|
||||
{
|
||||
return new Router(
|
||||
$this->pathVariablesAttributeName,
|
||||
$this->dispatcher
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform the request-response cycle.
|
||||
*
|
||||
* This method reads a server request, dispatches the request through the
|
||||
* server's stack of middleware, and outputs the response via a Transmitter.
|
||||
*/
|
||||
public function respond(): void
|
||||
{
|
||||
$request = $this->getRequest();
|
||||
foreach ($this->attributes as $name => $value) {
|
||||
$request = $request->withAttribute($name, $value);
|
||||
}
|
||||
|
||||
$next = function (
|
||||
ServerRequestInterface $rqst,
|
||||
ResponseInterface $resp
|
||||
): ResponseInterface {
|
||||
return $resp;
|
||||
};
|
||||
|
||||
$response = $this->response;
|
||||
|
||||
$response = $this->dispatcher->dispatch(
|
||||
$this->stack,
|
||||
$request,
|
||||
$response,
|
||||
$next
|
||||
);
|
||||
|
||||
$this->transmitter->transmit($request, $response);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
/* Configuration */
|
||||
|
||||
/**
|
||||
* @param array $attributes
|
||||
* @return Server
|
||||
*/
|
||||
public function setAttributes(array $attributes): Server
|
||||
{
|
||||
$this->attributes = $attributes;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param DispatcherInterface $dispatcher
|
||||
* @return Server
|
||||
*/
|
||||
public function setDispatcher(DispatcherInterface $dispatcher): Server
|
||||
{
|
||||
$this->dispatcher = $dispatcher;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $name
|
||||
* @return Server
|
||||
*/
|
||||
public function setPathVariablesAttributeName(string $name): Server
|
||||
{
|
||||
$this->pathVariablesAttributeName = $name;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ServerRequestInterface $request
|
||||
* @return Server
|
||||
*/
|
||||
public function setRequest(ServerRequestInterface $request): Server
|
||||
{
|
||||
$this->request = $request;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ResponseInterface $response
|
||||
* @return Server
|
||||
*/
|
||||
public function setResponse(ResponseInterface $response): Server
|
||||
{
|
||||
$this->response = $response;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param TransmitterInterface $transmitter
|
||||
* @return Server
|
||||
*/
|
||||
public function setTransmitter(TransmitterInterface $transmitter): Server
|
||||
{
|
||||
$this->transmitter = $transmitter;
|
||||
return $this;
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
/* Defaults */
|
||||
|
||||
private function getRequest(): ServerRequestInterface
|
||||
{
|
||||
if (!$this->request) {
|
||||
$marshaller = new ServerRequestMarshaller();
|
||||
return $marshaller->getServerRequest();
|
||||
}
|
||||
return $this->request;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,104 @@
|
|||
<?php
|
||||
|
||||
namespace WellRESTed\Transmission;
|
||||
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Psr\Http\Message\StreamInterface;
|
||||
|
||||
class Transmitter implements TransmitterInterface
|
||||
{
|
||||
/** @var int */
|
||||
private $chunkSize = 8192;
|
||||
|
||||
/**
|
||||
* Outputs a response to the client.
|
||||
*
|
||||
* This method outputs the status line, headers, and body to the client.
|
||||
*
|
||||
* This method will also provide a Content-length header if:
|
||||
* - Response does not have a Content-length header
|
||||
* - Response does not have a Transfer-encoding: chunked header
|
||||
* - Response body stream is readable and reports a non-null size
|
||||
*
|
||||
* @param ServerRequestInterface $request
|
||||
* @param ResponseInterface $response Response to output
|
||||
*/
|
||||
public function transmit(
|
||||
ServerRequestInterface $request,
|
||||
ResponseInterface $response
|
||||
): void {
|
||||
// Prepare the response for output.
|
||||
$response = $this->prepareResponse($request, $response);
|
||||
|
||||
// Status Line
|
||||
header($this->getStatusLine($response));
|
||||
// Headers
|
||||
foreach ($response->getHeaders() as $key => $headers) {
|
||||
$replace = true;
|
||||
foreach ($headers as $header) {
|
||||
header("$key: $header", $replace);
|
||||
$replace = false;
|
||||
}
|
||||
}
|
||||
// Body
|
||||
$body = $response->getBody();
|
||||
if ($body->isReadable()) {
|
||||
$this->outputBody($response->getBody());
|
||||
}
|
||||
}
|
||||
|
||||
public function setChunkSize(int $chunkSize): void
|
||||
{
|
||||
$this->chunkSize = $chunkSize;
|
||||
}
|
||||
|
||||
private function prepareResponse(
|
||||
ServerRequestInterface $request,
|
||||
ResponseInterface $response
|
||||
): ResponseInterface {
|
||||
|
||||
// Add Content-length header to the response when all of these are true:
|
||||
//
|
||||
// - Response does not have a Content-length header
|
||||
// - Response does not have a Transfer-encoding: chunked header
|
||||
// - Response body stream is readable and reports a non-null size
|
||||
//
|
||||
$contentLengthMissing = !$response->hasHeader('Content-length');
|
||||
$notChunked = strtolower($response->getHeaderLine('Transfer-encoding'))
|
||||
!== 'chunked';
|
||||
$size = $response->getBody()->getSize();
|
||||
|
||||
if ($contentLengthMissing && $notChunked && $size !== null) {
|
||||
$response = $response->withHeader('Content-length', (string) $size);
|
||||
}
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
private function getStatusLine(ResponseInterface $response): string
|
||||
{
|
||||
$protocol = $response->getProtocolVersion();
|
||||
$statusCode = $response->getStatusCode();
|
||||
$reasonPhrase = $response->getReasonPhrase();
|
||||
if ($reasonPhrase) {
|
||||
return "HTTP/$protocol $statusCode $reasonPhrase";
|
||||
} else {
|
||||
return "HTTP/$protocol $statusCode";
|
||||
}
|
||||
}
|
||||
|
||||
private function outputBody(StreamInterface $body): void
|
||||
{
|
||||
if ($this->chunkSize > 0) {
|
||||
if ($body->isSeekable()) {
|
||||
$body->rewind();
|
||||
}
|
||||
while (!$body->eof()) {
|
||||
print $body->read($this->chunkSize);
|
||||
}
|
||||
} else {
|
||||
print (string) $body;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
<?php
|
||||
|
||||
namespace WellRESTed\Transmission;
|
||||
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
|
||||
interface TransmitterInterface
|
||||
{
|
||||
/**
|
||||
* Outputs a response to the client.
|
||||
*
|
||||
* This method MUST output the status line, headers, and body to the client.
|
||||
*
|
||||
* Implementations MAY add response headers to ensure expected headers are
|
||||
* presents but MUST NOT alter existing headers.
|
||||
*
|
||||
* @param ServerRequestInterface $request
|
||||
* @param ResponseInterface $response Response to output
|
||||
*/
|
||||
public function transmit(ServerRequestInterface $request, ResponseInterface $response): void;
|
||||
}
|
||||
|
|
@ -1,131 +0,0 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* pjdietz\WellRESTed\Client
|
||||
*
|
||||
* @author PJ Dietz <pj@pjdietz.com>
|
||||
* @copyright Copyright 2015 by PJ Dietz
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
namespace pjdietz\WellRESTed;
|
||||
|
||||
use pjdietz\WellRESTed\Exceptions\CurlException;
|
||||
use pjdietz\WellRESTed\Interfaces\RequestInterface;
|
||||
use pjdietz\WellRESTed\Interfaces\ResponseInterface;
|
||||
|
||||
/**
|
||||
* Class for making HTTP requests using cURL.
|
||||
*/
|
||||
class Client
|
||||
{
|
||||
/** @var array Map of cURL options with cURL constants as keys */
|
||||
private $curlOpts;
|
||||
|
||||
/**
|
||||
* Create a new client.
|
||||
*
|
||||
* You may optionally provide an array of cURL options to use by default.
|
||||
* Options passed in the requset method will override these.
|
||||
*
|
||||
* @param array $curlOpts Optional array of cURL options
|
||||
*/
|
||||
public function __construct(array $curlOpts = null)
|
||||
{
|
||||
if (is_array($curlOpts)) {
|
||||
$this->curlOpts = $curlOpts;
|
||||
} else {
|
||||
$this->curlOpts = array();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Make an HTTP request and return a Response.
|
||||
*
|
||||
* @param RequestInterface $rqst
|
||||
* @param array $curlOpts Optional array of cURL options
|
||||
* @throws \pjdietz\WellRESTed\Exceptions\CurlException
|
||||
* @return ResponseInterface
|
||||
*/
|
||||
public function request(RequestInterface $rqst, $curlOpts = null)
|
||||
{
|
||||
$ch = curl_init();
|
||||
|
||||
$headers = array();
|
||||
foreach ($rqst->getHeaders() as $field => $value) {
|
||||
$headers[] = sprintf('%s: %s', $field, $value);
|
||||
}
|
||||
|
||||
$options = $this->curlOpts;
|
||||
$options[CURLOPT_URL] = $rqst->getUri();
|
||||
$options[CURLOPT_PORT] = $rqst->getPort();
|
||||
$options[CURLOPT_HEADER] = 1;
|
||||
$options[CURLOPT_RETURNTRANSFER] = 1;
|
||||
$options[CURLOPT_HTTPHEADER] = $headers;
|
||||
|
||||
// Set the method. Include the body, if needed.
|
||||
switch ($rqst->getMethod()) {
|
||||
case 'GET':
|
||||
$options[CURLOPT_HTTPGET] = 1;
|
||||
break;
|
||||
case 'POST':
|
||||
$options[CURLOPT_POST] = 1;
|
||||
$options[CURLOPT_POSTFIELDS] = $rqst->getBody();
|
||||
break;
|
||||
case 'PUT':
|
||||
$options[CURLOPT_CUSTOMREQUEST] = 'PUT';
|
||||
$options[CURLOPT_POSTFIELDS] = $rqst->getBody();
|
||||
break;
|
||||
default:
|
||||
$options[CURLOPT_CUSTOMREQUEST] = $rqst->getMethod();
|
||||
$options[CURLOPT_POSTFIELDS] = $rqst->getBody();
|
||||
break;
|
||||
}
|
||||
|
||||
// Override cURL options with the user options passed in.
|
||||
if ($curlOpts) {
|
||||
foreach ($curlOpts as $optKey => $optValue) {
|
||||
$options[$optKey] = $optValue;
|
||||
}
|
||||
}
|
||||
|
||||
// Set the cURL options.
|
||||
curl_setopt_array($ch, $options);
|
||||
|
||||
// Make the cURL request.
|
||||
$result = curl_exec($ch);
|
||||
|
||||
// Throw an exception in the event of a cURL error.
|
||||
if ($result === false) {
|
||||
$error = curl_error($ch);
|
||||
$errno = curl_errno($ch);
|
||||
curl_close($ch);
|
||||
throw new CurlException($error, $errno);
|
||||
}
|
||||
|
||||
// Make a reponse to populate and return with data obtained via cURL.
|
||||
$resp = new Response();
|
||||
|
||||
$resp->setStatusCode(curl_getinfo($ch, CURLINFO_HTTP_CODE));
|
||||
|
||||
// Split the result into headers and body.
|
||||
$headerSize = curl_getinfo($ch, CURLINFO_HEADER_SIZE);
|
||||
$headers = substr($result, 0, $headerSize);
|
||||
$body = substr($result, $headerSize);
|
||||
|
||||
// Set the body. Do not auto-add the Content-length header.
|
||||
$resp->setBody($body, false);
|
||||
|
||||
// Iterate over the headers line by line and add each one.
|
||||
foreach (explode("\r\n", $headers) as $header) {
|
||||
if (strpos($header, ':')) {
|
||||
list ($headerName, $headerValue) = explode(':', $header, 2);
|
||||
$resp->setHeader($headerName, ltrim($headerValue));
|
||||
}
|
||||
}
|
||||
|
||||
curl_close($ch);
|
||||
|
||||
return $resp;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,19 +0,0 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* pjdietz\WellRESTed\Exceptions\CurlException
|
||||
*
|
||||
* @author PJ Dietz <pj@pjdietz.com>
|
||||
* @copyright Copyright 2015 by PJ Dietz
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
namespace pjdietz\WellRESTed\Exceptions;
|
||||
|
||||
/**
|
||||
* Exception related to a cURL operations. The message and code should correspond
|
||||
* to the cURL error and error number that caused the excpetion.
|
||||
*/
|
||||
class CurlException extends WellRESTedException
|
||||
{
|
||||
}
|
||||
|
|
@ -1,108 +0,0 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* HttpException and its subclasses provides exceptions that correspond to HTTP
|
||||
* error status codes. The most common are included, but you may create
|
||||
* additional subclasses if needed by subclassing HttpException.
|
||||
*
|
||||
* The HttpException classes are intended to be used with Routers or Handler
|
||||
* subclasses (pjdietz\WellRESTed\Handler). These classes will catch
|
||||
* HttpExceptions and convert them to responses using the exception's code as
|
||||
* the HTTP status code and the exception's message as the response body.
|
||||
*
|
||||
* @author PJ Dietz <pj@pjdietz.com>
|
||||
* @copyright Copyright 2015 by PJ Dietz
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
namespace pjdietz\WellRESTed\Exceptions\HttpExceptions;
|
||||
|
||||
use pjdietz\WellRESTed\Exceptions\WellRESTedException;
|
||||
|
||||
/**
|
||||
* Base exception for HTTP-related errors. Also represents a 500 Internal Server error.
|
||||
*/
|
||||
class HttpException extends WellRESTedException
|
||||
{
|
||||
/** @var int HTTP Status Code */
|
||||
protected $code = 500;
|
||||
/** @var string Default description for the error */
|
||||
protected $message = "500 Internal Server Error";
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a 400 Bad Request error.
|
||||
*/
|
||||
class BadRequestException extends HttpException
|
||||
{
|
||||
/** @var int HTTP Status Code */
|
||||
protected $code = 400;
|
||||
/** @var string Default description for the error */
|
||||
protected $message = "400 Bad Request";
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a 401 Unauthorization error.
|
||||
*/
|
||||
class UnauthorizedException extends HttpException
|
||||
{
|
||||
/** @var int HTTP Status Code */
|
||||
protected $code = 401;
|
||||
/** @var string Default description for the error */
|
||||
protected $message = "401 Unauthorized";
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a 403 Forbidden error.
|
||||
*/
|
||||
class ForbiddenException extends HttpException
|
||||
{
|
||||
/** @var int HTTP Status Code */
|
||||
protected $code = 403;
|
||||
/** @var string Default description for the error */
|
||||
protected $message = "403 Forbidden";
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a 404 Not Found error.
|
||||
*/
|
||||
class NotFoundException extends HttpException
|
||||
{
|
||||
/** @var int HTTP Status Code */
|
||||
protected $code = 404;
|
||||
/** @var string Default description for the error */
|
||||
protected $message = "404 Not Found";
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a 405 Method Not Allowed error.
|
||||
*/
|
||||
class MethodNotAllowedException extends HttpException
|
||||
{
|
||||
/** @var int HTTP Status Code */
|
||||
protected $code = 405;
|
||||
/** @var string Default description for the error */
|
||||
protected $message = "405 Method Not Allowed";
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a 409 Conflict error.
|
||||
*/
|
||||
class ConflictException extends HttpException
|
||||
{
|
||||
/** @var int HTTP Status Code */
|
||||
protected $code = 409;
|
||||
/** @var string Default description for the error */
|
||||
protected $message = "409 Conflict";
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a 410 Gone error.
|
||||
*/
|
||||
class GoneException extends HttpException
|
||||
{
|
||||
/** @var int HTTP Status Code */
|
||||
protected $code = 410;
|
||||
/** @var string Default description for the error */
|
||||
protected $message = "410 Gone";
|
||||
}
|
||||
|
|
@ -1,18 +0,0 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* pjdietz\WellRESTed\Exceptions\ParseException
|
||||
*
|
||||
* @author PJ Dietz <pj@pjdietz.com>
|
||||
* @copyright Copyright 2015 by PJ Dietz
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
namespace pjdietz\WellRESTed\Exceptions;
|
||||
|
||||
/**
|
||||
* Exception related to a parsing data.
|
||||
*/
|
||||
class ParseException extends WellRESTedException
|
||||
{
|
||||
}
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* pjdietz\WellRESTed\Exceptions\WellRESTedException
|
||||
*
|
||||
* @author PJ Dietz <pj@pjdietz.com>
|
||||
* @copyright Copyright 2013 by PJ Dietz
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
namespace pjdietz\WellRESTed\Exceptions;
|
||||
|
||||
use Exception;
|
||||
|
||||
/**
|
||||
* Top-level class for custom exceptions thrown by WellRESTed.
|
||||
*/
|
||||
class WellRESTedException extends Exception
|
||||
{
|
||||
}
|
||||
|
|
@ -1,207 +0,0 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* pjdietz\WellRESTed\Handler
|
||||
*
|
||||
* @author PJ Dietz <pj@pjdietz.com>
|
||||
* @copyright Copyright 2015 by PJ Dietz
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
namespace pjdietz\WellRESTed;
|
||||
|
||||
use pjdietz\WellRESTed\Exceptions\HttpExceptions\HttpException;
|
||||
use pjdietz\WellRESTed\Interfaces\HandlerInterface;
|
||||
use pjdietz\WellRESTed\Interfaces\RequestInterface;
|
||||
use pjdietz\WellRESTed\Interfaces\ResponseInterface;
|
||||
|
||||
/**
|
||||
* Responds to a request based on the HTTP method.
|
||||
*
|
||||
* To use Handler, create a subclass and implement the methods for any HTTP
|
||||
* verbs you would like to support. (get() for GET, post() for POST, etc).
|
||||
*
|
||||
* - Access the request via the protected member $this->request
|
||||
* - Access a map of arguments via $this->args (e.g., URI path variables)
|
||||
* - Modify $this->response to provide the response the instance will return
|
||||
*/
|
||||
abstract class Handler implements HandlerInterface
|
||||
{
|
||||
/** @var array Map of variables to supplement the request, usually path variables. */
|
||||
protected $args;
|
||||
/** @var RequestInterface The HTTP request to respond to. */
|
||||
protected $request;
|
||||
/** @var ResponseInterface The HTTP response to send based on the request. */
|
||||
protected $response;
|
||||
|
||||
/**
|
||||
* Return the handled response.
|
||||
*
|
||||
* @param RequestInterface $request
|
||||
* @param array|null $args
|
||||
* @return ResponseInterface
|
||||
*/
|
||||
public function getResponse(RequestInterface $request, array $args = null)
|
||||
{
|
||||
$this->request = $request;
|
||||
$this->args = $args;
|
||||
$this->response = new Response();
|
||||
try {
|
||||
$this->buildResponse();
|
||||
} catch (HttpException $e) {
|
||||
$this->response->setStatusCode($e->getCode());
|
||||
$this->response->setBody($e->getMessage());
|
||||
}
|
||||
return $this->response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare the Response. Override this method if your subclass needs to
|
||||
* repond to any non-standard HTTP methods. Otherwise, override the
|
||||
* get, post, put, etc. methods.
|
||||
*
|
||||
* An uncaught HttpException (or subclass) will be converted to a response
|
||||
* using the exception's code as the status code and the exceptios message
|
||||
* as the body.
|
||||
*/
|
||||
protected function buildResponse()
|
||||
{
|
||||
switch ($this->request->getMethod()) {
|
||||
case 'GET':
|
||||
$this->get();
|
||||
break;
|
||||
case 'HEAD':
|
||||
$this->head();
|
||||
break;
|
||||
case 'POST':
|
||||
$this->post();
|
||||
break;
|
||||
case 'PUT':
|
||||
$this->put();
|
||||
break;
|
||||
case 'DELETE':
|
||||
$this->delete();
|
||||
break;
|
||||
case 'PATCH':
|
||||
$this->patch();
|
||||
break;
|
||||
case 'OPTIONS':
|
||||
$this->options();
|
||||
break;
|
||||
default:
|
||||
$this->respondWithMethodNotAllowed();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Method for handling HTTP GET requests.
|
||||
*
|
||||
* This method should modify the instance's response member.
|
||||
*/
|
||||
protected function get()
|
||||
{
|
||||
$this->respondWithMethodNotAllowed();
|
||||
}
|
||||
|
||||
/**
|
||||
* Method for handling HTTP HEAD requests.
|
||||
*
|
||||
* This method should modify the instance's response member.
|
||||
*/
|
||||
protected function head()
|
||||
{
|
||||
// The default function calls the instance's get() method, then sets
|
||||
// the resonse's body member to an empty string.
|
||||
$this->get();
|
||||
$this->response->setBody('', false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Method for handling HTTP POST requests.
|
||||
*
|
||||
* This method should modify the instance's response member.
|
||||
*/
|
||||
protected function post()
|
||||
{
|
||||
$this->respondWithMethodNotAllowed();
|
||||
}
|
||||
|
||||
/**
|
||||
* Method for handling HTTP PUT requests.
|
||||
*
|
||||
* This method should modify the instance's response member.
|
||||
*/
|
||||
protected function put()
|
||||
{
|
||||
$this->respondWithMethodNotAllowed();
|
||||
}
|
||||
|
||||
/**
|
||||
* Method for handling HTTP DELETE requests.
|
||||
*
|
||||
* This method should modify the instance's response member.
|
||||
*/
|
||||
protected function delete()
|
||||
{
|
||||
$this->respondWithMethodNotAllowed();
|
||||
}
|
||||
|
||||
/**
|
||||
* Method for handling HTTP PATCH requests.
|
||||
*
|
||||
* This method should modify the instance's response member.
|
||||
*/
|
||||
protected function patch()
|
||||
{
|
||||
$this->respondWithMethodNotAllowed();
|
||||
}
|
||||
|
||||
/**
|
||||
* Method for handling HTTP OPTION requests.
|
||||
*
|
||||
* This method should modify the instance's response member.
|
||||
*/
|
||||
protected function options()
|
||||
{
|
||||
if ($this->addAllowHeader()) {
|
||||
$this->response->setStatusCode(200);
|
||||
} else {
|
||||
$this->response->setStatusCode(405);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Provide a default response for unsupported methods.
|
||||
*/
|
||||
protected function respondWithMethodNotAllowed()
|
||||
{
|
||||
$this->response->setStatusCode(405);
|
||||
$this->addAllowHeader();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return an array of HTTP verbs this handler supports.
|
||||
*
|
||||
* For example, to support GET and POST, return array("GET","POST");
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function getAllowedMethods()
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an Allow: header using the methods returned by getAllowedMethods()
|
||||
*
|
||||
* @return bool The header was added.
|
||||
*/
|
||||
protected function addAllowHeader()
|
||||
{
|
||||
$methods = $this->getAllowedMethods();
|
||||
if ($methods) {
|
||||
$this->response->setHeader('Allow', join($methods, ', '));
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,37 +0,0 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* pjdietz\WellRESTed\HandlerUnpacker
|
||||
*
|
||||
* @author PJ Dietz <pj@pjdietz.com>
|
||||
* @copyright Copyright 2015 by PJ Dietz
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
namespace pjdietz\WellRESTed;
|
||||
|
||||
use pjdietz\WellRESTed\Interfaces\RequestInterface;
|
||||
|
||||
/**
|
||||
* Class for retreiving a handler or response from a callable, string, or instance.
|
||||
*/
|
||||
class HandlerUnpacker
|
||||
{
|
||||
/**
|
||||
* Return the handler or response from a callable, string, or instance.
|
||||
*
|
||||
* @param $handler
|
||||
* @param RequestInterface $request
|
||||
* @param array $args
|
||||
* @return mixed
|
||||
*/
|
||||
public function unpack($handler, RequestInterface $request = null, array $args = null)
|
||||
{
|
||||
if (is_callable($handler)) {
|
||||
$handler = $handler($request, $args);
|
||||
} elseif (is_string($handler)) {
|
||||
$handler = new $handler();
|
||||
}
|
||||
return $handler;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,26 +0,0 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* pjdietz\WellRESTed\Interfaces\HandlerInterface
|
||||
*
|
||||
* @author PJ Dietz <pj@pjdietz.com>
|
||||
* @copyright Copyright 2015 by PJ Dietz
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
namespace pjdietz\WellRESTed\Interfaces;
|
||||
|
||||
/**
|
||||
* Provides a mechanism for obtaining a response given a request.
|
||||
*/
|
||||
interface HandlerInterface
|
||||
{
|
||||
/**
|
||||
* Return the handled response.
|
||||
*
|
||||
* @param RequestInterface $request The request to respond to.
|
||||
* @param array|null $args Optional additional arguments.
|
||||
* @return ResponseInterface The handled response.
|
||||
*/
|
||||
public function getResponse(RequestInterface $request, array $args = null);
|
||||
}
|
||||
|
|
@ -1,84 +0,0 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* pjdietz\WellRESTed\Interfaces\RequestInterface
|
||||
*
|
||||
* @author PJ Dietz <pj@pjdietz.com>
|
||||
* @copyright Copyright 2015 by PJ Dietz
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
namespace pjdietz\WellRESTed\Interfaces;
|
||||
|
||||
/**
|
||||
* Interface for representing an HTTP request.
|
||||
*/
|
||||
interface RequestInterface
|
||||
{
|
||||
/**
|
||||
* Return the HTTP verb (e.g., GET, POST, PUT).
|
||||
*
|
||||
* @return string Request verb
|
||||
*/
|
||||
public function getMethod();
|
||||
|
||||
/**
|
||||
* Return the full URI for the request.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getUri();
|
||||
|
||||
/**
|
||||
* Return the hostname portion of the URI
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getHostname();
|
||||
|
||||
/**
|
||||
* Return path component of the request URI.
|
||||
*
|
||||
* @return string Path component
|
||||
*/
|
||||
public function getPath();
|
||||
|
||||
/**
|
||||
* Return the HTTP port
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getPort();
|
||||
|
||||
/**
|
||||
* Return an associative array of query paramters.
|
||||
*
|
||||
* @return array Query paramters
|
||||
*/
|
||||
public function getQuery();
|
||||
|
||||
/**
|
||||
* Return the value for a given header.
|
||||
*
|
||||
* Per RFC 2616, HTTP headers are case-insensitive. Take care to conform to
|
||||
* this when implementing.
|
||||
*
|
||||
* @param string $headerName Field name of the header
|
||||
* @return string Header field value
|
||||
*/
|
||||
public function getHeader($headerName);
|
||||
|
||||
/**
|
||||
* Return an associative array of headers.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getHeaders();
|
||||
|
||||
/**
|
||||
* Return the body of the request.
|
||||
*
|
||||
* @return string Request body
|
||||
*/
|
||||
public function getBody();
|
||||
}
|
||||
|
|
@ -1,70 +0,0 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* pjdietz\WellRESTed\Interfaces\ResponseInterface
|
||||
*
|
||||
* @author PJ Dietz <pj@pjdietz.com>
|
||||
* @copyright Copyright 2015 by PJ Dietz
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
namespace pjdietz\WellRESTed\Interfaces;
|
||||
|
||||
/**
|
||||
* Interface for representing an HTTP response.
|
||||
*/
|
||||
interface ResponseInterface
|
||||
{
|
||||
/**
|
||||
* Return the HTTP status code
|
||||
*
|
||||
* @return int HTTP status code
|
||||
*/
|
||||
public function getStatusCode();
|
||||
|
||||
/**
|
||||
* Set the status code for the response.
|
||||
*
|
||||
* @param int $statusCode HTTP status code
|
||||
*/
|
||||
public function setStatusCode($statusCode);
|
||||
|
||||
/**
|
||||
* Return the value for a given header.
|
||||
*
|
||||
* Per RFC 2616, HTTP headers are case-insensitive. Take care to conform to
|
||||
* this when implementing.
|
||||
*
|
||||
* @param string $headerName Field name of the header
|
||||
* @return string Header field value
|
||||
*/
|
||||
public function getHeader($headerName);
|
||||
|
||||
/**
|
||||
* Set the value for a given header.
|
||||
*
|
||||
* Per RFC 2616, HTTP headers are case-insensitive. Take care to conform to
|
||||
* this when implementing.
|
||||
*
|
||||
* @param string $headerName Field name
|
||||
* @param string $headerValue Field value
|
||||
*/
|
||||
public function setHeader($headerName, $headerValue);
|
||||
|
||||
/**
|
||||
* Return the body of the response.
|
||||
*
|
||||
* @return string Response body
|
||||
*/
|
||||
public function getBody();
|
||||
|
||||
/**
|
||||
* Set the body of the response.
|
||||
*
|
||||
* @param string $body Response body
|
||||
*/
|
||||
public function setBody($body);
|
||||
|
||||
/** Issue the reponse to the client. */
|
||||
public function respond();
|
||||
}
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* pjdietz\WellRESTed\Interfaces\Route\PrefixRouteInterface
|
||||
*
|
||||
* @author PJ Dietz <pj@pjdietz.com>
|
||||
* @copyright Copyright 2015 by PJ Dietz
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
namespace pjdietz\WellRESTed\Interfaces\Routes;
|
||||
|
||||
/**
|
||||
* Interface for routes that map to paths begining with a given prefix or prefixes
|
||||
*/
|
||||
interface PrefixRouteInterface
|
||||
{
|
||||
/**
|
||||
* Returns the path prefixes the instance maps to a target handler.
|
||||
*
|
||||
* @return string[] List array of path prefixes.
|
||||
*/
|
||||
public function getPrefixes();
|
||||
}
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* pjdietz\WellRESTed\Interfaces\Route\StaticRouteInterface
|
||||
*
|
||||
* @author PJ Dietz <pj@pjdietz.com>
|
||||
* @copyright Copyright 2015 by PJ Dietz
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
namespace pjdietz\WellRESTed\Interfaces\Routes;
|
||||
|
||||
/**
|
||||
* Interface for routes that map to an exact path or paths
|
||||
*/
|
||||
interface StaticRouteInterface
|
||||
{
|
||||
/**
|
||||
* Returns the paths the instance maps to a target handler.
|
||||
*
|
||||
* @return string[] List array of paths.
|
||||
*/
|
||||
public function getPaths();
|
||||
}
|
||||
|
|
@ -1,144 +0,0 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* pjdietz\WellRESTed\Message
|
||||
*
|
||||
* @author PJ Dietz <pj@pjdietz.com>
|
||||
* @copyright Copyright 2015 by PJ Dietz
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
namespace pjdietz\WellRESTed;
|
||||
|
||||
/**
|
||||
* Common base class for the Request and Response classes.
|
||||
*/
|
||||
abstract class Message
|
||||
{
|
||||
/** @var string Entity body of the message */
|
||||
protected $body;
|
||||
/** @var array Associative array of HTTP headers */
|
||||
protected $headers;
|
||||
/**
|
||||
* Associative array of lowercase header field names as keys with
|
||||
* corresponding case sensitive field names from the $headers array as
|
||||
* values.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $headerLookup;
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Create a new HTTP message.
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
$this->headers = array();
|
||||
$this->headerLookup = array();
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Accessors
|
||||
|
||||
/**
|
||||
* Return the body payload of the instance.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getBody()
|
||||
{
|
||||
return $this->body;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the body for the request.
|
||||
*
|
||||
* @param string $body
|
||||
*/
|
||||
public function setBody($body)
|
||||
{
|
||||
$this->body = $body;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return an associative array of all set headers.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getHeaders()
|
||||
{
|
||||
return $this->headers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the value of a given header, or null if it does not exist.
|
||||
*
|
||||
* @param string $name
|
||||
* @return string|null
|
||||
*/
|
||||
public function getHeader($name)
|
||||
{
|
||||
$lowerName = strtolower($name);
|
||||
if (isset($this->headerLookup[$lowerName])) {
|
||||
$realName = $this->headerLookup[$lowerName];
|
||||
return $this->headers[$realName];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add or update a header to a given value
|
||||
*
|
||||
* @param string $name
|
||||
* @param string $value
|
||||
*/
|
||||
public function setHeader($name, $value)
|
||||
{
|
||||
$lowerName = strtolower($name);
|
||||
|
||||
// Check if a mapping already exists for this header.
|
||||
// Remove it, if needed.
|
||||
if (isset($this->headerLookup[$lowerName])
|
||||
&& $this->headerLookup[$lowerName] !== $name
|
||||
) {
|
||||
unset($this->headers[$this->headerLookup[$lowerName]]);
|
||||
}
|
||||
|
||||
// Store the actual header.
|
||||
$this->headers[$name] = $value;
|
||||
|
||||
// Store a mapping to the user's prefered case.
|
||||
$this->headerLookup[$lowerName] = $name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return if the response contains a header with the given key.
|
||||
*
|
||||
* @param $name
|
||||
* @return bool
|
||||
*/
|
||||
public function issetHeader($name)
|
||||
{
|
||||
$lowerName = strtolower($name);
|
||||
return isset($this->headerLookup[$lowerName]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a header. This method does nothing if the header does not exist.
|
||||
*
|
||||
* @param string $name
|
||||
*/
|
||||
public function unsetHeader($name)
|
||||
{
|
||||
$lowerName = strtolower($name);
|
||||
if (isset($this->headerLookup[$lowerName])) {
|
||||
$realName = $this->headerLookup[$lowerName];
|
||||
if (isset($this->headers[$realName])) {
|
||||
unset($this->headers[$realName]);
|
||||
}
|
||||
unset($this->headerLookup[$lowerName]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,365 +0,0 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* pjdietz\WellRESTed\Request
|
||||
*
|
||||
* @author PJ Dietz <pj@pjdietz.com>
|
||||
* @copyright Copyright 2015 by PJ Dietz
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
namespace pjdietz\WellRESTed;
|
||||
|
||||
use InvalidArgumentException;
|
||||
use pjdietz\WellRESTed\Interfaces\RequestInterface;
|
||||
use UnexpectedValueException;
|
||||
|
||||
/**
|
||||
* A Request instance represents an HTTP request.
|
||||
*/
|
||||
class Request extends Message implements RequestInterface
|
||||
{
|
||||
/**
|
||||
* Singleton instance derived from reading the request sent to the server.
|
||||
*
|
||||
* @var Request
|
||||
* @static
|
||||
*/
|
||||
static protected $theRequest;
|
||||
/** @var string HTTP method or verb for the request */
|
||||
private $method = "GET";
|
||||
/** @var string Scheme for the request (Must be "http" or "https" */
|
||||
private $scheme;
|
||||
/** @var string The Hostname for the request */
|
||||
private $hostname = "localhost";
|
||||
/** @var string Path component of the URI for the request */
|
||||
private $path = "/";
|
||||
/** @var array Array of fragments of the path, delimited by slashes */
|
||||
private $pathParts;
|
||||
/** @var int HTTP Port */
|
||||
private $port = 80;
|
||||
/** @var array Associative array of query parameters */
|
||||
private $query;
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Create a new Request instance.
|
||||
*
|
||||
* @param string|null $uri
|
||||
* @param string $method
|
||||
*/
|
||||
public function __construct($uri = null, $method = "GET")
|
||||
{
|
||||
parent::__construct();
|
||||
if (!is_null($uri)) {
|
||||
$this->setUri($uri);
|
||||
}
|
||||
$this->method = $method;
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Return a reference to the singleton instance of the Request derived
|
||||
* from the server's information about the request sent to the server.
|
||||
*
|
||||
* @return Request
|
||||
* @static
|
||||
*/
|
||||
public static function getRequest()
|
||||
{
|
||||
if (!isset(self::$theRequest)) {
|
||||
$request = new Request();
|
||||
$request->readHttpRequest();
|
||||
self::$theRequest = $request;
|
||||
}
|
||||
return self::$theRequest;
|
||||
}
|
||||
|
||||
/**
|
||||
* Read and return all request headers from the request issued to the server.
|
||||
*
|
||||
* @return array Associative array of headers
|
||||
*/
|
||||
public static function getRequestHeaders()
|
||||
{
|
||||
// Prefer apache_request_headers is available.
|
||||
if (function_exists('apache_request_headers')) {
|
||||
return apache_request_headers();
|
||||
}
|
||||
|
||||
// http://www.php.net/manual/en/function.getallheaders.php#84262
|
||||
$headers = array();
|
||||
foreach ($_SERVER as $name => $value) {
|
||||
if (substr($name, 0, 5) === "HTTP_") {
|
||||
$headers[str_replace(" ", "-", ucwords(strtolower(str_replace("_", " ", substr($name, 5)))))] = $value;
|
||||
}
|
||||
}
|
||||
return $headers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set instance members based on the HTTP request sent to the server.
|
||||
*/
|
||||
public function readHttpRequest()
|
||||
{
|
||||
$this->setBody(file_get_contents("php://input"), false);
|
||||
$this->headers = self::getRequestHeaders();
|
||||
|
||||
// Add case insensitive headers to the lookup table.
|
||||
foreach ($this->headers as $key => $value) {
|
||||
$this->headerLookup[strtolower($key)] = $key;
|
||||
}
|
||||
|
||||
$this->setMethod($_SERVER["REQUEST_METHOD"]);
|
||||
$this->setUri($_SERVER["REQUEST_URI"]);
|
||||
$this->setHostname($_SERVER["HTTP_HOST"]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the method (e.g., GET, POST, PUT, DELETE)
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getMethod()
|
||||
{
|
||||
return $this->method;
|
||||
}
|
||||
|
||||
/**
|
||||
* Assign the method (e.g., GET, POST, PUT, DELETE)
|
||||
*
|
||||
* @param string $method
|
||||
*/
|
||||
public function setMethod($method)
|
||||
{
|
||||
$this->method = $method;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the full URI includeing protocol, hostname, path, and query.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getUri()
|
||||
{
|
||||
$uri = $this->scheme . "://" . $this->hostname;
|
||||
if ($this->port !== $this->getDefaultPort()) {
|
||||
$uri .= ":" . $this->port;
|
||||
}
|
||||
if ($this->path !== "/") {
|
||||
$uri .= $this->path;
|
||||
}
|
||||
if ($this->query) {
|
||||
$uri .= "?" . http_build_query($this->query);
|
||||
}
|
||||
return $uri;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the URI for the Request. This sets the other members: hostname,
|
||||
* path, port, and query.
|
||||
*
|
||||
* @param string $uri
|
||||
*/
|
||||
public function setUri($uri)
|
||||
{
|
||||
// Provide http and localhost if missing.
|
||||
if ($uri[0] === "/") {
|
||||
$uri = "http://localhost" . $uri;
|
||||
} elseif (strpos($uri, "://") === false) {
|
||||
$uri = "http://" . $uri;
|
||||
}
|
||||
|
||||
$parsed = parse_url($uri);
|
||||
|
||||
$scheme = isset($parsed["scheme"]) ? $parsed["scheme"] : "http";
|
||||
$this->setScheme($scheme);
|
||||
|
||||
$host = isset($parsed["host"]) ? $parsed["host"] : "localhost";
|
||||
$this->setHostname($host);
|
||||
|
||||
$port = isset($parsed["port"]) ? (int) $parsed["port"] : $this->getDefaultPort();
|
||||
$this->setPort($port);
|
||||
|
||||
$path = isset($parsed["path"]) ? $parsed["path"] : "/";
|
||||
$this->setPath($path);
|
||||
|
||||
$query = isset($parsed["query"]) ? $parsed["query"] : "";
|
||||
$this->setQuery($query);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the hostname portion of the URI
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getHostname()
|
||||
{
|
||||
return $this->hostname;
|
||||
}
|
||||
|
||||
/**
|
||||
* Assign the hostname portion of the URI
|
||||
*
|
||||
* @param string $hostname
|
||||
*/
|
||||
public function setHostname($hostname)
|
||||
{
|
||||
$this->hostname = $hostname;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the scheme for the request (either "http" or "https")
|
||||
*
|
||||
* @param string $scheme
|
||||
* @throws \UnexpectedValueException
|
||||
*/
|
||||
public function setScheme($scheme)
|
||||
{
|
||||
$scheme = strtolower($scheme);
|
||||
if (!in_array($scheme, array("http", "https"))) {
|
||||
throw new UnexpectedValueException('Scheme must be "http" or "https".');
|
||||
}
|
||||
$this->scheme = $scheme;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the scheme for the request (either "http" or "https")
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getScheme()
|
||||
{
|
||||
return $this->scheme;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the path part of the URI as a string.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getPath()
|
||||
{
|
||||
return $this->path;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the path and pathParts members.
|
||||
*
|
||||
* @param string $path
|
||||
*/
|
||||
public function setPath($path)
|
||||
{
|
||||
$this->path = $path;
|
||||
if ($path !== "/") {
|
||||
$this->pathParts = explode("/", substr($path, 1));
|
||||
} else {
|
||||
$this->pathParts = array();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return an array of the sections of the path delimited by slashes.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getPathParts()
|
||||
{
|
||||
return $this->pathParts;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the HTTP port
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getPort()
|
||||
{
|
||||
return $this->port;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the HTTP port
|
||||
*
|
||||
* @param int $port
|
||||
*/
|
||||
public function setPort($port = null)
|
||||
{
|
||||
if (is_null($port)) {
|
||||
$port = $this->getDefaultPort();
|
||||
}
|
||||
$this->port = $port;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return an associative array representing the query.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getQuery()
|
||||
{
|
||||
return $this->query;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the query. The value passed can be a query string of key-value pairs
|
||||
* joined by ampersands or it can be an associative array.
|
||||
*
|
||||
* @param string|array $query
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
public function setQuery($query)
|
||||
{
|
||||
if (is_string($query)) {
|
||||
$qs = $query;
|
||||
parse_str($qs, $query);
|
||||
} elseif (is_object($query)) {
|
||||
$query = (array) $query;
|
||||
}
|
||||
|
||||
if (is_array($query)) {
|
||||
ksort($query);
|
||||
$this->query = $query;
|
||||
} else {
|
||||
throw new InvalidArgumentException("Unable to parse query string.");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the form fields for this request.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getFormFields()
|
||||
{
|
||||
parse_str($this->getBody(), $fields);
|
||||
return $fields;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the body by supplying an associative array of form fields.
|
||||
*
|
||||
* In addition, add a "Content-type: application/x-www-form-urlencoded" header
|
||||
*
|
||||
* @param array $fields
|
||||
*/
|
||||
public function setFormFields(array $fields)
|
||||
{
|
||||
$this->setBody(http_build_query($fields));
|
||||
$this->setHeader("Content-type", "application/x-www-form-urlencoded");
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Return the default port for the currently set scheme.
|
||||
*
|
||||
* @return int;
|
||||
*/
|
||||
protected function getDefaultPort()
|
||||
{
|
||||
return $this->scheme === "http" ? 80 : 443;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,321 +0,0 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* pjdietz\WellRESTed\Response
|
||||
*
|
||||
* @author PJ Dietz <pj@pjdietz.com>
|
||||
* @copyright Copyright 2015 by PJ Dietz
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
namespace pjdietz\WellRESTed;
|
||||
|
||||
use InvalidArgumentException;
|
||||
use pjdietz\WellRESTed\Interfaces\ResponseInterface;
|
||||
|
||||
/**
|
||||
* A Response instance allows you to build an HTTP response and send it when
|
||||
* finished.
|
||||
*/
|
||||
class Response extends Message implements ResponseInterface
|
||||
{
|
||||
const CHUNK_SIZE = 1048576;
|
||||
|
||||
/** @var string Path to a file to read and output as the body. */
|
||||
private $bodyFilePath;
|
||||
/**
|
||||
* Text explanation of the HTTP Status Code. You only need to set this if
|
||||
* you are using nonstandard status codes. Otherwise, the instance will
|
||||
* set the when you update the status code.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private $reasonPhrase;
|
||||
/** @var int HTTP status code */
|
||||
private $statusCode;
|
||||
/** @var string HTTP protocol and version */
|
||||
private $protocol = "HTTP/1.1";
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Create a new Response instance, optionally passing a status code, body,
|
||||
* and headers.
|
||||
*
|
||||
* @param int $statusCode
|
||||
* @param string $body
|
||||
* @param array $headers
|
||||
*/
|
||||
public function __construct($statusCode = 500, $body = null, $headers = null)
|
||||
{
|
||||
parent::__construct();
|
||||
|
||||
$this->setStatusCode($statusCode);
|
||||
|
||||
if (is_array($headers)) {
|
||||
foreach ($headers as $key => $value) {
|
||||
$this->setHeader($key, $value);
|
||||
}
|
||||
}
|
||||
|
||||
if (!is_null($body)) {
|
||||
$this->body = $body;
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Accessors
|
||||
|
||||
/**
|
||||
* Provide a new entity body for the respone.
|
||||
* This method also updates the content-length header based on the length
|
||||
* of the new body string.
|
||||
*
|
||||
* @param string $value
|
||||
* @param bool $setContentLength Automatically add a Content-length header
|
||||
*/
|
||||
public function setBody($value, $setContentLength = true)
|
||||
{
|
||||
$this->body = $value;
|
||||
if ($setContentLength === true) {
|
||||
$this->setHeader('Content-Length', strlen($value));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Provide the path to a file to output as the response body.
|
||||
*
|
||||
* @param string $bodyFilePath Filepath
|
||||
*/
|
||||
public function setBodyFilePath($bodyFilePath)
|
||||
{
|
||||
$this->bodyFilePath = $bodyFilePath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the path to the file to output as the response body.
|
||||
*
|
||||
* @return string Filepath
|
||||
*/
|
||||
public function getBodyFilePath()
|
||||
{
|
||||
return $this->bodyFilePath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the portion of the status line explaining the status.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getReasonPhrase()
|
||||
{
|
||||
return $this->reasonPhrase;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true for status codes in the 1xx-3xx range.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function getSuccess()
|
||||
{
|
||||
return $this->statusCode < 400;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the HTTP status code for the response.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getStatusCode()
|
||||
{
|
||||
return $this->statusCode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the HTTP status line, e.g. HTTP/1.1 200 OK.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getStatusLine()
|
||||
{
|
||||
return $this->protocol . " " . $this->statusCode . " " . $this->reasonPhrase;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the status code and optionally the reason phrase explaining it.
|
||||
*
|
||||
* @param int $statusCode
|
||||
* @param string|null $reasonPhrase
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
public function setStatusCode($statusCode, $reasonPhrase = null)
|
||||
{
|
||||
$this->statusCode = (int) $statusCode;
|
||||
if (is_null($reasonPhrase)) {
|
||||
switch ($this->statusCode) {
|
||||
case 100:
|
||||
$text = 'Continue';
|
||||
break;
|
||||
case 101:
|
||||
$text = 'Switching Protocols';
|
||||
break;
|
||||
case 200:
|
||||
$text = 'OK';
|
||||
break;
|
||||
case 201:
|
||||
$text = 'Created';
|
||||
break;
|
||||
case 202:
|
||||
$text = 'Accepted';
|
||||
break;
|
||||
case 203:
|
||||
$text = 'Non-Authoritative Information';
|
||||
break;
|
||||
case 204:
|
||||
$text = 'No Content';
|
||||
break;
|
||||
case 205:
|
||||
$text = 'Reset Content';
|
||||
break;
|
||||
case 206:
|
||||
$text = 'Partial Content';
|
||||
break;
|
||||
case 300:
|
||||
$text = 'Multiple Choices';
|
||||
break;
|
||||
case 301:
|
||||
$text = 'Moved Permanently';
|
||||
break;
|
||||
case 302:
|
||||
$text = 'Found';
|
||||
break;
|
||||
case 303:
|
||||
$text = 'See Other';
|
||||
break;
|
||||
case 304:
|
||||
$text = 'Not Modified';
|
||||
break;
|
||||
case 305:
|
||||
$text = 'Use Proxy';
|
||||
break;
|
||||
case 400:
|
||||
$text = 'Bad Request';
|
||||
break;
|
||||
case 401:
|
||||
$text = 'Unauthorized';
|
||||
break;
|
||||
case 402:
|
||||
$text = 'Payment Required';
|
||||
break;
|
||||
case 403:
|
||||
$text = 'Forbidden';
|
||||
break;
|
||||
case 404:
|
||||
$text = 'Not Found';
|
||||
break;
|
||||
case 405:
|
||||
$text = 'Method Not Allowed';
|
||||
break;
|
||||
case 406:
|
||||
$text = 'Not Acceptable';
|
||||
break;
|
||||
case 407:
|
||||
$text = 'Proxy Authentication Required';
|
||||
break;
|
||||
case 408:
|
||||
$text = 'Request Timeout';
|
||||
break;
|
||||
case 409:
|
||||
$text = 'Conflict';
|
||||
break;
|
||||
case 410:
|
||||
$text = 'Gone';
|
||||
break;
|
||||
case 411:
|
||||
$text = 'Length Required';
|
||||
break;
|
||||
case 412:
|
||||
$text = 'Precondition Failed';
|
||||
break;
|
||||
case 413:
|
||||
$text = 'Request Entity Too Large';
|
||||
break;
|
||||
case 414:
|
||||
$text = 'Request-URI Too Long';
|
||||
break;
|
||||
case 415:
|
||||
$text = 'Unsupported Media Type';
|
||||
break;
|
||||
case 500:
|
||||
$text = 'Internal Server Error';
|
||||
break;
|
||||
case 501:
|
||||
$text = 'Not Implemented';
|
||||
break;
|
||||
case 502:
|
||||
$text = 'Bad Gateway';
|
||||
break;
|
||||
case 503:
|
||||
$text = 'Service Unavailable';
|
||||
break;
|
||||
case 504:
|
||||
$text = 'Gateway Timeout';
|
||||
break;
|
||||
case 505:
|
||||
$text = 'HTTP Version Not Supported';
|
||||
break;
|
||||
default:
|
||||
$text = 'Nonstandard';
|
||||
break;
|
||||
}
|
||||
$this->reasonPhrase = $text;
|
||||
} else {
|
||||
if (is_string($reasonPhrase)) {
|
||||
$this->reasonPhrase = $reasonPhrase;
|
||||
} else {
|
||||
throw new InvalidArgumentException('$reasonPhrase must be a string (or null to use standard HTTP Reason-Phrase');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Output the response to the client.
|
||||
*
|
||||
* @param bool $headersOnly Do not include the body, only the headers.
|
||||
*/
|
||||
public function respond($headersOnly = false)
|
||||
{
|
||||
// Output the HTTP status code.
|
||||
header($this->getStatusLine());
|
||||
|
||||
// Output each header.
|
||||
foreach ($this->headers as $header => $value) {
|
||||
header($header . ': ' . $value);
|
||||
}
|
||||
|
||||
// Output the entity body.
|
||||
if (!$headersOnly) {
|
||||
if (isset($this->bodyFilePath) && $this->bodyFilePath && file_exists($this->bodyFilePath)) {
|
||||
$this->outputBodyFile();
|
||||
} else {
|
||||
print $this->body;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/** Output the contents of a file */
|
||||
private function outputBodyFile()
|
||||
{
|
||||
$handle = fopen($this->getBodyFilePath(), 'rb');
|
||||
if ($handle !== false) {
|
||||
while (!feof($handle)) {
|
||||
$buffer = fread($handle, self::CHUNK_SIZE);
|
||||
print $buffer;
|
||||
flush();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,272 +0,0 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* pjdietz\WellRESTed\RouteBuilder
|
||||
*
|
||||
* @author PJ Dietz <pj@pjdietz.com>
|
||||
* @copyright Copyright 2015 by PJ Dietz
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
namespace pjdietz\WellRESTed;
|
||||
|
||||
use pjdietz\WellRESTed\Exceptions\ParseException;
|
||||
use pjdietz\WellRESTed\Interfaces\HandlerInterface;
|
||||
use pjdietz\WellRESTed\Routes\RegexRoute;
|
||||
use pjdietz\WellRESTed\Routes\StaticRoute;
|
||||
use pjdietz\WellRESTed\Routes\TemplateRoute;
|
||||
|
||||
/**
|
||||
* Class for facilitating constructing Routers.
|
||||
*
|
||||
* @deprecated Use {@see Router::add} instead.
|
||||
* @see Router::add
|
||||
*/
|
||||
class RouteBuilder
|
||||
{
|
||||
/** @var string Regex pattern to use for URI template patters. */
|
||||
private $defaultVariablePattern;
|
||||
/** @var string Common prefix to affix to handler class names. */
|
||||
private $handlerNamespace;
|
||||
/** @var array Associative array of variable names and regex patterns. */
|
||||
private $templateVariablePatterns;
|
||||
|
||||
/**
|
||||
* Create a new RouteBuilder
|
||||
*
|
||||
* @deprecated Use {@see Router::add} instead.
|
||||
* @see Router::add
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
trigger_error("RouteBuilder is deprecated. Use Router::add", E_USER_DEPRECATED);
|
||||
}
|
||||
|
||||
/**
|
||||
* Contruct and return an array of routes.
|
||||
*
|
||||
* If $data is a string, buildRoutes() will parse it as JSON with json_decode.
|
||||
*
|
||||
* If $data is an array, buildRoutes() assumes each item in the array is
|
||||
* an object it can translate into a route.
|
||||
*
|
||||
* If $data is an object, buildRoutes() assumes it will have a "routes"
|
||||
* property with an array value that is a collection of objects to
|
||||
* translate into routes. Any other properties will be read with
|
||||
* readConfiguration()
|
||||
*
|
||||
* @param string|array|object $data Description of routes to build.
|
||||
* @return array List of routes to add to a router.
|
||||
* @throws Exceptions\ParseException
|
||||
*/
|
||||
public function buildRoutes($data)
|
||||
{
|
||||
// If $data is a string, attempt to parse it as JSON.
|
||||
if (is_string($data)) {
|
||||
$data = json_decode($data);
|
||||
if (is_null($data)) {
|
||||
throw new ParseException("Unable to parse as JSON.");
|
||||
}
|
||||
}
|
||||
|
||||
// Locate the list of routes. This should be one of these:
|
||||
// - If $data is an object, $data->routes
|
||||
// - If $data is an array, $data
|
||||
if (is_array($data)) {
|
||||
$dataRoutes = $data;
|
||||
} elseif (is_object($data) && isset($data->routes) && is_array($data->routes)) {
|
||||
$dataRoutes = $data->routes;
|
||||
$this->readConfiguration($data);
|
||||
} else {
|
||||
throw new ParseException("Unable to parse. Missing array of routes.");
|
||||
}
|
||||
|
||||
// Build a route instance and append it to the list.
|
||||
$routes = array();
|
||||
foreach ($dataRoutes as $item) {
|
||||
$routes[] = $this->buildRoute($item);
|
||||
}
|
||||
return $routes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse an object and update the instances with the new configuration.
|
||||
*
|
||||
* handlerNamespace is passed to setHandlerNamesapce()
|
||||
|
||||
* variablePattern is passed to setDefaultVariablePattern()
|
||||
*
|
||||
* vars is passed to setTemplateVars()
|
||||
*
|
||||
* @param object
|
||||
*/
|
||||
public function readConfiguration($data)
|
||||
{
|
||||
if (isset($data->handlerNamespace)) {
|
||||
$this->setHandlerNamespace($data->handlerNamespace);
|
||||
}
|
||||
if (isset($data->variablePattern)) {
|
||||
$this->setDefaultVariablePattern($data->variablePattern);
|
||||
}
|
||||
if (isset($data->vars)) {
|
||||
$this->setTemplateVars((array) $data->vars);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the string to prepend to handler class names.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getHandlerNamespace()
|
||||
{
|
||||
return $this->handlerNamespace;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the prefix to prepend to handler class names.
|
||||
*
|
||||
* @param mixed $handlerNamespace
|
||||
*/
|
||||
public function setHandlerNamespace($handlerNamespace = "")
|
||||
{
|
||||
$this->handlerNamespace = $handlerNamespace;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return an associative array of variable names and regex patterns.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function getTemplateVars()
|
||||
{
|
||||
return $this->templateVariablePatterns;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the array of template variable patterns.
|
||||
*
|
||||
* Keys are names of variables for use in URI template (do not include {}).
|
||||
* Values are regex patterns or any of the following special names: SLUG,
|
||||
* ALPHA, ALPHANUM, DIGIT, NUM.
|
||||
*
|
||||
* If you wish to use additional named patterns, subclass RouteBuilder and
|
||||
* override getTemplateVariablePattern.
|
||||
*
|
||||
* @param array $vars Associative array of variable name => pattern
|
||||
*/
|
||||
public function setTemplateVars(array $vars)
|
||||
{
|
||||
foreach ($vars as $name => $var) {
|
||||
$vars[$name] = $this->getTemplateVariablePattern($var);
|
||||
}
|
||||
$this->templateVariablePatterns = $vars;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the default regex pattern to use for URI template variables.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getDefaultVariablePattern()
|
||||
{
|
||||
return $this->defaultVariablePattern;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the default regex pattern to use for URI template variables.
|
||||
*
|
||||
* $defaultVariablePattern may be a regex pattern or one of the following:
|
||||
* SLUG, ALPHA, ALPHANUM, DIGIT, NUM.
|
||||
*
|
||||
* If you wish to use additional named patterns, subclass RouteBuilder and
|
||||
* override getTemplateVariablePattern.
|
||||
*
|
||||
* @param mixed $defaultVariablePattern
|
||||
*/
|
||||
public function setDefaultVariablePattern($defaultVariablePattern)
|
||||
{
|
||||
$this->defaultVariablePattern = $this->getTemplateVariablePattern($defaultVariablePattern);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create and return an appropriate route given an object describing a route.
|
||||
*
|
||||
* $item must contain a "handler" property providing the classname for the
|
||||
* HandlerInterface to call getResponse() on if the route matches. "handler"
|
||||
* may be fully qualified and begin with "\". If it does not begin with "\",
|
||||
* the instance's $handlerNamespace is affixed to the begining.
|
||||
*
|
||||
* $item must also contain a "path", "template", or "pattern" property to
|
||||
* indicate how to create the StaticRoute, TemplateRoute, or RegexRoute.
|
||||
*
|
||||
* @param object|array $item
|
||||
* @return HandlerInterface
|
||||
* @throws Exceptions\ParseException
|
||||
*/
|
||||
protected function buildRoute($item)
|
||||
{
|
||||
// Determine the handler for this route.
|
||||
if (isset($item->handler)) {
|
||||
$handler = $item->handler;
|
||||
if ($handler[0] != "\\") {
|
||||
$handler = $this->getHandlerNamespace() . "\\" . $handler;
|
||||
}
|
||||
} else {
|
||||
throw new ParseException("Unable to parse. Route is missing a handler.");
|
||||
}
|
||||
|
||||
// Static Route
|
||||
if (isset($item->path)) {
|
||||
return new StaticRoute($item->path, $handler);
|
||||
}
|
||||
|
||||
// Template Route
|
||||
if (isset($item->template)) {
|
||||
$vars = isset($item->vars) ? (array) $item->vars : array();
|
||||
foreach ($vars as $name => $var) {
|
||||
$vars[$name] = $this->getTemplateVariablePattern($var);
|
||||
}
|
||||
if ($this->templateVariablePatterns) {
|
||||
$vars = array_merge($this->templateVariablePatterns, $vars);
|
||||
}
|
||||
return new TemplateRoute($item->template, $handler, $this->getDefaultVariablePattern(), $vars);
|
||||
}
|
||||
|
||||
// Regex Route
|
||||
if (isset($item->pattern)) {
|
||||
return new RegexRoute($item->pattern, $handler);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Provide a regular expression pattern given a name.
|
||||
*
|
||||
* The names SLUG, ALPHA, ALPHANUM, DIGIT, NUM convert to regex patterns.
|
||||
* Anything else passes through as is.
|
||||
*
|
||||
* If you wish to use additional named patterns, subclass RouteBuilder and
|
||||
* override getTemplateVariablePattern.
|
||||
*
|
||||
* @param string $variable Regex pattern or name (SLUG, ALPHA, ALPHANUM, DIGIT, NUM
|
||||
* @return string
|
||||
*/
|
||||
protected function getTemplateVariablePattern($variable)
|
||||
{
|
||||
switch ($variable) {
|
||||
case "SLUG":
|
||||
return TemplateRoute::RE_SLUG;
|
||||
case "ALPHA":
|
||||
return TemplateRoute::RE_ALPHA;
|
||||
case "ALPHANUM":
|
||||
return TemplateRoute::RE_ALPHANUM;
|
||||
case "DIGIT":
|
||||
case "NUM":
|
||||
return TemplateRoute::RE_NUM;
|
||||
default:
|
||||
return $variable;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,166 +0,0 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* pjdietz\WellRESTed\RouteTable
|
||||
*
|
||||
* @author PJ Dietz <pj@pjdietz.com>
|
||||
* @copyright Copyright 2015 by PJ Dietz
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
namespace pjdietz\WellRESTed;
|
||||
|
||||
use pjdietz\WellRESTed\Interfaces\HandlerInterface;
|
||||
use pjdietz\WellRESTed\Interfaces\RequestInterface;
|
||||
use pjdietz\WellRESTed\Interfaces\ResponseInterface;
|
||||
use pjdietz\WellRESTed\Interfaces\Routes\PrefixRouteInterface;
|
||||
use pjdietz\WellRESTed\Interfaces\Routes\StaticRouteInterface;
|
||||
|
||||
/**
|
||||
* RouteTable
|
||||
*
|
||||
* A RouteTable uses the request path to dispatche the best-matching handler.
|
||||
*/
|
||||
class RouteTable implements HandlerInterface
|
||||
{
|
||||
/** @var HandlerInterface[] Array of Route objects */
|
||||
private $routes;
|
||||
/** @var array Hash array mapping exact paths to routes */
|
||||
private $staticRoutes;
|
||||
/** @var array Hash array mapping path prefixes to routes */
|
||||
private $prefixRoutes;
|
||||
|
||||
/** Create a new RouteTable */
|
||||
public function __construct()
|
||||
{
|
||||
$this->routes = array();
|
||||
$this->prefixRoutes = array();
|
||||
$this->staticRoutes = array();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the response from the best matching route.
|
||||
*
|
||||
* @param RequestInterface $request
|
||||
* @param array|null $args
|
||||
* @return ResponseInterface
|
||||
*/
|
||||
public function getResponse(RequestInterface $request, array $args = null)
|
||||
{
|
||||
$response = null;
|
||||
|
||||
// First check if there is a static route.
|
||||
$response = $this->getStaticResponse($request, $args);
|
||||
if ($response) {
|
||||
return $response;
|
||||
}
|
||||
|
||||
// Check prefix routes for any routes that match. Use the longest matching prefix.
|
||||
$response = $this->getPrefixResponse($request, $args);
|
||||
if ($response) {
|
||||
return $response;
|
||||
}
|
||||
|
||||
// Try each of the routes.
|
||||
foreach ($this->routes as $route) {
|
||||
/** @var HandlerInterface $route */
|
||||
$response = $route->getResponse($request, $args);
|
||||
if ($response) {
|
||||
return $response;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the response from the matching static route, or null if none match.
|
||||
*
|
||||
* @param RequestInterface $request
|
||||
* @param array|null $args
|
||||
* @return ResponseInterface
|
||||
*/
|
||||
private function getStaticResponse(RequestInterface $request, array $args = null)
|
||||
{
|
||||
$path = $request->getPath();
|
||||
if (isset($this->staticRoutes[$path])) {
|
||||
$route = $this->staticRoutes[$path];
|
||||
return $route->getResponse($request, $args);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returning the response from the best-matching prefix handler, or null if none match.
|
||||
*
|
||||
* @param RequestInterface $request
|
||||
* @param array|null $args
|
||||
* @return ResponseInterface
|
||||
*/
|
||||
private function getPrefixResponse(RequestInterface $request, array $args = null)
|
||||
{
|
||||
$path = $request->getPath();
|
||||
|
||||
// Find all prefixes that match the start of this path.
|
||||
$prefixes = array_keys($this->prefixRoutes);
|
||||
$matches = array_filter(
|
||||
$prefixes,
|
||||
function ($prefix) use ($path) {
|
||||
return (strrpos($path, $prefix, -strlen($path)) !== false);
|
||||
}
|
||||
);
|
||||
|
||||
if ($matches) {
|
||||
if (count($matches) > 0) {
|
||||
// If there are multiple matches, sort them to find the one with the longest string length.
|
||||
$compareByLength = function ($a, $b) {
|
||||
return strlen($b) - strlen($a);
|
||||
};
|
||||
usort($matches, $compareByLength);
|
||||
}
|
||||
$route = $this->prefixRoutes[$matches[0]];
|
||||
return $route->getResponse($request, $args);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Append a new route.
|
||||
*
|
||||
* @param HandlerInterface $route
|
||||
*/
|
||||
public function addRoute(HandlerInterface $route)
|
||||
{
|
||||
if ($route instanceof StaticRouteInterface) {
|
||||
$this->addStaticRoute($route);
|
||||
} elseif ($route instanceof PrefixRouteInterface) {
|
||||
$this->addPrefixRoute($route);
|
||||
} else {
|
||||
$this->routes[] = $route;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a new static route.
|
||||
*
|
||||
* @param StaticRouteInterface $staticRoute
|
||||
*/
|
||||
private function addStaticRoute(StaticRouteInterface $staticRoute)
|
||||
{
|
||||
foreach ($staticRoute->getPaths() as $path) {
|
||||
$this->staticRoutes[$path] = $staticRoute;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a new prefix route.
|
||||
*
|
||||
* @param PrefixRouteInterface $prefixRoute
|
||||
*/
|
||||
private function addPrefixRoute(PrefixRouteInterface $prefixRoute)
|
||||
{
|
||||
foreach ($prefixRoute->getPrefixes() as $prefix) {
|
||||
$this->prefixRoutes[$prefix] = $prefixRoute;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,256 +0,0 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* pjdietz\WellRESTed\Router
|
||||
*
|
||||
* @author PJ Dietz <pj@pjdietz.com>
|
||||
* @copyright Copyright 2015 by PJ Dietz
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
namespace pjdietz\WellRESTed;
|
||||
|
||||
use pjdietz\WellRESTed\Exceptions\HttpExceptions\HttpException;
|
||||
use pjdietz\WellRESTed\Interfaces\HandlerInterface;
|
||||
use pjdietz\WellRESTed\Interfaces\RequestInterface;
|
||||
use pjdietz\WellRESTed\Interfaces\ResponseInterface;
|
||||
use pjdietz\WellRESTed\Routes\PrefixRoute;
|
||||
use pjdietz\WellRESTed\Routes\RouteFactory;
|
||||
use pjdietz\WellRESTed\Routes\StaticRoute;
|
||||
|
||||
/**
|
||||
* Router
|
||||
*
|
||||
* A Router uses a table of Routes to find the appropriate Handler for a request.
|
||||
*/
|
||||
class Router implements HandlerInterface
|
||||
{
|
||||
/** @var array Hash array of status code => error handler */
|
||||
private $errorHandlers;
|
||||
/** @var RouteTable Collection of routes */
|
||||
private $routeTable;
|
||||
|
||||
/** Create a new Router. */
|
||||
public function __construct()
|
||||
{
|
||||
$this->errorHandlers = array();
|
||||
$this->routeTable = new RouteTable();
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a route or series of routes to the Router.
|
||||
*
|
||||
* When adding a single route, the first argument should be the path, path
|
||||
* prefix, URI template, or regex pattern. The method will attempt to find
|
||||
* the best type of route based on this argument and send the remaining
|
||||
* arguments to the route's constructor. @see {RouteFactory::createRoute}
|
||||
*
|
||||
* To add multiple routes, pass arrays where each array contains an argument list.
|
||||
*/
|
||||
public function add()
|
||||
{
|
||||
$factory = new RouteFactory();
|
||||
|
||||
$args = func_get_args();
|
||||
if (count($args) > 1 && is_array($args[0])) {
|
||||
foreach ($args as $argumentList) {
|
||||
$route = call_user_func_array(array($factory, "createRoute"), $argumentList);
|
||||
$this->addRoute($route);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
$route = call_user_func_array(array($factory, "createRoute"), $args);
|
||||
$this->addRoute($route);
|
||||
}
|
||||
|
||||
/**
|
||||
* Append a series of routes.
|
||||
*
|
||||
* @param array $routes List array of routes
|
||||
*/
|
||||
public function addRoutes(array $routes)
|
||||
{
|
||||
foreach ($routes as $route) {
|
||||
if ($route instanceof HandlerInterface) {
|
||||
$this->addRoute($route);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Append a new route to the route table.
|
||||
*
|
||||
* @param HandlerInterface $route
|
||||
*/
|
||||
public function addRoute(HandlerInterface $route)
|
||||
{
|
||||
$this->routeTable->addRoute($route);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add custom error handlers.
|
||||
*
|
||||
* @param array $errorHandlers Array mapping integer error codes to handlers
|
||||
*/
|
||||
public function setErrorHandlers(array $errorHandlers)
|
||||
{
|
||||
foreach ($errorHandlers as $statusCode => $errorHandler) {
|
||||
$this->setErrorHandler($statusCode, $errorHandler);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a custom error handler.
|
||||
*
|
||||
* @param integer $statusCode The error status code
|
||||
* @param mixed $errorHandler
|
||||
*/
|
||||
public function setErrorHandler($statusCode, $errorHandler)
|
||||
{
|
||||
$this->errorHandlers[$statusCode] = $errorHandler;
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatch the server request through the router and output the response.
|
||||
*
|
||||
* Respond with a 404 Not Found if no route provides a response.
|
||||
* @param array|null $args
|
||||
*/
|
||||
public function respond($args = null)
|
||||
{
|
||||
$request = Request::getRequest();
|
||||
$response = $this->getResponse($request, $args);
|
||||
if (!$response) {
|
||||
$response = $this->getNoRouteResponse($request);
|
||||
}
|
||||
if ($response instanceof ResponseInterface) {
|
||||
$response->respond();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the response built by the handler based on the request
|
||||
*
|
||||
* @param RequestInterface $request
|
||||
* @param array|null $args
|
||||
* @return ResponseInterface
|
||||
*/
|
||||
public function getResponse(RequestInterface $request, array $args = null)
|
||||
{
|
||||
$response = $this->tryResponse($this->routeTable, $request, $args);
|
||||
if ($response && $response instanceof ResponseInterface) {
|
||||
// Check if the router has an error handler for this status code.
|
||||
$status = $response->getStatusCode();
|
||||
$errorResponse = $this->getErrorResponse($status, $request, $args, $response);
|
||||
if ($errorResponse) {
|
||||
return $errorResponse;
|
||||
}
|
||||
}
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare a response indicating a 404 Not Found error.
|
||||
*
|
||||
* Rather than subclassing and overriding this method, you may provide an
|
||||
* error handler for status code 404. (@see setErrorHandler)
|
||||
*
|
||||
* @param RequestInterface $request
|
||||
* @return ResponseInterface
|
||||
*/
|
||||
protected function getNoRouteResponse(RequestInterface $request)
|
||||
{
|
||||
$response = $this->getErrorResponse(404, $request);
|
||||
if ($response) {
|
||||
return $response;
|
||||
}
|
||||
|
||||
$response = new Response(404);
|
||||
$response->setBody('No resource at ' . $request->getPath());
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtain a response from the registered error handlers.
|
||||
*
|
||||
* @param int $status HTTP Status Code
|
||||
* @param RequestInterface $request The original request
|
||||
* @param null $args Optional additional data
|
||||
* @param null $response The response providing the error
|
||||
* @return mixed
|
||||
*/
|
||||
private function getErrorResponse($status, $request, $args = null, $response = null)
|
||||
{
|
||||
if (isset($this->errorHandlers[$status])) {
|
||||
// Pass the response triggering this along to the error handler.
|
||||
$errorArgs = array("response" => $response);
|
||||
if ($args) {
|
||||
$errorArgs = array_merge($args, $errorArgs);
|
||||
}
|
||||
$unpacker = new HandlerUnpacker();
|
||||
$handler = $unpacker->unpack($this->errorHandlers[$status], $request, $errorArgs);
|
||||
if (!is_null($handler) && $handler instanceof HandlerInterface) {
|
||||
return $handler->getResponse($request, $errorArgs);
|
||||
}
|
||||
return $handler;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wraps the getResponse method in a try-catch.
|
||||
*
|
||||
* In an HttpException is caught while trying to get a response, the method
|
||||
* returns a response based on the HttpException's error code and message.
|
||||
*
|
||||
* @param HandlerInterface $handler The Route or Handler to try.
|
||||
* @param RequestInterface $request The incoming request.
|
||||
* @param array $args The array of arguments.
|
||||
* @return Response
|
||||
*/
|
||||
private function tryResponse($handler, $request, $args)
|
||||
{
|
||||
$response = null;
|
||||
try {
|
||||
$response = $handler->getResponse($request, $args);
|
||||
} catch (HttpException $e) {
|
||||
$response = new Response();
|
||||
$response->setStatusCode($e->getCode());
|
||||
$response->setBody($e->getMessage());
|
||||
}
|
||||
return $response;
|
||||
}
|
||||
|
||||
////////////////
|
||||
// Deprecated //
|
||||
////////////////
|
||||
|
||||
/**
|
||||
* Set a route for specific prefix
|
||||
*
|
||||
* @deprecated Use {@see addRoute} instead.
|
||||
* @see addRoute
|
||||
* @param array|string $prefixes
|
||||
* @param mixed $handler
|
||||
*/
|
||||
public function setPrefixRoute($prefixes, $handler)
|
||||
{
|
||||
$this->addRoute(new PrefixRoute($prefixes, $handler));
|
||||
trigger_error("Router::setPrefixRoute is deprecated. Use addRoute", E_USER_DEPRECATED);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a route for a given path
|
||||
*
|
||||
* @deprecated Use {@see addRoute} instead.
|
||||
* @see addRoute
|
||||
* @param array|string $paths
|
||||
* @param mixed $handler
|
||||
*/
|
||||
public function setStaticRoute($paths, $handler)
|
||||
{
|
||||
$this->addRoute(new StaticRoute($paths, $handler));
|
||||
trigger_error("Router::setStaticRoute is deprecated. Use addRoute", E_USER_DEPRECATED);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,62 +0,0 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* pjdietz\WellRESTed\BaseRoute
|
||||
*
|
||||
* @author PJ Dietz <pj@pjdietz.com>
|
||||
* @copyright Copyright 2015 by PJ Dietz
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
namespace pjdietz\WellRESTed\Routes;
|
||||
|
||||
use pjdietz\WellRESTed\HandlerUnpacker;
|
||||
use pjdietz\WellRESTed\Interfaces\HandlerInterface;
|
||||
use pjdietz\WellRESTed\Interfaces\RequestInterface;
|
||||
use pjdietz\WellRESTed\Interfaces\ResponseInterface;
|
||||
|
||||
/**
|
||||
* Base class for Routes.
|
||||
*/
|
||||
abstract class BaseRoute implements HandlerInterface
|
||||
{
|
||||
/** @var callable|string|HandlerInterface Handler to dispatch */
|
||||
private $target;
|
||||
|
||||
/**
|
||||
* Create a new route that will dispatch an instance of the given handler.
|
||||
*
|
||||
* $target may be:
|
||||
* - A callable
|
||||
* - A string containing the fully qualified class of a HandlerInterface
|
||||
* - A HandlerInterface instance
|
||||
*
|
||||
* Callable targets should expect to receive the same arguments as would
|
||||
* be passed to a HandlerInterface's getResponse() method. The callable
|
||||
* should return a HandlerInterface instance, a ResponseInterface instance,
|
||||
* or null.
|
||||
*
|
||||
* @param mixed $target Handler to dispatch
|
||||
*/
|
||||
public function __construct($target)
|
||||
{
|
||||
$this->target = $target;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the handled response.
|
||||
*
|
||||
* @param RequestInterface $request The request to respond to.
|
||||
* @param array|null $args Optional additional arguments.
|
||||
* @return ResponseInterface The response.
|
||||
*/
|
||||
protected function getResponseFromTarget(RequestInterface $request, array $args = null)
|
||||
{
|
||||
$unpacker = new HandlerUnpacker();
|
||||
$target = $unpacker->unpack($this->target, $request, $args);
|
||||
if (!is_null($target) && $target instanceof HandlerInterface) {
|
||||
return $target->getResponse($request, $args);
|
||||
}
|
||||
return $target;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,80 +0,0 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* pjdietz\WellRESTed\Routes\PrefixRoute
|
||||
*
|
||||
* @author PJ Dietz <pj@pjdietz.com>
|
||||
* @copyright Copyright 2015 by PJ Dietz
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
namespace pjdietz\WellRESTed\Routes;
|
||||
|
||||
use InvalidArgumentException;
|
||||
use pjdietz\WellRESTed\Interfaces\RequestInterface;
|
||||
use pjdietz\WellRESTed\Interfaces\ResponseInterface;
|
||||
use pjdietz\WellRESTed\Interfaces\Routes\PrefixRouteInterface;
|
||||
|
||||
/**
|
||||
* Maps a list of static URI paths to a Handler
|
||||
*/
|
||||
class PrefixRoute extends BaseRoute implements PrefixRouteInterface
|
||||
{
|
||||
/** @var string[] List of static URI path prefixes*/
|
||||
private $prefixes;
|
||||
|
||||
/**
|
||||
* Create a new PrefixRoute for a given prefix or prefixes and a handler class.
|
||||
*
|
||||
* @param string|string[] $prefix Path or list of paths the request must match
|
||||
* @param mixed $target Handler to dispatch
|
||||
* @throws \InvalidArgumentException
|
||||
*
|
||||
* @see BaseRoute for details about $target
|
||||
*/
|
||||
public function __construct($prefix, $target)
|
||||
{
|
||||
parent::__construct($target);
|
||||
if (is_string($prefix)) {
|
||||
$this->prefixes = array($prefix);
|
||||
} elseif (is_array($prefix)) {
|
||||
$this->prefixes = $prefix;
|
||||
} else {
|
||||
throw new InvalidArgumentException("$prefix must be a string or array of string");
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
/* HandlerInterface */
|
||||
|
||||
/**
|
||||
* Return the handled response.
|
||||
*
|
||||
* @param RequestInterface $request The request to respond to.
|
||||
* @param array|null $args Optional additional arguments.
|
||||
* @return ResponseInterface
|
||||
*/
|
||||
public function getResponse(RequestInterface $request, array $args = null)
|
||||
{
|
||||
$requestPath = $request->getPath();
|
||||
foreach ($this->prefixes as $prefix) {
|
||||
if (strrpos($requestPath, $prefix, -strlen($requestPath)) !== false) {
|
||||
return $this->getResponseFromTarget($request, $args);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
/* PrefixRouteInterface */
|
||||
|
||||
/**
|
||||
* Returns the path prefixes the instance maps to a target handler.
|
||||
*
|
||||
* @return string[] List array of path prefixes.
|
||||
*/
|
||||
public function getPrefixes()
|
||||
{
|
||||
return $this->prefixes;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,79 +0,0 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* pjdietz\WellRESTed\RegexRout
|
||||
*
|
||||
* @author PJ Dietz <pj@pjdietz.com>
|
||||
* @copyright Copyright 2015 by PJ Dietz
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
namespace pjdietz\WellRESTed\Routes;
|
||||
|
||||
use pjdietz\WellRESTed\Exceptions\ParseException;
|
||||
use pjdietz\WellRESTed\Interfaces\RequestInterface;
|
||||
use pjdietz\WellRESTed\Interfaces\ResponseInterface;
|
||||
|
||||
/**
|
||||
* Maps a regular expression pattern for a URI path to a Handler
|
||||
*/
|
||||
class RegexRoute extends BaseRoute
|
||||
{
|
||||
/** @var string Regular expression pattern for the route. */
|
||||
private $pattern;
|
||||
|
||||
/**
|
||||
* Create a new route mapping a regex pattern to a handler.
|
||||
*
|
||||
* @param string $pattern Regular expression the path must match.
|
||||
* @param mixed $target Handler to dispatch
|
||||
* @throws \InvalidArgumentException
|
||||
*
|
||||
* @see BaseRoute for details about $target
|
||||
*/
|
||||
public function __construct($pattern, $target)
|
||||
{
|
||||
parent::__construct($target);
|
||||
$this->pattern = $pattern;
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
/* HandlerInterface */
|
||||
|
||||
/**
|
||||
* Return the handled response or null.
|
||||
*
|
||||
* A null return value indicates that this route failed to match the request.
|
||||
*
|
||||
* @param RequestInterface $request The request to respond to.
|
||||
* @param array|null $args Optional additional arguments.
|
||||
* @return ResponseInterface The handled response.
|
||||
* @throws ParseException
|
||||
*/
|
||||
public function getResponse(RequestInterface $request, array $args = null)
|
||||
{
|
||||
$matched = @preg_match($this->getPattern(), $request->getPath(), $matches);
|
||||
if ($matched) {
|
||||
if (is_null($args)) {
|
||||
$args = array();
|
||||
}
|
||||
$args = array_merge($args, $matches);
|
||||
return $this->getResponseFromTarget($request, $args);
|
||||
} elseif ($matched === false) {
|
||||
throw new ParseException("Invalid regular expression: " . $this->getPattern());
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Return the regex pattern for the route.
|
||||
*
|
||||
* @return string Regex pattern
|
||||
*/
|
||||
protected function getPattern()
|
||||
{
|
||||
return $this->pattern;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,71 +0,0 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* pjdietz\WellRESTed\RouteCreator
|
||||
*
|
||||
* @author PJ Dietz <pj@pjdietz.com>
|
||||
* @copyright Copyright 2015 by PJ Dietz
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
namespace pjdietz\WellRESTed\Routes;
|
||||
|
||||
use pjdietz\WellRESTed\Interfaces\HandlerInterface;
|
||||
use ReflectionClass;
|
||||
|
||||
/**
|
||||
* Class for creating routes
|
||||
*/
|
||||
class RouteFactory
|
||||
{
|
||||
/**
|
||||
* Create and return a route given a string path, a handler, and optional
|
||||
* extra arguments.
|
||||
*
|
||||
* The method will determine the most appropriate route subclass to use
|
||||
* and will forward the arguments on to the subclass's constructor.
|
||||
*
|
||||
* - Paths with no special characters will generate StaticRoutes
|
||||
* - Paths ending with * will generate PrefixRoutes
|
||||
* - Paths containing URI variables (e.g., {id}) will generate TemplateRoutes
|
||||
* - Regular exressions will generate RegexRoutes
|
||||
*
|
||||
* @param mixed
|
||||
* @return HandlerInterface
|
||||
*/
|
||||
public function createRoute()
|
||||
{
|
||||
$args = func_get_args();
|
||||
$path = $args[0];
|
||||
|
||||
if ($path[0] === "/") {
|
||||
|
||||
// Possible static, prefix, or template
|
||||
|
||||
// PrefixRoutes end with *
|
||||
if (substr($path, -1) === "*") {
|
||||
// Remove the trailing *, since the PrefixRoute constructor doesn't expect it.
|
||||
$path = substr($path, 0, -1);
|
||||
$constructorArgs = $args;
|
||||
$constructorArgs[0] = $path;
|
||||
$reflector = new ReflectionClass("\\pjdietz\\WellRESTed\\Routes\\PrefixRoute");
|
||||
return $reflector->newInstanceArgs($constructorArgs);
|
||||
}
|
||||
|
||||
// TempalateRoutes contain {variable}
|
||||
if (preg_match(TemplateRoute::URI_TEMPLATE_EXPRESSION_RE, $path)) {
|
||||
$reflector = new ReflectionClass("\\pjdietz\\WellRESTed\\Routes\\TemplateRoute");
|
||||
return $reflector->newInstanceArgs($args);
|
||||
}
|
||||
|
||||
// StaticRoute
|
||||
$reflector = new ReflectionClass("\\pjdietz\\WellRESTed\\Routes\\StaticRoute");
|
||||
return $reflector->newInstanceArgs($args);
|
||||
|
||||
}
|
||||
|
||||
// Regex
|
||||
$reflector = new ReflectionClass("\\pjdietz\\WellRESTed\\Routes\\RegexRoute");
|
||||
return $reflector->newInstanceArgs($args);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,77 +0,0 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* pjdietz\WellRESTed\StaticRoute
|
||||
*
|
||||
* @author PJ Dietz <pj@pjdietz.com>
|
||||
* @copyright Copyright 2015 by PJ Dietz
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
namespace pjdietz\WellRESTed\Routes;
|
||||
|
||||
use InvalidArgumentException;
|
||||
use pjdietz\WellRESTed\Interfaces\RequestInterface;
|
||||
use pjdietz\WellRESTed\Interfaces\ResponseInterface;
|
||||
use pjdietz\WellRESTed\Interfaces\Routes\StaticRouteInterface;
|
||||
|
||||
/**
|
||||
* Maps a list of static URI paths to a Handler
|
||||
*/
|
||||
class StaticRoute extends BaseRoute implements StaticRouteInterface
|
||||
{
|
||||
/** @var string[] List of static URI paths */
|
||||
private $paths;
|
||||
|
||||
/**
|
||||
* Create a new StaticRoute for a given path or paths and a handler.
|
||||
*
|
||||
* @param string|array $path Path or list of paths the request must match
|
||||
* @param mixed $target Handler to dispatch
|
||||
* @throws \InvalidArgumentException
|
||||
*
|
||||
* @see BaseRoute for details about $target
|
||||
*/
|
||||
public function __construct($path, $target)
|
||||
{
|
||||
parent::__construct($target);
|
||||
if (is_string($path)) {
|
||||
$this->paths = array($path);
|
||||
} elseif (is_array($path)) {
|
||||
$this->paths = $path;
|
||||
} else {
|
||||
throw new InvalidArgumentException("$path must be a string or array of strings");
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
/* HandlerInterface */
|
||||
|
||||
/**
|
||||
* Return the handled response.
|
||||
*
|
||||
* @param RequestInterface $request The request to respond to.
|
||||
* @param array|null $args Optional additional arguments.
|
||||
* @return ResponseInterface
|
||||
*/
|
||||
public function getResponse(RequestInterface $request, array $args = null)
|
||||
{
|
||||
if (in_array($request->getPath(), $this->paths)) {
|
||||
return $this->getResponseFromTarget($request, $args);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
/* StaticRouteInterface */
|
||||
|
||||
/**
|
||||
* Returns the paths the instance maps to a target handler.
|
||||
*
|
||||
* @return string[] List array of paths.
|
||||
*/
|
||||
public function getPaths()
|
||||
{
|
||||
return $this->paths;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,105 +0,0 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* pjdietz\WellRESTed\TemplateRoute
|
||||
*
|
||||
* @author PJ Dietz <pj@pjdietz.com>
|
||||
* @copyright Copyright 2015 by PJ Dietz
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
namespace pjdietz\WellRESTed\Routes;
|
||||
|
||||
/**
|
||||
* Maps a URI template to a Handler
|
||||
*/
|
||||
class TemplateRoute extends RegexRoute
|
||||
{
|
||||
/**
|
||||
* Regular expression matching URL friendly characters (i.e., letters,
|
||||
* digits, hyphen and underscore)
|
||||
*/
|
||||
const RE_SLUG = '[0-9a-zA-Z\-_]+';
|
||||
/** Regular expression matching digitis */
|
||||
const RE_NUM = '[0-9]+';
|
||||
/** Regular expression matching letters */
|
||||
const RE_ALPHA = '[a-zA-Z]+';
|
||||
/** Regular expression matching letters and digits */
|
||||
const RE_ALPHANUM = '[0-9a-zA-Z]+';
|
||||
/** Regular expression matching a URI template variable (e.g., {id}) */
|
||||
const URI_TEMPLATE_EXPRESSION_RE = '/{([[a-zA-Z][a-zA-Z0-_]*)}/';
|
||||
|
||||
/**
|
||||
* Create a new route that matches a URI template to a handler.
|
||||
*
|
||||
* Optionally provide patterns for the variables in the template.
|
||||
*
|
||||
* @param string $template URI template the path must match
|
||||
* @param mixed $target Handler to dispatch
|
||||
* @param string $defaultPattern Regular expression for variables
|
||||
* @param array $variablePatterns Map of variable names and partial regular expression
|
||||
*
|
||||
* @see BaseRoute for details about $target
|
||||
*/
|
||||
public function __construct(
|
||||
$template,
|
||||
$target,
|
||||
$defaultPattern = self::RE_SLUG,
|
||||
$variablePatterns = null
|
||||
) {
|
||||
$pattern = $this->buildPattern($template, $defaultPattern, $variablePatterns);
|
||||
parent::__construct($pattern, $target);
|
||||
}
|
||||
|
||||
/**
|
||||
* Translate the URI template into a regular expression.
|
||||
*
|
||||
* @param string $template URI template the path must match
|
||||
* @param string $defaultPattern Regular expression for variables
|
||||
* @param array $variablePatterns Map of variable names and regular expression
|
||||
* @return string
|
||||
*/
|
||||
private function buildPattern($template, $defaultPattern, $variablePatterns)
|
||||
{
|
||||
// Ensure $variablePatterns is an array.
|
||||
if (is_null($variablePatterns)) {
|
||||
$variablePatterns = array();
|
||||
} elseif (is_object($variablePatterns)) {
|
||||
$variablePatterns = (array) $variablePatterns;
|
||||
}
|
||||
|
||||
// Ensure a default is set.
|
||||
if (!$defaultPattern) {
|
||||
$defaultPattern = self::RE_SLUG;
|
||||
}
|
||||
|
||||
// Convert the template into the pattern
|
||||
$pattern = $template;
|
||||
|
||||
// Escape allowable characters with regex meaning.
|
||||
$pattern = str_replace(
|
||||
array("-", "."),
|
||||
array("\\-", "\\."),
|
||||
$pattern);
|
||||
|
||||
// Replace * with .* AFTER escaping to avoid escaping .*
|
||||
$pattern = str_replace("*", ".*", $pattern);
|
||||
|
||||
// Surround the pattern with delimiters.
|
||||
$pattern = "~^{$pattern}$~";
|
||||
|
||||
// Replace all template variables with matching subpatterns.
|
||||
$callback = function ($matches) use ($variablePatterns, $defaultPattern) {
|
||||
$key = $matches[1];
|
||||
if (isset($variablePatterns[$key])) {
|
||||
$pattern = $variablePatterns[$key];
|
||||
} else {
|
||||
$pattern = $defaultPattern;
|
||||
}
|
||||
return "(?<{$key}>{$pattern})";
|
||||
};
|
||||
$pattern = preg_replace_callback(self::URI_TEMPLATE_EXPRESSION_RE, $callback, $pattern);
|
||||
|
||||
return $pattern;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +0,0 @@
|
|||
<?php
|
||||
|
||||
print file_get_contents("php://input");
|
||||
exit;
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
<?php
|
||||
|
||||
print(json_encode($_POST));
|
||||
exit;
|
||||
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
<?php
|
||||
|
||||
// http://www.php.net/manual/en/function.getallheaders.php#84262
|
||||
$headers = [];
|
||||
foreach ($_SERVER as $name => $value) {
|
||||
if (substr($name, 0, 5) === 'HTTP_') {
|
||||
$headers[str_replace(' ', '-', ucwords(strtolower(str_replace('_', ' ', substr($name, 5)))))] = $value;
|
||||
}
|
||||
}
|
||||
print json_encode($headers);
|
||||
|
|
@ -1,4 +0,0 @@
|
|||
<?php
|
||||
|
||||
print $_SERVER["REQUEST_METHOD"];
|
||||
exit;
|
||||
|
|
@ -1,102 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace pjdietz\WellRESTed\Test\Integration;
|
||||
|
||||
use pjdietz\WellRESTed\Router;
|
||||
use Prophecy\Argument;
|
||||
|
||||
/**
|
||||
* @coversNothing
|
||||
*/
|
||||
class RouterTest extends \PHPUnit_Framework_TestCase
|
||||
{
|
||||
private $request;
|
||||
private $response;
|
||||
|
||||
public function setUp()
|
||||
{
|
||||
$this->request = $this->prophesize("\\pjdietz\\WellRESTed\\Interfaces\\RequestInterface");
|
||||
$this->request->getPath()->willReturn("/");
|
||||
$this->request->getMethod()->willReturn("GET");
|
||||
$this->response = $this->prophesize("\\pjdietz\\WellRESTed\\Interfaces\\ResponseInterface");
|
||||
$this->response->getStatusCode()->willReturn(200);
|
||||
$this->response->getBody()->willReturn("Hello, world!");
|
||||
}
|
||||
|
||||
public function testDispatchesCallable()
|
||||
{
|
||||
$response = $this->response;
|
||||
|
||||
$router = new Router();
|
||||
$router->add("/", function () use ($response) {
|
||||
return $response->reveal();
|
||||
});
|
||||
|
||||
$result = $router->getResponse($this->request->reveal());
|
||||
$this->assertSame($response->reveal(), $result);
|
||||
}
|
||||
|
||||
public function testDispatchesCallableWithArguments()
|
||||
{
|
||||
$response = $this->response;
|
||||
$args = ["cat" => "molly"];
|
||||
|
||||
$router = new Router();
|
||||
$router->add("/", function ($rqst, $args) use ($response) {
|
||||
$response->getBody()->willReturn($args["cat"]);
|
||||
return $response->reveal();
|
||||
});
|
||||
|
||||
$result = $router->getResponse($this->request->reveal(), $args);
|
||||
$this->assertEquals("molly", $result->getBody());
|
||||
}
|
||||
|
||||
/**
|
||||
* @runInSeparateProcess
|
||||
* @preserveGlobalState disabled
|
||||
*/
|
||||
public function testStopsDispatchingCallablesAfterFirstNonNull()
|
||||
{
|
||||
$router = new Router();
|
||||
$router->add("/cats/{cat}", function () {
|
||||
echo "Hello, cat!";
|
||||
return true;
|
||||
});
|
||||
$router->add("/cats/{cat}", function () {
|
||||
echo "Hello, cat!";
|
||||
});
|
||||
|
||||
$this->request->getPath()->willReturn("/cats/molly");
|
||||
|
||||
ob_start();
|
||||
$router->getResponse($this->request->reveal());
|
||||
$captured = ob_get_contents();
|
||||
ob_end_clean();
|
||||
|
||||
$this->assertEquals("Hello, cat!", $captured);
|
||||
}
|
||||
|
||||
/**
|
||||
* @runInSeparateProcess
|
||||
* @preserveGlobalState disabled
|
||||
*/
|
||||
public function testRouterRespondsWithNoisyCallable()
|
||||
{
|
||||
$_SERVER["REQUEST_URI"] = "/cats/molly";
|
||||
$_SERVER["HTTP_HOST"] = "localhost";
|
||||
$_SERVER["REQUEST_METHOD"] = "GET";
|
||||
|
||||
$router = new Router();
|
||||
$router->add("/cats/{cat}", function () {
|
||||
echo "Hello, cat!";
|
||||
return true;
|
||||
});
|
||||
|
||||
ob_start();
|
||||
@$router->respond();
|
||||
$captured = ob_get_contents();
|
||||
ob_end_clean();
|
||||
|
||||
$this->assertEquals("Hello, cat!", $captured);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,25 +0,0 @@
|
|||
<?php
|
||||
|
||||
// This file must be in the global namespace to add apache_request_headers
|
||||
|
||||
use pjdietz\WellRESTed\Request;
|
||||
|
||||
class ApacheRequestHeadersTest extends \PHPUnit_Framework_TestCase
|
||||
{
|
||||
/**
|
||||
* @covers pjdietz\WellRESTed\Request::getRequestHeaders
|
||||
* @runInSeparateProcess
|
||||
* @preserveGlobalState disabled
|
||||
*/
|
||||
public function testReadsApacheRequestHeaders()
|
||||
{
|
||||
if (!function_exists('apache_request_headers')) {
|
||||
function apache_request_headers() {
|
||||
return array();
|
||||
}
|
||||
}
|
||||
|
||||
$headers = Request::getRequestHeaders();
|
||||
$this->assertNotNull($headers);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,242 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace pjdietz\WellRESTed\Test;
|
||||
|
||||
use Faker\Factory;
|
||||
use pjdietz\ShamServer\ShamServer;
|
||||
use pjdietz\WellRESTed\Client;
|
||||
use pjdietz\WellRESTed\Request;
|
||||
|
||||
/**
|
||||
* @covers pjdietz\WellRESTed\Client
|
||||
*/
|
||||
class ClientTest extends \PHPUnit_Framework_TestCase
|
||||
{
|
||||
/**
|
||||
* @dataProvider httpMethodProvider
|
||||
*/
|
||||
public function testSendsHttpMethod($method)
|
||||
{
|
||||
$host = "localhost";
|
||||
$port = $this->getRandomNumberInRange(getenv("PORT"));
|
||||
$script = realpath(__DIR__ . "/../../sham-routers/method.php");
|
||||
|
||||
$server = new ShamServer($host, $port, $script);
|
||||
|
||||
$rqst = $this->prophesize("\\pjdietz\\WellRESTed\\Interfaces\\RequestInterface");
|
||||
$rqst->getUri()->willReturn("http://$host:$port");
|
||||
$rqst->getMethod()->willReturn($method);
|
||||
$rqst->getPort()->willReturn($port);
|
||||
$rqst->getHeaders()->willReturn([]);
|
||||
$rqst->getBody()->willReturn(null);
|
||||
|
||||
$client = new Client();
|
||||
$resp = $client->request($rqst->reveal());
|
||||
$body = trim($resp->getBody());
|
||||
$this->assertEquals($method, $body);
|
||||
|
||||
$server->stop();
|
||||
}
|
||||
|
||||
public function httpMethodProvider()
|
||||
{
|
||||
return [
|
||||
["GET"],
|
||||
["POST"],
|
||||
["PUT"],
|
||||
["DELETE"],
|
||||
["PATCH"],
|
||||
["OPTIONS"]
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider httpHeaderProvider
|
||||
*/
|
||||
public function testSendsHttpHeaders($headerKey, $headerValue)
|
||||
{
|
||||
$host = "localhost";
|
||||
$port = $this->getRandomNumberInRange(getenv("PORT"));
|
||||
$script = realpath(__DIR__ . "/../../sham-routers/headers.php");
|
||||
|
||||
$server = new ShamServer($host, $port, $script);
|
||||
|
||||
$rqst = $this->prophesize("\\pjdietz\\WellRESTed\\Interfaces\\RequestInterface");
|
||||
$rqst->getUri()->willReturn("http://$host:$port");
|
||||
$rqst->getMethod()->willReturn("GET");
|
||||
$rqst->getPort()->willReturn($port);
|
||||
$rqst->getHeaders()->willReturn([$headerKey => $headerValue]);
|
||||
$rqst->getBody()->willReturn(null);
|
||||
|
||||
$client = new Client();
|
||||
$resp = $client->request($rqst->reveal());
|
||||
$headers = json_decode($resp->getBody());
|
||||
$this->assertEquals($headerValue, $headers->{$headerKey});
|
||||
|
||||
$server->stop();
|
||||
}
|
||||
|
||||
public function httpHeaderProvider()
|
||||
{
|
||||
return [
|
||||
["Cache-Control", "max-age=0"],
|
||||
["X-Custom-Header", "custom value"],
|
||||
["Accept-Charset", "utf-8"]
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider bodyProvider
|
||||
*/
|
||||
public function testSendsBody($body)
|
||||
{
|
||||
$host = "localhost";
|
||||
$port = $this->getRandomNumberInRange(getenv("PORT"));
|
||||
$script = realpath(__DIR__ . "/../../sham-routers/body.php");
|
||||
$server = new ShamServer($host, $port, $script);
|
||||
|
||||
$rqst = $this->prophesize("\\pjdietz\\WellRESTed\\Interfaces\\RequestInterface");
|
||||
$rqst->getUri()->willReturn("http://$host:$port");
|
||||
$rqst->getMethod()->willReturn("POST");
|
||||
$rqst->getPort()->willReturn($port);
|
||||
$rqst->getHeaders()->willReturn([]);
|
||||
$rqst->getBody()->willReturn($body);
|
||||
|
||||
$client = new Client();
|
||||
$resp = $client->request($rqst->reveal());
|
||||
$this->assertEquals($body, $resp->getBody());
|
||||
$server->stop();
|
||||
}
|
||||
|
||||
public function bodyProvider()
|
||||
{
|
||||
$faker = Factory::create();
|
||||
return [
|
||||
[$faker->text()],
|
||||
[$faker->text()],
|
||||
[$faker->text()]
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider formProvider
|
||||
*/
|
||||
public function testSendsForm($form)
|
||||
{
|
||||
$host = "localhost";
|
||||
$port = $this->getRandomNumberInRange(getenv("PORT"));
|
||||
$script = realpath(__DIR__ . "/../../sham-routers/formFields.php");
|
||||
$server = new ShamServer($host, $port, $script);
|
||||
|
||||
$rqst = new Request("http://$host:$port");
|
||||
$rqst->setMethod("POST");
|
||||
$rqst->setFormFields($form);
|
||||
$client = new Client();
|
||||
$resp = $client->request($rqst);
|
||||
|
||||
$body = json_decode($resp->getBody(), true);
|
||||
$this->assertEquals($form, $body);
|
||||
|
||||
$server->stop();
|
||||
}
|
||||
|
||||
public function formProvider()
|
||||
{
|
||||
$faker = Factory::create();
|
||||
return [
|
||||
[
|
||||
[
|
||||
"firstName" => $faker->firstName,
|
||||
"lastName" => $faker->lastName,
|
||||
"email" => $faker->email
|
||||
]
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
public function testSetsCustomCurlOptionsOnInstantiation()
|
||||
{
|
||||
$host = "localhost";
|
||||
$port = $this->getRandomNumberInRange(getenv("PORT"));
|
||||
$script = realpath(__DIR__ . "/../../sham-routers/headers.php");
|
||||
$server = new ShamServer($host, $port, $script);
|
||||
|
||||
$rqst = $this->prophesize("\\pjdietz\\WellRESTed\\Interfaces\\RequestInterface");
|
||||
$rqst->getUri()->willReturn("http://$host:$port");
|
||||
$rqst->getMethod()->willReturn("GET");
|
||||
$rqst->getPort()->willReturn($port);
|
||||
$rqst->getHeaders()->willReturn([]);
|
||||
$rqst->getBody()->willReturn(null);
|
||||
|
||||
$cookieValue = "key=value";
|
||||
$client = new Client([CURLOPT_COOKIE => $cookieValue]);
|
||||
$resp = $client->request($rqst->reveal());
|
||||
$headers = json_decode($resp->getBody());
|
||||
$this->assertEquals($cookieValue, $headers->Cookie);
|
||||
|
||||
$server->stop();
|
||||
}
|
||||
|
||||
public function testSetsCustomCurlOptionsOnRequest()
|
||||
{
|
||||
$host = "localhost";
|
||||
$port = $this->getRandomNumberInRange(getenv("PORT"));
|
||||
$script = realpath(__DIR__ . "/../../sham-routers/headers.php");
|
||||
$server = new ShamServer($host, $port, $script);
|
||||
|
||||
$rqst = $this->prophesize("\\pjdietz\\WellRESTed\\Interfaces\\RequestInterface");
|
||||
$rqst->getUri()->willReturn("http://$host:$port");
|
||||
$rqst->getMethod()->willReturn("GET");
|
||||
$rqst->getPort()->willReturn($port);
|
||||
$rqst->getHeaders()->willReturn([]);
|
||||
$rqst->getBody()->willReturn(null);
|
||||
|
||||
$cookieValue = "key=value";
|
||||
$client = new Client();
|
||||
$resp = $client->request($rqst->reveal(), [CURLOPT_COOKIE => $cookieValue]);
|
||||
$headers = json_decode($resp->getBody());
|
||||
$this->assertEquals($cookieValue, $headers->Cookie);
|
||||
|
||||
$server->stop();
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider curlErrorProvider
|
||||
* @expectedException \pjdietz\WellRESTed\Exceptions\CurlException
|
||||
*/
|
||||
public function testThrowsCurlException($uri, $opts)
|
||||
{
|
||||
$rqst = $this->prophesize("\\pjdietz\\WellRESTed\\Interfaces\\RequestInterface");
|
||||
$rqst->getUri()->willReturn($uri);
|
||||
$rqst->getMethod()->willReturn("GET");
|
||||
$rqst->getPort()->willReturn(parse_url($uri, PHP_URL_PORT));
|
||||
$rqst->getHeaders()->willReturn([]);
|
||||
$rqst->getBody()->willReturn(null);
|
||||
|
||||
$client = new Client();
|
||||
$client->request($rqst->reveal(), $opts);
|
||||
}
|
||||
|
||||
public function curlErrorProvider()
|
||||
{
|
||||
$port = $this->getRandomNumberInRange(getenv("FAIL_PORT"));
|
||||
return [
|
||||
["http://localhost:{$port}", [
|
||||
CURLOPT_FAILONERROR, true,
|
||||
CURLOPT_TIMEOUT_MS, 10
|
||||
]],
|
||||
];
|
||||
}
|
||||
|
||||
private function getRandomNumberInRange($range)
|
||||
{
|
||||
static $pattern = '/(\d+)\-(\d+)/';
|
||||
if (preg_match($pattern, $range, $matches)) {
|
||||
$lower = $matches[1];
|
||||
$upper = $matches[2];
|
||||
return rand($lower, $upper);
|
||||
} else {
|
||||
return $range;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,83 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace pjdietz\WellRESTed\Test;
|
||||
|
||||
use pjdietz\WellRESTed\Exceptions\HttpExceptions\NotFoundException;
|
||||
use pjdietz\WellRESTed\Handler;
|
||||
|
||||
/**
|
||||
* @covers pjdietz\WellRESTed\Handler
|
||||
*/
|
||||
class HandlerTest extends \PHPUnit_Framework_TestCase
|
||||
{
|
||||
public function testReturnsResponse()
|
||||
{
|
||||
$request = $this->prophesize("\\pjdietz\\WellRESTed\\Interfaces\\RequestInterface");
|
||||
$handler = $this->getMockForAbstractClass("\\pjdietz\\WellRESTed\\Handler");
|
||||
$response = $handler->getResponse($request->reveal());
|
||||
$this->assertNotNull($response);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider verbProvider
|
||||
*/
|
||||
public function testCallsMethodForHttpVerb($method)
|
||||
{
|
||||
$request = $this->prophesize("\\pjdietz\\WellRESTed\\Interfaces\\RequestInterface");
|
||||
$request->getMethod()->willReturn($method);
|
||||
$handler = $this->getMockForAbstractClass("\\pjdietz\\WellRESTed\\Handler");
|
||||
$response = $handler->getResponse($request->reveal());
|
||||
$this->assertNotNull($response);
|
||||
}
|
||||
|
||||
public function verbProvider()
|
||||
{
|
||||
return [
|
||||
["GET"],
|
||||
["POST"],
|
||||
["PUT"],
|
||||
["DELETE"],
|
||||
["HEAD"],
|
||||
["PATCH"],
|
||||
["OPTIONS"],
|
||||
["NOTALLOWED"]
|
||||
];
|
||||
}
|
||||
|
||||
public function testTranslatesHttpExceptionToResponse()
|
||||
{
|
||||
$request = $this->prophesize("\\pjdietz\\WellRESTed\\Interfaces\\RequestInterface");
|
||||
$request->getMethod()->willReturn("GET");
|
||||
|
||||
$handler = new ExceptionHandler();
|
||||
$response = $handler->getResponse($request->reveal());
|
||||
$this->assertEquals(404, $response->getStatusCode());
|
||||
}
|
||||
|
||||
public function testProvidesAllowHeader()
|
||||
{
|
||||
$request = $this->prophesize("\\pjdietz\\WellRESTed\\Interfaces\\RequestInterface");
|
||||
$request->getMethod()->willReturn("OPTIONS");
|
||||
|
||||
$handler = new OptionsHandler();
|
||||
$response = $handler->getResponse($request->reveal());
|
||||
$this->assertEquals("GET, POST", $response->getHeader("Allow"));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class OptionsHandler extends Handler
|
||||
{
|
||||
protected function getAllowedMethods()
|
||||
{
|
||||
return ["GET","POST"];
|
||||
}
|
||||
}
|
||||
|
||||
class ExceptionHandler extends Handler
|
||||
{
|
||||
protected function get()
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
}
|
||||
|
|
@ -1,70 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace pjdietz\WellRESTed\Test;
|
||||
|
||||
use pjdietz\WellRESTed\HandlerUnpacker;
|
||||
use pjdietz\WellRESTed\Interfaces\HandlerInterface;
|
||||
use pjdietz\WellRESTed\Interfaces\RequestInterface;
|
||||
|
||||
/**
|
||||
* @covers pjdietz\WellRESTed\HandlerUnpacker
|
||||
*/
|
||||
class HandlerUnpackerTest extends \PHPUnit_Framework_TestCase
|
||||
{
|
||||
public function testUnpacksFromCallable()
|
||||
{
|
||||
$handlerContainer = function () {
|
||||
return new HandlerUnpackerTest_Handler();
|
||||
};
|
||||
$handlerUnpacker = new HandlerUnpacker();
|
||||
$handler = $handlerUnpacker->unpack($handlerContainer);
|
||||
$this->assertInstanceOf("\\pjdietz\\WellRESTed\\Interfaces\\HandlerInterface", $handler);
|
||||
}
|
||||
|
||||
public function testPropagatesArgumentsToCallable()
|
||||
{
|
||||
$request = $this->prophesize("\\pjdietz\\WellRESTed\\Interfaces\\RequestInterface");
|
||||
$args = [
|
||||
"cat" => "Molly"
|
||||
];
|
||||
|
||||
$callableRequest = null;
|
||||
$callableArguments = null;
|
||||
|
||||
$handlerCallable = function ($rqst, $args) use (&$callableRequest, &$callableArguments) {
|
||||
$callableRequest = $rqst;
|
||||
$callableArguments = $args;
|
||||
return null;
|
||||
};
|
||||
|
||||
$handlerUnpacker = new HandlerUnpacker();
|
||||
$handlerUnpacker->unpack($handlerCallable, $request->reveal(), $args);
|
||||
|
||||
$this->assertSame($callableRequest, $request->reveal());
|
||||
$this->assertSame($callableArguments, $args);
|
||||
}
|
||||
|
||||
public function testUnpacksFromString()
|
||||
{
|
||||
$handlerContainer = __NAMESPACE__ . "\\HandlerUnpackerTest_Handler";
|
||||
$handlerUnpacker = new HandlerUnpacker();
|
||||
$handler = $handlerUnpacker->unpack($handlerContainer);
|
||||
$this->assertInstanceOf("\\pjdietz\\WellRESTed\\Interfaces\\HandlerInterface", $handler);
|
||||
}
|
||||
|
||||
public function testUnpacksInstance()
|
||||
{
|
||||
$handler = new HandlerUnpackerTest_Handler();
|
||||
$handlerUnpacker = new HandlerUnpacker();
|
||||
$handler = $handlerUnpacker->unpack($handler);
|
||||
$this->assertInstanceOf("\\pjdietz\\WellRESTed\\Interfaces\\HandlerInterface", $handler);
|
||||
}
|
||||
}
|
||||
|
||||
class HandlerUnpackerTest_Handler implements HandlerInterface
|
||||
{
|
||||
public function getResponse(RequestInterface $request, array $args = null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,95 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace pjdietz\WellRESTed\Test;
|
||||
|
||||
/**
|
||||
* @covers pjdietz\WellRESTed\Message
|
||||
*/
|
||||
class MessageTest extends \PHPUnit_Framework_TestCase
|
||||
{
|
||||
public function testSetsBody()
|
||||
{
|
||||
$message = $this->getMockForAbstractClass("\\pjdietz\\WellRESTed\\Message");
|
||||
$body = "This is the body";
|
||||
$message->setBody($body);
|
||||
$this->assertEquals($body, $message->getBody());
|
||||
}
|
||||
|
||||
public function testBodyIsNullByDefault()
|
||||
{
|
||||
$message = $this->getMockForAbstractClass("\\pjdietz\\WellRESTed\\Message");
|
||||
$this->assertNull($message->getBody());
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider headerProvider
|
||||
*/
|
||||
public function testSetsHeader($headerKey, $headerValue, $badCapsKey)
|
||||
{
|
||||
$message = $this->getMockForAbstractClass("\\pjdietz\\WellRESTed\\Message");
|
||||
$message->setHeader($headerKey, $headerValue);
|
||||
$this->assertEquals($headerValue, $message->getHeader($badCapsKey));
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider headerProvider
|
||||
*/
|
||||
public function testUpdatesHeader($headerKey, $headerValue, $testName)
|
||||
{
|
||||
$message = $this->getMockForAbstractClass("\\pjdietz\\WellRESTed\\Message");
|
||||
$message->setHeader($headerKey, $headerValue);
|
||||
$newValue = "newvalue";
|
||||
$message->setHeader($testName, "newvalue");
|
||||
$this->assertEquals($newValue, $message->getHeader($testName));
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider headerProvider
|
||||
*/
|
||||
public function testNonsetHeaderIsNull()
|
||||
{
|
||||
$message = $this->getMockForAbstractClass("\\pjdietz\\WellRESTed\\Message");
|
||||
$this->assertNull($message->getHeader("no-header"));
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider headerProvider
|
||||
*/
|
||||
public function testUnsetHeaderIsNull($headerKey, $headerValue, $testName)
|
||||
{
|
||||
$message = $this->getMockForAbstractClass("\\pjdietz\\WellRESTed\\Message");
|
||||
$message->setHeader($headerKey, $headerValue);
|
||||
$message->unsetHeader($testName);
|
||||
$this->assertNull($message->getHeader($headerKey));
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider headerProvider
|
||||
*/
|
||||
public function testChecksIfHeaderIsSet($headerKey, $headerValue, $testName)
|
||||
{
|
||||
$message = $this->getMockForAbstractClass("\\pjdietz\\WellRESTed\\Message");
|
||||
$message->setHeader($headerKey, $headerValue);
|
||||
$this->assertTrue($message->issetHeader($testName));
|
||||
}
|
||||
|
||||
public function headerProvider()
|
||||
{
|
||||
return [
|
||||
["Accept-Charset", "utf-8", "accept-charset"],
|
||||
["Accept-Encoding", "gzip, deflate", "ACCEPT-ENCODING"],
|
||||
["Cache-Control", "no-cache", "Cache-Control"],
|
||||
];
|
||||
}
|
||||
|
||||
public function testReturnsListOfHeaders()
|
||||
{
|
||||
$message = $this->getMockForAbstractClass("\\pjdietz\\WellRESTed\\Message");
|
||||
$headers = $this->headerProvider();
|
||||
foreach ($headers as $header) {
|
||||
$message->setHeader($header[0], $header[1]);
|
||||
}
|
||||
$this->assertEquals(count($headers), count($message->getHeaders()));
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -1,404 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace pjdietz\WellRESTed\Test;
|
||||
|
||||
use Faker\Factory;
|
||||
use pjdietz\WellRESTed\Request;
|
||||
use pjdietz\WellRESTed\Test;
|
||||
|
||||
/**
|
||||
* @covers pjdietz\WellRESTed\Request
|
||||
*/
|
||||
class RequestTest extends \PHPUnit_Framework_TestCase
|
||||
{
|
||||
/**
|
||||
* @dataProvider methodProvider
|
||||
*/
|
||||
public function testSetsMethod($method)
|
||||
{
|
||||
$rqst = new Request();
|
||||
$rqst->setMethod($method);
|
||||
$this->assertEquals($method, $rqst->getMethod());
|
||||
}
|
||||
|
||||
public function methodProvider()
|
||||
{
|
||||
return [
|
||||
["GET"],
|
||||
["POST"],
|
||||
["PUT"],
|
||||
["DELETE"],
|
||||
["OPTIONS"],
|
||||
["HEAD"]
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider uriProvider
|
||||
*/
|
||||
public function testSetsUri($uri, $data)
|
||||
{
|
||||
$rqst = new Request($uri);
|
||||
$this->assertEquals($data->uri, $rqst->getUri());
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider uriProvider
|
||||
*/
|
||||
public function testParsesSchemeFromUri($uri, $data)
|
||||
{
|
||||
$rqst = new Request($uri);
|
||||
$this->assertEquals($data->scheme, $rqst->getScheme());
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider uriProvider
|
||||
*/
|
||||
public function testParsesHostnameFromUri($uri, $data)
|
||||
{
|
||||
$rqst = new Request($uri);
|
||||
$this->assertEquals($data->hostname, $rqst->getHostname());
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider uriProvider
|
||||
*/
|
||||
public function testParsesPortFromUri($uri, $data)
|
||||
{
|
||||
$rqst = new Request($uri);
|
||||
$this->assertEquals($data->port, $rqst->getPort());
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider uriProvider
|
||||
*/
|
||||
public function testParsesPathFromUri($uri, $data)
|
||||
{
|
||||
$rqst = new Request($uri);
|
||||
$this->assertEquals($data->path, $rqst->getPath());
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider uriProvider
|
||||
*/
|
||||
public function testParsesPathPartsFromUri($uri, $data)
|
||||
{
|
||||
$rqst = new Request($uri);
|
||||
$this->assertEquals($data->parts, $rqst->getPathParts());
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider uriProvider
|
||||
*/
|
||||
public function testParsesQueryFromUri($uri, $data)
|
||||
{
|
||||
$rqst = new Request($uri);
|
||||
$this->assertEquals($data->query, $rqst->getQuery());
|
||||
}
|
||||
|
||||
public function uriProvider()
|
||||
{
|
||||
return [
|
||||
[
|
||||
"http://www.google.com",
|
||||
(object) [
|
||||
"uri" => "http://www.google.com",
|
||||
"scheme" => "http",
|
||||
"hostname" => "www.google.com",
|
||||
"port" => 80,
|
||||
"path" => "/",
|
||||
"query" => [],
|
||||
"parts" => []
|
||||
]
|
||||
],
|
||||
[
|
||||
"https://www.google.com",
|
||||
(object) [
|
||||
"uri" => "https://www.google.com",
|
||||
"scheme" => "https",
|
||||
"hostname" => "www.google.com",
|
||||
"port" => 443,
|
||||
"path" => "/",
|
||||
"query" => [],
|
||||
"parts" => []
|
||||
]
|
||||
],
|
||||
[
|
||||
"localhost:8080/my/path/with/parts",
|
||||
(object) [
|
||||
"uri" => "http://localhost:8080/my/path/with/parts",
|
||||
"scheme" => "http",
|
||||
"hostname" => "localhost",
|
||||
"port" => 8080,
|
||||
"path" => "/my/path/with/parts",
|
||||
"query" => [],
|
||||
"parts" => ["my", "path", "with", "parts"]
|
||||
]
|
||||
],
|
||||
[
|
||||
"localhost?dog=bear&cat=molly",
|
||||
(object) [
|
||||
"uri" => "http://localhost?cat=molly&dog=bear",
|
||||
"scheme" => "http",
|
||||
"hostname" => "localhost",
|
||||
"port" => 80,
|
||||
"path" => "/",
|
||||
"query" => [
|
||||
"cat" => "molly",
|
||||
"dog" => "bear"
|
||||
],
|
||||
"parts" => []
|
||||
]
|
||||
],
|
||||
[
|
||||
"/my-page?id=2",
|
||||
(object) [
|
||||
"uri" => "http://localhost/my-page?id=2",
|
||||
"scheme" => "http",
|
||||
"hostname" => "localhost",
|
||||
"port" => 80,
|
||||
"path" => "/my-page",
|
||||
"query" => [
|
||||
"id" => "2"
|
||||
],
|
||||
"parts" => ["my-page"]
|
||||
]
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider formProvider
|
||||
*/
|
||||
public function testEncodesFormFields($form)
|
||||
{
|
||||
$rqst = new Request();
|
||||
$rqst->setFormFields($form);
|
||||
$body = $rqst->getBody();
|
||||
parse_str($body, $fields);
|
||||
$this->assertEquals($form, $fields);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider formProvider
|
||||
*/
|
||||
public function testDecodesFormFields($form)
|
||||
{
|
||||
$rqst = new Request();
|
||||
$rqst->setFormFields($form);
|
||||
$fields = $rqst->getFormFields();
|
||||
$this->assertEquals($form, $fields);
|
||||
}
|
||||
|
||||
public function formProvider()
|
||||
{
|
||||
$faker = Factory::create();
|
||||
return [
|
||||
[
|
||||
[
|
||||
"firstName" => $faker->firstName,
|
||||
"lastName" => $faker->lastName,
|
||||
"username" => $faker->userName
|
||||
]
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider queryProvider
|
||||
*/
|
||||
public function testSetsQuery($input, $expected)
|
||||
{
|
||||
$rqst = new Request();
|
||||
$rqst->setQuery($input);
|
||||
$this->assertEquals($expected, $rqst->getQuery());
|
||||
}
|
||||
|
||||
public function queryProvider()
|
||||
{
|
||||
return [
|
||||
[
|
||||
"cat=molly&dog=bear",
|
||||
[
|
||||
"cat" => "molly",
|
||||
"dog" => "bear"
|
||||
]
|
||||
],
|
||||
[
|
||||
["id" => "1"],
|
||||
["id" => "1"]
|
||||
],
|
||||
[
|
||||
(object)["dog" => "bear"],
|
||||
["dog" => "bear"]
|
||||
],
|
||||
["", []],
|
||||
[[], []],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider invalidQueryProvider
|
||||
* @expectedException \InvalidArgumentException
|
||||
*/
|
||||
public function testThrowsExceptionOnInvalidQuery($query)
|
||||
{
|
||||
$rqst = new Request();
|
||||
$rqst->setQuery($query);
|
||||
}
|
||||
|
||||
public function invalidQueryProvider()
|
||||
{
|
||||
return [
|
||||
[11],
|
||||
[false],
|
||||
[true],
|
||||
[null]
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider invalidSchemeProvider
|
||||
* @expectedException \UnexpectedValueException
|
||||
*/
|
||||
public function testThrowsExceptionOnInvalidScheme($scheme)
|
||||
{
|
||||
$rqst = new Request();
|
||||
$rqst->setScheme($scheme);
|
||||
}
|
||||
|
||||
public function invalidSchemeProvider()
|
||||
{
|
||||
return [
|
||||
[""],
|
||||
["ftp"],
|
||||
["ssh"],
|
||||
[null],
|
||||
[0]
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider defaultPortProvider
|
||||
*/
|
||||
public function testSetsDefaultPort($scheme, $port)
|
||||
{
|
||||
$rqst = new Request("http://localhost:9999");
|
||||
$rqst->setScheme($scheme);
|
||||
$rqst->setPort();
|
||||
$this->assertEquals($port, $rqst->getPort());
|
||||
}
|
||||
|
||||
public function defaultPortProvider()
|
||||
{
|
||||
return [
|
||||
["http", 80],
|
||||
["https", 443]
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider serverProvider
|
||||
* @preserveGlobalState disabled
|
||||
*/
|
||||
public function testReadsServerRequestMethod($serverVars, $expected)
|
||||
{
|
||||
$_SERVER = array_merge($_SERVER, $serverVars);
|
||||
$rqst = new Request();
|
||||
$rqst->readHttpRequest();
|
||||
$this->assertEquals($expected->method, $rqst->getMethod());
|
||||
}
|
||||
/**
|
||||
* @dataProvider serverProvider
|
||||
* @preserveGlobalState disabled
|
||||
*/
|
||||
public function testReadsServerRequestHost($serverVars, $expected)
|
||||
{
|
||||
$_SERVER = array_merge($_SERVER, $serverVars);
|
||||
$rqst = new Request();
|
||||
$rqst->readHttpRequest();
|
||||
$this->assertEquals($expected->host, $rqst->getHostname());
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider serverProvider
|
||||
* @preserveGlobalState disabled
|
||||
*/
|
||||
public function testReadsServerRequestPath($serverVars, $expected)
|
||||
{
|
||||
$_SERVER = array_merge($_SERVER, $serverVars);
|
||||
$rqst = new Request();
|
||||
$rqst->readHttpRequest();
|
||||
$this->assertEquals($expected->path, $rqst->getPath());
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider serverProvider
|
||||
* @preserveGlobalState disabled
|
||||
*/
|
||||
public function testReadsServerRequestHeaders($serverVars, $expected)
|
||||
{
|
||||
$_SERVER = array_merge($_SERVER, $serverVars);
|
||||
$rqst = new Request();
|
||||
$rqst->readHttpRequest();
|
||||
foreach ($expected->headers as $name => $value) {
|
||||
$this->assertEquals($value, $rqst->getHeader($name));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @preserveGlobalState disabled
|
||||
*/
|
||||
public function testReadsStaticRequest()
|
||||
{
|
||||
$data = $this->serverProvider();
|
||||
$serverVars = $data[0][0];
|
||||
$expected = $data[0][1];
|
||||
|
||||
$_SERVER = array_merge($_SERVER, $serverVars);
|
||||
$rqst = Request::getRequest();
|
||||
$this->assertEquals($expected->host, $rqst->getHostname());
|
||||
|
||||
$rqst2 = Request::getRequest();
|
||||
$this->assertSame($rqst2, $rqst);
|
||||
}
|
||||
|
||||
public function serverProvider()
|
||||
{
|
||||
return [
|
||||
[
|
||||
[
|
||||
"REQUEST_METHOD" => "GET",
|
||||
"REQUEST_URI" => "/",
|
||||
"HTTP_ACCEPT_CHARSET" => "utf-8",
|
||||
"HTTP_HOST" => "localhost"
|
||||
],
|
||||
(object) [
|
||||
"method" => "GET",
|
||||
"host" => "localhost",
|
||||
"path" => "/",
|
||||
"headers" => [
|
||||
"Accept-charset" => "utf-8"
|
||||
]
|
||||
]
|
||||
],
|
||||
[
|
||||
[
|
||||
"REQUEST_METHOD" => "POST",
|
||||
"REQUEST_URI" => "/my/page",
|
||||
"HTTP_ACCEPT_CHARSET" => "utf-8",
|
||||
"HTTP_HOST" => "mysite.com"
|
||||
],
|
||||
(object) [
|
||||
"method" => "POST",
|
||||
"host" => "mysite.com",
|
||||
"path" => "/my/page",
|
||||
"headers" => [
|
||||
"Accept-charset" => "utf-8"
|
||||
]
|
||||
]
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -1,213 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace pjdietz\WellRESTed\Test;
|
||||
|
||||
use Faker\Factory;
|
||||
use pjdietz\WellRESTed\Response;
|
||||
|
||||
/**
|
||||
* @covers pjdietz\WellRESTed\Response
|
||||
*/
|
||||
class ResponseTest extends \PHPUnit_Framework_TestCase
|
||||
{
|
||||
/**
|
||||
* @dataProvider statusCodeProvider
|
||||
*/
|
||||
public function testSetsStatusCodeInConstructor($statusCode, $reasonPhrase, $statusLine)
|
||||
{
|
||||
$resp = new Response($statusCode);
|
||||
$this->assertEquals($statusCode, $resp->getStatusCode());
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider statusCodeProvider
|
||||
*/
|
||||
public function testProvidesReasonPhrase($statusCode, $reasonPhrase, $statusLine)
|
||||
{
|
||||
$resp = new Response();
|
||||
$resp->setStatusCode($statusCode, $reasonPhrase);
|
||||
$this->assertEquals(substr($statusLine, 13), $resp->getReasonPhrase());
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider statusCodeProvider
|
||||
*/
|
||||
public function testProvidesStatusLine($statusCode, $reasonPhrase, $statusLine)
|
||||
{
|
||||
$resp = new Response();
|
||||
$resp->setStatusCode($statusCode, $reasonPhrase);
|
||||
$this->assertEquals($statusLine, $resp->getStatusLine());
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider statusCodeProvider
|
||||
*/
|
||||
public function testDeterminesSuccessFromStatusCode($statusCode, $reasonPhrase, $statusLine)
|
||||
{
|
||||
$resp = new Response();
|
||||
$resp->setStatusCode($statusCode, $reasonPhrase);
|
||||
if ($statusCode < 400) {
|
||||
$this->assertTrue($resp->getSuccess());
|
||||
} else {
|
||||
$this->assertFalse($resp->getSuccess());
|
||||
}
|
||||
}
|
||||
|
||||
public function statusCodeProvider()
|
||||
{
|
||||
return [
|
||||
[100, null, "HTTP/1.1 100 Continue"],
|
||||
[101, null, "HTTP/1.1 101 Switching Protocols"],
|
||||
[200, null, "HTTP/1.1 200 OK"],
|
||||
[201, null, "HTTP/1.1 201 Created"],
|
||||
[202, null, "HTTP/1.1 202 Accepted"],
|
||||
[203, null, "HTTP/1.1 203 Non-Authoritative Information"],
|
||||
[204, null, "HTTP/1.1 204 No Content"],
|
||||
[205, null, "HTTP/1.1 205 Reset Content"],
|
||||
[206, null, "HTTP/1.1 206 Partial Content"],
|
||||
[300, null, "HTTP/1.1 300 Multiple Choices"],
|
||||
[301, null, "HTTP/1.1 301 Moved Permanently"],
|
||||
[302, null, "HTTP/1.1 302 Found"],
|
||||
[303, null, "HTTP/1.1 303 See Other"],
|
||||
[304, null, "HTTP/1.1 304 Not Modified"],
|
||||
[305, null, "HTTP/1.1 305 Use Proxy"],
|
||||
[400, null, "HTTP/1.1 400 Bad Request"],
|
||||
[401, null, "HTTP/1.1 401 Unauthorized"],
|
||||
[402, null, "HTTP/1.1 402 Payment Required"],
|
||||
[403, null, "HTTP/1.1 403 Forbidden"],
|
||||
[404, null, "HTTP/1.1 404 Not Found"],
|
||||
[405, null, "HTTP/1.1 405 Method Not Allowed"],
|
||||
[406, null, "HTTP/1.1 406 Not Acceptable"],
|
||||
[407, null, "HTTP/1.1 407 Proxy Authentication Required"],
|
||||
[408, null, "HTTP/1.1 408 Request Timeout"],
|
||||
[409, null, "HTTP/1.1 409 Conflict"],
|
||||
[410, null, "HTTP/1.1 410 Gone"],
|
||||
[411, null, "HTTP/1.1 411 Length Required"],
|
||||
[412, null, "HTTP/1.1 412 Precondition Failed"],
|
||||
[413, null, "HTTP/1.1 413 Request Entity Too Large"],
|
||||
[414, null, "HTTP/1.1 414 Request-URI Too Long"],
|
||||
[415, null, "HTTP/1.1 415 Unsupported Media Type"],
|
||||
[500, null, "HTTP/1.1 500 Internal Server Error"],
|
||||
[501, null, "HTTP/1.1 501 Not Implemented"],
|
||||
[502, null, "HTTP/1.1 502 Bad Gateway"],
|
||||
[503, null, "HTTP/1.1 503 Service Unavailable"],
|
||||
[504, null, "HTTP/1.1 504 Gateway Timeout"],
|
||||
[505, null, "HTTP/1.1 505 HTTP Version Not Supported"],
|
||||
[598, null, "HTTP/1.1 598 Nonstandard"],
|
||||
[599, "Smelly", "HTTP/1.1 599 Smelly"]
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider invalidReasonPhraseProvider
|
||||
* @expectedException \InvalidArgumentException
|
||||
*/
|
||||
public function testThrowsExceptionOnInvalidReasonPhrase($statusCode, $reasonPhrase)
|
||||
{
|
||||
$resp = new Response();
|
||||
$resp->setStatusCode($statusCode, $reasonPhrase);
|
||||
}
|
||||
|
||||
public function invalidReasonPhraseProvider()
|
||||
{
|
||||
return [
|
||||
[599, false],
|
||||
["100", true],
|
||||
["*", []]
|
||||
];
|
||||
}
|
||||
|
||||
public function testSetsBody()
|
||||
{
|
||||
$faker = Factory::create();
|
||||
$body = $faker->text();
|
||||
$resp = new Response();
|
||||
$resp->setBody($body);
|
||||
$this->assertEquals($body, $resp->getBody());
|
||||
}
|
||||
|
||||
public function testSetsBodyInConstructor()
|
||||
{
|
||||
$faker = Factory::create();
|
||||
$body = $faker->text();
|
||||
$resp = new Response(200, $body);
|
||||
$this->assertEquals($body, $resp->getBody());
|
||||
}
|
||||
|
||||
public function testSetsBodyFile()
|
||||
{
|
||||
$path = tempnam(sys_get_temp_dir(), "TST");
|
||||
$resp = new Response();
|
||||
$resp->setBodyFilePath($path);
|
||||
$this->assertEquals($path, $resp->getBodyFilePath());
|
||||
unlink($path);
|
||||
}
|
||||
|
||||
/**
|
||||
* @runInSeparateProcess
|
||||
* @preserveGlobalState disabled
|
||||
*/
|
||||
public function testOutputsResponse()
|
||||
{
|
||||
$faker = Factory::create();
|
||||
$body = $faker->text();
|
||||
|
||||
$resp = new Response(200, $body, ["Content-type" => "text/plain"]);
|
||||
ob_start();
|
||||
$resp->respond();
|
||||
$captured = ob_get_contents();
|
||||
ob_end_clean();
|
||||
|
||||
$this->assertEquals($body, $captured);
|
||||
}
|
||||
|
||||
/**
|
||||
* @runInSeparateProcess
|
||||
* @preserveGlobalState disabled
|
||||
*/
|
||||
public function testOutputsResponseFromFile()
|
||||
{
|
||||
$path = tempnam(sys_get_temp_dir(), "TST");
|
||||
$faker = Factory::create();
|
||||
$body = $faker->text();
|
||||
|
||||
$f = fopen($path, "w");
|
||||
fwrite($f, $body);
|
||||
fclose($f);
|
||||
|
||||
$resp = new Response();
|
||||
$resp->setStatusCode(200);
|
||||
$resp->setBodyFilePath($path);
|
||||
|
||||
ob_start();
|
||||
$resp->respond();
|
||||
$captured = ob_get_contents();
|
||||
ob_end_clean();
|
||||
|
||||
unlink($path);
|
||||
|
||||
$this->assertEquals($captured, $body);
|
||||
}
|
||||
|
||||
/**
|
||||
* @runInSeparateProcess
|
||||
* @preserveGlobalState disabled
|
||||
*/
|
||||
public function testRespondsWithNoBodyWhenResponseFileIsMissing()
|
||||
{
|
||||
$path = tempnam(sys_get_temp_dir(), "TST");
|
||||
|
||||
$resp = new Response();
|
||||
$resp->setStatusCode(200);
|
||||
$resp->setBodyFilePath($path);
|
||||
unlink($path);
|
||||
|
||||
ob_start();
|
||||
$resp->respond();
|
||||
$captured = ob_get_contents();
|
||||
ob_end_clean();
|
||||
|
||||
$this->assertEquals("", $captured);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -1,211 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace pjdietz\WellRESTed\Test;
|
||||
|
||||
use pjdietz\WellRESTed\RouteBuilder;
|
||||
use pjdietz\WellRESTed\Routes\TemplateRoute;
|
||||
use stdClass;
|
||||
|
||||
class RouteBuilderTest extends \PHPUnit_Framework_TestCase
|
||||
{
|
||||
/*
|
||||
* Parse JSON and get the correct number of routes.
|
||||
*/
|
||||
public function testBuildRoutesFromJson()
|
||||
{
|
||||
$json = <<<'JSON'
|
||||
{
|
||||
"handlerNamespace": "\\myapi\\Handlers",
|
||||
"routes": [
|
||||
{
|
||||
"path": "/",
|
||||
"handler": "RootHandler"
|
||||
},
|
||||
{
|
||||
"path": "/cats/",
|
||||
"handler": "CatCollectionHandler"
|
||||
},
|
||||
{
|
||||
"tempalte": "/cats/{id}",
|
||||
"handler": "CatItemHandler"
|
||||
}
|
||||
]
|
||||
}
|
||||
JSON;
|
||||
|
||||
$builder = @new RouteBuilder();
|
||||
$routes = $builder->buildRoutes($json);
|
||||
$this->assertEquals(3, count($routes));
|
||||
}
|
||||
|
||||
/**
|
||||
* Fail properly on malformed JSON
|
||||
*
|
||||
* @expectedException \pjdietz\WellRESTed\Exceptions\ParseException
|
||||
* @expectedExceptionMessage Unable to parse as JSON.
|
||||
*/
|
||||
public function testThrowsExceptionBuildingRoutesFromInvalidJson()
|
||||
{
|
||||
$json = "jadhjaksd";
|
||||
$builder = @new RouteBuilder();
|
||||
$builder->buildRoutes($json);
|
||||
}
|
||||
|
||||
public function testSetsNamesapce()
|
||||
{
|
||||
$namespace = "\\test\\Namespace";
|
||||
$builder = @new RouteBuilder();
|
||||
$builder->setHandlerNamespace($namespace);
|
||||
$this->assertEquals($namespace, $builder->getHandlerNamespace());
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider varProvider
|
||||
*/
|
||||
public function testSetsDefaultVariablePatternThroughAccessor($name, $pattern, $expected)
|
||||
{
|
||||
$builder = @new RouteBuilder();
|
||||
$builder->setDefaultVariablePattern($pattern);
|
||||
$this->assertEquals($builder->getDefaultVariablePattern(), $expected);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider varProvider
|
||||
*/
|
||||
public function testSetsDefaultVariablePatternThroughConfiguration($name, $pattern, $expected)
|
||||
{
|
||||
$builder = @new RouteBuilder();
|
||||
$conf = new stdClass();
|
||||
$conf->variablePattern = $pattern;
|
||||
$builder->readConfiguration($conf);
|
||||
$this->assertEquals($builder->getDefaultVariablePattern(), $expected);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider varProvider
|
||||
*/
|
||||
public function testSetsTemplateVariablesThroughAccessor($name, $pattern, $expected)
|
||||
{
|
||||
$builder = @new RouteBuilder();
|
||||
$builder->setTemplateVars(array($name => $pattern));
|
||||
$vars = $builder->getTemplateVars();
|
||||
$this->assertEquals($vars[$name], $expected);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider varProvider
|
||||
*/
|
||||
public function testSetsTemplateVariablesThroughConfiguration($name, $pattern, $expected)
|
||||
{
|
||||
$builder = @new RouteBuilder();
|
||||
$conf = new stdClass();
|
||||
$conf->vars = [$name => $pattern];
|
||||
$builder->readConfiguration($conf);
|
||||
$vars = $builder->getTemplateVars();
|
||||
$this->assertEquals($vars[$name], $expected);
|
||||
}
|
||||
|
||||
public function varProvider()
|
||||
{
|
||||
return [
|
||||
["slug", "SLUG", TemplateRoute::RE_SLUG],
|
||||
["name", "ALPHA", TemplateRoute::RE_ALPHA],
|
||||
["name", "ALPHANUM", TemplateRoute::RE_ALPHANUM],
|
||||
["id", "DIGIT", TemplateRoute::RE_NUM],
|
||||
["id", "NUM", TemplateRoute::RE_NUM],
|
||||
["custom", ".*", ".*"]
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider routeDescriptionProvider
|
||||
*/
|
||||
public function testBuildsRoutesFromRoutesArray($key, $value, $expectedClass)
|
||||
{
|
||||
$mockHander = $this->getMock('\pjdietz\WellRESTed\Interfaces\HandlerInterface');
|
||||
$routes = [
|
||||
(object) [
|
||||
$key => $value,
|
||||
"handler" => get_class($mockHander)
|
||||
]
|
||||
];
|
||||
$builder = @new RouteBuilder();
|
||||
$routes = $builder->buildRoutes($routes);
|
||||
$route = $routes[0];
|
||||
$this->assertInstanceOf($expectedClass, $route);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider routeDescriptionProvider
|
||||
*/
|
||||
public function testBuildsRoutesFromConfigurationObject($key, $value, $expectedClass)
|
||||
{
|
||||
$mockHander = $this->getMock('\pjdietz\WellRESTed\Interfaces\HandlerInterface');
|
||||
$conf = (object) [
|
||||
"routes" => [
|
||||
(object) [
|
||||
$key => $value,
|
||||
"handler" => get_class($mockHander)
|
||||
]
|
||||
]
|
||||
];
|
||||
$builder = @new RouteBuilder();
|
||||
$routes = $builder->buildRoutes($conf);
|
||||
$route = $routes[0];
|
||||
$this->assertInstanceOf($expectedClass, $route);
|
||||
}
|
||||
|
||||
public function routeDescriptionProvider()
|
||||
{
|
||||
return [
|
||||
["path", "/", '\pjdietz\WellRESTed\Routes\StaticRoute'],
|
||||
["pattern", "/cat/[0-9]+", '\pjdietz\WellRESTed\Routes\RegexRoute'],
|
||||
["template", "/cat/{id}", '\pjdietz\WellRESTed\Routes\TemplateRoute'],
|
||||
];
|
||||
}
|
||||
|
||||
public function testBuildsRoutesWithTemplateVariables()
|
||||
{
|
||||
$mock = $this->getMock('\pjdietz\WellRESTed\Interfaces\HandlerInterface');
|
||||
$routes = [
|
||||
(object) [
|
||||
"template" => "/cats/{catId}",
|
||||
"handler" => get_class($mock),
|
||||
"vars" => [
|
||||
"catId" => "SLUG"
|
||||
]
|
||||
]
|
||||
];
|
||||
$builder = @new RouteBuilder();
|
||||
$builder->setTemplateVars(["dogId" => "NUM"]);
|
||||
$routes = $builder->buildRoutes($routes);
|
||||
$route = $routes[0];
|
||||
$this->assertInstanceOf('\pjdietz\WellRESTed\Routes\TemplateRoute', $route);
|
||||
}
|
||||
|
||||
/**
|
||||
* @expectedException \pjdietz\WellRESTed\Exceptions\ParseException
|
||||
* @expectedExceptionMessage Unable to parse. Missing array of routes.
|
||||
*/
|
||||
public function testThrowsExceptionWhenConfigurationObjectIsMissingRoutesArray()
|
||||
{
|
||||
$conf = new stdClass();
|
||||
$builder = @new RouteBuilder();
|
||||
$builder->buildRoutes($conf);
|
||||
}
|
||||
|
||||
/**
|
||||
* @expectedException \pjdietz\WellRESTed\Exceptions\ParseException
|
||||
* @expectedExceptionMessage Unable to parse. Route is missing a handler.
|
||||
*/
|
||||
public function testThrowsExceptionWhenMissingHandler()
|
||||
{
|
||||
$routes = [
|
||||
(object) [
|
||||
"path" => "/"
|
||||
]
|
||||
];
|
||||
$builder = @new RouteBuilder();
|
||||
$builder->buildRoutes($routes);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,201 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace pjdietz\WellRESTed\Test;
|
||||
|
||||
use pjdietz\WellRESTed\RouteTable;
|
||||
use Prophecy\Argument;
|
||||
|
||||
/**
|
||||
* @covers pjdietz\WellRESTed\RouteTable
|
||||
*/
|
||||
class RouteTableTest extends \PHPUnit_Framework_TestCase
|
||||
{
|
||||
private $handler;
|
||||
private $request;
|
||||
private $response;
|
||||
private $route;
|
||||
|
||||
public function setUp()
|
||||
{
|
||||
$this->request = $this->prophesize("\\pjdietz\\WellRESTed\\Interfaces\\RequestInterface");
|
||||
$this->response = $this->prophesize("\\pjdietz\\WellRESTed\\Interfaces\\ResponseInterface");
|
||||
$this->route = $this->prophesize("\\pjdietz\\WellRESTed\\Interfaces\\HandlerInterface");
|
||||
$this->handler = $this->prophesize("\\pjdietz\\WellRESTed\\Interfaces\\HandlerInterface");
|
||||
}
|
||||
|
||||
public function testReturnsNullWhenNoRoutesMatch()
|
||||
{
|
||||
$table = new RouteTable();
|
||||
$response = $table->getResponse($this->request->reveal());
|
||||
$this->assertNull($response);
|
||||
}
|
||||
|
||||
public function testMatchesStaticRoute()
|
||||
{
|
||||
$this->route->willImplement("\\pjdietz\\WellRESTed\\Interfaces\\Routes\\StaticRouteInterface");
|
||||
$this->route->getPaths()->willReturn(["/cats/"]);
|
||||
$this->route->getResponse(Argument::cetera())->willReturn($this->response->reveal());
|
||||
|
||||
$this->request->getPath()->willReturn("/cats/");
|
||||
|
||||
$table = new RouteTable();
|
||||
$table->addRoute($this->route->reveal());
|
||||
$table->getResponse($this->request->reveal());
|
||||
|
||||
$this->route->getResponse(Argument::cetera())->shouldHaveBeenCalled();
|
||||
}
|
||||
|
||||
public function testMatchesPrefixRoute()
|
||||
{
|
||||
$this->route->willImplement("\\pjdietz\\WellRESTed\\Interfaces\\Routes\\PrefixRouteInterface");
|
||||
$this->route->getPrefixes()->willReturn(["/cats/"]);
|
||||
$this->route->getResponse(Argument::cetera())->willReturn($this->response->reveal());
|
||||
|
||||
$this->request->getPath()->willReturn("/cats/molly");
|
||||
|
||||
$table = new RouteTable();
|
||||
$table->addRoute($this->route->reveal());
|
||||
$table->getResponse($this->request->reveal());
|
||||
|
||||
$this->route->getResponse(Argument::cetera())->shouldHaveBeenCalled();
|
||||
}
|
||||
|
||||
public function testMatchesBestPrefixRoute()
|
||||
{
|
||||
$route1 = $this->prophesize("\\pjdietz\\WellRESTed\\Interfaces\\HandlerInterface");
|
||||
$route1->willImplement("\\pjdietz\\WellRESTed\\Interfaces\\Routes\\PrefixRouteInterface");
|
||||
$route1->getPrefixes()->willReturn(["/animals/"]);
|
||||
$route1->getResponse(Argument::cetera())->willReturn($this->response->reveal());
|
||||
|
||||
$route2 = $this->prophesize("\\pjdietz\\WellRESTed\\Interfaces\\HandlerInterface");
|
||||
$route2->willImplement("\\pjdietz\\WellRESTed\\Interfaces\\Routes\\PrefixRouteInterface");
|
||||
$route2->getPrefixes()->willReturn(["/animals/cats/"]);
|
||||
$route2->getResponse(Argument::cetera())->willReturn($this->response->reveal());
|
||||
|
||||
$this->request->getPath()->willReturn("/animals/cats/molly");
|
||||
|
||||
$table = new RouteTable();
|
||||
$table->addRoute($route1->reveal());
|
||||
$table->addRoute($route2->reveal());
|
||||
$table->getResponse($this->request->reveal());
|
||||
|
||||
$route1->getResponse(Argument::cetera())->shouldNotHaveBeenCalled();
|
||||
$route2->getResponse(Argument::cetera())->shouldHaveBeenCalled();
|
||||
}
|
||||
|
||||
public function testMatchesStaticRouteBeforePrefixRoute()
|
||||
{
|
||||
$route1 = $this->prophesize("\\pjdietz\\WellRESTed\\Interfaces\\HandlerInterface");
|
||||
$route1->willImplement("\\pjdietz\\WellRESTed\\Interfaces\\Routes\\PrefixRouteInterface");
|
||||
$route1->getPrefixes()->willReturn(["/animals/cats/"]);
|
||||
$route1->getResponse(Argument::cetera())->willReturn($this->response->reveal());
|
||||
|
||||
$route2 = $this->prophesize("\\pjdietz\\WellRESTed\\Interfaces\\HandlerInterface");
|
||||
$route2->willImplement("\\pjdietz\\WellRESTed\\Interfaces\\Routes\\StaticRouteInterface");
|
||||
$route2->getPaths()->willReturn(["/animals/cats/molly"]);
|
||||
$route2->getResponse(Argument::cetera())->willReturn($this->response->reveal());
|
||||
|
||||
$this->request->getPath()->willReturn("/animals/cats/molly");
|
||||
|
||||
$table = new RouteTable();
|
||||
$table->addRoute($route1->reveal());
|
||||
$table->addRoute($route2->reveal());
|
||||
$table->getResponse($this->request->reveal());
|
||||
|
||||
$route1->getResponse(Argument::cetera())->shouldNotHaveBeenCalled();
|
||||
$route2->getResponse(Argument::cetera())->shouldHaveBeenCalled();
|
||||
}
|
||||
|
||||
public function testMatchesPrefixRouteBeforeHandlerRoute()
|
||||
{
|
||||
$route1 = $this->prophesize("\\pjdietz\\WellRESTed\\Interfaces\\HandlerInterface");
|
||||
$route1->willImplement("\\pjdietz\\WellRESTed\\Interfaces\\Routes\\PrefixRouteInterface");
|
||||
$route1->getPrefixes()->willReturn(["/animals/cats/"]);
|
||||
$route1->getResponse(Argument::cetera())->willReturn($this->response->reveal());
|
||||
|
||||
$route2 = $this->prophesize("\\pjdietz\\WellRESTed\\Interfaces\\HandlerInterface");
|
||||
$route2->getResponse(Argument::cetera())->willReturn(null);
|
||||
|
||||
$this->request->getPath()->willReturn("/animals/cats/molly");
|
||||
|
||||
$table = new RouteTable();
|
||||
$table->addRoute($route1->reveal());
|
||||
$table->addRoute($route2->reveal());
|
||||
$table->getResponse($this->request->reveal());
|
||||
|
||||
$route1->getResponse(Argument::cetera())->shouldHaveBeenCalled();
|
||||
$route2->getResponse(Argument::cetera())->shouldNotHaveBeenCalled();
|
||||
}
|
||||
|
||||
public function testReturnsFirstNonNullResponse()
|
||||
{
|
||||
$route1 = $this->prophesize("\\pjdietz\\WellRESTed\\Interfaces\\HandlerInterface");
|
||||
$route1->getResponse(Argument::cetera())->willReturn(null);
|
||||
|
||||
$route2 = $this->prophesize("\\pjdietz\\WellRESTed\\Interfaces\\HandlerInterface");
|
||||
$route2->getResponse(Argument::cetera())->willReturn($this->response->reveal());
|
||||
|
||||
$route3 = $this->prophesize("\\pjdietz\\WellRESTed\\Interfaces\\HandlerInterface");
|
||||
$route3->getResponse(Argument::cetera())->willReturn(null);
|
||||
|
||||
$this->request->getPath()->willReturn("/");
|
||||
|
||||
$table = new RouteTable();
|
||||
$table->addRoute($route1->reveal());
|
||||
$table->addRoute($route2->reveal());
|
||||
$table->addRoute($route3->reveal());
|
||||
$response = $table->getResponse($this->request->reveal());
|
||||
|
||||
$this->assertNotNull($response);
|
||||
$route1->getResponse(Argument::cetera())->shouldHaveBeenCalled();
|
||||
$route2->getResponse(Argument::cetera())->shouldHaveBeenCalled();
|
||||
$route3->getResponse(Argument::cetera())->shouldNotHaveBeenCalled();
|
||||
}
|
||||
|
||||
public function testPropagatesArgumentsToStaticRoute()
|
||||
{
|
||||
$this->route->willImplement("\\pjdietz\\WellRESTed\\Interfaces\\Routes\\StaticRouteInterface");
|
||||
$this->route->getPaths()->willReturn(["/cats/"]);
|
||||
$this->route->getResponse(Argument::cetera())->willReturn($this->response->reveal());
|
||||
|
||||
$this->request->getPath()->willReturn("/cats/");
|
||||
|
||||
$args = ["cat" => "molly"];
|
||||
|
||||
$table = new RouteTable();
|
||||
$table->addRoute($this->route->reveal());
|
||||
$table->getResponse($this->request->reveal(), $args);
|
||||
|
||||
$this->route->getResponse($this->request->reveal(), $args)->shouldHaveBeenCalled();
|
||||
}
|
||||
|
||||
public function testPropagatesArgumentsToPrefixRoute()
|
||||
{
|
||||
$this->route->willImplement("\\pjdietz\\WellRESTed\\Interfaces\\Routes\\PrefixRouteInterface");
|
||||
$this->route->getPrefixes()->willReturn(["/cats/"]);
|
||||
$this->route->getResponse(Argument::cetera())->willReturn($this->response->reveal());
|
||||
|
||||
$this->request->getPath()->willReturn("/cats/");
|
||||
|
||||
$args = ["cat" => "molly"];
|
||||
|
||||
$table = new RouteTable();
|
||||
$table->addRoute($this->route->reveal());
|
||||
$table->getResponse($this->request->reveal(), $args);
|
||||
|
||||
$this->route->getResponse($this->request->reveal(), $args)->shouldHaveBeenCalled();
|
||||
}
|
||||
|
||||
public function testPropagatesArwgumentsToRoute()
|
||||
{
|
||||
$this->route->getResponse(Argument::cetera())->willReturn($this->response->reveal());
|
||||
$this->request->getPath()->willReturn("/cats/");
|
||||
$args = ["cat" => "molly"];
|
||||
|
||||
$table = new RouteTable();
|
||||
$table->addRoute($this->route->reveal());
|
||||
$table->getResponse($this->request->reveal(), $args);
|
||||
|
||||
$this->route->getResponse($this->request->reveal(), $args)->shouldHaveBeenCalled();
|
||||
}
|
||||
}
|
||||
|
|
@ -1,323 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace pjdietz\WellRESTed\Test;
|
||||
|
||||
use pjdietz\WellRESTed\Exceptions\HttpExceptions\HttpException;
|
||||
use pjdietz\WellRESTed\Router;
|
||||
use pjdietz\WellRESTed\Routes\TemplateRoute;
|
||||
use Prophecy\Argument;
|
||||
|
||||
/**
|
||||
* @covers pjdietz\WellRESTed\Router
|
||||
*/
|
||||
class RouterTest extends \PHPUnit_Framework_TestCase
|
||||
{
|
||||
private $handler;
|
||||
private $request;
|
||||
private $response;
|
||||
private $route;
|
||||
|
||||
public function setUp()
|
||||
{
|
||||
$this->request = $this->prophesize("\\pjdietz\\WellRESTed\\Interfaces\\RequestInterface");
|
||||
$this->request->getPath()->willReturn("/");
|
||||
$this->request->getMethod()->willReturn("GET");
|
||||
$this->response = $this->prophesize("\\pjdietz\\WellRESTed\\Interfaces\\ResponseInterface");
|
||||
$this->route = $this->prophesize("\\pjdietz\\WellRESTed\\Interfaces\\HandlerInterface");
|
||||
$this->handler = $this->prophesize("\\pjdietz\\WellRESTed\\Interfaces\\HandlerInterface");
|
||||
}
|
||||
|
||||
public function testAddsSingleRoute()
|
||||
{
|
||||
$this->request->getPath()->willReturn("/cats/");
|
||||
$this->handler->getResponse(Argument::cetera())->willReturn($this->response->reveal());
|
||||
|
||||
$router = new Router();
|
||||
$router->add("/cats/", $this->handler->reveal());
|
||||
$response = $router->getResponse($this->request->reveal());
|
||||
$this->assertNotNull($response);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider pathProvider
|
||||
*/
|
||||
public function testAddsMultpleRoutes($path, $exptectedSuccess)
|
||||
{
|
||||
$this->request->getPath()->willReturn($path);
|
||||
$this->handler->getResponse(Argument::cetera())->willReturn($this->response->reveal());
|
||||
|
||||
$router = new Router();
|
||||
$router->add(
|
||||
["/cats/", $this->handler->reveal()],
|
||||
["/cats/*", $this->handler->reveal()],
|
||||
["/dogs/{name}", $this->handler->reveal(), TemplateRoute::RE_ALPHA],
|
||||
["~/hamsters/[a-z]+~", $this->handler->reveal()]
|
||||
);
|
||||
$response = $router->getResponse($this->request->reveal());
|
||||
$this->assertEquals($exptectedSuccess, !is_null($response));
|
||||
}
|
||||
|
||||
public function pathProvider()
|
||||
{
|
||||
return [
|
||||
["/cats/", true],
|
||||
["/cats/molly", true],
|
||||
["/dogs/bear", true],
|
||||
["/hamsters/fizzgig", true],
|
||||
["/dogs/", false],
|
||||
["/birds/", false],
|
||||
["/hamsters/23", false]
|
||||
];
|
||||
}
|
||||
|
||||
public function testAddsSingleRouteInstance()
|
||||
{
|
||||
$router = new Router();
|
||||
$router->addRoute($this->route->reveal());
|
||||
$router->getResponse($this->request->reveal());
|
||||
$this->route->getResponse(Argument::cetera())->shouldHaveBeenCalled();
|
||||
}
|
||||
|
||||
public function testAddsMultipleRouteInstances()
|
||||
{
|
||||
$router = new Router();
|
||||
$router->addRoutes([$this->route->reveal()]);
|
||||
$router->getResponse($this->request->reveal());
|
||||
$this->route->getResponse(Argument::cetera())->shouldHaveBeenCalled();
|
||||
}
|
||||
|
||||
public function testPropagatesArgumentsToRouteTable()
|
||||
{
|
||||
$this->route->getResponse(Argument::cetera())->willReturn($this->response->reveal());
|
||||
$this->request->getPath()->willReturn("/cats/");
|
||||
$args = ["cat" => "molly"];
|
||||
|
||||
$router = new Router();
|
||||
$router->addRoute($this->route->reveal());
|
||||
$router->getResponse($this->request->reveal(), $args);
|
||||
|
||||
$this->route->getResponse($this->request->reveal(), $args)->shouldHaveBeenCalled();
|
||||
}
|
||||
|
||||
public function testRespondsWithErrorResponseForHttpException()
|
||||
{
|
||||
$this->route->getResponse(Argument::cetera())->willThrow(new HttpException());
|
||||
|
||||
$router = new Router();
|
||||
$router->addRoute($this->route->reveal());
|
||||
$response = $router->getResponse($this->request->reveal());
|
||||
$this->assertEquals(500, $response->getStatusCode());
|
||||
}
|
||||
|
||||
public function testDispatchesErrorHandlerForStatusCode()
|
||||
{
|
||||
$this->response->getStatusCode()->willReturn(403);
|
||||
$this->route->getResponse(Argument::cetera())->willReturn($this->response->reveal());
|
||||
|
||||
$errorHandler = $this->prophesize("\\pjdietz\\WellRESTed\\Interfaces\\HandlerInterface");
|
||||
$errorHandler->getResponse(Argument::cetera())->willReturn($this->response->reveal());
|
||||
|
||||
$router = new Router();
|
||||
$router->addRoute($this->route->reveal());
|
||||
$router->setErrorHandlers([403 => $errorHandler->reveal()]);
|
||||
$router->getResponse($this->request->reveal());
|
||||
|
||||
$errorHandler->getResponse(Argument::cetera())->shouldHaveBeenCalled();
|
||||
}
|
||||
|
||||
public function testDispatchesErrorHandlerWithOriginalRequest()
|
||||
{
|
||||
$this->request->getPath()->willReturn("/");
|
||||
$this->response->getStatusCode()->willReturn(403);
|
||||
$this->route->getResponse(Argument::cetera())->willReturn($this->response->reveal());
|
||||
|
||||
$errorHandler = $this->prophesize("\\pjdietz\\WellRESTed\\Interfaces\\HandlerInterface");
|
||||
$errorHandler->getResponse(Argument::cetera())->willReturn($this->response->reveal());
|
||||
|
||||
$request = $this->request->reveal();
|
||||
|
||||
$router = new Router();
|
||||
$router->addRoute($this->route->reveal());
|
||||
$router->setErrorHandlers([403 => $errorHandler->reveal()]);
|
||||
$router->getResponse($request);
|
||||
|
||||
$errorHandler->getResponse(
|
||||
Argument::that(
|
||||
function ($arg) use ($request) {
|
||||
return $arg === $request;
|
||||
}
|
||||
),
|
||||
Argument::any()
|
||||
)->shouldHaveBeenCalled();
|
||||
}
|
||||
|
||||
public function testDispatchesErrorHandlerWithOriginalArguments()
|
||||
{
|
||||
$this->request->getPath()->willReturn("/");
|
||||
$this->response->getStatusCode()->willReturn(403);
|
||||
$response = $this->response->reveal();
|
||||
$this->route->getResponse(Argument::cetera())->willReturn($response);
|
||||
|
||||
$errorHandler = $this->prophesize("\\pjdietz\\WellRESTed\\Interfaces\\HandlerInterface");
|
||||
$errorHandler->getResponse(Argument::cetera())->willReturn($this->response->reveal());
|
||||
|
||||
$arguments = [
|
||||
"cat" => "Molly",
|
||||
"dog" => "Bear"
|
||||
];
|
||||
|
||||
$router = new Router();
|
||||
$router->addRoute($this->route->reveal());
|
||||
$router->setErrorHandlers([403 => $errorHandler->reveal()]);
|
||||
$router->getResponse($this->request->reveal(), $arguments);
|
||||
|
||||
$errorHandler->getResponse(
|
||||
Argument::any(),
|
||||
Argument::that(
|
||||
function ($args) use ($arguments) {
|
||||
return count(array_diff_assoc($arguments, $args)) === 0;
|
||||
}
|
||||
)
|
||||
)->shouldHaveBeenCalled();
|
||||
}
|
||||
|
||||
public function testDispatchesErrorHandlerWithPreviousResponse()
|
||||
{
|
||||
$this->request->getPath()->willReturn("/");
|
||||
$this->response->getStatusCode()->willReturn(403);
|
||||
$response = $this->response->reveal();
|
||||
$this->route->getResponse(Argument::cetera())->willReturn($response);
|
||||
|
||||
$errorHandler = $this->prophesize("\\pjdietz\\WellRESTed\\Interfaces\\HandlerInterface");
|
||||
$errorHandler->getResponse(Argument::cetera())->willReturn($this->response->reveal());
|
||||
|
||||
$router = new Router();
|
||||
$router->addRoute($this->route->reveal());
|
||||
$router->setErrorHandlers([403 => $errorHandler->reveal()]);
|
||||
$router->getResponse($this->request->reveal());
|
||||
|
||||
$errorHandler->getResponse(
|
||||
Argument::any(),
|
||||
Argument::that(
|
||||
function ($arg) use ($response) {
|
||||
return isset($arg["response"]) && $arg["response"] === $response;
|
||||
}
|
||||
)
|
||||
)->shouldHaveBeenCalled();
|
||||
}
|
||||
|
||||
public function testDispatchesErrorCallable()
|
||||
{
|
||||
$this->request->getPath()->willReturn("/");
|
||||
$this->response->getStatusCode()->willReturn(403);
|
||||
$this->route->getResponse(Argument::cetera())->willReturn($this->response->reveal());
|
||||
|
||||
$errorResponse = $this->prophesize("\\pjdietz\\WellRESTed\\Interfaces\\ResponseInterface");
|
||||
$errorResponse->respond()->willReturn();
|
||||
|
||||
$errorCallable = function () use ($errorResponse) {
|
||||
return $errorResponse->reveal();
|
||||
};
|
||||
|
||||
$router = new Router();
|
||||
$router->addRoute($this->route->reveal());
|
||||
$router->setErrorHandlers([403 => $errorCallable]);
|
||||
$result = $router->getResponse($this->request->reveal());
|
||||
|
||||
$this->assertSame($errorResponse->reveal(), $result);
|
||||
}
|
||||
|
||||
/**
|
||||
* @runInSeparateProcess
|
||||
* @preserveGlobalState disabled
|
||||
*/
|
||||
public function testRoutesServerRequest()
|
||||
{
|
||||
$_SERVER["REQUEST_URI"] = "/cats/";
|
||||
$_SERVER["HTTP_HOST"] = "localhost";
|
||||
$_SERVER["REQUEST_METHOD"] = "GET";
|
||||
|
||||
$this->response->getStatusCode()->willReturn(200);
|
||||
$this->response->respond()->willReturn();
|
||||
|
||||
$this->route->willImplement("\\pjdietz\\WellRESTed\\Interfaces\\Routes\\StaticRouteInterface");
|
||||
$this->route->getPaths()->willReturn(["/cats/"]);
|
||||
$this->route->getResponse(Argument::cetera())->willReturn($this->response->reveal());
|
||||
|
||||
$router = new Router();
|
||||
$router->addRoute($this->route->reveal());
|
||||
|
||||
ob_start();
|
||||
$router->respond();
|
||||
ob_end_clean();
|
||||
|
||||
$this->response->respond()->shouldHaveBeenCalled();
|
||||
}
|
||||
|
||||
/**
|
||||
* @runInSeparateProcess
|
||||
* @preserveGlobalState disabled
|
||||
*/
|
||||
public function testRoutesStaticRequestToNoRouteResponse()
|
||||
{
|
||||
$_SERVER["REQUEST_URI"] = "/cats/";
|
||||
$_SERVER["HTTP_HOST"] = "localhost";
|
||||
$_SERVER["REQUEST_METHOD"] = "GET";
|
||||
|
||||
$router = new Router();
|
||||
|
||||
ob_start();
|
||||
$router->respond();
|
||||
$captured = ob_get_contents();
|
||||
ob_end_clean();
|
||||
|
||||
$this->assertEquals("No resource at /cats/", $captured);
|
||||
}
|
||||
|
||||
/**
|
||||
* @runInSeparateProcess
|
||||
* @preserveGlobalState disabled
|
||||
*/
|
||||
public function testRoutesStaticRequestTo404ErrorHandler()
|
||||
{
|
||||
$_SERVER["REQUEST_URI"] = "/cats/";
|
||||
$_SERVER["HTTP_HOST"] = "localhost";
|
||||
$_SERVER["REQUEST_METHOD"] = "GET";
|
||||
|
||||
$errorHandler = $this->prophesize("\\pjdietz\\WellRESTed\\Interfaces\\HandlerInterface");
|
||||
$errorHandler->getResponse(Argument::cetera())->willReturn($this->response->reveal());
|
||||
|
||||
$router = new Router();
|
||||
$router->setErrorHandler(404, $errorHandler->reveal());
|
||||
|
||||
ob_start();
|
||||
$router->respond();
|
||||
ob_end_clean();
|
||||
|
||||
$errorHandler->getResponse(Argument::cetera())->shouldHaveBeenCalled();
|
||||
}
|
||||
|
||||
public function testDeprecatedSetsStaticRoute()
|
||||
{
|
||||
$this->handler->getResponse(Argument::cetera())->willReturn($this->response->reveal());
|
||||
$this->request->getPath()->willReturn("/cats/");
|
||||
|
||||
$router = new Router();
|
||||
@$router->setStaticRoute(["/cats/"], $this->handler->reveal());
|
||||
$router->getResponse($this->request->reveal());
|
||||
|
||||
$this->handler->getResponse(Argument::cetera())->shouldHaveBeenCalled();
|
||||
}
|
||||
|
||||
public function testDeprecatedSetsPrefixRoute()
|
||||
{
|
||||
$this->handler->getResponse(Argument::cetera())->willReturn($this->response->reveal());
|
||||
$this->request->getPath()->willReturn("/cats/molly");
|
||||
|
||||
$router = new Router();
|
||||
@$router->setPrefixRoute(["/cats/"], $this->handler->reveal());
|
||||
$router->getResponse($this->request->reveal());
|
||||
|
||||
$this->handler->getResponse(Argument::cetera())->shouldHaveBeenCalled();
|
||||
}
|
||||
}
|
||||
|
|
@ -1,74 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace pjdietz\WellRESTed\Test;
|
||||
|
||||
use pjdietz\WellRESTed\Routes\StaticRoute;
|
||||
use Prophecy\Argument;
|
||||
|
||||
/**
|
||||
* @covers pjdietz\WellRESTed\Routes\BaseRoute
|
||||
*/
|
||||
class BaseRouteTest extends \PHPUnit_Framework_TestCase
|
||||
{
|
||||
public function testDispatchesHandlerTarget()
|
||||
{
|
||||
$request = $this->prophesize("\\pjdietz\\WellRESTed\\Interfaces\\RequestInterface");
|
||||
$request->getPath()->willReturn("/");
|
||||
|
||||
$response = $this->prophesize("\\pjdietz\\WellRESTed\\Interfaces\\ResponseInterface");
|
||||
|
||||
$handler = $this->prophesize("\\pjdietz\\WellRESTed\\Interfaces\\HandlerInterface");
|
||||
$handler->getResponse(Argument::cetera())->willReturn($response->reveal());
|
||||
|
||||
$route = new StaticRoute("/", $handler->reveal());
|
||||
$result = $route->getResponse($request->reveal());
|
||||
|
||||
$this->assertSame($response->reveal(), $result);
|
||||
$handler->getResponse(Argument::cetera())->shouldHaveBeenCalled();
|
||||
}
|
||||
|
||||
public function testDispatchesResponseTarget()
|
||||
{
|
||||
$request = $this->prophesize("\\pjdietz\\WellRESTed\\Interfaces\\RequestInterface");
|
||||
$request->getPath()->willReturn("/");
|
||||
|
||||
$response = $this->prophesize("\\pjdietz\\WellRESTed\\Interfaces\\ResponseInterface");
|
||||
|
||||
$route = new StaticRoute("/", $response->reveal());
|
||||
$result = $route->getResponse($request->reveal());
|
||||
|
||||
$this->assertSame($response->reveal(), $result);
|
||||
}
|
||||
|
||||
public function testDispatchesNullTarget()
|
||||
{
|
||||
$request = $this->prophesize("\\pjdietz\\WellRESTed\\Interfaces\\RequestInterface");
|
||||
$request->getPath()->willReturn("/");
|
||||
|
||||
$route = new StaticRoute("/", function () { return null; });
|
||||
$result = $route->getResponse($request->reveal());
|
||||
|
||||
$this->assertNull($result);
|
||||
}
|
||||
|
||||
public function testPropagatesArgumentsToCallable()
|
||||
{
|
||||
$callableRequest = null;
|
||||
$callableArgs = null;
|
||||
$callable = function ($request, $args) use (&$callableRequest, &$callableArgs) {
|
||||
$callableRequest = $request;
|
||||
$callableArgs = $args;
|
||||
};
|
||||
|
||||
$request = $this->prophesize("\\pjdietz\\WellRESTed\\Interfaces\\RequestInterface");
|
||||
$request->getPath()->willReturn("/");
|
||||
|
||||
$args = ["cat" => "Molly"];
|
||||
|
||||
$route = new StaticRoute("/", $callable);
|
||||
$route->getResponse($request->reveal(), $args);
|
||||
|
||||
$this->assertSame($request->reveal(), $callableRequest);
|
||||
$this->assertSame($args, $callableArgs);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,100 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace pjdietz\WellRESTed\Test;
|
||||
|
||||
use pjdietz\WellRESTed\Routes\PrefixRoute;
|
||||
use Prophecy\Argument;
|
||||
|
||||
/**
|
||||
* @covers pjdietz\WellRESTed\Routes\PrefixRoute
|
||||
*/
|
||||
class PrefixRouteTest extends \PHPUnit_Framework_TestCase
|
||||
{
|
||||
private $handler;
|
||||
private $request;
|
||||
private $response;
|
||||
|
||||
public function testMatchesSinglePathExactly()
|
||||
{
|
||||
$this->request->getPath()->willReturn("/cats/");
|
||||
$route = new PrefixRoute("/cats/", $this->handler->reveal());
|
||||
$resp = $route->getResponse($this->request->reveal());
|
||||
$this->assertNotNull($resp);
|
||||
}
|
||||
|
||||
public function testMatchesSinglePathWithPrefix()
|
||||
{
|
||||
$this->request->getPath()->willReturn("/cats/molly");
|
||||
$route = new PrefixRoute("/cats/", $this->handler->reveal());
|
||||
$resp = $route->getResponse($this->request->reveal());
|
||||
$this->assertNotNull($resp);
|
||||
}
|
||||
|
||||
public function testMatchesPathInList()
|
||||
{
|
||||
$this->request->getPath()->willReturn("/cats/molly");
|
||||
$route = new PrefixRoute(array("/cats/", "/dogs/"), $this->handler->reveal());
|
||||
$resp = $route->getResponse($this->request->reveal());
|
||||
$this->assertNotNull($resp);
|
||||
}
|
||||
|
||||
public function testFailsToMatchPath()
|
||||
{
|
||||
$this->request->getPath()->willReturn("/dogs/");
|
||||
$route = new PrefixRoute("/cats/", $this->handler->reveal());
|
||||
$resp = $route->getResponse($this->request->reveal());
|
||||
$this->assertNull($resp);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider invalidPathsProvider
|
||||
* @expectedException \InvalidArgumentException
|
||||
*/
|
||||
public function testThrowsExceptionOnInvalidPath($path)
|
||||
{
|
||||
new PrefixRoute($path, "\\NoClass");
|
||||
}
|
||||
|
||||
public function invalidPathsProvider()
|
||||
{
|
||||
return array(
|
||||
array(false),
|
||||
array(17),
|
||||
array(null)
|
||||
);
|
||||
}
|
||||
|
||||
public function testReturnsPrefixes()
|
||||
{
|
||||
$paths = array("/cats/", "/dogs/");
|
||||
$route = new PrefixRoute($paths, $this->handler->reveal());
|
||||
$this->assertEquals($paths, $route->getPrefixes());
|
||||
}
|
||||
|
||||
public function testPropagatesArgumentsToCallable()
|
||||
{
|
||||
$callableRequest = null;
|
||||
$callableArgs = null;
|
||||
$callable = function ($request, $args) use (&$callableRequest, &$callableArgs) {
|
||||
$callableRequest = $request;
|
||||
$callableArgs = $args;
|
||||
};
|
||||
|
||||
$this->request->getPath()->willReturn("/");
|
||||
$args = ["cat" => "Molly"];
|
||||
|
||||
$route = new PrefixRoute("/", $callable);
|
||||
$route->getResponse($this->request->reveal(), $args);
|
||||
|
||||
$this->assertSame($this->request->reveal(), $callableRequest);
|
||||
$this->assertSame($args, $callableArgs);
|
||||
}
|
||||
|
||||
public function setUp()
|
||||
{
|
||||
$this->request = $this->prophesize("\\pjdietz\\WellRESTed\\Interfaces\\RequestInterface");
|
||||
$this->response = $this->prophesize("\\pjdietz\\WellRESTed\\Interfaces\\ResponseInterface");
|
||||
$this->handler = $this->prophesize("\\pjdietz\\WellRESTed\\Interfaces\\HandlerInterface");
|
||||
$this->handler->getResponse(Argument::cetera())->willReturn($this->response->reveal());
|
||||
}
|
||||
}
|
||||
|
|
@ -1,127 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace pjdietz\WellRESTed\Test;
|
||||
|
||||
use pjdietz\WellRESTed\Routes\RegexRoute;
|
||||
use Prophecy\Argument;
|
||||
|
||||
/**
|
||||
* @covers pjdietz\WellRESTed\Routes\RegexRoute
|
||||
*/
|
||||
class RegexRouteTest extends \PHPUnit_Framework_TestCase
|
||||
{
|
||||
private $handler;
|
||||
private $request;
|
||||
private $response;
|
||||
|
||||
/**
|
||||
* @dataProvider matchingRouteProvider
|
||||
*/
|
||||
public function testMatchesPattern($pattern, $path)
|
||||
{
|
||||
$this->request->getPath()->willReturn($path);
|
||||
|
||||
$route = new RegexRoute($pattern, $this->handler->reveal());
|
||||
$resp = $route->getResponse($this->request->reveal());
|
||||
$this->assertNotNull($resp);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider matchingRouteProvider
|
||||
*/
|
||||
public function testExtractsCaptures($pattern, $path, $captures)
|
||||
{
|
||||
$this->request->getPath()->willReturn($path);
|
||||
|
||||
$route = new RegexRoute($pattern, $this->handler->reveal());
|
||||
$route->getResponse($this->request->reveal());
|
||||
$this->handler->getResponse(Argument::any(), Argument::that(
|
||||
function ($args) use ($captures) {
|
||||
return $args = $captures;
|
||||
}))->shouldHaveBeenCalled();
|
||||
}
|
||||
|
||||
public function matchingRouteProvider()
|
||||
{
|
||||
return [
|
||||
["~/cat/[0-9]+~", "/cat/2", [0 => "/cat/2"]],
|
||||
["#/dog/.*#", "/dog/his-name-is-bear", [0 => "/dog/his-name-is-bear"]],
|
||||
["~/cat/([0-9]+)~", "/cat/2", [
|
||||
0 => "/cat/2",
|
||||
1 => "2"
|
||||
]],
|
||||
["~/dog/(?<id>[0-9+])~", "/dog/2", [
|
||||
0 => "/dog/2",
|
||||
1 => "2",
|
||||
"id" => "2"
|
||||
]]
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider mismatchingRouteProvider
|
||||
*/
|
||||
public function testFailsToMatchMismatchingPattern($pattern, $path)
|
||||
{
|
||||
$this->request->getPath()->willReturn($path);
|
||||
|
||||
$route = new RegexRoute($pattern, $this->handler->reveal());
|
||||
$resp = $route->getResponse($this->request->reveal());
|
||||
$this->assertNull($resp);
|
||||
}
|
||||
|
||||
public function mismatchingRouteProvider()
|
||||
{
|
||||
return [
|
||||
["~/cat/[0-9]+~", "/cat/molly"],
|
||||
["~/cat/[0-9]+~", "/dog/bear"],
|
||||
["#/dog/.*#", "/dog"]
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider invalidRouteProvider
|
||||
* @expectedException \pjdietz\WellRESTed\Exceptions\ParseException
|
||||
*/
|
||||
public function testThrowsExceptionOnInvalidPattern($pattern)
|
||||
{
|
||||
$route = new RegexRoute($pattern, $this->handler->reveal());
|
||||
$route->getResponse($this->request->reveal());
|
||||
}
|
||||
|
||||
public function invalidRouteProvider()
|
||||
{
|
||||
return [
|
||||
["~/unterminated"],
|
||||
["/nope"]
|
||||
];
|
||||
}
|
||||
|
||||
public function testPropagatesArgumentsToCallable()
|
||||
{
|
||||
$callableRequest = null;
|
||||
$callableArgs = null;
|
||||
$callable = function ($request, $args) use (&$callableRequest, &$callableArgs) {
|
||||
$callableRequest = $request;
|
||||
$callableArgs = $args;
|
||||
};
|
||||
|
||||
$this->request->getPath()->willReturn("/dog/bear");
|
||||
$args = ["cat" => "Molly"];
|
||||
|
||||
$route = new RegexRoute("~/dog/(?<dog>[a-z]+)~", $callable);
|
||||
$route->getResponse($this->request->reveal(), $args);
|
||||
|
||||
$this->assertSame($this->request->reveal(), $callableRequest);
|
||||
$this->assertArraySubset($args, $callableArgs);
|
||||
$this->assertArraySubset(["dog" => "bear"], $callableArgs);
|
||||
}
|
||||
|
||||
public function setUp()
|
||||
{
|
||||
$this->request = $this->prophesize("\\pjdietz\\WellRESTed\\Interfaces\\RequestInterface");
|
||||
$this->response = $this->prophesize("\\pjdietz\\WellRESTed\\Interfaces\\ResponseInterface");
|
||||
$this->handler = $this->prophesize("\\pjdietz\\WellRESTed\\Interfaces\\HandlerInterface");
|
||||
$this->handler->getResponse(Argument::cetera())->willReturn($this->response->reveal());
|
||||
}
|
||||
}
|
||||
|
|
@ -1,38 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace pjdietz\WellRESTed\Test;
|
||||
|
||||
use pjdietz\WellRESTed\Routes\RouteFactory;
|
||||
use Prophecy\Argument;
|
||||
|
||||
/**
|
||||
* @covers pjdietz\WellRESTed\Routes\RouteFactory
|
||||
*/
|
||||
class RouteFactoryTest extends \PHPUnit_Framework_TestCase
|
||||
{
|
||||
/**
|
||||
* @dataProvider routeProvider
|
||||
*/
|
||||
public function testCreatesRouteOfCorrectType($path, $expectedType)
|
||||
{
|
||||
$factory = new RouteFactory();
|
||||
$route = $factory->createRoute($path, "\\MyHandler");
|
||||
$this->assertInstanceOf($expectedType, $route);
|
||||
}
|
||||
|
||||
public function routeProvider()
|
||||
{
|
||||
$static = "\\pjdietz\\WellRESTed\\Routes\\StaticRoute";
|
||||
$prefix = "\\pjdietz\\WellRESTed\\Routes\\PrefixRoute";
|
||||
$template = "\\pjdietz\\WellRESTed\\Routes\\TemplateRoute";
|
||||
$regex = "\\pjdietz\\WellRESTed\\Routes\\RegexRoute";
|
||||
|
||||
return [
|
||||
["/cats/", $static],
|
||||
["/cats/*", $prefix],
|
||||
["/cats/{catId}", $template],
|
||||
["~/cat/[0-9]+~", $regex]
|
||||
];
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -1,93 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace pjdietz\WellRESTed\Test;
|
||||
|
||||
use pjdietz\WellRESTed\Routes\StaticRoute;
|
||||
use Prophecy\Argument;
|
||||
|
||||
/**
|
||||
* @covers pjdietz\WellRESTed\Routes\StaticRoute
|
||||
*/
|
||||
class StaticRouteTest extends \PHPUnit_Framework_TestCase
|
||||
{
|
||||
private $handler;
|
||||
private $request;
|
||||
private $response;
|
||||
|
||||
public function testMatchesSinglePath()
|
||||
{
|
||||
$this->request->getPath()->willReturn("/cats/");
|
||||
$route = new StaticRoute("/cats/", $this->handler->reveal());
|
||||
$resp = $route->getResponse($this->request->reveal());
|
||||
$this->assertNotNull($resp);
|
||||
}
|
||||
|
||||
public function testMatchesPathInList()
|
||||
{
|
||||
$this->request->getPath()->willReturn("/cats/");
|
||||
$route = new StaticRoute(array("/cats/", "/dogs/"), $this->handler->reveal());
|
||||
$resp = $route->getResponse($this->request->reveal());
|
||||
$this->assertNotNull($resp);
|
||||
}
|
||||
|
||||
public function testFailsToMatchPath()
|
||||
{
|
||||
$this->request->getPath()->willReturn("/dogs/");
|
||||
$route = new StaticRoute("/cats/", $this->handler->reveal());
|
||||
$resp = $route->getResponse($this->request->reveal());
|
||||
$this->assertNull($resp);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider invalidPathsProvider
|
||||
* @expectedException \InvalidArgumentException
|
||||
*/
|
||||
public function testThrowsExceptionOnInvalidPath($path)
|
||||
{
|
||||
new StaticRoute($path, "\\NoClass");
|
||||
}
|
||||
|
||||
public function invalidPathsProvider()
|
||||
{
|
||||
return array(
|
||||
array(false),
|
||||
array(17),
|
||||
array(null)
|
||||
);
|
||||
}
|
||||
|
||||
public function testReturnsPaths()
|
||||
{
|
||||
$paths = array("/cats/", "/dogs/");
|
||||
$route = new StaticRoute($paths, $this->handler->reveal());
|
||||
$this->assertEquals($paths, $route->getPaths());
|
||||
}
|
||||
|
||||
public function testPropagatesArgumentsToCallable()
|
||||
{
|
||||
$callableRequest = null;
|
||||
$callableArgs = null;
|
||||
$callable = function ($request, $args) use (&$callableRequest, &$callableArgs) {
|
||||
$callableRequest = $request;
|
||||
$callableArgs = $args;
|
||||
};
|
||||
|
||||
$this->request->getPath()->willReturn("/");
|
||||
|
||||
$args = ["cat" => "Molly"];
|
||||
|
||||
$route = new StaticRoute("/", $callable);
|
||||
$route->getResponse($this->request->reveal(), $args);
|
||||
|
||||
$this->assertSame($this->request->reveal(), $callableRequest);
|
||||
$this->assertSame($args, $callableArgs);
|
||||
}
|
||||
|
||||
public function setUp()
|
||||
{
|
||||
$this->request = $this->prophesize("\\pjdietz\\WellRESTed\\Interfaces\\RequestInterface");
|
||||
$this->response = $this->prophesize("\\pjdietz\\WellRESTed\\Interfaces\\ResponseInterface");
|
||||
$this->handler = $this->prophesize("\\pjdietz\\WellRESTed\\Interfaces\\HandlerInterface");
|
||||
$this->handler->getResponse(Argument::cetera())->willReturn($this->response->reveal());
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue