Compare commits
177 Commits
v3.0.0-bet
...
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 |
|
|
@ -1,8 +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
|
||||
/test export-ignore
|
||||
/phpunit.xml.dist export-ignore
|
||||
/Vagrantfile export-ignore
|
||||
/vagrant export-ignore
|
||||
/tests export-ignore
|
||||
/phpunit.xml* export-ignore
|
||||
/psalm.xml export-ignore
|
||||
/public export-ignore
|
||||
/vendor export-ignore
|
||||
|
|
@ -5,8 +5,12 @@ vendor/
|
|||
phpdoc/
|
||||
|
||||
# Code coverage report
|
||||
coverage/
|
||||
report/
|
||||
|
||||
# Cache
|
||||
.php_cs.cache
|
||||
|
||||
# Sphinx Documentation
|
||||
docs/build
|
||||
|
||||
|
|
@ -14,15 +18,13 @@ docs/build
|
|||
README.html
|
||||
preview
|
||||
|
||||
# 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
|
||||
139
README.md
139
README.md
|
|
@ -1,16 +1,20 @@
|
|||
WellRESTed
|
||||
==========
|
||||
|
||||
[](https://travis-ci.org/wellrestedphp/wellrested)
|
||||
[](https://readthedocs.org/projects/wellrested/?badge=latest)
|
||||
[](https://insight.sensiolabs.com/projects/b0a2efcb-49f8-4a90-a5bd-0c14e409f59e)
|
||||
[](https://php.net/)
|
||||
[](http://wellrested.readthedocs.org/en/latest/)
|
||||
|
||||
WellRESTed is a library for creating RESTful Web services in PHP.
|
||||
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.4
|
||||
### 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
|
||||
-------
|
||||
|
|
@ -20,7 +24,7 @@ Add an entry for "wellrested/wellrested" to your composer.json file's `require`
|
|||
```json
|
||||
{
|
||||
"require": {
|
||||
"wellrested/wellrested": "~3.0"
|
||||
"wellrested/wellrested": "^5"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
|
@ -28,7 +32,7 @@ Add an entry for "wellrested/wellrested" to your composer.json file's `require`
|
|||
Documentation
|
||||
-------------
|
||||
|
||||
See [the documentation](http://wellrested.readthedocs.org/en/latest/) to get started.
|
||||
See [the documentation](https://wellrested.readthedocs.org/en/latest/) to get started.
|
||||
|
||||
Example
|
||||
-------
|
||||
|
|
@ -36,60 +40,89 @@ Example
|
|||
```php
|
||||
<?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;
|
||||
|
||||
require_once "vendor/autoload.php";
|
||||
// Create a handler using the PSR-15 RequestHandlerInterface
|
||||
class HomePageHandler implements RequestHandlerInterface
|
||||
{
|
||||
public function handle(ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
|
||||
// Build some middleware. We'll register these with a server below.
|
||||
// We're using callables to fit this all in one example, but these
|
||||
// could also be classes implementing WellRESTed\MiddlewareInterface.
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
// Set the status code and provide the greeting as the response body.
|
||||
$hello = function ($request, $response, $next) {
|
||||
|
||||
// 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 = $response->withStatus(200)
|
||||
->withHeader("Content-type", "text/plain")
|
||||
->withBody(new Stream("Hello, $name!"));
|
||||
|
||||
// Propagate to the next middleware, if any, and return the response.
|
||||
return $next($request, $response);
|
||||
|
||||
};
|
||||
|
||||
// Add a header to the response.
|
||||
$headerAdder = function ($request, $response, $next) {
|
||||
// Add the header.
|
||||
$response = $response->withHeader("X-example", "hello world");
|
||||
// Propagate to the next middleware, if any, and return the response.
|
||||
return $next($request, $response);
|
||||
};
|
||||
|
||||
// Create a server
|
||||
// Create a new Server instance.
|
||||
$server = new Server();
|
||||
|
||||
// Start each request-response cycle by dispatching the header adder.
|
||||
$server->add($headerAdder);
|
||||
|
||||
// The header adder will propagate to this router, which will dispatch the
|
||||
// $hello middleware, possibly with a {name} variable.
|
||||
$server->add($server->createRouter()
|
||||
->register("GET", "/hello", $hello)
|
||||
->register("GET", "/hello/{name}", $hello)
|
||||
);
|
||||
|
||||
// Read the request from the client, dispatch middleware, and output.
|
||||
// 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();
|
||||
|
||||
```
|
||||
|
||||
Development
|
||||
-----------
|
||||
|
||||
Use Docker to run unit tests, manage Composer dependencies, and render a preview of the documentation site.
|
||||
|
||||
To get started, run:
|
||||
|
||||
```bash
|
||||
docker-compose build
|
||||
docker-compose run --rm php composer install
|
||||
```
|
||||
|
||||
To run PHPUnit tests, use the `php` service:
|
||||
|
||||
```bash
|
||||
docker-compose run --rm php phpunit
|
||||
```
|
||||
|
||||
To run Psalm for static analysis:
|
||||
|
||||
```bash
|
||||
docker-compose run --rm php psalm
|
||||
```
|
||||
|
||||
To run PHP Coding Standards Fixer:
|
||||
|
||||
```bash
|
||||
docker-compose run --rm php php-cs-fixer fix
|
||||
```
|
||||
|
||||
To generate documentation, use the `docs` service:
|
||||
|
||||
```bash
|
||||
# Generate
|
||||
docker-compose run --rm docs
|
||||
# Clean
|
||||
docker-compose run --rm docs make clean -C docs
|
||||
```
|
||||
|
||||
To run a local playground site, use:
|
||||
|
||||
```bash
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
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,8 +1,7 @@
|
|||
{
|
||||
"name": "wellrested/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": [
|
||||
|
|
@ -12,15 +11,19 @@
|
|||
}
|
||||
],
|
||||
"require": {
|
||||
"php": ">=5.4.0",
|
||||
"psr/http-message": "~1.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": {
|
||||
"phpunit/phpunit": "~4.6"
|
||||
"provide": {
|
||||
"psr/http-message-implementation": "1.0",
|
||||
"psr/http-factory-implementation": "1.0"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"WellRESTed\\" : "src/"
|
||||
"WellRESTed\\": "src/"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -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
|
||||
|
|
@ -6,7 +6,6 @@ from pygments.lexers.web import PhpLexer
|
|||
|
||||
lexers['php'] = PhpLexer(startinline=True, linenos=1)
|
||||
lexers['php-annotations'] = PhpLexer(startinline=True, linenos=1)
|
||||
primary_domain = 'php'
|
||||
|
||||
# Add any Sphinx extension module names here, as strings. They can be
|
||||
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
|
||||
|
|
@ -26,9 +25,9 @@ master_doc = 'index'
|
|||
|
||||
# General information about the project.
|
||||
project = u'WellRESTed'
|
||||
copyright = u'2015, PJ Dietz'
|
||||
version = '3.0.0'
|
||||
release = '3.0.0'
|
||||
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.
|
||||
|
|
|
|||
|
|
@ -1,109 +1,63 @@
|
|||
Dependency Injection
|
||||
====================
|
||||
|
||||
Here are a few strategies for how to make a dependency injection container availble to middleware with WellRESTed.
|
||||
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).
|
||||
|
||||
Request Attribute
|
||||
^^^^^^^^^^^^^^^^^
|
||||
This section describes the recommended way of using WellRESTed with Pimple_, a common dependency injection container for PHP.
|
||||
|
||||
``Psr\Http\Message\ServerRequestInterface`` provides "attributes" that allow you attach arbitrary data to a request. You can use this to make your dependcy container available to any dispatched middleware.
|
||||
|
||||
When you instatiate a ``WellRESTed\Server``, you can provide an array of attributes that the server will add to the request.
|
||||
Imaging we have a ``FooHandler`` that depends on a ``BarInterface``, and ``BazInterface``. Our handler looks something like this:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
$container = new MySuperCoolDependencyContainer();
|
||||
class FooHandler implements RequestHandlerInterface
|
||||
{
|
||||
private $bar;
|
||||
private $baz;
|
||||
|
||||
$server = new WellRESTed\Server(["container" => $container]);
|
||||
// ... Add middleware, routes, etc. ...
|
||||
|
||||
When the server dispatches middleware, the middleware will be able to read the contain as the "container" attribute.
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
function ($request, $response, $next) {
|
||||
$container = $request->getAttribute("container");
|
||||
// It's a super cool dependency container!
|
||||
public function __construct(BarInterface $bar, BazInterface $baz)
|
||||
{
|
||||
$this->bar = $bar;
|
||||
$this->baz = $baz;
|
||||
}
|
||||
|
||||
Callables
|
||||
^^^^^^^^^
|
||||
|
||||
Another approach is to use callables that return ``MiddlewareInterface`` instances when you assign middleware. This approach provides an oppurtunity to pass the container into the middleware's constructor.
|
||||
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
Class CatHandler implements WellRESTed\MiddlewareInterface
|
||||
public function handle(ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
private $container;
|
||||
|
||||
public function __construct($container)
|
||||
{
|
||||
$this->container = $container;
|
||||
}
|
||||
|
||||
public function __invoke(ServerRequestInterface $request, ResponseInterface $response, $next);
|
||||
{
|
||||
// Do something with the $this->container, and make a response.
|
||||
// Do something with the bar and baz and return a response...
|
||||
// ...
|
||||
return $response;
|
||||
}
|
||||
}
|
||||
|
||||
When you add the middleware to the server or register it with a router, use a callable that passes container into the contructor.
|
||||
We can register the handler and these dependencies in a Pimple_ service provider.
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
$container = new MySuperCoolDependencyContainer();
|
||||
|
||||
$catHandler = function () use ($container) {
|
||||
return new CatHandler($container);
|
||||
}
|
||||
|
||||
$server = new Server();
|
||||
$server->add(
|
||||
$server->createRoute()
|
||||
->register("GET", "/cats/{cat}", $catHandler)
|
||||
);
|
||||
$server->respond();
|
||||
|
||||
For extra fun, store the callable that provides the handler in the container. Here's an example using Pimple_).
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
$c = new Pimple\Container();
|
||||
$c["catHandler"] = $c->protect(function () use ($c) {
|
||||
return new CatHandler($c);
|
||||
});
|
||||
|
||||
$server = new Server();
|
||||
$server->add(
|
||||
$server->createRoute()
|
||||
->register("GET", "/cats/{cat}", $c["catHandler"])
|
||||
);
|
||||
$server->respond();
|
||||
|
||||
Combined
|
||||
^^^^^^^^
|
||||
|
||||
Of course these two approaches are not mutually exclusive. You can even obtain your server from the container as well, for good measure.
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
$c = new Pimple\Container();
|
||||
$c["server"] = function ($c) {
|
||||
return new Server(["container" => $c);
|
||||
class MyServiceProvider implements ServiceProviderInterface
|
||||
{
|
||||
public function register(Container $c)
|
||||
{
|
||||
// Register the Bar and Baz as services.
|
||||
$c['bar'] = function ($c) {
|
||||
return new Bar();
|
||||
};
|
||||
$c["catHandler"] = $c->protect(function () use ($c) {
|
||||
return new CatHandler($c);
|
||||
$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']);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
$server = $c["server"];
|
||||
$server->add(
|
||||
$server->createRoute()
|
||||
->register("GET", "/cats/{cat}", $c["catHandler"])
|
||||
);
|
||||
$server->respond();
|
||||
To register this handler with a router, we can pass the service:
|
||||
|
||||
.. _Pimple: http://pimple.sensiolabs.org
|
||||
.. 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
|
||||
|
|
@ -3,15 +3,14 @@ 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 middleware and routing and show how to read variables from the request path.
|
||||
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 some middleware_, and transmits a response back to the client.
|
||||
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 middelware is a function that returns a response with the status code set to ``200`` and the body set to "Hello, world!".
|
||||
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!"
|
||||
|
|
@ -20,68 +19,58 @@ Our middelware is a function that returns a response with the status code set to
|
|||
|
||||
<?php
|
||||
|
||||
use Psr\Http\Server\RequestHandlerInterface;
|
||||
use WellRESTed\Message\Response;
|
||||
use WellRESTed\Message\Stream;
|
||||
use WellRESTed\Server;
|
||||
|
||||
require_once "vendor/autoload.php";
|
||||
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 middleware to dispatch that will return a response.
|
||||
// In this case, we'll use an anonymous function.
|
||||
$server->add(function ($request, $response, $next) {
|
||||
// Update the response with the greeting, status, and content-type.
|
||||
$response = $response->withStatus(200)
|
||||
->withHeader("Content-type", "text/plain")
|
||||
->withBody(new Stream("Hello, world!"));
|
||||
// Use $next to forward the request on to the next middleware, if any.
|
||||
return $next($request, $response);
|
||||
});
|
||||
// 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 middleware 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.
|
||||
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\StreamInteface``.
|
||||
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_ is a special type of middleware_ that examines the request and routes the request through to the middleware that matches.
|
||||
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
|
||||
|
||||
<?php
|
||||
|
||||
use WellRESTed\Message\Stream;
|
||||
use WellRESTed\Server;
|
||||
|
||||
require_once "vendor/autoload.php";
|
||||
|
||||
// Create a new server and use it to create a new router.
|
||||
// Create a new server.
|
||||
$server = new Server();
|
||||
|
||||
// Create a router to map methods and endpoints to handlers.
|
||||
$router = $server->createRouter();
|
||||
|
||||
// Map middleware to an endpoint and method(s).
|
||||
$router->register("GET", "/hello", function ($request, $response, $next) {
|
||||
// Update the response with the greeting, status, and content-type.
|
||||
$response = $response->withStatus(200)
|
||||
->withHeader("Content-type", "text/plain")
|
||||
->withBody(new Stream("Hello, world!"));
|
||||
// Use $next to forward the request on to the next middleware, if any.
|
||||
return $next($request, $response);
|
||||
});
|
||||
|
||||
// Add the router to the server.
|
||||
$router->register('GET', '/hello', new HelloHandler());
|
||||
$server->add($router);
|
||||
|
||||
// Read the request sent to the server and use it to output a response.
|
||||
|
|
@ -92,33 +81,27 @@ 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
|
||||
|
||||
<?php
|
||||
|
||||
use WellRESTed\Message\Stream;
|
||||
use WellRESTed\Server;
|
||||
|
||||
require_once "vendor/autoload.php";
|
||||
|
||||
// Define middleware.
|
||||
$hello = function ($request, $response, $next) {
|
||||
|
||||
class HelloHandler implements RequestHandlerInterface
|
||||
{
|
||||
public function handle(ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
// Check for a "name" attribute which may have been provided as a
|
||||
// path variable. The second parameters allows us to set a default.
|
||||
// path variable. Use "world" as a default.
|
||||
$name = $request->getAttribute("name", "world");
|
||||
|
||||
// Update the response with the greeting, status, and content-type.
|
||||
$response = $response->withStatus(200)
|
||||
// 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 $next($request, $response);
|
||||
// Return the response.
|
||||
return $response;
|
||||
}
|
||||
}
|
||||
|
||||
// Create the server and router.
|
||||
|
|
@ -129,65 +112,58 @@ Routes can be static (like the one above that matches only ``/hello``), or they
|
|||
$router->register("GET", "/hello", $hello);
|
||||
// Register to match a pattern with a variable.
|
||||
$router->register("GET", "/hello/{name}", $hello);
|
||||
|
||||
$server->add($router);
|
||||
|
||||
$server->respond();
|
||||
|
||||
Multiple Middleware
|
||||
^^^^^^^^^^^^^^^^^^^
|
||||
Middleware
|
||||
^^^^^^^^^^
|
||||
|
||||
One thing we haven't seen yet is how middleware work together. For the next example, we'll use an additional middleware that sets an ``X-example: hello world``.
|
||||
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
|
||||
|
||||
<?php
|
||||
// This middleware will add a custom header to every response.
|
||||
class CustomHeaderMiddleware implements MiddlewareInterface
|
||||
{
|
||||
public function process(
|
||||
ServerRequestInterface $request,
|
||||
RequestHandlerInterface $handler
|
||||
): ResponseInterface {
|
||||
|
||||
use WellRESTed\Message\Stream;
|
||||
use WellRESTed\Server;
|
||||
// Delegate to the next handler in the chain to obtain a response.
|
||||
$response = $handler->handle($request);
|
||||
|
||||
require_once "vendor/autoload.php";
|
||||
|
||||
// Set the status code and provide the greeting as the response body.
|
||||
$hello = function ($request, $response, $next) {
|
||||
|
||||
// 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 = $response->withStatus(200)
|
||||
->withHeader("Content-type", "text/plain")
|
||||
->withBody(new Stream("Hello, $name!"));
|
||||
|
||||
// Propagate to the next middleware, if any, and return the response.
|
||||
return $next($request, $response);
|
||||
|
||||
};
|
||||
|
||||
// Add a header to the response.
|
||||
$headerAdder = function ($request, $response, $next) {
|
||||
// Add the header.
|
||||
// Add the header to the response we got back from upstream.
|
||||
$response = $response->withHeader("X-example", "hello world");
|
||||
// Propagate to the next middleware, if any, and return the response.
|
||||
return $next($request, $response);
|
||||
};
|
||||
|
||||
// Return the altered response.
|
||||
return $response;
|
||||
}
|
||||
}
|
||||
|
||||
// Create a server
|
||||
$server = new Server();
|
||||
|
||||
// Add $headerAdder to the server first to make it the first to run.
|
||||
$server->add($headerAdder);
|
||||
// Add the header-adding middleware to the server first so that it will
|
||||
// forward requests on to the router.
|
||||
$server->add(new CustomHeaderMiddleware());
|
||||
|
||||
// When $headerAdder calls $next, it will dispatch the router because it is
|
||||
// added to the server right after.
|
||||
$server->add($server->createRouter()
|
||||
->register("GET", "/hello", $hello)
|
||||
->register("GET", "/hello/{name}", $hello)
|
||||
);
|
||||
// Create a router to map methods and endpoints to handlers.
|
||||
$router = $server->createRouter();
|
||||
|
||||
// Read the request from the client, dispatch middleware, and output.
|
||||
$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/
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
WellRESTed
|
||||
==========
|
||||
|
||||
WellRESTed is a library for creating RESTful APIs and websites in PHP that provides abstraction for HTTP messages, a powerful middleware system, and a flexible router.
|
||||
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
|
||||
--------
|
||||
|
|
@ -13,26 +13,32 @@ Request and response messages are built to the interfaces standardized by PSR-7_
|
|||
|
||||
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.
|
||||
|
||||
Middleware
|
||||
^^^^^^^^^^
|
||||
PSR-15 Handler Interfaces
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
The middleware_ system allows you to map build sequences of modular code that propagate from one 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.
|
||||
|
||||
Most middleware is never autoloaded or instantiated until it is needed, so a Web service with hundreds of middleware still only creates instances required for the current request-respose cycle.
|
||||
|
||||
You can register middleware directly, register callables that return middleware (e.g., dependency container services), or register strings containing the middleware classnames to autoload and instantiate on demand.
|
||||
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 automates responding to ``OPTIONS`` requests for each endpoint based on the method you assign. ``405 Method Not Allowed`` come free of charge as well for any methods you have not implemented on a given endpoint.
|
||||
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
|
||||
^^^^^^^^^^
|
||||
|
||||
All 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 different interface, you can provide your own custom ``DispatcherInterface`` implentation.
|
||||
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
|
||||
-------
|
||||
|
|
@ -45,54 +51,74 @@ The site will also provide an ``X-example: hello world`` using dedicated middlew
|
|||
|
||||
<?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";
|
||||
|
||||
// Build some middleware. We'll register these with a server below.
|
||||
// We're using callables to fit this all in one example, but these
|
||||
// could also be classes implementing WellRESTed\MiddlewareInterface.
|
||||
|
||||
// Set the status code and provide the greeting as the response body.
|
||||
$hello = function ($request, $response, $next) {
|
||||
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 = $response->withStatus(200)
|
||||
$response = (new Response(200))
|
||||
->withHeader("Content-type", "text/plain")
|
||||
->withBody(new Stream("Hello, $name!"));
|
||||
|
||||
// Propagate to the next middleware, if any, and return the response.
|
||||
return $next($request, $response);
|
||||
// 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 a header to the response.
|
||||
$headerAdder = function ($request, $response, $next) {
|
||||
// Add the header.
|
||||
$response = $response->withHeader("X-example", "hello world");
|
||||
// Propagate to the next middleware, if any, and return the response.
|
||||
return $next($request, $response);
|
||||
};
|
||||
|
||||
// Return the altered response.
|
||||
return $response;
|
||||
}
|
||||
}
|
||||
|
||||
// Create a server
|
||||
$server = new Server();
|
||||
|
||||
// Start each request-response cycle by dispatching the header adder.
|
||||
$server->add($headerAdder);
|
||||
// Add the header-adding middleware to the server first so that it will
|
||||
// forward requests on to the router.
|
||||
$server->add(new CustomerHeaderMiddleware());
|
||||
|
||||
// The header adder will propagate to this router, which will dispatch the
|
||||
// $hello middleware, possibly with a {name} variable.
|
||||
$server->add($server->createRouter()
|
||||
->register("GET", "/hello", $hello)
|
||||
->register("GET", "/hello/{name}", $hello)
|
||||
);
|
||||
// Create a router to map methods and endpoints to handlers.
|
||||
$router = $server->createRouter();
|
||||
|
||||
// Read the request from the client, dispatch middleware, and output.
|
||||
$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
|
||||
|
|
@ -103,13 +129,19 @@ Contents
|
|||
|
||||
overview
|
||||
getting-started
|
||||
middleware
|
||||
messages
|
||||
handlers-and-middleware
|
||||
router
|
||||
uri-templates
|
||||
uri-templates-advanced
|
||||
extending
|
||||
dependency-injection
|
||||
additional
|
||||
web-server-configuration
|
||||
|
||||
.. _PSR-7: http://www.php-fig.org/psr/psr-7/
|
||||
.. _middleware: middleware.html
|
||||
.. _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
|
||||
|
|
@ -1,295 +0,0 @@
|
|||
Middleware
|
||||
==========
|
||||
|
||||
Okay, so what exactly **is** middleware? It's a nebulous term, and it's a bit reminscent of the Underpants gnomes.
|
||||
|
||||
- Phase 1: Request
|
||||
- Phase 2: ???
|
||||
- Phase 3: Respose
|
||||
|
||||
Middleware is indeed Phase 2. It's something (a callable or object) that takes a request and a response as inputs, does something with the response, and sends the altered response back out.
|
||||
|
||||
A Web service can built from many, many pieces of middleware, with each piece managing a specific task such as authentication or parsing representations. When each middleware runs, it is responsible for propagating the request through to the next middleware in the sequence—or deciding not to.
|
||||
|
||||
So what's it look like? In essence, a single piece of middleware looks something like this:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
function ($request, $response, $next) {
|
||||
|
||||
// Update the response.
|
||||
/* $response = ... */
|
||||
|
||||
// Determine if any other middleware should be called after this.
|
||||
if (/* Stop now without calling more middleware? */) {
|
||||
// Return the response without calling any other middleware.
|
||||
return $response;
|
||||
}
|
||||
|
||||
// Let the next middleware work on the response. This propagates "up"
|
||||
// the chain of middleware, and will eventually return a response.
|
||||
$response = $next($request, $response);
|
||||
|
||||
// Possibly update the response some more.
|
||||
/* $response = ... */
|
||||
|
||||
// Return the response.
|
||||
return $response;
|
||||
|
||||
}
|
||||
|
||||
Defining Middleware
|
||||
^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
Middleware can be a callable (as in the `Getting Started`_) or an implementation of the ``WellRESTed\MiddlewareInterface`` (which implements ``__invoke`` so is technically a callable, too).
|
||||
|
||||
.. rubric:: Callable
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
/**
|
||||
* @param Psr\Http\Message\ServerRequestInterface $request
|
||||
* @param Psr\Http\Message\ResponseInterface $response
|
||||
* @param callable $next
|
||||
* @return Psr\Http\Message\ResponseInterface
|
||||
*/
|
||||
function ($request, $response, $next) { }
|
||||
|
||||
.. rubric:: MiddlewareInterface
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
|
||||
namespace WellRESTed;
|
||||
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
|
||||
interface MiddlewareInterface
|
||||
{
|
||||
/**
|
||||
* @param ServerRequestInterface $request
|
||||
* @param ResponseInterface $response
|
||||
* @param callable $next
|
||||
* @return ResponseInterface
|
||||
*/
|
||||
public function __invoke(ServerRequestInterface $request, ResponseInterface $response, $next);
|
||||
}
|
||||
|
||||
Using Middleware
|
||||
^^^^^^^^^^^^^^^^
|
||||
|
||||
Methods that accept middleware (e.g., ``Server::add``, ``Router::register``) allow you to provide middleware in a number of ways. For example, when you can provide a callable, a string containing a class name, an instance, or even an array containing a sequence of middleware.
|
||||
|
||||
Fully Qualified Class Name (FQCN)
|
||||
---------------------------------
|
||||
|
||||
Assume your Web service has an autoloadable class named ``Webservice\Widgets\WidgetHandler``. You can register it with a router by passing a string containing the fully qualified class name (FQCN):
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
$router->register("GET,PUT,DELETE", "/widgets/{id}", 'Webservice\Widgets\WidgetHandler');
|
||||
|
||||
The class is not loaded, and no instances are created, until the route is matched and dispatched. Even for a router with 100 routes, no middleware registered by string name is loaded, except for the one that matches the request.
|
||||
|
||||
Callable Provider
|
||||
-----------------
|
||||
|
||||
You can also use a callable to instantiate and return a ``MiddlewareInterface`` instance or middleware callable.
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
$router->add("GET,PUT,DELETE", "/widgets/{id}", function () {
|
||||
return new \Webservice\Widgets\WidgetHandler();
|
||||
});
|
||||
|
||||
This still delays instantiation, but gives you some added flexibility. For example, you could define middleware that receives some configuration upon construction.
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
$container = new MySuperCoolDependencyContainer();
|
||||
|
||||
$router->add("GET,PUT,DELETE", "/widgets/{id}", function () use ($container) {
|
||||
return new \Webservice\Widgets\WidgetHandler($container);
|
||||
});
|
||||
|
||||
This is one approach to `dependency injection`_.
|
||||
|
||||
Middleware Callable
|
||||
-------------------
|
||||
|
||||
Use a middleware callable directly.
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
$router->add("GET,PUT,DELETE", "/widgets/{id}", function ($request, $response, $next) {
|
||||
$response = $response->withStatus(200)
|
||||
->withHeader("Content-type", "text/plain")
|
||||
->withBody(new \WellRESTed\Message\Stream("It's a bunch of widgets!");
|
||||
return $next($request, $response);
|
||||
});
|
||||
|
||||
Instance
|
||||
--------
|
||||
|
||||
You can also provide pass an instnace directly as middleware.
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
$router->add("GET,PUT,DELETE", "/widgets/{id}", new \Webservice\Widgets\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 though only one middleware will actually be used for a given request-response cycle. You may find this approach useful for testing, but avoid if for production code.
|
||||
|
||||
Array
|
||||
-----
|
||||
|
||||
Why use one middleware when you can use more?
|
||||
|
||||
Provide a sequence of middleware as an array. Each component of the array can be any of the varieties listed in this section.
|
||||
|
||||
When disptached, the middleware in the array will run in order, with each calling the one following via the ``$next`` parameter.
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
$router->add("GET", "/widgets/{id}", ['Webservice\Auth', $jsonParser, $widgetHandler]);
|
||||
|
||||
Chaining Middleware
|
||||
^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
Chaining middleware together allows you to build your Web service in a discrete, modular pieces. Each middleware in the chain makes the decision to either move the request up the chain by calling ``$next``, or stop propagation by returning a response without calling ``$next``.
|
||||
|
||||
Propagating Up the Chain
|
||||
------------------------
|
||||
|
||||
Imagine we want to add authorization to the ``/widgets/{id}`` endpoint. We can do this without altering the existing middleware that deals with the widget itself.
|
||||
|
||||
What we do is create an additional middleware that performs just the authorization task. This middleware will inspect the incoming request for authorization headers, and either move the request on up the chain to the next middleware if all looks good, or send a request back out with an appropriate status code.
|
||||
|
||||
Here's an example authorization middleware using pseudocode.
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
namespace Webserice;
|
||||
|
||||
class Authorization implements \WellRESTed\MiddlewareInterface
|
||||
{
|
||||
public function __invoke(ServerRequestInterface $request, ResponseInterface $response, $next)
|
||||
{
|
||||
// Validate the headers in the request.
|
||||
try {
|
||||
$validateUser($request);
|
||||
} catch (InvalidHeaderException $e) {
|
||||
// User did not supply the right headers.
|
||||
// Respond with a 401 Unauthorized status.
|
||||
return $response->withStatus(401);
|
||||
} catch (BadUserException $e) {
|
||||
// User is not permitted to access this resource.
|
||||
// Respond with a 403 Forbidden status.
|
||||
return $response->withStatus(403);
|
||||
}
|
||||
|
||||
// No exception was thrown, so propagate to the next middleware.
|
||||
return $next($request, $response);
|
||||
}
|
||||
}
|
||||
|
||||
We can add authorization for just the ``/widgets/{id}`` endpoint like this:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
$router->register("GET,PUT,DELETE", "/widgets/{id}", [
|
||||
'Webservice\Auhtorizaiton',
|
||||
'Webservice\Widgets\WidgetHandler'
|
||||
]);
|
||||
|
||||
Or, if you wanted to use the authorization for the entire service, you can add it to the ``Server`` in front of the ``Router``.
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
$server = new \WellRESTed\Server();
|
||||
$server
|
||||
->add('Webservice\Auhtorizaiton')
|
||||
->add($server->createRouter()
|
||||
->register("GET,PUT,DELETE", "/widgets/{id}", 'Webservice\Widgets\WidgetHandler')
|
||||
)
|
||||
->respond();
|
||||
|
||||
Moving Back Down the Chain
|
||||
--------------------------
|
||||
|
||||
The authorization example returned ``$next($request, $response)`` immidiately, but you can do some interesting things by working with the response that comes back from ``$next``. Think of the request as taking a round trip on the subway with each middleware being a stop along the way. Each of the stops you go through going up the chain, you also go through on the way back down.
|
||||
|
||||
We could add a caching middleware in front of ``GET`` requests for a specific widget. This middleware will check if a cached representation exists for the resource the client requested. If it exists, it will send it out to the client without ever bothering the ``WidgetHandler``. If there's no representation cached, it will call ``$next`` to propgate the request up the chain. On the return trip (when the call to ``$next`` finishes), the caching middleware will inspect the response and store the body to the cache for next time.
|
||||
|
||||
Here's a pseudocode example:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
namespace Webserice;
|
||||
|
||||
class Cache implements \WellRESTed\MiddlewareInterface
|
||||
{
|
||||
public function dispatch(ServerRequestInterface $request, ResponseInterface $response, $next)
|
||||
{
|
||||
// Inspect the request path to see if there is a representation on
|
||||
// hand for this resource.
|
||||
$representation = $this->getCachedRepresentation($request);
|
||||
if ($representation !== null) {
|
||||
// There is already a cached representation. Send it out
|
||||
// without propagating.
|
||||
return $reponse
|
||||
->withStatus(200)
|
||||
->withBody($representation);
|
||||
}
|
||||
|
||||
// No representation exists. Propagate to the next middleware.
|
||||
$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 represetnation.
|
||||
// ...
|
||||
}
|
||||
}
|
||||
|
||||
We can add this caching middleware in the chain between the authorization middleware and the Widget.
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
$router->register("GET,PUT,DELETE", "/widgets/{id}", [
|
||||
'Webservice\Auhtorizaiton',
|
||||
'Webservice\Cache',
|
||||
'Webservice\Widgets\WidgetHandler'
|
||||
]);
|
||||
|
||||
Or, if you wanted to use the authorization and caching middelware for the entire service, you can add them to the ``Server`` in front of the ``Router``.
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
$server = new \WellRESTed\Server();
|
||||
$server
|
||||
->add('Webservice\Auhtorizaiton')
|
||||
->add('Webservice\Cache')
|
||||
->add($server->createRouter()
|
||||
->register("GET,PUT,DELETE", "/widgets/{id}", 'Webservice\Widgets\WidgetHandler')
|
||||
)
|
||||
->respond();
|
||||
|
||||
.. _Dependency Injection: dependency-injection.html
|
||||
.. _Getting Started: getting-started.html
|
||||
|
|
@ -4,20 +4,20 @@ Overview
|
|||
Installation
|
||||
^^^^^^^^^^^^
|
||||
|
||||
The recommended method for installing WellRESTed is to use the PHP dependency manager Composer_. Add an entry for WellRESTed in your project's ``composer.json`` file.
|
||||
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": "~3.0"
|
||||
"wellrested/wellrested": "^5"
|
||||
}
|
||||
}
|
||||
|
||||
Requirements
|
||||
^^^^^^^^^^^^
|
||||
|
||||
- PHP 5.4.0
|
||||
- PHP 7.3
|
||||
|
||||
License
|
||||
^^^^^^^
|
||||
|
|
@ -26,7 +26,7 @@ Licensed using the `MIT license <http://opensource.org/licenses/MIT>`_.
|
|||
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015 PJ Dietz
|
||||
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
|
||||
|
|
@ -46,5 +46,4 @@ Licensed using the `MIT license <http://opensource.org/licenses/MIT>`_.
|
|||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
|
||||
.. _Composer: http://getcomposer.org/
|
||||
.. _PHP cURL: http://php.net/manual/en/book.curl.php
|
||||
.. _Composer: https://getcomposer.org/
|
||||
|
|
|
|||
|
|
@ -1,8 +1,7 @@
|
|||
Router
|
||||
======
|
||||
|
||||
A router is a type of middleware_ that organizes the components of a site by associating URI paths with other 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 middleware_. The dispatched middleware_ is then responsible for reacting to the request and providing a response.
|
||||
|
||||
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
|
||||
^^^^^^^^^^^
|
||||
|
|
@ -14,13 +13,13 @@ Typically, you will want to use the ``WellRESTed\Server::createRouter`` method t
|
|||
$server = new WellRESTed\Server();
|
||||
$router = $server->createRouter();
|
||||
|
||||
Suppose ``$catHandler`` is a middleware 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.
|
||||
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 fluid, so you can add multiple routes in either of these styles:
|
||||
The ``register`` method is fluent, so you can add multiple routes in either of these styles:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
|
|
@ -42,12 +41,12 @@ The ``register`` method is fluid, so you can add multiple routes in either of th
|
|||
Paths
|
||||
^^^^^
|
||||
|
||||
A router can map middleware to an exact path, or to a pattern of 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 middleware to an exact path.
|
||||
The simplest type of route is called a "static route". It maps a handler to an exact path.
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
|
|
@ -77,19 +76,16 @@ For example, this template will match requests to ``/cats/12``, ``/cats/molly``,
|
|||
|
||||
$router->register("GET", "/cats/{cat}", $catHandler);
|
||||
|
||||
When the router dispatches a route matched by a template route, it provides the extracted variables as an associative array. To access a variable, call the request object's ``getAttribute`` method method and pass the variable's name.
|
||||
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
|
||||
|
||||
$catHandler = function ($request, $response, $next) {
|
||||
$name = $request->getAttribute("cat");
|
||||
// molly
|
||||
...
|
||||
}
|
||||
// "molly"
|
||||
|
||||
Template routes are very powerful, and this only scratches the surface. See `URI Templates`_ for a full explaination of the syntax supported.
|
||||
Template routes are very powerful, and this only scratches the surface. See `URI Templates`_ for a full explanation of the syntax supported.
|
||||
|
||||
Regex Routes
|
||||
------------
|
||||
|
|
@ -106,7 +102,6 @@ For a request to ``/cats/molly-90``:
|
|||
|
||||
.. code-block:: php
|
||||
|
||||
$catHandler = function ($request, $response, $next) {
|
||||
$vars = $request->getAttributes();
|
||||
/*
|
||||
Array
|
||||
|
|
@ -116,22 +111,19 @@ For a request to ``/cats/molly-90``:
|
|||
[1] => molly
|
||||
[number] => 12
|
||||
[2] => 12
|
||||
... Plus any other attributes that were set ...
|
||||
)
|
||||
*/
|
||||
...
|
||||
}
|
||||
|
||||
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:
|
||||
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, disptach 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 added. Dispatch the first route that matches.
|
||||
#. If no pattern routes match, return a reponse with a ``404 Not Found`` status.
|
||||
#. 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
|
||||
~~~~~~~~~~~~~~~~~
|
||||
|
|
@ -174,12 +166,12 @@ Given these routes:
|
|||
->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`` 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 disptached. 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 middleware 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``.
|
||||
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:
|
||||
|
||||
|
|
@ -199,17 +191,17 @@ This will **NOT** work:
|
|||
// 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.
|
||||
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 indcate any method.
|
||||
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 middleware for a path and method by including the method as the first parameter.
|
||||
Specify a specific handler for a path and method by including the method as the first parameter.
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
|
|
@ -222,7 +214,7 @@ Specify a specific middleware for a path and method by including the method as t
|
|||
Registering by Method List
|
||||
--------------------------
|
||||
|
||||
Specify the same middleware for multiple methods for a given path by proving a comma-separated list of methods as the first parameter.
|
||||
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
|
||||
|
||||
|
|
@ -238,7 +230,7 @@ Specify the same middleware for multiple methods for a given path by proving a c
|
|||
Registering by Wildcard
|
||||
-----------------------
|
||||
|
||||
Specify middleware for all methods for a given path by proving a ``*`` wildcard.
|
||||
Specify a handler for all methods for a given path by proving a ``*`` wildcard.
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
|
|
@ -253,7 +245,7 @@ Specify middleware for all methods for a given path by proving a ``*`` wildcard.
|
|||
|
||||
.. 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 automatcially for non-wildcard routes.
|
||||
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
|
||||
----
|
||||
|
|
@ -282,7 +274,7 @@ An ``OPTIONS`` request to ``/cats/12`` will provide a response like:
|
|||
HTTP/1.1 200 OK
|
||||
Allow: GET,PUT,DELETE,HEAD,OPTIONS
|
||||
|
||||
Likewise, a request to an unsupport method will return a ``405 Method Not Allowed`` response with a descriptive ``Allow`` header.
|
||||
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:
|
||||
|
||||
|
|
@ -291,44 +283,51 @@ A ``POST`` request to ``/cats/12`` will provide:
|
|||
HTTP/1.1 405 Method Not Allowed
|
||||
Allow: GET,PUT,DELETE,HEAD,OPTIONS
|
||||
|
||||
|
||||
Error Responses
|
||||
^^^^^^^^^^^^^^^
|
||||
|
||||
When a router is unable to dispatch a route because either the path or method does not match a defined route, it will provide an appropriate error response code—either ``404 Not Found`` or ``405 Method Not Allowed``.
|
||||
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``.
|
||||
|
||||
The router always checks the path first. If route for that path matches, the router responds ``404 Not Found``.
|
||||
|
||||
If the 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``.
|
||||
|
||||
Given this router:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
$router
|
||||
->register("GET", "/cats/", $catReader)
|
||||
->register("POST", "/cats/", $catWriter)
|
||||
->register("GET", "/dogs/", $catItemReader)
|
||||
|
||||
The following requests wil provide these responses:
|
||||
|
||||
====== ========== ========
|
||||
Method Path Response
|
||||
====== ========== ========
|
||||
GET /hamsters/ 404 Not Found
|
||||
PUT /cats/ 405 Method Not Allowed
|
||||
====== ========== ========
|
||||
When a router is unable to match the route, it will delegate to the next middleware.
|
||||
|
||||
.. note::
|
||||
|
||||
When the router fails to dispatch a route, or when it responds to an ``OPTIONS`` request, is will stop propagation, and any middleware that comes after the router will not be dispatched.
|
||||
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 subrouters.
|
||||
|
||||
This works because a ``Router`` is type of middleware, and can be used wherever middleware can be used.
|
||||
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.
|
||||
|
||||
|
|
@ -354,7 +353,6 @@ Here's an example where all of the traffic beginning with ``/cats/`` is sent to
|
|||
|
||||
$server->respond();
|
||||
|
||||
.. _preg_match: http://php.net/manual/en/function.preg-match.php
|
||||
.. _preg_match: https://php.net/manual/en/function.preg-match.php
|
||||
.. _URI Template: `URI Templates`_s
|
||||
.. _URI Templates: uri-templates.html
|
||||
.. _middleware: middleware.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
|
||||
|
|
@ -1,14 +1,15 @@
|
|||
URI Templates
|
||||
=============
|
||||
|
||||
WellRESTed allows you to register middleware with a router using URI Templates. These templates include variables (enclosed in curly braces) which are extracted and made availble to the disptached middleware.
|
||||
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.
|
||||
|
||||
The URI Template syntax is based on the URI Templates defined in `RFC 6570`_.
|
||||
Reading Variables
|
||||
^^^^^^^^^^^^^^^^^
|
||||
|
||||
Example
|
||||
-------
|
||||
Basic Usage
|
||||
-----------
|
||||
|
||||
Register middleware 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.
|
||||
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
|
||||
|
||||
|
|
@ -16,166 +17,123 @@ Register middleware with a URI Template by providing a path that include at leas
|
|||
|
||||
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, the ``$widgetHandler`` middleware inspects the request attribute named "id".
|
||||
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
|
||||
|
||||
$widgetHandler = function ($request, $response, $next) {
|
||||
|
||||
// Read the variable extracted form the path.
|
||||
// For a request to /widgets/12
|
||||
$id = $request->getAttribute("id");
|
||||
// "12"
|
||||
|
||||
};
|
||||
// For a request to /widgets/mega-widget
|
||||
$id = $request->getAttribute("id");
|
||||
// "mega-widget"
|
||||
|
||||
Syntax
|
||||
.. 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
|
||||
------
|
||||
|
||||
To illustrate the syntax for various matches, we'll start by providing table of variable names and values that will be used throughout the examples.
|
||||
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.
|
||||
|
||||
.. class:: code-table
|
||||
.. list-table:: Variable names and values
|
||||
.. 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
|
||||
|
||||
* - Name
|
||||
* - Path
|
||||
- Value
|
||||
* - empty
|
||||
- ""
|
||||
* - half
|
||||
- "50%"
|
||||
* - hello
|
||||
- "Hello, World!"
|
||||
* - path
|
||||
- "/foo/bar"
|
||||
* - var
|
||||
- "value"
|
||||
* - who
|
||||
- "fred"
|
||||
* - x
|
||||
- "1024"
|
||||
* - y
|
||||
- "768"
|
||||
* - count
|
||||
- ["one", "two", "three"]
|
||||
* - list
|
||||
- ["red", "green", "blue"]
|
||||
* - /users/123
|
||||
- "123"
|
||||
* - /users/zoidberg
|
||||
- "zoidberg"
|
||||
* - /users/zoidberg%40planetexpress.com
|
||||
- "zoidberg@planetexpress.com"
|
||||
|
||||
Simple Strings
|
||||
##############
|
||||
A request for ``GET /uses/zoidberg@planetexpress.com`` will **not** match this template, because ``@`` is a reserved character and is not percent encoded.
|
||||
|
||||
Variables by default match any unreserved characters. This includes all alpha-numeric characters, plus ``-``, ``.``, ``_``, and ``~``. All other characers MUST be percent encoded.
|
||||
Reserved Characters
|
||||
-------------------
|
||||
|
||||
.. class:: code-table
|
||||
.. list-table:: Simple Strings
|
||||
:header-rows: 1
|
||||
If you need to match a non-percent-encoded reserved character like ``@`` or ``/``, use the ``+`` operator at the beginning of the variable name.
|
||||
|
||||
* - URI Template
|
||||
- Request Path
|
||||
* - /{var}
|
||||
- /value
|
||||
* - /{hello}
|
||||
- /Hello%20World%21
|
||||
* - /{x,hello,y}
|
||||
- /1024,Hello%20World%21,768
|
||||
Using the template ``/users/{+user}``, we can match all of the paths above, plus ``/users/zoidberg@planetexpress.com``.
|
||||
|
||||
Reserver Charaters
|
||||
##################
|
||||
Reserved matching also allows matching unencoded slashes (``/``). For example, given this template:
|
||||
|
||||
To match reserved character, begin the expression with a ``+``.
|
||||
.. code-block:: php
|
||||
|
||||
.. class:: code-table
|
||||
.. list-table:: Reserved Characters
|
||||
:header-rows: 1
|
||||
$router->register("GET", "/my-favorite-path{+path}", $pathHandler);
|
||||
|
||||
* - URI Template
|
||||
- Request Path
|
||||
* - /{+var}
|
||||
- /value
|
||||
* - /{+hello}
|
||||
- /Hello%20World!
|
||||
* - {+path}/here
|
||||
- /foo/bar/here
|
||||
The router will dispatch ``$pathHandler`` with for a request to ``GET /my-favorite-path/has/a/few/slashes.jpg``
|
||||
|
||||
Dot Prefix
|
||||
##########
|
||||
.. code-block:: php
|
||||
|
||||
Expressions beginning with ``.`` indicate that each variable matched begins with ``.``.
|
||||
$path = $request->getAttribute("path");
|
||||
// "/has/a/few/slashes.jpg"
|
||||
|
||||
.. class:: code-table
|
||||
.. list-table:: Dot Prefix
|
||||
:header-rows: 1
|
||||
.. note::
|
||||
|
||||
* - URI Template
|
||||
- Request Path
|
||||
* - /{.who}
|
||||
- /.fred
|
||||
* - /{.half,who}
|
||||
- /.50%25.fred
|
||||
* - /X{.empty}
|
||||
- /X.
|
||||
|
||||
Path Segments
|
||||
#############
|
||||
|
||||
Expressions beginning with ``/`` indicate that each variable matched beings with ``/``.
|
||||
|
||||
.. class:: code-table
|
||||
.. list-table:: Path Segments
|
||||
:header-rows: 1
|
||||
|
||||
* - URI Template
|
||||
- Request Path
|
||||
* - {/who}
|
||||
- /fred
|
||||
* - {/half,who}
|
||||
- /50%25/fred
|
||||
* - {/var,empty}
|
||||
- /value/
|
||||
|
||||
Explosion
|
||||
#########
|
||||
|
||||
The explosion operator (``*``) at the end of an expression indicates that the extracted value is a list, and will be provided to the middleware as an array.
|
||||
|
||||
.. class:: code-table
|
||||
.. list-table:: Explosion
|
||||
:header-rows: 1
|
||||
|
||||
* - URI Template
|
||||
- Request Path
|
||||
* - /{count*}
|
||||
- /one,two,three
|
||||
* - {/count*}
|
||||
- /one/two/three
|
||||
* - X{.list*}
|
||||
- X.red.green.blue
|
||||
|
||||
Limitations
|
||||
-----------
|
||||
|
||||
While WellRESTed's URI templates are modeled after `RFC 6570`_, there are some parts of the RFC that WellRESTed does not implement. Some of these are because WellRESTed's uses a URI template and a given path to extract variables, whereas `RFC 6570`_ describes using a URI template and variables to create a path. Other parts are just not implemented yet, but may be in future releases.
|
||||
|
||||
Query and Fragment
|
||||
##################
|
||||
|
||||
Anything relating to the query or fragment is omitted. This is because routing is based only on the path component of the request's URI and does not make use of the query or fragment for routing purposes. Furthur, a request for a valid resource with an invalid query **should** generally result in a ``400 Bad Request`` response, not a ``404 Not Found`` response, but taking the query into account for routing would make the ``404`` response happen automatically. A developer may have reason to respond ``404`` based on the query, but this should not be the library's default behavior.
|
||||
|
||||
Path-Style Parameter Expansion
|
||||
##############################
|
||||
|
||||
`RFC 6570 Section 3.2.7`_ describes "path-style parameter expansion" where semi-colon-prefixed expressions (e.g., ``{;x,y}``) expand to key-value pairs (e.g., ``;x=1024;y=768``). This is not currently implemented in WellRESTed, although later releases may provide this functionality.
|
||||
|
||||
Variable Names
|
||||
##############
|
||||
|
||||
Variable names MUST contain only alphanumeric characters and ``_``.
|
||||
|
||||
Unique Variable Names
|
||||
#####################
|
||||
|
||||
Variables names within a given URI Template MUST be unique.
|
||||
|
||||
Although some examples in `RFC 6570`_ include the same variable name multiple times, this is not supported by WellRESTed.
|
||||
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
|
||||
|
|
|
|||
|
|
@ -1,30 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<phpunit
|
||||
bootstrap="test/bootstrap.php"
|
||||
checkForUnintentionallyCoveredCode="true"
|
||||
colors="true"
|
||||
convertErrorsToExceptions="true"
|
||||
convertNoticesToExceptions="true"
|
||||
convertWarningsToExceptions="true"
|
||||
processIsolation="false"
|
||||
stopOnFailure="false"
|
||||
syntaxCheck="false"
|
||||
verbose="true"
|
||||
>
|
||||
<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();
|
||||
|
|
@ -2,6 +2,8 @@
|
|||
|
||||
namespace WellRESTed\Dispatching;
|
||||
|
||||
class DispatchException extends \InvalidArgumentException
|
||||
use InvalidArgumentException;
|
||||
|
||||
class DispatchException extends InvalidArgumentException
|
||||
{
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,12 +10,11 @@ use Psr\Http\Message\ServerRequestInterface;
|
|||
*/
|
||||
class DispatchStack implements DispatchStackInterface
|
||||
{
|
||||
/** @var mixed[] */
|
||||
private $stack;
|
||||
/** @var DispatcherInterface */
|
||||
private $dispatcher;
|
||||
|
||||
/**
|
||||
* @param DispatcherInterface $dispatcher
|
||||
*/
|
||||
public function __construct(DispatcherInterface $dispatcher)
|
||||
{
|
||||
$this->dispatcher = $dispatcher;
|
||||
|
|
@ -26,7 +25,7 @@ class DispatchStack implements DispatchStackInterface
|
|||
* Push a new middleware onto the stack.
|
||||
*
|
||||
* @param mixed $middleware Middleware to dispatch in sequence
|
||||
* @return self
|
||||
* @return static
|
||||
*/
|
||||
public function add($middleware)
|
||||
{
|
||||
|
|
@ -40,11 +39,11 @@ class DispatchStack implements DispatchStackInterface
|
|||
* The first middleware that was added is dispatched first.
|
||||
*
|
||||
* Each middleware, when dispatched, receives a $next callable that, when
|
||||
* called, will dispatch the next middleware in the sequence.
|
||||
* 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 receieved.
|
||||
* $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.
|
||||
|
|
@ -54,8 +53,11 @@ class DispatchStack implements DispatchStackInterface
|
|||
* @param callable $next
|
||||
* @return ResponseInterface
|
||||
*/
|
||||
public function __invoke(ServerRequestInterface $request, ResponseInterface $response, $next)
|
||||
{
|
||||
public function __invoke(
|
||||
ServerRequestInterface $request,
|
||||
ResponseInterface $response,
|
||||
$next
|
||||
) {
|
||||
$dispatcher = $this->dispatcher;
|
||||
|
||||
// This flag will be set to true when the last middleware calls $next.
|
||||
|
|
@ -63,19 +65,30 @@ class DispatchStack implements DispatchStackInterface
|
|||
|
||||
// The final middleware's $next returns $response unchanged and sets
|
||||
// the $stackCompleted flag to indicate the stack has completed.
|
||||
$chain = function ($request, $response) use (&$stackCompleted) {
|
||||
$chain = function (
|
||||
ServerRequestInterface $request,
|
||||
ResponseInterface $response
|
||||
) use (&$stackCompleted): ResponseInterface {
|
||||
$stackCompleted = true;
|
||||
return $response;
|
||||
};
|
||||
|
||||
// Create a chain of callables.
|
||||
//
|
||||
// Each callable wil take $request and $response parameters, and will
|
||||
// contain a dispatcher, the associated middleware, and a $next
|
||||
// that is the links to the next middleware in the chain.
|
||||
// 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 ($request, $response) use ($dispatcher, $middleware, $chain) {
|
||||
return $dispatcher->dispatch($middleware, $request, $response, $chain);
|
||||
$chain = function (
|
||||
ServerRequestInterface $request,
|
||||
ResponseInterface $response
|
||||
) use ($dispatcher, $middleware, $chain): ResponseInterface {
|
||||
return $dispatcher->dispatch(
|
||||
$middleware,
|
||||
$request,
|
||||
$response,
|
||||
$chain
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ interface DispatchStackInterface extends MiddlewareInterface
|
|||
* This method MUST preserve the order in which middleware are added.
|
||||
*
|
||||
* @param mixed $middleware Middleware to dispatch in sequence
|
||||
* @return self
|
||||
* @return static
|
||||
*/
|
||||
public function add($middleware);
|
||||
|
||||
|
|
@ -32,21 +32,25 @@ interface DispatchStackInterface extends MiddlewareInterface
|
|||
* returns the response unchanged.
|
||||
*
|
||||
* When any middleware returns a response without calling the $next
|
||||
* argument it recieved, the stack instance MUST stop propogating and MUST
|
||||
* 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 receieved.
|
||||
* - 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 receieved.
|
||||
* $next it received.
|
||||
*
|
||||
* @param ServerRequestInterface $request
|
||||
* @param ResponseInterface $response
|
||||
* @param callable $next
|
||||
* @return ResponseInterface
|
||||
*/
|
||||
public function __invoke(ServerRequestInterface $request, ResponseInterface $response, $next);
|
||||
public function __invoke(
|
||||
ServerRequestInterface $request,
|
||||
ResponseInterface $response,
|
||||
$next
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,40 +4,78 @@ 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
|
||||
{
|
||||
/**
|
||||
* @param mixed $middleware
|
||||
* 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($middleware, ServerRequestInterface $request, ResponseInterface $response, $next)
|
||||
{
|
||||
if (is_callable($middleware)) {
|
||||
$middleware = $middleware($request, $response, $next);
|
||||
} elseif (is_string($middleware)) {
|
||||
$middleware = new $middleware();
|
||||
} elseif (is_array($middleware)) {
|
||||
$middleware = $this->getDispatchStack($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($middleware)) {
|
||||
return $middleware($request, $response, $next);
|
||||
} elseif ($middleware instanceof ResponseInterface) {
|
||||
return $middleware;
|
||||
|
||||
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 middleware.");
|
||||
throw new DispatchException('Unable to dispatch handler.');
|
||||
}
|
||||
}
|
||||
|
||||
protected function getDispatchStack($middlewares)
|
||||
/**
|
||||
* @param mixed[] $dispatchables
|
||||
* @return DispatchStack
|
||||
*/
|
||||
private function getDispatchStack($dispatchables)
|
||||
{
|
||||
$stack = new DispatchStack($this);
|
||||
foreach ($middlewares as $middleware) {
|
||||
$stack->add($middleware);
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
|
@ -6,38 +6,47 @@ use Psr\Http\Message\ResponseInterface;
|
|||
use Psr\Http\Message\ServerRequestInterface;
|
||||
|
||||
/**
|
||||
* Dispatches middleware
|
||||
* Runs a handler or middleware with a request and return the response.
|
||||
*/
|
||||
interface DispatcherInterface
|
||||
{
|
||||
/**
|
||||
* Dispatch middleware and return the response.
|
||||
* Run a handler or middleware with a request and return the response.
|
||||
*
|
||||
* This method MUST pass $request, $response, and $next to the middleware
|
||||
* to be dispatched.
|
||||
*
|
||||
* $middleware comes in a number of varieties (e.g., instance, string,
|
||||
* callable). DispatcherInterface interface exist to unpack the middleware
|
||||
* and dispatch it.
|
||||
* 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 MiddlewareInterface
|
||||
* - 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 MiddlewareInterface
|
||||
* - A callable that returns an instance implementing MiddlewareInterface
|
||||
* - A callable with a signature matching MiddlewareInterface::__invoke
|
||||
* 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 $middware that is not of a type it can
|
||||
* dispatch, it MUST throw a DispatchException.
|
||||
* When an implementation receives a $dispatchable that is not of a type it
|
||||
* can dispatch, it MUST throw a DispatchException.
|
||||
*
|
||||
* @param mixed $middleware
|
||||
* @param mixed $dispatchable
|
||||
* @param ServerRequestInterface $request
|
||||
* @param ResponseInterface $response
|
||||
* @param callable $next
|
||||
* @return ResponseInterface
|
||||
* @throws DispatchException Unable to dispatch $middleware
|
||||
*/
|
||||
public function dispatch($middleware, ServerRequestInterface $request, ResponseInterface $response, $next);
|
||||
public function dispatch(
|
||||
$dispatchable,
|
||||
ServerRequestInterface $request,
|
||||
ResponseInterface $response,
|
||||
$next
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,59 +6,52 @@ use ArrayAccess;
|
|||
use Iterator;
|
||||
|
||||
/**
|
||||
* HeaderCollection provides case-insenstive access to lists of header values.
|
||||
*
|
||||
* This class is an internal class used by Message and is not intended for
|
||||
* direct use by consumers.
|
||||
* 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 insesitively.
|
||||
* 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
|
||||
{
|
||||
/**
|
||||
* @var array
|
||||
*
|
||||
* Hash array mapping lowercase header names to original case header names.
|
||||
*
|
||||
* @var array<string, string>
|
||||
*/
|
||||
private $fields;
|
||||
private $fields = [];
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*
|
||||
* Hash array mapping lowercase header names to values as string[]
|
||||
*
|
||||
* @var array<string, string[]>
|
||||
*/
|
||||
private $values;
|
||||
private $values = [];
|
||||
|
||||
/**
|
||||
* @var string[]
|
||||
* List array of lowercase header names.
|
||||
*
|
||||
* List arrary of lowercase header names.
|
||||
* @var string[]
|
||||
*/
|
||||
private $keys;
|
||||
private $keys = [];
|
||||
|
||||
/** @var int */
|
||||
private $position = 0;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->keys = [];
|
||||
$this->fields = [];
|
||||
$this->values = [];
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// -------------------------------------------------------------------------
|
||||
// ArrayAccess
|
||||
|
||||
/**
|
||||
* @param string $offset
|
||||
* @return bool
|
||||
*/
|
||||
public function offsetExists($offset)
|
||||
public function offsetExists($offset): bool
|
||||
{
|
||||
return isset($this->values[strtolower($offset)]);
|
||||
}
|
||||
|
|
@ -67,7 +60,7 @@ class HeaderCollection implements ArrayAccess, Iterator
|
|||
* @param mixed $offset
|
||||
* @return string[]
|
||||
*/
|
||||
public function offsetGet($offset)
|
||||
public function offsetGet($offset): array
|
||||
{
|
||||
return $this->values[strtolower($offset)];
|
||||
}
|
||||
|
|
@ -76,7 +69,7 @@ class HeaderCollection implements ArrayAccess, Iterator
|
|||
* @param string $offset
|
||||
* @param string $value
|
||||
*/
|
||||
public function offsetSet($offset, $value)
|
||||
public function offsetSet($offset, $value): void
|
||||
{
|
||||
$normalized = strtolower($offset);
|
||||
|
||||
|
|
@ -99,42 +92,45 @@ class HeaderCollection implements ArrayAccess, Iterator
|
|||
/**
|
||||
* @param string $offset
|
||||
*/
|
||||
public function offsetUnset($offset)
|
||||
public function offsetUnset($offset): void
|
||||
{
|
||||
$normalized = strtolower($offset);
|
||||
unset($this->fields[$normalized]);
|
||||
unset($this->values[$normalized]);
|
||||
// Remove and renormalize the list of keys.
|
||||
// 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
|
||||
|
||||
public function current()
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
public function current(): array
|
||||
{
|
||||
return $this->values[$this->keys[$this->position]];
|
||||
}
|
||||
|
||||
public function next()
|
||||
public function next(): void
|
||||
{
|
||||
++$this->position;
|
||||
}
|
||||
|
||||
public function key()
|
||||
public function key(): string
|
||||
{
|
||||
return $this->fields[$this->keys[$this->position]];
|
||||
}
|
||||
|
||||
public function valid()
|
||||
public function valid(): bool
|
||||
{
|
||||
return isset($this->keys[$this->position]);
|
||||
}
|
||||
|
||||
public function rewind()
|
||||
public function rewind(): void
|
||||
{
|
||||
$this->position = 0;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
namespace WellRESTed\Message;
|
||||
|
||||
use InvalidArgumentException;
|
||||
use Psr\Http\Message\MessageInterface;
|
||||
use Psr\Http\Message\StreamInterface;
|
||||
|
||||
|
|
@ -15,39 +16,38 @@ abstract class Message implements MessageInterface
|
|||
/** @var StreamInterface */
|
||||
protected $body;
|
||||
/** @var string */
|
||||
protected $protcolVersion = "1.1";
|
||||
protected $protocolVersion = '1.1';
|
||||
|
||||
/**
|
||||
* Create a new Message, optionally with 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.
|
||||
* $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 of headers fields with header
|
||||
* field names as keys and list arrays of field values as values
|
||||
* @param StreamInterface $body A stream representation of the message
|
||||
* @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 = null, StreamInterface $body = null)
|
||||
{
|
||||
public function __construct(
|
||||
array $headers = [],
|
||||
?StreamInterface $body = null
|
||||
) {
|
||||
$this->headers = new HeaderCollection();
|
||||
if ($headers) {
|
||||
|
||||
foreach ($headers as $name => $values) {
|
||||
if (is_string($values)) {
|
||||
$values = [$values];
|
||||
}
|
||||
foreach ($values as $value) {
|
||||
$this->headers[$name] = $value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($body !== null) {
|
||||
$this->body = $body;
|
||||
} else {
|
||||
$this->body = new NullStream();
|
||||
}
|
||||
$this->body = $body ?? new Stream('');
|
||||
}
|
||||
|
||||
public function __clone()
|
||||
|
|
@ -55,34 +55,29 @@ abstract class Message implements MessageInterface
|
|||
$this->headers = clone $this->headers;
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// -------------------------------------------------------------------------
|
||||
// Psr\Http\Message\MessageInterface
|
||||
|
||||
/**
|
||||
* Retrieves the HTTP protocol version as a string.
|
||||
*
|
||||
* The string MUST contain only the HTTP version number (e.g., "1.1", "1.0").
|
||||
*
|
||||
* @return string HTTP protocol version.
|
||||
*/
|
||||
public function getProtocolVersion()
|
||||
{
|
||||
return $this->protcolVersion;
|
||||
return $this->protocolVersion;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new instance with the specified HTTP protocol version.
|
||||
*
|
||||
* The version string MUST contain only the HTTP version number (e.g.,
|
||||
* "1.1", "1.0").
|
||||
*
|
||||
* @param string $version HTTP protocol version
|
||||
* @return self
|
||||
* @return static
|
||||
*/
|
||||
public function withProtocolVersion($version)
|
||||
{
|
||||
$message = clone $this;
|
||||
$message->protcolVersion = $version;
|
||||
$message->protocolVersion = $version;
|
||||
return $message;
|
||||
}
|
||||
|
||||
|
|
@ -94,7 +89,7 @@ abstract class Message implements MessageInterface
|
|||
*
|
||||
* // Represent the headers as a string
|
||||
* foreach ($message->getHeaders() as $name => $values) {
|
||||
* echo $name . ": " . implode(", ", $values);
|
||||
* echo $name . ': ' . implode(', ', $values);
|
||||
* }
|
||||
*
|
||||
* // Emit headers iteratively:
|
||||
|
|
@ -107,7 +102,7 @@ abstract class Message implements MessageInterface
|
|||
* While header names are not case-sensitive, getHeaders() will preserve the
|
||||
* exact case in which headers were originally specified.
|
||||
*
|
||||
* @return array Returns an associative array of the message's headers.
|
||||
* @return string[][] Returns an associative array of the message's headers.
|
||||
*/
|
||||
public function getHeaders()
|
||||
{
|
||||
|
|
@ -177,9 +172,9 @@ abstract class Message implements MessageInterface
|
|||
public function getHeaderLine($name)
|
||||
{
|
||||
if (isset($this->headers[$name])) {
|
||||
return join(", ", $this->headers[$name]);
|
||||
return join(', ', $this->headers[$name]);
|
||||
} else {
|
||||
return "";
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -192,8 +187,8 @@ abstract class Message implements MessageInterface
|
|||
*
|
||||
* @param string $name Case-insensitive header field name.
|
||||
* @param string|string[] $value Header value(s).
|
||||
* @return self
|
||||
* @throws \InvalidArgumentException for invalid header names or values.
|
||||
* @return static
|
||||
* @throws InvalidArgumentException for invalid header names or values.
|
||||
*/
|
||||
public function withHeader($name, $value)
|
||||
{
|
||||
|
|
@ -216,8 +211,8 @@ abstract class Message implements MessageInterface
|
|||
*
|
||||
* @param string $name Case-insensitive header field name to add.
|
||||
* @param string|string[] $value Header value(s).
|
||||
* @return self
|
||||
* @throws \InvalidArgumentException for invalid header names or values.
|
||||
* @return static
|
||||
* @throws InvalidArgumentException for invalid header names or values.
|
||||
*/
|
||||
public function withAddedHeader($name, $value)
|
||||
{
|
||||
|
|
@ -234,7 +229,7 @@ abstract class Message implements MessageInterface
|
|||
* Creates a new instance, without the specified header.
|
||||
*
|
||||
* @param string $name Case-insensitive header field name to remove.
|
||||
* @return self
|
||||
* @return static
|
||||
*/
|
||||
public function withoutHeader($name)
|
||||
{
|
||||
|
|
@ -259,8 +254,8 @@ abstract class Message implements MessageInterface
|
|||
* The body MUST be a StreamInterface object.
|
||||
*
|
||||
* @param StreamInterface $body Body.
|
||||
* @return self
|
||||
* @throws \InvalidArgumentException When the body is not valid.
|
||||
* @return static
|
||||
* @throws InvalidArgumentException When the body is not valid.
|
||||
*/
|
||||
public function withBody(StreamInterface $body)
|
||||
{
|
||||
|
|
@ -269,24 +264,34 @@ abstract class Message implements MessageInterface
|
|||
return $message;
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
private function getValidatedHeaders($name, $value)
|
||||
/**
|
||||
* @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)
|
||||
{
|
||||
$is_allowed = function ($item) {
|
||||
return is_string($item) || is_numeric($item);
|
||||
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));
|
||||
};
|
||||
|
||||
if (!is_string($name)) {
|
||||
throw new \InvalidArgumentException("Header name must be a string");
|
||||
$invalid = array_filter($values, $isNotStringOrNumber);
|
||||
if ($invalid) {
|
||||
throw new InvalidArgumentException('Header values must be a string or string[]');
|
||||
}
|
||||
|
||||
if ($is_allowed($value)) {
|
||||
return [$value];
|
||||
} elseif (is_array($value) && count($value) === count(array_filter($value, $is_allowed))) {
|
||||
return $value;
|
||||
} else {
|
||||
throw new \InvalidArgumentException("Header values must be a string or string[]");
|
||||
}
|
||||
return array_map('strval', $values);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,9 +3,10 @@
|
|||
namespace WellRESTed\Message;
|
||||
|
||||
use Psr\Http\Message\StreamInterface;
|
||||
use RuntimeException;
|
||||
|
||||
/**
|
||||
* NullStream is a minimal, always-empty, non-writeable stream.
|
||||
* NullStream is a minimal, always-empty, non-writable stream.
|
||||
*
|
||||
* Use this for messages with no body.
|
||||
*/
|
||||
|
|
@ -18,7 +19,7 @@ class NullStream implements StreamInterface
|
|||
*/
|
||||
public function __toString()
|
||||
{
|
||||
return "";
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -37,6 +38,7 @@ class NullStream implements StreamInterface
|
|||
*/
|
||||
public function detach()
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -50,9 +52,10 @@ class NullStream implements StreamInterface
|
|||
}
|
||||
|
||||
/**
|
||||
* Returns 0
|
||||
* Returns the current position of the file read/write pointer
|
||||
*
|
||||
* @return int|bool Position of the file pointer or false on error.
|
||||
* @return int Position of the file pointer
|
||||
* @throws RuntimeException on error.
|
||||
*/
|
||||
public function tell()
|
||||
{
|
||||
|
|
@ -89,11 +92,12 @@ class NullStream implements StreamInterface
|
|||
* 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.
|
||||
* @throws \RuntimeException on failure.
|
||||
* @return void
|
||||
* @throws RuntimeException on failure.
|
||||
*/
|
||||
public function seek($offset, $whence = SEEK_SET)
|
||||
{
|
||||
throw new \RuntimeException("Unable to seek to position.");
|
||||
throw new RuntimeException('Unable to seek to position.');
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -101,11 +105,12 @@ class NullStream implements StreamInterface
|
|||
*
|
||||
* @see seek()
|
||||
* @link http://www.php.net/manual/en/function.fseek.php
|
||||
* @throws \RuntimeException on failure.
|
||||
* @return void
|
||||
* @throws RuntimeException on failure.
|
||||
*/
|
||||
public function rewind()
|
||||
{
|
||||
throw new \RuntimeException("Unable to rewind srream.");
|
||||
throw new RuntimeException('Unable to rewind stream.');
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -123,11 +128,11 @@ class NullStream implements StreamInterface
|
|||
*
|
||||
* @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.
|
||||
* @throws RuntimeException on failure.
|
||||
*/
|
||||
public function write($string)
|
||||
{
|
||||
throw new \RuntimeException("Unable to write to stream.");
|
||||
throw new RuntimeException('Unable to write to stream.');
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -148,23 +153,23 @@ class NullStream implements StreamInterface
|
|||
* 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.
|
||||
* @throws RuntimeException if an error occurs.
|
||||
*/
|
||||
public function read($length)
|
||||
{
|
||||
return "";
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the remaining contents in a string
|
||||
*
|
||||
* @return string
|
||||
* @throws \RuntimeException if unable to read or an error occurs while
|
||||
* @throws RuntimeException if unable to read or an error occurs while
|
||||
* reading.
|
||||
*/
|
||||
public function getContents()
|
||||
{
|
||||
return "";
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
namespace WellRESTed\Message;
|
||||
|
||||
use InvalidArgumentException;
|
||||
use Psr\Http\Message\RequestInterface;
|
||||
use Psr\Http\Message\StreamInterface;
|
||||
use Psr\Http\Message\UriInterface;
|
||||
|
|
@ -22,37 +23,42 @@ class Request extends Message implements RequestInterface
|
|||
{
|
||||
/** @var string */
|
||||
protected $method;
|
||||
/** @var string */
|
||||
/** @var string|null */
|
||||
protected $requestTarget;
|
||||
/** @var UriInterface */
|
||||
protected $uri;
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Create a new Request.
|
||||
*
|
||||
* @see WellRESTed\Message\Message
|
||||
* @param UriInterface $uri
|
||||
* $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 array $headers
|
||||
* @param StreamInterface $body
|
||||
* @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(
|
||||
UriInterface $uri = null,
|
||||
$method = "GET",
|
||||
array $headers = null,
|
||||
StreamInterface $body = null
|
||||
string $method = 'GET',
|
||||
$uri = '',
|
||||
array $headers = [],
|
||||
?StreamInterface $body = null
|
||||
) {
|
||||
parent::__construct($headers, $body);
|
||||
|
||||
if ($uri !== null) {
|
||||
$this->uri = $uri;
|
||||
} else {
|
||||
$this->uri = new Uri();
|
||||
}
|
||||
|
||||
$this->method = $method;
|
||||
if (!($uri instanceof UriInterface)) {
|
||||
$uri = new Uri($uri);
|
||||
}
|
||||
$this->uri = $uri;
|
||||
$this->requestTarget = null;
|
||||
}
|
||||
|
||||
public function __clone()
|
||||
|
|
@ -61,7 +67,7 @@ class Request extends Message implements RequestInterface
|
|||
parent::__clone();
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// -------------------------------------------------------------------------
|
||||
// Psr\Http\Message\RequestInterface
|
||||
|
||||
/**
|
||||
|
|
@ -83,7 +89,7 @@ class Request extends Message implements RequestInterface
|
|||
public function getRequestTarget()
|
||||
{
|
||||
// Use the explicitly set request target first.
|
||||
if (isset($this->requestTarget)) {
|
||||
if ($this->requestTarget !== null) {
|
||||
return $this->requestTarget;
|
||||
}
|
||||
|
||||
|
|
@ -91,11 +97,11 @@ class Request extends Message implements RequestInterface
|
|||
$target = $this->uri->getPath();
|
||||
$query = $this->uri->getQuery();
|
||||
if ($query) {
|
||||
$target .= "?" . $query;
|
||||
$target .= '?' . $query;
|
||||
}
|
||||
|
||||
// Return "/" if the origin form is empty.
|
||||
return $target ?: "/";
|
||||
return $target ?: '/';
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -109,7 +115,7 @@ class Request extends Message implements RequestInterface
|
|||
* @link http://tools.ietf.org/html/rfc7230#section-2.7 (for the various
|
||||
* request-target forms allowed in request messages)
|
||||
* @param mixed $requestTarget
|
||||
* @return self
|
||||
* @return static
|
||||
*/
|
||||
public function withRequestTarget($requestTarget)
|
||||
{
|
||||
|
|
@ -136,8 +142,8 @@ class Request extends Message implements RequestInterface
|
|||
* modify the given string.
|
||||
*
|
||||
* @param string $method Case-insensitive method.
|
||||
* @return self
|
||||
* @throws \InvalidArgumentException for invalid HTTP methods.
|
||||
* @return static
|
||||
* @throws InvalidArgumentException for invalid HTTP methods.
|
||||
*/
|
||||
public function withMethod($method)
|
||||
{
|
||||
|
|
@ -182,25 +188,25 @@ class Request extends Message implements RequestInterface
|
|||
* @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 self
|
||||
* @return static
|
||||
*/
|
||||
public function withUri(UriInterface $uri, $preserveHost = false)
|
||||
{
|
||||
$request = clone $this;
|
||||
|
||||
$newHost = $uri->getHost();
|
||||
$oldHost = isset($request->headers["Host"]) ? $request->headers["Host"] : "";
|
||||
$oldHost = $request->headers['Host'] ?? '';
|
||||
|
||||
if ($preserveHost === false) {
|
||||
// Update Host
|
||||
if ($newHost && $newHost !== $oldHost) {
|
||||
unset($request->headers["Host"]);
|
||||
$request->headers["Host"] = $newHost;
|
||||
unset($request->headers['Host']);
|
||||
$request->headers['Host'] = $newHost;
|
||||
}
|
||||
} else {
|
||||
// Preserve Host
|
||||
if (!$oldHost && $newHost) {
|
||||
$request->headers["Host"] = $newHost;
|
||||
$request->headers['Host'] = $newHost;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -208,21 +214,21 @@ class Request extends Message implements RequestInterface
|
|||
return $request;
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @param string $method
|
||||
* @param mixed $method
|
||||
* @return string
|
||||
* @throws \InvalidArgumentException
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
private function getValidatedMethod($method)
|
||||
{
|
||||
if (!is_string($method)) {
|
||||
throw new \InvalidArgumentException("Method must be a string.");
|
||||
throw new InvalidArgumentException('Method must be a string.');
|
||||
}
|
||||
$method = trim($method);
|
||||
if (strpos($method, " ") !== false) {
|
||||
throw new \InvalidArgumentException("Method cannot contain spaces.");
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
namespace WellRESTed\Message;
|
||||
|
||||
use InvalidArgumentException;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\StreamInterface;
|
||||
|
||||
|
|
@ -33,19 +34,23 @@ class Response extends Message implements ResponseInterface
|
|||
* If no StreamInterface is provided for $body, the instance will create
|
||||
* a NullStream instance for the message body.
|
||||
*
|
||||
* @see WellRESTed\Message\Message
|
||||
* @see \WellRESTed\Message\Message
|
||||
*
|
||||
* @param int $statusCode
|
||||
* @param array $headers
|
||||
* @param StreamInterface $body
|
||||
* @param StreamInterface|null $body
|
||||
*/
|
||||
public function __construct($statusCode = 500, array $headers = null, StreamInterface $body = null)
|
||||
{
|
||||
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
|
||||
|
||||
/**
|
||||
|
|
@ -54,7 +59,7 @@ class Response extends Message implements ResponseInterface
|
|||
* The status code is a 3-digit integer result code of the server's attempt
|
||||
* to understand and satisfy the request.
|
||||
*
|
||||
* @return integer Status code.
|
||||
* @return int Status code.
|
||||
*/
|
||||
public function getStatusCode()
|
||||
{
|
||||
|
|
@ -69,14 +74,14 @@ class Response extends Message implements ResponseInterface
|
|||
* reason phrase, if possible.
|
||||
*
|
||||
* @link http://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml
|
||||
* @param integer $code The 3-digit integer result code to set.
|
||||
* @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 self
|
||||
* @throws \InvalidArgumentException For invalid status code arguments.
|
||||
* @return static
|
||||
* @throws InvalidArgumentException For invalid status code arguments.
|
||||
*/
|
||||
public function withStatus($code, $reasonPhrase = "")
|
||||
public function withStatus($code, $reasonPhrase = '')
|
||||
{
|
||||
$response = clone $this;
|
||||
$response->statusCode = $code;
|
||||
|
|
@ -103,73 +108,70 @@ class Response extends Message implements ResponseInterface
|
|||
|
||||
/**
|
||||
* @param int $statusCode
|
||||
* @return string|null
|
||||
* @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"
|
||||
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'
|
||||
];
|
||||
if (isset($reasonPhraseLookup[$statusCode])) {
|
||||
return $reasonPhraseLookup[$statusCode];
|
||||
}
|
||||
return null;
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
|
@ -2,9 +2,11 @@
|
|||
|
||||
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.
|
||||
|
|
@ -49,24 +51,37 @@ class ServerRequest extends Request implements ServerRequestInterface
|
|||
/** @var array */
|
||||
private $uploadedFiles;
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Creates a new, empty representation of a server-side HTTP request.
|
||||
* Create a new ServerRequest.
|
||||
*
|
||||
* To obtain a ServerRequest representing the request sent to the server
|
||||
* instantiaing the request, use the factory method
|
||||
* ServerRequest::getServerRequest
|
||||
* $headers is an optional associative array with header field names as
|
||||
* string keys and values as either string or string[].
|
||||
*
|
||||
* @see ServerRequest::getServerRequest
|
||||
* 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()
|
||||
{
|
||||
parent::__construct();
|
||||
$this->attributes = [];
|
||||
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->serverParams = [];
|
||||
$this->attributes = [];
|
||||
$this->uploadedFiles = [];
|
||||
}
|
||||
|
||||
|
|
@ -78,7 +93,7 @@ class ServerRequest extends Request implements ServerRequestInterface
|
|||
parent::__clone();
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// -------------------------------------------------------------------------
|
||||
// Psr\Http\Message\ServerRequestInterface
|
||||
|
||||
/**
|
||||
|
|
@ -115,7 +130,7 @@ class ServerRequest extends Request implements ServerRequestInterface
|
|||
* be injected at instantiation.
|
||||
*
|
||||
* @param array $cookies Array of key/value pairs representing cookies.
|
||||
* @return self
|
||||
* @return static
|
||||
*/
|
||||
public function withCookieParams(array $cookies)
|
||||
{
|
||||
|
|
@ -157,7 +172,7 @@ class ServerRequest extends Request implements ServerRequestInterface
|
|||
*
|
||||
* @param array $query Array of query string arguments, typically from
|
||||
* $_GET.
|
||||
* @return self
|
||||
* @return static
|
||||
*/
|
||||
public function withQueryParams(array $query)
|
||||
{
|
||||
|
|
@ -187,14 +202,15 @@ class ServerRequest extends Request implements ServerRequestInterface
|
|||
* Create a new instance with the specified uploaded files.
|
||||
*
|
||||
* @param array $uploadedFiles An array tree of UploadedFileInterface instances.
|
||||
* @return self
|
||||
* @throws \InvalidArgumentException if an invalid structure is provided.
|
||||
* @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.");
|
||||
throw new InvalidArgumentException(
|
||||
'withUploadedFiles expects an array tree with UploadedFileInterface leaves.'
|
||||
);
|
||||
}
|
||||
|
||||
$request = clone $this;
|
||||
|
|
@ -242,12 +258,12 @@ class ServerRequest extends Request implements ServerRequestInterface
|
|||
*
|
||||
* @param null|array|object $data The deserialized body data. This will
|
||||
* typically be in an array or object.
|
||||
* @return self
|
||||
* @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.");
|
||||
throw new InvalidArgumentException('Parsed body must be null, array, or object.');
|
||||
}
|
||||
|
||||
$request = clone $this;
|
||||
|
|
@ -303,7 +319,7 @@ class ServerRequest extends Request implements ServerRequestInterface
|
|||
* @see getAttributes()
|
||||
* @param string $name The attribute name.
|
||||
* @param mixed $value The value of the attribute.
|
||||
* @return self
|
||||
* @return static
|
||||
*/
|
||||
public function withAttribute($name, $value)
|
||||
{
|
||||
|
|
@ -325,7 +341,7 @@ class ServerRequest extends Request implements ServerRequestInterface
|
|||
*
|
||||
* @see getAttributes()
|
||||
* @param string $name The attribute name.
|
||||
* @return self
|
||||
* @return static
|
||||
*/
|
||||
public function withoutAttribute($name)
|
||||
{
|
||||
|
|
@ -334,155 +350,7 @@ class ServerRequest extends Request implements ServerRequestInterface
|
|||
return $request;
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
|
||||
protected function readFromServerRequest(array $attributes = null)
|
||||
{
|
||||
$this->attributes = $attributes ?: [];
|
||||
$this->serverParams = $_SERVER;
|
||||
$this->cookieParams = $_COOKIE;
|
||||
$this->readUploadedFiles($_FILES);
|
||||
$this->queryParams = [];
|
||||
$this->uri = $this->readUri();
|
||||
if (isset($_SERVER["QUERY_STRING"])) {
|
||||
parse_str($_SERVER["QUERY_STRING"], $this->queryParams);
|
||||
}
|
||||
if (isset($_SERVER["SERVER_PROTOCOL"]) && $_SERVER["SERVER_PROTOCOL"] === "HTTP/1.0") {
|
||||
// The default is 1.1, so only update if 1.0
|
||||
$this->protcolVersion = "1.0";
|
||||
}
|
||||
if (isset($_SERVER["REQUEST_METHOD"])) {
|
||||
$this->method = $_SERVER["REQUEST_METHOD"];
|
||||
}
|
||||
$headers = $this->getServerRequestHeaders();
|
||||
foreach ($headers as $key => $value) {
|
||||
$this->headers[$key] = $value;
|
||||
}
|
||||
$this->body = $this->getStreamForBody();
|
||||
|
||||
$contentType = $this->getHeader("Content-type");
|
||||
if ($contentType === ["application/x-www-form-urlencoded"] || $contentType === ["multipart/form-data"]) {
|
||||
$this->parsedBody = $_POST;
|
||||
}
|
||||
}
|
||||
|
||||
protected function readUploadedFiles($input)
|
||||
{
|
||||
$uploadedFiles = [];
|
||||
foreach ($input as $name => $value) {
|
||||
$this->addUploadedFilesToBranch($uploadedFiles, $name, $value);
|
||||
}
|
||||
$this->uploadedFiles = $uploadedFiles;
|
||||
}
|
||||
|
||||
protected function addUploadedFilesToBranch(&$branch, $name, $value)
|
||||
{
|
||||
// Check for each of the expected keys.
|
||||
if (isset($value["name"], $value["type"], $value["tmp_name"], $value["error"], $value["size"])) {
|
||||
// This is a file. It may be a single file, or a list of files.
|
||||
|
||||
// Check if these items are arrays.
|
||||
if (is_array($value["name"])
|
||||
&& is_array($value["type"])
|
||||
&& is_array($value["tmp_name"])
|
||||
&& is_array($value["error"])
|
||||
&& is_array($value["size"])
|
||||
) {
|
||||
// Each item is an array. This is a list of uploaded files.
|
||||
$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 {
|
||||
// All expected keys are present and are not arrays. This is an 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) {
|
||||
$this->addUploadedFilesToBranch($nextBranch, $nextName, $nextValue);
|
||||
}
|
||||
$branch[$name] = $nextBranch;
|
||||
}
|
||||
}
|
||||
|
||||
protected function readUri()
|
||||
{
|
||||
$uri = "";
|
||||
|
||||
$scheme = "http";
|
||||
if (isset($_SERVER["HTTPS"]) && $_SERVER["HTTPS"] && $_SERVER["HTTPS"] !== "off") {
|
||||
$scheme = "https";
|
||||
}
|
||||
|
||||
if (isset($_SERVER["HTTP_HOST"])) {
|
||||
$authority = $_SERVER["HTTP_HOST"];
|
||||
$uri .= "$scheme://$authority";
|
||||
}
|
||||
|
||||
// Path and query string
|
||||
if (isset($_SERVER["REQUEST_URI"])) {
|
||||
$uri .= $_SERVER["REQUEST_URI"];
|
||||
}
|
||||
|
||||
return new Uri($uri);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a reference to the singleton instance of the Request derived
|
||||
* from the server's information about the request sent to the server.
|
||||
*
|
||||
* @param array $attributes Key-value pairs to add to the request.
|
||||
* @return self
|
||||
* @static
|
||||
*/
|
||||
public static function getServerRequest(array $attributes = null)
|
||||
{
|
||||
$request = new static();
|
||||
$request->readFromServerRequest($attributes);
|
||||
return $request;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a stream representing the request's body.
|
||||
*
|
||||
* Override this method to use a specific StreamInterface implementation.
|
||||
*
|
||||
* @return StreamInterface
|
||||
*/
|
||||
protected function getStreamForBody()
|
||||
{
|
||||
return new Stream(fopen("php://input", "r"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Read and return all request headers from the request issued to the server.
|
||||
*
|
||||
* @return array Associative array of headers
|
||||
*/
|
||||
protected function getServerRequestHeaders()
|
||||
{
|
||||
// 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;
|
||||
}
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @param array $root
|
||||
|
|
@ -497,7 +365,7 @@ class ServerRequest extends Request implements ServerRequestInterface
|
|||
|
||||
// If not empty, the array MUST have all string keys.
|
||||
$keys = array_keys($root);
|
||||
if (count($keys) !== count(array_filter($keys, "is_string"))) {
|
||||
if (count($keys) !== count(array_filter($keys, 'is_string'))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
@ -510,7 +378,11 @@ class ServerRequest extends Request implements ServerRequestInterface
|
|||
return true;
|
||||
}
|
||||
|
||||
private function isValidUploadedFilesBranch($branch)
|
||||
/**
|
||||
* @param UploadedFileInterface|array $branch
|
||||
* @return bool
|
||||
*/
|
||||
private function isValidUploadedFilesBranch($branch): bool
|
||||
{
|
||||
if (is_array($branch)) {
|
||||
// Branch.
|
||||
|
|
|
|||
|
|
@ -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']);
|
||||
}
|
||||
}
|
||||
|
|
@ -2,15 +2,21 @@
|
|||
|
||||
namespace WellRESTed\Message;
|
||||
|
||||
use Exception;
|
||||
use InvalidArgumentException;
|
||||
use Psr\Http\Message\StreamInterface;
|
||||
use RuntimeException;
|
||||
|
||||
class Stream implements StreamInterface
|
||||
{
|
||||
/** @var resource */
|
||||
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 passing either a stream resource handle (e.g.,
|
||||
* 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,
|
||||
|
|
@ -19,17 +25,17 @@ class Stream implements StreamInterface
|
|||
* @param resource|string $resource A file system pointer resource or
|
||||
* string
|
||||
*/
|
||||
public function __construct($resource)
|
||||
public function __construct($resource = '')
|
||||
{
|
||||
if (is_resource($resource) && get_resource_type($resource) === "stream") {
|
||||
if (is_resource($resource) && get_resource_type($resource) === 'stream') {
|
||||
$this->resource = $resource;
|
||||
} elseif (is_string($resource)) {
|
||||
$this->resource = fopen("php://temp", "r+");
|
||||
if ($resource !== "") {
|
||||
$this->resource = fopen('php://temp', 'wb+');
|
||||
if ($resource !== '') {
|
||||
$this->write($resource);
|
||||
}
|
||||
} else {
|
||||
throw new \InvalidArgumentException("Expected a resource handler.");
|
||||
throw new InvalidArgumentException('Expected resource or string.');
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -46,16 +52,16 @@ class Stream implements StreamInterface
|
|||
*/
|
||||
public function __toString()
|
||||
{
|
||||
$string = "";
|
||||
try {
|
||||
if ($this->isSeekable()) {
|
||||
rewind($this->resource);
|
||||
$this->rewind();
|
||||
}
|
||||
$string = $this->getContents();
|
||||
} catch (\Exception $e) {
|
||||
// Silence exceptions in order to conform with PHP's string casting operations.
|
||||
return $this->getContents();
|
||||
} catch (Exception $e) {
|
||||
// Silence exceptions in order to conform with PHP's string casting
|
||||
// operations.
|
||||
return '';
|
||||
}
|
||||
return $string;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -65,7 +71,13 @@ class Stream implements StreamInterface
|
|||
*/
|
||||
public function close()
|
||||
{
|
||||
fclose($this->resource);
|
||||
if ($this->resource === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
$resource = $this->resource;
|
||||
fclose($resource);
|
||||
$this->resource = null;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -77,9 +89,9 @@ class Stream implements StreamInterface
|
|||
*/
|
||||
public function detach()
|
||||
{
|
||||
$stream = $this->resource;
|
||||
$resource = $this->resource;
|
||||
$this->resource = null;
|
||||
return $stream;
|
||||
return $resource;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -89,23 +101,32 @@ class Stream implements StreamInterface
|
|||
*/
|
||||
public function getSize()
|
||||
{
|
||||
if ($this->resource === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$statistics = fstat($this->resource);
|
||||
return $statistics["size"] ?: null;
|
||||
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.
|
||||
* @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) {
|
||||
// @codeCoverageIgnoreStart
|
||||
throw new \RuntimeException("Unable to retrieve current position of file pointer.");
|
||||
// @codeCoverageIgnoreEnd
|
||||
throw new RuntimeException('Unable to retrieve current position of file pointer.');
|
||||
}
|
||||
return $position;
|
||||
}
|
||||
|
|
@ -117,6 +138,10 @@ class Stream implements StreamInterface
|
|||
*/
|
||||
public function eof()
|
||||
{
|
||||
if ($this->resource === null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return feof($this->resource);
|
||||
}
|
||||
|
||||
|
|
@ -127,7 +152,11 @@ class Stream implements StreamInterface
|
|||
*/
|
||||
public function isSeekable()
|
||||
{
|
||||
return $this->getMetadata("seekable") == 1;
|
||||
if ($this->resource === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $this->getMetadata('seekable') == 1;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -140,18 +169,21 @@ class Stream implements StreamInterface
|
|||
* 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.
|
||||
* @throws \RuntimeException on failure.
|
||||
* @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) {
|
||||
// @codeCoverageIgnoreStart
|
||||
throw new \RuntimeException("Unable to seek to position.");
|
||||
// @codeCoverageIgnoreEnd
|
||||
throw new RuntimeException('Unable to seek to position.');
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -163,18 +195,21 @@ class Stream implements StreamInterface
|
|||
*
|
||||
* @see seek()
|
||||
* @link http://www.php.net/manual/en/function.fseek.php
|
||||
* @throws \RuntimeException on failure.
|
||||
* @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) {
|
||||
// @codeCoverageIgnoreStart
|
||||
throw new \RuntimeException("Unable to seek to position.");
|
||||
// @codeCoverageIgnoreEnd
|
||||
throw new RuntimeException('Unable to rewind.');
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -185,8 +220,12 @@ class Stream implements StreamInterface
|
|||
*/
|
||||
public function isWritable()
|
||||
{
|
||||
$mode = $this->getMetadata("mode");
|
||||
return $mode[0] !== "r" || strpos($mode, "+") !== false;
|
||||
if ($this->resource === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$mode = $this->getBasicMode();
|
||||
return in_array($mode, self::WRITABLE_MODES);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -194,16 +233,20 @@ class Stream implements StreamInterface
|
|||
*
|
||||
* @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.
|
||||
* @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.");
|
||||
throw new RuntimeException('Unable to write to stream.');
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
|
@ -215,8 +258,12 @@ class Stream implements StreamInterface
|
|||
*/
|
||||
public function isReadable()
|
||||
{
|
||||
$mode = $this->getMetadata("mode");
|
||||
return strpos($mode, "r") !== false || strpos($mode, "+") !== false;
|
||||
if ($this->resource === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$mode = $this->getBasicMode();
|
||||
return in_array($mode, self::READABLE_MODES);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -227,16 +274,20 @@ class Stream implements StreamInterface
|
|||
* 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.
|
||||
* @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.");
|
||||
throw new RuntimeException('Unable to read from stream.');
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
|
@ -245,17 +296,21 @@ class Stream implements StreamInterface
|
|||
* Returns the remaining contents in a string
|
||||
*
|
||||
* @return string
|
||||
* @throws \RuntimeException if unable to read or an error occurs while
|
||||
* @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.");
|
||||
throw new RuntimeException('Unable to read from stream.');
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
|
@ -267,13 +322,17 @@ class Stream implements StreamInterface
|
|||
* stream_get_meta_data() function.
|
||||
*
|
||||
* @link http://php.net/manual/en/function.stream-get-meta-data.php
|
||||
* @param string $key Specific metadata to retrieve.
|
||||
* @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;
|
||||
|
|
@ -281,4 +340,14 @@ class Stream implements StreamInterface
|
|||
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);
|
||||
}
|
||||
}
|
||||
|
|
@ -2,28 +2,37 @@
|
|||
|
||||
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 Uri. The arguments correspond with keys from arrays
|
||||
* Create a new UploadedFile. The arguments correspond with keys from arrays
|
||||
* provided by $_FILES. For example, given this structure for $_FILES:
|
||||
*
|
||||
* array(
|
||||
* 'avatar' => arrary(
|
||||
* 'avatar' => array(
|
||||
* 'name' => 'my-avatar.png',
|
||||
* 'type' => 'image/png',
|
||||
* 'size' => 90996,
|
||||
|
|
@ -57,10 +66,11 @@ class UploadedFile implements UploadedFileInterface
|
|||
$this->size = $size;
|
||||
|
||||
if (file_exists($tmpName)) {
|
||||
$this->stream = new Stream(fopen($tmpName, 'rb'));
|
||||
$this->tmpName = $tmpName;
|
||||
$this->stream = new Stream(fopen($tmpName, "r"));
|
||||
} else {
|
||||
$this->stream = new NullStream();
|
||||
$this->tmpName = null;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -77,16 +87,19 @@ class UploadedFile implements UploadedFileInterface
|
|||
* raise an exception.
|
||||
*
|
||||
* @return StreamInterface Stream representation of the uploaded file.
|
||||
* @throws \RuntimeException in cases when no stream is available or can
|
||||
* @throws RuntimeException in cases when no stream is available or can
|
||||
* be created.
|
||||
*/
|
||||
public function getStream()
|
||||
{
|
||||
if ($this->moved) {
|
||||
throw new \RuntimeException("File has already been moved");
|
||||
if ($this->tmpName === null) {
|
||||
throw new RuntimeException('Unable to read uploaded file.');
|
||||
}
|
||||
if (php_sapi_name() !== "cli" && !is_uploaded_file($this->tmpName)) {
|
||||
throw new \RuntimeException("File is not an 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;
|
||||
}
|
||||
|
|
@ -104,20 +117,21 @@ class UploadedFile implements UploadedFileInterface
|
|||
*
|
||||
* @see http://php.net/is_uploaded_file
|
||||
* @see http://php.net/move_uploaded_file
|
||||
* @param string $path Path to which to move the uploaded file.
|
||||
* @throws \InvalidArgumentException if the $path specified is invalid.
|
||||
* @throws \RuntimeException on any error during the move operation, or on
|
||||
* @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($path)
|
||||
public function moveTo($targetPath)
|
||||
{
|
||||
if ($this->tmpName === null || !file_exists($this->tmpName)) {
|
||||
throw new \RuntimeException("File " . $this->tmpName . " does not exist.");
|
||||
throw new RuntimeException("File {$this->tmpName} does not exist.");
|
||||
}
|
||||
if (php_sapi_name() === "cli") {
|
||||
rename($this->tmpName, $path);
|
||||
if (php_sapi_name() === 'cli') {
|
||||
rename($this->tmpName, $targetPath);
|
||||
} else {
|
||||
move_uploaded_file($this->tmpName, $path);
|
||||
move_uploaded_file($this->tmpName, $targetPath);
|
||||
}
|
||||
$this->moved = true;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
namespace WellRESTed\Message;
|
||||
|
||||
use InvalidArgumentException;
|
||||
use Psr\Http\Message\UriInterface;
|
||||
|
||||
/**
|
||||
|
|
@ -17,60 +18,41 @@ use Psr\Http\Message\UriInterface;
|
|||
*/
|
||||
class Uri implements UriInterface
|
||||
{
|
||||
const MIN_PORT = 0;
|
||||
const MAX_PORT = 65535;
|
||||
private const MIN_PORT = 0;
|
||||
private const MAX_PORT = 65535;
|
||||
|
||||
/** @var string */
|
||||
private $scheme = "";
|
||||
private $scheme;
|
||||
/** @var string */
|
||||
private $user;
|
||||
/** @var string */
|
||||
private $user = "";
|
||||
/** @var string|null */
|
||||
private $password;
|
||||
/** @var string */
|
||||
private $host = "";
|
||||
private $host;
|
||||
/** @var int|null */
|
||||
private $port;
|
||||
/** @var string */
|
||||
private $path = "";
|
||||
private $path;
|
||||
/** @var string */
|
||||
private $query = "";
|
||||
private $query;
|
||||
/** @var string */
|
||||
private $fragment = "";
|
||||
private $fragment;
|
||||
|
||||
/**
|
||||
* @param string $uri A string representation of a URI.
|
||||
*/
|
||||
public function __construct($uri = "")
|
||||
public function __construct(string $uri = '')
|
||||
{
|
||||
if (is_string($uri) && $uri !== "") {
|
||||
$parsed = parse_url($uri);
|
||||
if ($parsed !== false) {
|
||||
if (isset($parsed["scheme"])) {
|
||||
$this->scheme = $parsed["scheme"];
|
||||
}
|
||||
if (isset($parsed["host"])) {
|
||||
$this->host = strtolower($parsed["host"]);
|
||||
}
|
||||
if (isset($parsed["port"])) {
|
||||
$this->port = $parsed["port"];
|
||||
}
|
||||
if (isset($parsed["user"])) {
|
||||
$this->user = $parsed["user"];
|
||||
}
|
||||
if (isset($parsed["pass"])) {
|
||||
$this->password = $parsed["pass"];
|
||||
}
|
||||
if (isset($parsed["path"])) {
|
||||
$this->path = $parsed["path"];
|
||||
}
|
||||
if (isset($parsed["query"])) {
|
||||
$this->query = $parsed["query"];
|
||||
}
|
||||
if (isset($parsed["fragment"])) {
|
||||
$this->fragment = $parsed["fragment"];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$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'] ?? '';
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -112,33 +94,38 @@ class Uri implements UriInterface
|
|||
*/
|
||||
public function getAuthority()
|
||||
{
|
||||
$authority = "";
|
||||
|
||||
$host = $this->getHost();
|
||||
if ($host !== "") {
|
||||
if (!$host) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$authority = '';
|
||||
|
||||
// User Info
|
||||
$userInfo = $this->getUserInfo();
|
||||
if ($userInfo !== "") {
|
||||
$authority .= $userInfo . "@";
|
||||
if ($userInfo) {
|
||||
$authority .= $userInfo . '@';
|
||||
}
|
||||
|
||||
// Host
|
||||
$authority .= $host;
|
||||
|
||||
// Port: Include only if set AND non-standard.
|
||||
$port = $this->getPort();
|
||||
if ($port !== null) {
|
||||
$scheme = $this->getScheme();
|
||||
if (($scheme === "http" && $port !== 80 ) || ($scheme === "https" && $port !== 443)) {
|
||||
$authority .= ":" . $port;
|
||||
}
|
||||
}
|
||||
// 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.
|
||||
*
|
||||
|
|
@ -158,7 +145,7 @@ class Uri implements UriInterface
|
|||
{
|
||||
$userInfo = $this->user;
|
||||
if ($userInfo && $this->password) {
|
||||
$userInfo .= ":" . $this->password;
|
||||
$userInfo .= ':' . $this->password;
|
||||
}
|
||||
return $userInfo;
|
||||
}
|
||||
|
|
@ -198,9 +185,9 @@ class Uri implements UriInterface
|
|||
{
|
||||
if ($this->port === null) {
|
||||
switch ($this->scheme) {
|
||||
case "http":
|
||||
case 'http':
|
||||
return 80;
|
||||
case "https":
|
||||
case 'https':
|
||||
return 443;
|
||||
default:
|
||||
return null;
|
||||
|
|
@ -236,10 +223,10 @@ class Uri implements UriInterface
|
|||
*/
|
||||
public function getPath()
|
||||
{
|
||||
if ($this->path === "*") {
|
||||
if ($this->path === '*') {
|
||||
return $this->path;
|
||||
}
|
||||
return $this->percentEncode($this->path, '/');
|
||||
return $this->percentEncode($this->path);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -264,7 +251,7 @@ class Uri implements UriInterface
|
|||
*/
|
||||
public function getQuery()
|
||||
{
|
||||
return $this->percentEncode($this->query, '&=');
|
||||
return $this->percentEncode($this->query);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -300,14 +287,14 @@ class Uri implements UriInterface
|
|||
* An empty scheme is equivalent to removing the scheme.
|
||||
*
|
||||
* @param string $scheme The scheme to use with the new instance.
|
||||
* @return self A new instance with the specified scheme.
|
||||
* @throws \InvalidArgumentException for invalid or unsupported schemes.
|
||||
* @return static A new instance with the specified scheme.
|
||||
* @throws InvalidArgumentException for invalid or unsupported schemes.
|
||||
*/
|
||||
public function withScheme($scheme)
|
||||
{
|
||||
$scheme = $scheme ? strtolower($scheme) : "";
|
||||
if (!in_array($scheme, ["", "http", "https"])) {
|
||||
throw new \InvalidArgumentException("Scheme must be http, https, or empty.");
|
||||
$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;
|
||||
|
|
@ -326,13 +313,13 @@ class Uri implements UriInterface
|
|||
*
|
||||
* @param string $user The user name to use for authority.
|
||||
* @param null|string $password The password associated with $user.
|
||||
* @return self A new instance with the specified user information.
|
||||
* @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;
|
||||
$uri->password = $password ?? '';
|
||||
return $uri;
|
||||
}
|
||||
|
||||
|
|
@ -345,13 +332,13 @@ class Uri implements UriInterface
|
|||
* An empty host value is equivalent to removing the host.
|
||||
*
|
||||
* @param string $host The hostname to use with the new instance.
|
||||
* @return self A new instance with the specified host.
|
||||
* @throws \InvalidArgumentException for invalid hostnames.
|
||||
* @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.");
|
||||
throw new InvalidArgumentException('Host must be a string.');
|
||||
}
|
||||
|
||||
$uri = clone $this;
|
||||
|
|
@ -373,19 +360,19 @@ class Uri implements UriInterface
|
|||
*
|
||||
* @param null|int $port The port to use with the new instance; a null value
|
||||
* removes the port information.
|
||||
* @return self A new instance with the specified port.
|
||||
* @throws \InvalidArgumentException for invalid ports.
|
||||
* @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);
|
||||
$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.");
|
||||
throw new InvalidArgumentException('Port must be an int or null.');
|
||||
}
|
||||
|
||||
$uri = clone $this;
|
||||
|
|
@ -407,13 +394,13 @@ class Uri implements UriInterface
|
|||
* Implementations ensure the correct encoding as outlined in getPath().
|
||||
*
|
||||
* @param string $path The path to use with the new instance.
|
||||
* @return self A new instance with the specified path.
|
||||
* @throws \InvalidArgumentException for invalid paths.
|
||||
* @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");
|
||||
throw new InvalidArgumentException('Path must be a string');
|
||||
}
|
||||
$uri = clone $this;
|
||||
$uri->path = $path;
|
||||
|
|
@ -432,8 +419,8 @@ class Uri implements UriInterface
|
|||
* 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 self A new instance with the specified query string.
|
||||
* @throws \InvalidArgumentException for invalid query strings.
|
||||
* @return static A new instance with the specified query string.
|
||||
* @throws InvalidArgumentException for invalid query strings.
|
||||
*/
|
||||
public function withQuery($query)
|
||||
{
|
||||
|
|
@ -454,12 +441,12 @@ class Uri implements UriInterface
|
|||
* An empty fragment value is equivalent to removing the fragment.
|
||||
*
|
||||
* @param string $fragment The fragment to use with the new instance.
|
||||
* @return self A new instance with the specified fragment.
|
||||
* @return static A new instance with the specified fragment.
|
||||
*/
|
||||
public function withFragment($fragment)
|
||||
{
|
||||
$uri = clone $this;
|
||||
$uri->fragment = $fragment;
|
||||
$uri->fragment = $fragment ?? '';
|
||||
return $uri;
|
||||
}
|
||||
|
||||
|
|
@ -488,29 +475,29 @@ class Uri implements UriInterface
|
|||
*/
|
||||
public function __toString()
|
||||
{
|
||||
$string = "";
|
||||
$string = '';
|
||||
|
||||
$authority = $this->getAuthority();
|
||||
if ($authority !== "") {
|
||||
if ($authority !== '') {
|
||||
$scheme = $this->getScheme();
|
||||
if ($scheme !== "") {
|
||||
$string = $scheme . ":";
|
||||
if ($scheme !== '') {
|
||||
$string = $scheme . ':';
|
||||
}
|
||||
$string .= "//$authority";
|
||||
}
|
||||
|
||||
$path = $this->getPath();
|
||||
if ($path !== "") {
|
||||
if ($path !== '') {
|
||||
$string .= $path;
|
||||
}
|
||||
|
||||
$query = $this->getQuery();
|
||||
if ($query !== "") {
|
||||
if ($query !== '') {
|
||||
$string .= "?$query";
|
||||
}
|
||||
|
||||
$fragment = $this->getFragment();
|
||||
if ($fragment !== "") {
|
||||
if ($fragment !== '') {
|
||||
$string .= "#$fragment";
|
||||
}
|
||||
|
||||
|
|
@ -521,19 +508,24 @@ class Uri implements UriInterface
|
|||
* Return a percent-encoded string.
|
||||
*
|
||||
* This method encode each character that is not:
|
||||
* - A precent sign ("%") that is followed by a hex character (0-9, a-f, A-F)
|
||||
* - An "unreserved character" per RFC 3986 (ALPHA / DIGIT / "-" / "." / "_" / "~")
|
||||
* - A slash ("/")
|
||||
* - 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
|
||||
* @param string $whitelist
|
||||
* @return string
|
||||
*/
|
||||
private function percentEncode($subject, $whitelist = "")
|
||||
private function percentEncode(string $subject)
|
||||
{
|
||||
$whitelist = preg_quote($whitelist);
|
||||
$pattern = '~(?:%(?![a-fA-F0-9]{2}))|(?:[^%a-zA-Z0-9\-\.\_\~' . $whitelist . ']{1})~';
|
||||
$callback = function ($matches) {
|
||||
$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);
|
||||
|
|
|
|||
|
|
@ -17,17 +17,18 @@ interface MiddlewareInterface
|
|||
* $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 propgation
|
||||
* and prevents subsequent middleware from running.
|
||||
* propagation; returning a response without calling $next halts
|
||||
* propagation and prevents subsequent middleware from running.
|
||||
*
|
||||
* Implementations MAY call $next to continue propagation. After calling
|
||||
* $next, implementations MUST return the response returned by $next or
|
||||
* use $next's returned response to determine the response it will
|
||||
* ulitimately return. Implementations MUST NOT call $next and disregard
|
||||
* $next's returned response.
|
||||
* 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.
|
||||
*
|
||||
* Implementaitons MAY return a response without calling $next to halt
|
||||
* propagation.
|
||||
* 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
|
||||
|
|
|
|||
|
|
@ -1,47 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace WellRESTed\Routing;
|
||||
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use WellRESTed\MiddlewareInterface;
|
||||
|
||||
/**
|
||||
* Maps HTTP methods to middleware
|
||||
*/
|
||||
interface MethodMapInterface extends MiddlewareInterface
|
||||
{
|
||||
/**
|
||||
* Evaluate $request's method and dispatches matching middleware.
|
||||
*
|
||||
* Implementations MUST pass $request, $response, and $next to the matching
|
||||
* middleware.
|
||||
*
|
||||
* @param ServerRequestInterface $request
|
||||
* @param ResponseInterface $response
|
||||
* @param callable $next
|
||||
* @return ResponseInterface
|
||||
*/
|
||||
public function __invoke(ServerRequestInterface $request, ResponseInterface $response, $next);
|
||||
|
||||
/**
|
||||
* Register middleware with a method.
|
||||
*
|
||||
* $method may be:
|
||||
* - A single verb ("GET"),
|
||||
* - A comma-separated list of verbs ("GET,PUT,DELETE")
|
||||
* - "*" to indicate any method.
|
||||
*
|
||||
* $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 maching the signature of MiddlewareInteraface::dispatch
|
||||
* @see DispatcherInterface::dispatch
|
||||
*
|
||||
* @param string $method
|
||||
* @param mixed $middleware
|
||||
*/
|
||||
public function register($method, $middleware);
|
||||
}
|
||||
|
|
@ -1,17 +1,23 @@
|
|||
<?php
|
||||
|
||||
namespace WellRESTed\Routing;
|
||||
namespace WellRESTed\Routing\Route;
|
||||
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use WellRESTed\Dispatching\DispatcherInterface;
|
||||
use WellRESTed\MiddlewareInterface;
|
||||
|
||||
class MethodMap implements MethodMapInterface
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
class MethodMap implements MiddlewareInterface
|
||||
{
|
||||
/** @var DispatcherInterface */
|
||||
private $dispatcher;
|
||||
/** @var array */
|
||||
private $map;
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
public function __construct(DispatcherInterface $dispatcher)
|
||||
{
|
||||
|
|
@ -19,41 +25,33 @@ class MethodMap implements MethodMapInterface
|
|||
$this->dispatcher = $dispatcher;
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// MethodMapInterface
|
||||
|
||||
/**
|
||||
* Register middleware with a method.
|
||||
* 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.
|
||||
*
|
||||
* $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 maching the signature of MiddlewareInteraface::dispatch
|
||||
* @see DispatchedInterface::dispatch
|
||||
* $dispatchable may be anything a Dispatcher can dispatch.
|
||||
* @see DispatcherInterface::dispatch
|
||||
*
|
||||
* $middleware may also be null, in which case any previously set
|
||||
* middleware for that method or methods will be unset.
|
||||
* $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 $middleware
|
||||
* @param mixed $dispatchable
|
||||
*/
|
||||
public function register($method, $middleware)
|
||||
public function register(string $method, $dispatchable): void
|
||||
{
|
||||
$methods = explode(",", $method);
|
||||
$methods = array_map("trim", $methods);
|
||||
$methods = explode(',', $method);
|
||||
$methods = array_map('trim', $methods);
|
||||
foreach ($methods as $method) {
|
||||
$this->map[$method] = $middleware;
|
||||
$this->map[$method] = $dispatchable;
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// -------------------------------------------------------------------------
|
||||
// MiddlewareInterface
|
||||
|
||||
/**
|
||||
|
|
@ -62,8 +60,11 @@ class MethodMap implements MethodMapInterface
|
|||
* @param callable $next
|
||||
* @return ResponseInterface
|
||||
*/
|
||||
public function __invoke(ServerRequestInterface $request, ResponseInterface $response, $next)
|
||||
{
|
||||
public function __invoke(
|
||||
ServerRequestInterface $request,
|
||||
ResponseInterface $response,
|
||||
$next
|
||||
) {
|
||||
$method = $request->getMethod();
|
||||
// Dispatch middleware registered with the explicitly matching method.
|
||||
if (isset($this->map[$method])) {
|
||||
|
|
@ -71,18 +72,18 @@ class MethodMap implements MethodMapInterface
|
|||
return $this->dispatchMiddleware($middleware, $request, $response, $next);
|
||||
}
|
||||
// For HEAD, dispatch GET by default.
|
||||
if ($method === "HEAD" && isset($this->map["GET"])) {
|
||||
$middleware = $this->map["GET"];
|
||||
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["*"];
|
||||
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") {
|
||||
if ($method === 'OPTIONS') {
|
||||
$response = $response->withStatus(200);
|
||||
} else {
|
||||
$response = $response->withStatus(405);
|
||||
|
|
@ -90,37 +91,44 @@ class MethodMap implements MethodMapInterface
|
|||
return $this->addAllowHeader($response);
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
private function addAllowHeader(ResponseInterface $response)
|
||||
private function addAllowHeader(ResponseInterface $response): ResponseInterface
|
||||
{
|
||||
$methods = join(",", $this->getAllowedMethods());
|
||||
return $response->withHeader("Allow", $methods);
|
||||
$methods = join(',', $this->getAllowedMethods());
|
||||
return $response->withHeader('Allow', $methods);
|
||||
}
|
||||
|
||||
private function getAllowedMethods()
|
||||
/**
|
||||
* @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";
|
||||
if (in_array('GET', $methods) && !in_array('HEAD', $methods)) {
|
||||
$methods[] = 'HEAD';
|
||||
}
|
||||
// Add OPTIONS if not already present.
|
||||
if (!in_array("OPTIONS", $methods)) {
|
||||
$methods[] = "OPTIONS";
|
||||
if (!in_array('OPTIONS', $methods)) {
|
||||
$methods[] = 'OPTIONS';
|
||||
}
|
||||
return $methods;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $middleware
|
||||
* @param mixed $middleware
|
||||
* @param ServerRequestInterface $request
|
||||
* @param ResponseInterface $response
|
||||
* @param $next
|
||||
* @param callable $next
|
||||
* @return ResponseInterface
|
||||
*/
|
||||
private function dispatchMiddleware($middleware, ServerRequestInterface $request, ResponseInterface &$response, $next)
|
||||
{
|
||||
private function dispatchMiddleware(
|
||||
$middleware,
|
||||
ServerRequestInterface $request,
|
||||
ResponseInterface $response,
|
||||
$next
|
||||
) {
|
||||
return $this->dispatcher->dispatch($middleware, $request, $response, $next);
|
||||
}
|
||||
}
|
||||
|
|
@ -2,26 +2,28 @@
|
|||
|
||||
namespace WellRESTed\Routing\Route;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
class PrefixRoute extends Route
|
||||
{
|
||||
public function __construct($target, $methodMap)
|
||||
public function __construct(string $target, MethodMap $methodMap)
|
||||
{
|
||||
$this->target = rtrim($target, "*");
|
||||
$this->methodMap = $methodMap;
|
||||
parent::__construct(rtrim($target, '*'), $methodMap);
|
||||
}
|
||||
|
||||
public function getType()
|
||||
public function getType(): int
|
||||
{
|
||||
return RouteInterface::TYPE_PREFIX;
|
||||
return Route::TYPE_PREFIX;
|
||||
}
|
||||
|
||||
/**
|
||||
* Examines a request target to see if it is a match for the route.
|
||||
*
|
||||
* @param string $requestTarget
|
||||
* @return boolean
|
||||
* @return bool
|
||||
*/
|
||||
public function matchesRequestTarget($requestTarget)
|
||||
public function matchesRequestTarget(string $requestTarget): bool
|
||||
{
|
||||
return strrpos($requestTarget, $this->target, -strlen($requestTarget)) !== false;
|
||||
}
|
||||
|
|
@ -29,7 +31,7 @@ class PrefixRoute extends Route
|
|||
/**
|
||||
* Always returns an empty array.
|
||||
*/
|
||||
public function getPathVariables()
|
||||
public function getPathVariables(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,22 +2,28 @@
|
|||
|
||||
namespace WellRESTed\Routing\Route;
|
||||
|
||||
use RuntimeException;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
class RegexRoute extends Route
|
||||
{
|
||||
private $captures;
|
||||
/** @var array */
|
||||
private $captures = [];
|
||||
|
||||
public function getType()
|
||||
public function getType(): int
|
||||
{
|
||||
return RouteInterface::TYPE_PATTERN;
|
||||
return Route::TYPE_PATTERN;
|
||||
}
|
||||
|
||||
/**
|
||||
* Examines a request target to see if it is a match for the route.
|
||||
*
|
||||
* @param string $requestTarget
|
||||
* @return boolean
|
||||
* @return bool
|
||||
*/
|
||||
public function matchesRequestTarget($requestTarget)
|
||||
public function matchesRequestTarget(string $requestTarget): bool
|
||||
{
|
||||
$this->captures = [];
|
||||
$matched = preg_match($this->getTarget(), $requestTarget, $captures);
|
||||
|
|
@ -25,7 +31,7 @@ class RegexRoute extends Route
|
|||
$this->captures = $captures;
|
||||
return true;
|
||||
} elseif ($matched === false) {
|
||||
throw new \RuntimeException("Invalid regular expression: " . $this->getTarget());
|
||||
throw new RuntimeException('Invalid regular expression: ' . $this->getTarget());
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
|
@ -36,7 +42,7 @@ class RegexRoute extends Route
|
|||
* @see \preg_match
|
||||
* @return array
|
||||
*/
|
||||
public function getPathVariables()
|
||||
public function getPathVariables(): array
|
||||
{
|
||||
return $this->captures;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,39 +4,106 @@ namespace WellRESTed\Routing\Route;
|
|||
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use WellRESTed\Routing\MethodMapInterface;
|
||||
use RuntimeException;
|
||||
use WellRESTed\MiddlewareInterface;
|
||||
|
||||
abstract class Route implements RouteInterface
|
||||
/**
|
||||
* @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 MethodMapInterface */
|
||||
/** @var MethodMap */
|
||||
protected $methodMap;
|
||||
|
||||
public function __construct($target, $methodMap)
|
||||
public function __construct(string $target, MethodMap $methodMap)
|
||||
{
|
||||
$this->target = $target;
|
||||
$this->methodMap = $methodMap;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the instance mapping methods to middleware for this route.
|
||||
* Return the Route::TYPE_ constants that identifies the type.
|
||||
*
|
||||
* @return MethodMapInterface
|
||||
* 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.
|
||||
*/
|
||||
public function getMethodMap()
|
||||
{
|
||||
return $this->methodMap;
|
||||
}
|
||||
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()
|
||||
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;
|
||||
|
|
|
|||
|
|
@ -3,12 +3,11 @@
|
|||
namespace WellRESTed\Routing\Route;
|
||||
|
||||
use WellRESTed\Dispatching\DispatcherInterface;
|
||||
use WellRESTed\Routing\MethodMap;
|
||||
|
||||
/**
|
||||
* Class for creating routes
|
||||
* @internal
|
||||
*/
|
||||
class RouteFactory implements RouteFactoryInterface
|
||||
class RouteFactory
|
||||
{
|
||||
private $dispatcher;
|
||||
|
||||
|
|
@ -23,23 +22,23 @@ class RouteFactory implements RouteFactoryInterface
|
|||
* - 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 exressions will create RegexRoutes
|
||||
* - Regular expressions will create RegexRoutes
|
||||
*
|
||||
* @param string $target Route target or target pattern
|
||||
* @return RouteInterface
|
||||
* @return Route
|
||||
*/
|
||||
public function create($target)
|
||||
public function create(string $target): Route
|
||||
{
|
||||
if ($target[0] === "/") {
|
||||
if ($target[0] === '/') {
|
||||
|
||||
// Possible static, prefix, or template
|
||||
|
||||
// PrefixRoutes end with *
|
||||
if (substr($target, -1) === "*") {
|
||||
if (substr($target, -1) === '*') {
|
||||
return new PrefixRoute($target, new MethodMap($this->dispatcher));
|
||||
}
|
||||
|
||||
// TempalateRoutes contain {variable}
|
||||
// TemplateRoutes contain {variable}
|
||||
if (preg_match(TemplateRoute::URI_TEMPLATE_EXPRESSION_RE, $target)) {
|
||||
return new TemplateRoute($target, new MethodMap($this->dispatcher));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,19 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace WellRESTed\Routing\Route;
|
||||
|
||||
interface RouteFactoryInterface
|
||||
{
|
||||
/**
|
||||
* Creates a route for the given target.
|
||||
*
|
||||
* - Targets with no special characters will create StaticRoutes
|
||||
* - Targets ending with * will create PrefixRoutes
|
||||
* - Targets containing URI variables (e.g., {id}) will create TemplateRoutes
|
||||
* - Regular exressions will create RegexRoutes
|
||||
*
|
||||
* @param string $target Route target or target pattern
|
||||
* @return RouteInterface
|
||||
*/
|
||||
public function create($target);
|
||||
}
|
||||
|
|
@ -1,67 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace WellRESTed\Routing\Route;
|
||||
|
||||
use WellRESTed\MiddlewareInterface;
|
||||
use WellRESTed\Routing\MethodMapInterface;
|
||||
|
||||
interface RouteInterface extends MiddlewareInterface
|
||||
{
|
||||
/** Matches when path is an exact match only */
|
||||
const TYPE_STATIC = 0;
|
||||
/** Matches when path has the expected beginning */
|
||||
const TYPE_PREFIX = 1;
|
||||
/** Matches by pattern. Use matchesRequestTarget to test for matches */
|
||||
const TYPE_PATTERN = 2;
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getTarget();
|
||||
|
||||
/**
|
||||
* Return the RouteInterface::TYPE_ contants that identifies the type.
|
||||
*
|
||||
* TYPE_STATIC indicates the route MUST match only when the path is an
|
||||
* exact match to the route's 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 RouteInterface::TYPE_ constants.
|
||||
*/
|
||||
public function getType();
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
public function getPathVariables();
|
||||
|
||||
/**
|
||||
* Return the instance mapping methods to middleware for this route.
|
||||
*
|
||||
* @return MethodMapInterface
|
||||
*/
|
||||
public function getMethodMap();
|
||||
|
||||
/**
|
||||
* Examines a request target to see if it is a match for the route.
|
||||
*
|
||||
* @param string $requestTarget
|
||||
* @return boolean
|
||||
* @throw \RuntimeException Error occured testing the target such as an
|
||||
* invalid regular expression
|
||||
*/
|
||||
public function matchesRequestTarget($requestTarget);
|
||||
}
|
||||
|
|
@ -2,20 +2,23 @@
|
|||
|
||||
namespace WellRESTed\Routing\Route;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
class StaticRoute extends Route
|
||||
{
|
||||
public function getType()
|
||||
public function getType(): int
|
||||
{
|
||||
return RouteInterface::TYPE_STATIC;
|
||||
return Route::TYPE_STATIC;
|
||||
}
|
||||
|
||||
/**
|
||||
* Examines a request target to see if it is a match for the route.
|
||||
*
|
||||
* @param string $requestTarget
|
||||
* @return boolean
|
||||
* @return bool
|
||||
*/
|
||||
public function matchesRequestTarget($requestTarget)
|
||||
public function matchesRequestTarget(string $requestTarget): bool
|
||||
{
|
||||
return $requestTarget === $this->getTarget();
|
||||
}
|
||||
|
|
@ -23,7 +26,7 @@ class StaticRoute extends Route
|
|||
/**
|
||||
* Always returns an empty array.
|
||||
*/
|
||||
public function getPathVariables()
|
||||
public function getPathVariables(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,36 +2,41 @@
|
|||
|
||||
namespace WellRESTed\Routing\Route;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
class TemplateRoute extends Route
|
||||
{
|
||||
private $pathVariables;
|
||||
private $explosions;
|
||||
|
||||
/** 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 / "-" / "." / "_" / "~"
|
||||
*/
|
||||
const RE_UNRESERVED = '[0-9a-zA-Z\-._\~%]*';
|
||||
/** Regular expression matching a URI template variable (e.g., {id}) */
|
||||
const URI_TEMPLATE_EXPRESSION_RE = '/{([+.\/]?[a-zA-Z0-9_,]+\*?)}/';
|
||||
private const RE_UNRESERVED = '[0-9a-zA-Z\-._\~%]*';
|
||||
|
||||
public function getType()
|
||||
/** @var array */
|
||||
private $pathVariables = [];
|
||||
/** @var array */
|
||||
private $explosions = [];
|
||||
|
||||
public function getType(): int
|
||||
{
|
||||
return RouteInterface::TYPE_PATTERN;
|
||||
return Route::TYPE_PATTERN;
|
||||
}
|
||||
|
||||
public function getPathVariables()
|
||||
public function getPathVariables(): array
|
||||
{
|
||||
return $this->pathVariables ?: [];
|
||||
return $this->pathVariables;
|
||||
}
|
||||
|
||||
/**
|
||||
* Examines a request target to see if it is a match for the route.
|
||||
*
|
||||
* @param string $requestTarget
|
||||
* @return boolean
|
||||
* @return bool
|
||||
*/
|
||||
public function matchesRequestTarget($requestTarget)
|
||||
public function matchesRequestTarget(string $requestTarget): bool
|
||||
{
|
||||
$this->pathVariables = [];
|
||||
$this->explosions = [];
|
||||
|
|
@ -49,54 +54,55 @@ class TemplateRoute extends Route
|
|||
return false;
|
||||
}
|
||||
|
||||
private function matchesStartOfRequestTarget($requestTarget)
|
||||
private function matchesStartOfRequestTarget(string $requestTarget): bool
|
||||
{
|
||||
$firstVarPos = strpos($this->target, "{");
|
||||
return (substr($requestTarget, 0, $firstVarPos) === substr($this->target, 0, $firstVarPos));
|
||||
$firstVarPos = strpos($this->target, '{');
|
||||
if ($firstVarPos === false) {
|
||||
return $requestTarget === $this->target;
|
||||
}
|
||||
return substr($requestTarget, 0, $firstVarPos) === substr($this->target, 0, $firstVarPos);
|
||||
}
|
||||
|
||||
private function processMatches($matches)
|
||||
private function processMatches(array $matches): array
|
||||
{
|
||||
$variables = [];
|
||||
|
||||
// Isolate the named captures.
|
||||
$keys = array_filter(array_keys($matches), "is_string");
|
||||
$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);
|
||||
$variables[$key] = array_map('urldecode', $values);
|
||||
} else {
|
||||
$value = urldecode($value);
|
||||
$variables[$key] = $value;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return $variables;
|
||||
}
|
||||
|
||||
private function getMatchingPattern()
|
||||
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);
|
||||
|
||||
|
|
@ -105,53 +111,61 @@ class TemplateRoute extends Route
|
|||
|
||||
$pattern = preg_replace_callback(
|
||||
self::URI_TEMPLATE_EXPRESSION_RE,
|
||||
[$this, "uriVariableReplacementCallback"],
|
||||
[$this, 'uriVariableReplacementCallback'],
|
||||
$pattern
|
||||
);
|
||||
|
||||
return $pattern;
|
||||
}
|
||||
|
||||
private function uriVariableReplacementCallback($matches)
|
||||
private function uriVariableReplacementCallback(array $matches): string
|
||||
{
|
||||
$name = $matches[1];
|
||||
$pattern = self::RE_UNRESERVED;
|
||||
|
||||
$prefix = "";
|
||||
$delimiter = ",";
|
||||
$explodeDelimiter = ",";
|
||||
$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 "+":
|
||||
case '+':
|
||||
$name = substr($name, 1);
|
||||
$pattern = ".*";
|
||||
$pattern = '.*';
|
||||
break;
|
||||
case ".":
|
||||
case '.':
|
||||
$name = substr($name, 1);
|
||||
$prefix = "\\.";
|
||||
$delimiter = "\\.";
|
||||
$explodeDelimiter = ".";
|
||||
$prefix = '\\.';
|
||||
$delimiter = '\\.';
|
||||
$explodeDelimiter = '.';
|
||||
break;
|
||||
case "/":
|
||||
case '/':
|
||||
$name = substr($name, 1);
|
||||
$prefix = "\\/";
|
||||
$delimiter = "\\/";
|
||||
$explodeDelimiter = "/";
|
||||
$prefix = '\\/';
|
||||
$delimiter = '\\/';
|
||||
if ($explosion) {
|
||||
$pattern = '[0-9a-zA-Z\-._\~%,\/]*'; // Unreserved + "," and "/"
|
||||
$explodeDelimiter = '/';
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// Explosion
|
||||
if (substr($name, -1, 1) === "*") {
|
||||
if ($explosion) {
|
||||
$name = substr($name, 0, -1);
|
||||
$pattern = ".*";
|
||||
if ($pattern === self::RE_UNRESERVED) {
|
||||
$pattern = '[0-9a-zA-Z\-._\~%,]*'; // Unreserved + ","
|
||||
}
|
||||
$this->explosions[$name] = $explodeDelimiter;
|
||||
}
|
||||
|
||||
$names = explode(",", $name);
|
||||
$names = explode(',', $name);
|
||||
$results = [];
|
||||
foreach ($names as $name) {
|
||||
$results[] = "(?<{$name}>{$pattern})";
|
||||
|
|
|
|||
|
|
@ -4,32 +4,37 @@ 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;
|
||||
use WellRESTed\Routing\Route\RouteFactoryInterface;
|
||||
use WellRESTed\Routing\Route\RouteInterface;
|
||||
|
||||
class Router implements RouterInterface
|
||||
class Router implements MiddlewareInterface
|
||||
{
|
||||
/** @var string ServerRequestInterface attribute name for matched path variables */
|
||||
/** @var string|null Attribute name for matched path variables */
|
||||
private $pathVariablesAttributeName;
|
||||
/** @var DispatcherInterface */
|
||||
private $dispatcher;
|
||||
/** @var RouteFactoryInterface */
|
||||
/** @var RouteFactory */
|
||||
private $factory;
|
||||
/** @var RouteInterface[] Array of Route objects */
|
||||
/** @var Route[] Array of Route objects */
|
||||
private $routes;
|
||||
/** @var RouteInterface[] Hash array mapping exact paths to routes */
|
||||
/** @var Route[] Hash array mapping exact paths to routes */
|
||||
private $staticRoutes;
|
||||
/** @var RouteInterface[] Hash array mapping path prefixes to routes */
|
||||
/** @var Route[] Hash array mapping path prefixes to routes */
|
||||
private $prefixRoutes;
|
||||
/** @var RouteInterface[] Hash array mapping path prefixes to routes */
|
||||
/** @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 containg path variables matches, the path
|
||||
* By default, when a route containing path variables matches, the path
|
||||
* variables are stored individually as attributes on the
|
||||
* ServerRequestInterface.
|
||||
*
|
||||
|
|
@ -37,40 +42,57 @@ class Router implements RouterInterface
|
|||
* stored with the name. The value will be an array containing all of the
|
||||
* path variables.
|
||||
*
|
||||
* @param DispatcherInterface $dispatcher Instance to use for dispatching
|
||||
* middleware.
|
||||
* @param string|null $pathVariablesAttributeName Attribute name for
|
||||
* matched path variables. A null value sets attributes directly.
|
||||
* 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(DispatcherInterface $dispatcher = null, $pathVariablesAttributeName = null)
|
||||
{
|
||||
$this->dispatcher = $dispatcher;
|
||||
public function __construct(
|
||||
?string $pathVariablesAttributeName = null,
|
||||
?DispatcherInterface $dispatcher = null,
|
||||
?RouteFactory $routeFactory = null
|
||||
) {
|
||||
$this->pathVariablesAttributeName = $pathVariablesAttributeName;
|
||||
$this->factory = $this->getRouteFactory($this->dispatcher);
|
||||
$this->dispatcher = $dispatcher ?? new Dispatcher();
|
||||
$this->factory = $routeFactory ?? new RouteFactory($this->dispatcher);
|
||||
$this->routes = [];
|
||||
$this->staticRoutes = [];
|
||||
$this->prefixRoutes = [];
|
||||
$this->patternRoutes = [];
|
||||
$this->stack = [];
|
||||
}
|
||||
|
||||
public function __invoke(ServerRequestInterface $request, ResponseInterface $response, $next)
|
||||
{
|
||||
// Use only the path for routing.
|
||||
$requestTarget = parse_url($request->getRequestTarget(), PHP_URL_PATH);
|
||||
/**
|
||||
* @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($requestTarget);
|
||||
$route = $this->getStaticRoute($path);
|
||||
if ($route) {
|
||||
return $route($request, $response, $next);
|
||||
return $this->dispatch($route, $request, $response, $next);
|
||||
}
|
||||
|
||||
$route = $this->getPrefixRoute($requestTarget);
|
||||
$route = $this->getPrefixRoute($path);
|
||||
if ($route) {
|
||||
return $route($request, $response, $next);
|
||||
return $this->dispatch($route, $request, $response, $next);
|
||||
}
|
||||
|
||||
// Try each of the routes.
|
||||
foreach ($this->patternRoutes as $route) {
|
||||
if ($route->matchesRequestTarget($requestTarget)) {
|
||||
if ($route->matchesRequestTarget($path)) {
|
||||
$pathVariables = $route->getPathVariables();
|
||||
if ($this->pathVariablesAttributeName) {
|
||||
$request = $request->withAttribute($this->pathVariablesAttributeName, $pathVariables);
|
||||
|
|
@ -79,66 +101,122 @@ class Router implements RouterInterface
|
|||
$request = $request->withAttribute($name, $value);
|
||||
}
|
||||
}
|
||||
return $route($request, $response, $next);
|
||||
return $this->dispatch($route, $request, $response, $next);
|
||||
}
|
||||
}
|
||||
|
||||
// If no route exists, set the status code of the response to 404 and
|
||||
// return the response without propagating.
|
||||
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 middleware with the router for a given path and method.
|
||||
* 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.
|
||||
* @see MethodMapInterface::register
|
||||
*
|
||||
* $target may be:
|
||||
* - An exact path (e.g., "/path/")
|
||||
* - An prefix path ending with "*"" ("/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 maching the signature of MiddlewareInteraface::dispatch
|
||||
* - A callable matching the signature of MiddlewareInterface::dispatch
|
||||
* @see DispatchedInterface::dispatch
|
||||
*
|
||||
* @param string $target Request target or pattern to match
|
||||
* @param string $method HTTP method(s) to match
|
||||
* @param mixed $middleware Middleware to dispatch
|
||||
* @return self
|
||||
* @param mixed $middleware Middleware to dispatch in sequence
|
||||
* @return static
|
||||
*/
|
||||
public function register($method, $target, $middleware)
|
||||
public function add($middleware): Router
|
||||
{
|
||||
$route = $this->getRouteForTarget($target);
|
||||
$route->getMethodMap()->register($method, $middleware);
|
||||
$this->stack[] = $middleware;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param DispatcherInterface
|
||||
* @return RouteFactoryInterface
|
||||
* Configure the instance to delegate to the next middleware when no route
|
||||
* matches.
|
||||
*
|
||||
* @return static
|
||||
*/
|
||||
protected function getRouteFactory($dispatcher)
|
||||
public function continueOnNotFound(): Router
|
||||
{
|
||||
return new RouteFactory($dispatcher);
|
||||
$this->continueOnNotFound = true;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the route for a given target.
|
||||
*
|
||||
* @param $target
|
||||
* @return RouteInterface
|
||||
*/
|
||||
private function getRouteForTarget($target)
|
||||
private function getRouteForTarget(string $target): Route
|
||||
{
|
||||
if (isset($this->routes[$target])) {
|
||||
$route = $this->routes[$target];
|
||||
|
|
@ -149,26 +227,26 @@ class Router implements RouterInterface
|
|||
return $route;
|
||||
}
|
||||
|
||||
private function registerRouteForTarget($route, $target)
|
||||
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 RouteInterface::TYPE_STATIC:
|
||||
case Route::TYPE_STATIC:
|
||||
$this->staticRoutes[$route->getTarget()] = $route;
|
||||
break;
|
||||
case RouteInterface::TYPE_PREFIX:
|
||||
$this->prefixRoutes[rtrim($route->getTarget(), "*")] = $route;
|
||||
case Route::TYPE_PREFIX:
|
||||
$this->prefixRoutes[rtrim($route->getTarget(), '*')] = $route;
|
||||
break;
|
||||
case RouteInterface::TYPE_PATTERN:
|
||||
case Route::TYPE_PATTERN:
|
||||
$this->patternRoutes[] = $route;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private function getStaticRoute($requestTarget)
|
||||
private function getStaticRoute(string $requestTarget): ?Route
|
||||
{
|
||||
if (isset($this->staticRoutes[$requestTarget])) {
|
||||
return $this->staticRoutes[$requestTarget];
|
||||
|
|
@ -176,28 +254,37 @@ class Router implements RouterInterface
|
|||
return null;
|
||||
}
|
||||
|
||||
private function getPrefixRoute($requestTarget)
|
||||
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 (strrpos($requestTarget, $prefix, -strlen($requestTarget)) !== false);
|
||||
return $this->startsWith($requestTarget, $prefix);
|
||||
}
|
||||
);
|
||||
|
||||
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) {
|
||||
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);
|
||||
}
|
||||
$route = $this->prefixRoutes[$matches[0]];
|
||||
return $route;
|
||||
|
||||
$bestMatch = array_values($matches)[0];
|
||||
return $this->prefixRoutes[$bestMatch];
|
||||
}
|
||||
return null;
|
||||
|
||||
private function startsWith(string $haystack, string $needle): bool
|
||||
{
|
||||
$length = strlen($needle);
|
||||
return substr($haystack, 0, $length) === $needle;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,56 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace WellRESTed\Routing;
|
||||
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use WellRESTed\MiddlewareInterface;
|
||||
|
||||
/**
|
||||
* Maps HTTP methods and paths to middleware
|
||||
*/
|
||||
interface RouterInterface extends MiddlewareInterface
|
||||
{
|
||||
/**
|
||||
* Evaluate $request's path and method and dispatches matching middleware.
|
||||
*
|
||||
* Implementations MUST pass $request, $response, and $next to the matching
|
||||
* middleware.
|
||||
*
|
||||
* @param ServerRequestInterface $request
|
||||
* @param ResponseInterface $response
|
||||
* @param callable $next
|
||||
* @return ResponseInterface
|
||||
*/
|
||||
public function __invoke(ServerRequestInterface $request, ResponseInterface $response, $next);
|
||||
|
||||
/**
|
||||
* Register 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
|
||||
* @see MethodMapInterface::register
|
||||
*
|
||||
* $target may be:
|
||||
* - An exact path (e.g., "/path/")
|
||||
* - A prefix path ending with "*"" ("/path/*"")
|
||||
* - A URI template with one or more variables ("/path/{id}")
|
||||
* - A regular expression ("~/cat/([0-9]+)~")
|
||||
*
|
||||
* $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 maching the signature of MiddlewareInteraface::dispatch
|
||||
* @see DispatchedInterface::dispatch
|
||||
*
|
||||
* @param string $target Request target or pattern to match
|
||||
* @param string $method HTTP method(s) to match
|
||||
* @param mixed $middleware Middleware to dispatch
|
||||
* @return self
|
||||
*/
|
||||
public function register($method, $target, $middleware);
|
||||
}
|
||||
231
src/Server.php
231
src/Server.php
|
|
@ -7,194 +7,165 @@ use Psr\Http\Message\ServerRequestInterface;
|
|||
use WellRESTed\Dispatching\Dispatcher;
|
||||
use WellRESTed\Dispatching\DispatcherInterface;
|
||||
use WellRESTed\Message\Response;
|
||||
use WellRESTed\Message\ServerRequest;
|
||||
use WellRESTed\Message\ServerRequestMarshaller;
|
||||
use WellRESTed\Routing\Router;
|
||||
use WellRESTed\Transmission\Transmitter;
|
||||
use WellRESTed\Transmission\TransmitterInterface;
|
||||
|
||||
class Server
|
||||
{
|
||||
/** @var array */
|
||||
private $attributes;
|
||||
|
||||
/** @var mixed[] */
|
||||
private $attributes = [];
|
||||
/** @var DispatcherInterface */
|
||||
private $dispatcher;
|
||||
|
||||
/** @var string ServerRequestInterface attribute name for matched path variables */
|
||||
private $pathVariablesAttributeName;
|
||||
|
||||
/** @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;
|
||||
|
||||
/**
|
||||
* Create a new server.
|
||||
*
|
||||
* By default, when a route containg 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.
|
||||
*
|
||||
* @param array $attributes key-value pairs to register as attributes
|
||||
* with the server request.
|
||||
* @param DispatcherInterface $dispatcher Dispatches middleware. If no
|
||||
* object is passed, the Server will create a
|
||||
* WellRESTed\Dispatching\Dispatcher
|
||||
* @param string|null $pathVariablesAttributeName Attribute name for
|
||||
* matched path variables. A null value sets attributes directly.
|
||||
*/
|
||||
public function __construct(
|
||||
array $attributes = null,
|
||||
DispatcherInterface $dispatcher = null,
|
||||
$pathVariablesAttributeName = null
|
||||
) {
|
||||
if ($attributes === null) {
|
||||
$attributes = [];
|
||||
}
|
||||
$this->attributes = $attributes;
|
||||
if ($dispatcher === null) {
|
||||
$dispatcher = $this->getDispatcher();
|
||||
}
|
||||
$this->dispatcher = $dispatcher;
|
||||
$this->pathVariablesAttributeName = $pathVariablesAttributeName;
|
||||
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 self
|
||||
* @return Server
|
||||
*/
|
||||
public function add($middleware)
|
||||
public function add($middleware): Server
|
||||
{
|
||||
$this->stack[] = $middleware;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatch the contained middleware in the order in which they were added.
|
||||
*
|
||||
* The first middleware added to the stack is the first to be dispatched.
|
||||
*
|
||||
* Each middleware, when dispatched, will receive a $next callable that
|
||||
* dispatches the middleware that follows it. The only exception to this is
|
||||
* the last middleware in the stack which much receive a $next callable the
|
||||
* returns the response unchanged.
|
||||
*
|
||||
* If the instance is dispatched with no middleware added, the instance
|
||||
* MUST call $next passing $request and $response and return the returned
|
||||
* response.
|
||||
*
|
||||
* @param ServerRequestInterface $request
|
||||
* @param ResponseInterface $response
|
||||
* @param callable $next
|
||||
* @return ResponseInterface
|
||||
*/
|
||||
public function dispatch(ServerRequestInterface $request, ResponseInterface $response, $next)
|
||||
{
|
||||
return $this->dispatcher->dispatch($this->stack, $request, $response, $next);
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Return a new Router that uses the server's dispatcher.
|
||||
* Return a new Router that uses the server's configuration.
|
||||
*
|
||||
* @return Router
|
||||
*/
|
||||
public function createRouter()
|
||||
public function createRouter(): Router
|
||||
{
|
||||
return new Router($this->dispatcher, $this->pathVariablesAttributeName);
|
||||
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.
|
||||
*
|
||||
* @param ServerRequestInterface $request Request provided by the client
|
||||
* @param ResponseInterface $response Initial starting place response to
|
||||
* propogate to middleware.
|
||||
* @param TransmitterInterface $transmitter Instance to outputing the
|
||||
* final response to the client.
|
||||
* server's stack of middleware, and outputs the response via a Transmitter.
|
||||
*/
|
||||
public function respond(
|
||||
ServerRequestInterface $request = null,
|
||||
ResponseInterface $response = null,
|
||||
TransmitterInterface $transmitter = null
|
||||
) {
|
||||
if ($request === null) {
|
||||
public function respond(): void
|
||||
{
|
||||
$request = $this->getRequest();
|
||||
}
|
||||
foreach ($this->attributes as $name => $value) {
|
||||
$request = $request->withAttribute($name, $value);
|
||||
}
|
||||
if ($response === null) {
|
||||
$response = $this->getResponse();
|
||||
}
|
||||
if ($transmitter === null) {
|
||||
$transmitter = $this->getTransmitter();
|
||||
}
|
||||
|
||||
$next = function ($request, $response) {
|
||||
return $response;
|
||||
$next = function (
|
||||
ServerRequestInterface $rqst,
|
||||
ResponseInterface $resp
|
||||
): ResponseInterface {
|
||||
return $resp;
|
||||
};
|
||||
$response = $this->dispatch($request, $response, $next);
|
||||
$transmitter->transmit($request, $response);
|
||||
|
||||
$response = $this->response;
|
||||
|
||||
$response = $this->dispatcher->dispatch(
|
||||
$this->stack,
|
||||
$request,
|
||||
$response,
|
||||
$next
|
||||
);
|
||||
|
||||
$this->transmitter->transmit($request, $response);
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// The following method provide instances using default classes. To use
|
||||
// custom classes, subclass Server and override methods as needed.
|
||||
// -------------------------------------------------------------------------
|
||||
/* Configuration */
|
||||
|
||||
/**
|
||||
* Return an instance to dispatch middleware.
|
||||
*
|
||||
* @return DispatcherInterface
|
||||
* @param array $attributes
|
||||
* @return Server
|
||||
*/
|
||||
protected function getDispatcher()
|
||||
public function setAttributes(array $attributes): Server
|
||||
{
|
||||
return new Dispatcher();
|
||||
}
|
||||
|
||||
// @codeCoverageIgnoreStart
|
||||
|
||||
/**
|
||||
* Return an instance representing the request submitted to the server.
|
||||
*
|
||||
* @return ServerRequestInterface
|
||||
*/
|
||||
protected function getRequest()
|
||||
{
|
||||
return ServerRequest::getServerRequest();
|
||||
$this->attributes = $attributes;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return an instance that will output the response to the client.
|
||||
*
|
||||
* @return TransmitterInterface
|
||||
* @param DispatcherInterface $dispatcher
|
||||
* @return Server
|
||||
*/
|
||||
protected function getTransmitter()
|
||||
public function setDispatcher(DispatcherInterface $dispatcher): Server
|
||||
{
|
||||
return new Transmitter();
|
||||
$this->dispatcher = $dispatcher;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a "blank" response instance to populate.
|
||||
*
|
||||
* The response will be dispatched through the middleware and eventually
|
||||
* output to the client.
|
||||
*
|
||||
* @return ResponseInterface
|
||||
* @param string $name
|
||||
* @return Server
|
||||
*/
|
||||
protected function getResponse()
|
||||
public function setPathVariablesAttributeName(string $name): Server
|
||||
{
|
||||
return new Response();
|
||||
$this->pathVariablesAttributeName = $name;
|
||||
return $this;
|
||||
}
|
||||
|
||||
// @codeCoverageIgnoreEnd
|
||||
/**
|
||||
* @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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,21 +5,11 @@ namespace WellRESTed\Transmission;
|
|||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Psr\Http\Message\StreamInterface;
|
||||
use WellRESTed\Dispatching\Dispatcher;
|
||||
use WellRESTed\Dispatching\DispatcherInterface;
|
||||
|
||||
class Transmitter implements TransmitterInterface
|
||||
{
|
||||
/** @var int */
|
||||
private $chunkSize = 0;
|
||||
|
||||
public function __construct(DispatcherInterface $dispatcher = null)
|
||||
{
|
||||
if ($dispatcher === null) {
|
||||
$dispatcher = new Dispatcher();
|
||||
}
|
||||
$this->dispatcher = $dispatcher;
|
||||
}
|
||||
private $chunkSize = 8192;
|
||||
|
||||
/**
|
||||
* Outputs a response to the client.
|
||||
|
|
@ -28,14 +18,16 @@ class Transmitter implements TransmitterInterface
|
|||
*
|
||||
* This method will also provide a Content-length header if:
|
||||
* - Response does not have a Content-length header
|
||||
* - Response does not have a Tranfser-encoding: chunked 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)
|
||||
{
|
||||
public function transmit(
|
||||
ServerRequestInterface $request,
|
||||
ResponseInterface $response
|
||||
): void {
|
||||
// Prepare the response for output.
|
||||
$response = $this->prepareResponse($request, $response);
|
||||
|
||||
|
|
@ -56,34 +48,35 @@ class Transmitter implements TransmitterInterface
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $chunkSize
|
||||
*/
|
||||
public function setChunkSize($chunkSize)
|
||||
public function setChunkSize(int $chunkSize): void
|
||||
{
|
||||
$this->chunkSize = $chunkSize;
|
||||
}
|
||||
|
||||
protected function prepareResponse(ServerRequestInterface $request, ResponseInterface $response)
|
||||
{
|
||||
// Add a Content-length header to the response when all of these are true:
|
||||
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 Tranfser-encoding: chunked header
|
||||
// - Response does not have a Transfer-encoding: chunked header
|
||||
// - Response body stream is readable and reports a non-null size
|
||||
//
|
||||
if (!$response->hasHeader("Content-length")
|
||||
&& !(strtolower($response->getHeaderLine("Transfer-encoding")) === "chunked")
|
||||
) {
|
||||
$contentLengthMissing = !$response->hasHeader('Content-length');
|
||||
$notChunked = strtolower($response->getHeaderLine('Transfer-encoding'))
|
||||
!== 'chunked';
|
||||
$size = $response->getBody()->getSize();
|
||||
if ($size !== null) {
|
||||
$response = $response->withHeader("Content-length", (string) $size);
|
||||
}
|
||||
|
||||
if ($contentLengthMissing && $notChunked && $size !== null) {
|
||||
$response = $response->withHeader('Content-length', (string) $size);
|
||||
}
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
private function getStatusLine(ResponseInterface $response)
|
||||
private function getStatusLine(ResponseInterface $response): string
|
||||
{
|
||||
$protocol = $response->getProtocolVersion();
|
||||
$statusCode = $response->getStatusCode();
|
||||
|
|
@ -95,10 +88,12 @@ class Transmitter implements TransmitterInterface
|
|||
}
|
||||
}
|
||||
|
||||
private function outputBody(StreamInterface $body)
|
||||
private function outputBody(StreamInterface $body): void
|
||||
{
|
||||
if ($this->chunkSize > 0) {
|
||||
if ($body->isSeekable()) {
|
||||
$body->rewind();
|
||||
}
|
||||
while (!$body->eof()) {
|
||||
print $body->read($this->chunkSize);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,5 +18,5 @@ interface TransmitterInterface
|
|||
* @param ServerRequestInterface $request
|
||||
* @param ResponseInterface $response Response to output
|
||||
*/
|
||||
public function transmit(ServerRequestInterface $request, ResponseInterface $response);
|
||||
public function transmit(ServerRequestInterface $request, ResponseInterface $response): void;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +0,0 @@
|
|||
<?php
|
||||
|
||||
error_reporting(E_ALL);
|
||||
|
||||
require_once __DIR__ . "/../vendor/autoload.php";
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace WellRESTed\Transmission;
|
||||
|
||||
class HeaderStack
|
||||
{
|
||||
private static $headers;
|
||||
|
||||
public static function reset()
|
||||
{
|
||||
self::$headers = [];
|
||||
}
|
||||
|
||||
public static function push($header)
|
||||
{
|
||||
self::$headers[] = $header;
|
||||
}
|
||||
|
||||
public static function getHeaders()
|
||||
{
|
||||
return self::$headers;
|
||||
}
|
||||
}
|
||||
|
||||
function header($string, $dummy = true)
|
||||
{
|
||||
HeaderStack::push($string);
|
||||
}
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace WellRESTed\Message;
|
||||
|
||||
class UploadedFileState
|
||||
{
|
||||
public static $php_sapi_name;
|
||||
public static $is_uploaded_file;
|
||||
}
|
||||
|
||||
function php_sapi_name()
|
||||
{
|
||||
return UploadedFileState::$php_sapi_name;
|
||||
}
|
||||
|
||||
function move_uploaded_file($source, $target)
|
||||
{
|
||||
return rename($source, $target);
|
||||
}
|
||||
|
||||
function is_uploaded_file($file)
|
||||
{
|
||||
return UploadedFileState::$is_uploaded_file;
|
||||
}
|
||||
|
|
@ -1,202 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace WellRESTed\Test\Integration;
|
||||
|
||||
use Prophecy\Argument;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use WellRESTed\Message\Response;
|
||||
use WellRESTed\Message\ServerRequest;
|
||||
use WellRESTed\Message\Stream;
|
||||
use WellRESTed\MiddlewareInterface;
|
||||
use WellRESTed\Server;
|
||||
use WellRESTed\Transmission\TransmitterInterface;
|
||||
|
||||
/**
|
||||
* @coversNothing
|
||||
*/
|
||||
class ServerTest extends \PHPUnit_Framework_TestCase
|
||||
{
|
||||
public function testDispatchesMiddleware()
|
||||
{
|
||||
$server = new Server();
|
||||
$server->add(function ($rqst, $resp, $next) {
|
||||
$resp = $resp->withStatus(200)
|
||||
->withBody(new Stream("Hello, world!"));
|
||||
return $next($rqst, $resp);
|
||||
});
|
||||
|
||||
$request = new ServerRequest();
|
||||
$response = new Response();
|
||||
$transmitter = new CallableTransmitter(function ($request, $response) {
|
||||
$this->assertEquals("Hello, world!", (string) $response->getBody());
|
||||
});
|
||||
$server->respond($request, $response, $transmitter);
|
||||
}
|
||||
|
||||
public function testDispatchesMiddlewareChain()
|
||||
{
|
||||
$server = new Server();
|
||||
$server->add(function ($rqst, $resp, $next) {
|
||||
return $next($rqst, $resp);
|
||||
});
|
||||
$server->add(function ($rqst, $resp, $next) {
|
||||
$resp = $resp->withStatus(200)
|
||||
->withBody(new Stream("Hello, world!"));
|
||||
return $next($rqst, $resp);
|
||||
});
|
||||
|
||||
$request = new ServerRequest();
|
||||
$response = new Response();
|
||||
$transmitter = new CallableTransmitter(function ($request, $response) {
|
||||
$this->assertEquals("Hello, world!", (string) $response->getBody());
|
||||
});
|
||||
$server->respond($request, $response, $transmitter);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider routeProvider
|
||||
*/
|
||||
public function testDispatchesAssortedMiddlewareTypesByPath($requestTarget, $expectedBody)
|
||||
{
|
||||
$stringMiddlewareWrapper = function ($string) {
|
||||
return new StringMiddleware($string);
|
||||
};
|
||||
|
||||
$server = new Server();
|
||||
$server->add(function ($rqst, $resp, $next) {
|
||||
return $next($rqst, $resp);
|
||||
});
|
||||
$server->add($server->createRouter()
|
||||
->register("GET", "/fry", [
|
||||
new StringMiddleware("Philip "),
|
||||
new StringMiddleware("J. "),
|
||||
new StringMiddleware("Fry")
|
||||
])
|
||||
->register("GET", "/leela", new StringMiddleware("Turanga Leela"))
|
||||
->register("GET", "/bender", __NAMESPACE__ . '\BenderMiddleware')
|
||||
->register("GET", "/professor", $stringMiddlewareWrapper("Professor Hubert J. Farnsworth"))
|
||||
->register("GET", "/amy", function ($request, $response, $next) {
|
||||
$message = "Amy Wong";
|
||||
$body = $response->getBody();
|
||||
if ($body->isWritable()) {
|
||||
$body->write($message);
|
||||
} else {
|
||||
$response = $response->withBody(new Stream($message));
|
||||
}
|
||||
return $next($request, $response);
|
||||
})
|
||||
->register("GET", "/hermes", [
|
||||
new StringMiddleware("Hermes "),
|
||||
new StringMiddleware("Conrad", false),
|
||||
new StringMiddleware(", CPA")
|
||||
])
|
||||
->register("GET", "/zoidberg", [
|
||||
function ($request, $response, $next) {
|
||||
// Prepend "Doctor " to the dispatched response on the return trip.
|
||||
$response = $next($request, $response);
|
||||
$message = "Doctor " . (string) $response->getBody();
|
||||
return $response->withBody(new Stream($message));
|
||||
},
|
||||
new StringMiddleware("John "),
|
||||
new StringMiddleware("Zoidberg")
|
||||
])
|
||||
);
|
||||
$server->add(function ($rqst, $resp, $next) {
|
||||
$resp = $resp->withStatus(200);
|
||||
return $next($rqst, $resp);
|
||||
});
|
||||
|
||||
$request = (new ServerRequest())->withRequestTarget($requestTarget);
|
||||
$response = new Response();
|
||||
|
||||
$transmitter = new CallableTransmitter(function ($request, $response) use ($expectedBody) {
|
||||
$this->assertEquals($expectedBody, (string) $response->getBody());
|
||||
});
|
||||
$server->respond($request, $response, $transmitter);
|
||||
}
|
||||
|
||||
public function routeProvider()
|
||||
{
|
||||
return [
|
||||
["/fry", "Philip J. Fry"],
|
||||
["/leela", "Turanga Leela"],
|
||||
["/bender", "Bender Bending Rodriguez"],
|
||||
["/professor", "Professor Hubert J. Farnsworth"],
|
||||
["/amy", "Amy Wong"],
|
||||
["/hermes", "Hermes Conrad"],
|
||||
["/zoidberg", "Doctor John Zoidberg"]
|
||||
];
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class CallableTransmitter implements TransmitterInterface
|
||||
{
|
||||
private $callable;
|
||||
|
||||
public function __construct($callable)
|
||||
{
|
||||
$this->callable = $callable;
|
||||
}
|
||||
|
||||
public function transmit(ServerRequestInterface $request, ResponseInterface $response)
|
||||
{
|
||||
$callable = $this->callable;
|
||||
$callable($request, $response);
|
||||
}
|
||||
}
|
||||
|
||||
class StringMiddleware implements MiddlewareInterface
|
||||
{
|
||||
private $string;
|
||||
private $propagate;
|
||||
|
||||
public function __construct($string, $propagate = true)
|
||||
{
|
||||
$this->string = $string;
|
||||
$this->propagate = $propagate;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ServerRequestInterface $request
|
||||
* @param ResponseInterface $response
|
||||
* @param callable $next
|
||||
* @return ResponseInterface
|
||||
*/
|
||||
public function __invoke(ServerRequestInterface $request, ResponseInterface $response, $next)
|
||||
{
|
||||
$body = $response->getBody();
|
||||
if ($body->isWritable()) {
|
||||
$body->write($this->string);
|
||||
} else {
|
||||
$response = $response->withBody(new Stream($this->string));
|
||||
}
|
||||
if ($this->propagate) {
|
||||
return $next($request, $response);
|
||||
} else {
|
||||
return $response;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class BenderMiddleware implements MiddlewareInterface
|
||||
{
|
||||
/**
|
||||
* @param ServerRequestInterface $request
|
||||
* @param ResponseInterface $response
|
||||
* @param callable $next
|
||||
* @return ResponseInterface
|
||||
*/
|
||||
public function __invoke(ServerRequestInterface $request, ResponseInterface $response, $next)
|
||||
{
|
||||
$message = "Bender Bending Rodriguez";
|
||||
$body = $response->getBody();
|
||||
if ($body->isWritable()) {
|
||||
$body->write($message);
|
||||
} else {
|
||||
$response = $response->withBody(new Stream($message));
|
||||
}
|
||||
return $next($request, $response);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,144 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace WellRESTed\Test\Unit\Dispatching;
|
||||
|
||||
use Prophecy\Argument;
|
||||
use WellRESTed\Dispatching\DispatchStack;
|
||||
|
||||
/**
|
||||
* @coversDefaultClass WellRESTed\Dispatching\DispatchStack
|
||||
* @uses WellRESTed\Dispatching\DispatchStack
|
||||
* @group dispatching
|
||||
*/
|
||||
class DispatchStackTest extends \PHPUnit_Framework_TestCase
|
||||
{
|
||||
private $request;
|
||||
private $response;
|
||||
private $next;
|
||||
private $dispatcher;
|
||||
|
||||
public function setUp()
|
||||
{
|
||||
parent::setUp();
|
||||
$this->request = $this->prophesize('Psr\Http\Message\ServerRequestInterface');
|
||||
$this->response = $this->prophesize('Psr\Http\Message\ResponseInterface');
|
||||
$this->next = function ($request, $response) {
|
||||
return $response;
|
||||
};
|
||||
$this->dispatcher = $this->prophesize('WellRESTed\Dispatching\DispatcherInterface');
|
||||
$this->dispatcher->dispatch(Argument::cetera())->will(function ($args) {
|
||||
list($middleware, $request, $response, $next) = $args;
|
||||
return $middleware($request, $response, $next);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::__construct
|
||||
*/
|
||||
public function testCreatesInstance()
|
||||
{
|
||||
$stack = new DispatchStack($this->dispatcher->reveal());
|
||||
$this->assertNotNull($stack);
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::add
|
||||
*/
|
||||
public function testAddIsFluid()
|
||||
{
|
||||
$stack = new DispatchStack($this->dispatcher->reveal());
|
||||
$this->assertSame($stack, $stack->add("middleware1"));
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::__invoke
|
||||
*/
|
||||
public function testDispachesMiddlewareInOrderAdded()
|
||||
{
|
||||
// Each middelware will add its "name" to this array.
|
||||
$callOrder = [];
|
||||
|
||||
$stack = new DispatchStack($this->dispatcher->reveal());
|
||||
$stack->add(function ($request, $response, $next) use (&$callOrder) {
|
||||
$callOrder[] = "first";
|
||||
return $next($request, $response);
|
||||
});
|
||||
$stack->add(function ($request, $response, $next) use (&$callOrder) {
|
||||
$callOrder[] = "second";
|
||||
return $next($request, $response);
|
||||
});
|
||||
$stack->add(function ($request, $response, $next) use (&$callOrder) {
|
||||
$callOrder[] = "third";
|
||||
return $next($request, $response);
|
||||
});
|
||||
$stack($this->request->reveal(), $this->response->reveal(), $this->next);
|
||||
$this->assertEquals(["first", "second", "third"], $callOrder);
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::__invoke
|
||||
*/
|
||||
public function testCallsNextAfterDispatchingEmptyStack()
|
||||
{
|
||||
$nextCalled = false;
|
||||
$next = function ($request, $response) use (&$nextCalled) {
|
||||
$nextCalled = true;
|
||||
return $response;
|
||||
};
|
||||
|
||||
$stack = new DispatchStack($this->dispatcher->reveal());
|
||||
$stack($this->request->reveal(), $this->response->reveal(), $next);
|
||||
$this->assertTrue($nextCalled);
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::__invoke
|
||||
*/
|
||||
public function testCallsNextAfterDispatchingStack()
|
||||
{
|
||||
$nextCalled = false;
|
||||
$next = function ($request, $response) use (&$nextCalled) {
|
||||
$nextCalled = true;
|
||||
return $response;
|
||||
};
|
||||
|
||||
$middleware = function ($request, $response, $next) use (&$callOrder) {
|
||||
return $next($request, $response);
|
||||
};
|
||||
|
||||
$stack = new DispatchStack($this->dispatcher->reveal());
|
||||
$stack->add($middleware);
|
||||
$stack->add($middleware);
|
||||
$stack->add($middleware);
|
||||
|
||||
$stack($this->request->reveal(), $this->response->reveal(), $next);
|
||||
$this->assertTrue($nextCalled);
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::__invoke
|
||||
*/
|
||||
public function testDoesNotCallNextWhenStackStopsEarly()
|
||||
{
|
||||
$nextCalled = false;
|
||||
$next = function ($request, $response) use (&$nextCalled) {
|
||||
$nextCalled = true;
|
||||
return $response;
|
||||
};
|
||||
|
||||
$middlewareGo = function ($request, $response, $next) use (&$callOrder) {
|
||||
return $next($request, $response);
|
||||
};
|
||||
$middlewareStop = function ($request, $response, $next) use (&$callOrder) {
|
||||
return $response;
|
||||
};
|
||||
|
||||
$stack = new DispatchStack($this->dispatcher->reveal());
|
||||
$stack->add($middlewareGo);
|
||||
$stack->add($middlewareStop);
|
||||
$stack->add($middlewareStop);
|
||||
|
||||
$stack($this->request->reveal(), $this->response->reveal(), $next);
|
||||
$this->assertFalse($nextCalled);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,107 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace WellRESTed\Test\Unit\Dispatching;
|
||||
|
||||
use Prophecy\Argument;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use WellRESTed\Dispatching\Dispatcher;
|
||||
use WellRESTed\MiddlewareInterface;
|
||||
|
||||
/**
|
||||
* @covers WellRESTed\Dispatching\Dispatcher
|
||||
* @group dispatching
|
||||
*/
|
||||
class DispatcherTest extends \PHPUnit_Framework_TestCase
|
||||
{
|
||||
private $request;
|
||||
private $response;
|
||||
private $next;
|
||||
|
||||
public function setUp()
|
||||
{
|
||||
$this->request = $this->prophesize('Psr\Http\Message\ServerRequestInterface');
|
||||
$this->response = $this->prophesize('Psr\Http\Message\ResponseInterface');
|
||||
$this->response->withStatus(Argument::any())->will(
|
||||
function ($args) {
|
||||
$this->getStatusCode()->willReturn($args[0]);
|
||||
return $this;
|
||||
}
|
||||
);
|
||||
$this->next = function ($request, $response) {
|
||||
return $response;
|
||||
};
|
||||
}
|
||||
|
||||
public function testDispatchesCallableThatReturnsResponse()
|
||||
{
|
||||
$middleware = function ($request, $response, $next) {
|
||||
return $next($request, $response->withStatus(200));
|
||||
};
|
||||
|
||||
$dispatcher = new Dispatcher();
|
||||
$response = $dispatcher->dispatch($middleware, $this->request->reveal(), $this->response->reveal(), $this->next);
|
||||
$this->assertEquals(200, $response->getStatusCode());
|
||||
}
|
||||
|
||||
public function testDispatchesMiddlewareInstanceFromCallable()
|
||||
{
|
||||
$middleware = function () {
|
||||
return new DispatcherTest_Middleware();
|
||||
};
|
||||
|
||||
$dispatcher = new Dispatcher();
|
||||
$response = $dispatcher->dispatch($middleware, $this->request->reveal(), $this->response->reveal(), $this->next);
|
||||
$this->assertEquals(200, $response->getStatusCode());
|
||||
}
|
||||
|
||||
public function testDispatchesMiddlewareFromClassNameString()
|
||||
{
|
||||
$middleware = __NAMESPACE__ . '\DispatcherTest_Middleware';
|
||||
|
||||
$dispatcher = new Dispatcher();
|
||||
$response = $dispatcher->dispatch($middleware, $this->request->reveal(), $this->response->reveal(), $this->next);
|
||||
$this->assertEquals(200, $response->getStatusCode());
|
||||
}
|
||||
|
||||
public function testDispatchesMiddlewareInstance()
|
||||
{
|
||||
$middleware = new DispatcherTest_Middleware();
|
||||
|
||||
$dispatcher = new Dispatcher();
|
||||
$response = $dispatcher->dispatch($middleware, $this->request->reveal(), $this->response->reveal(), $this->next);
|
||||
$this->assertEquals(200, $response->getStatusCode());
|
||||
}
|
||||
|
||||
/**
|
||||
* @uses WellRESTed\Dispatching\DispatchStack
|
||||
*/
|
||||
public function testDispatchesArrayAsDispatchStack()
|
||||
{
|
||||
$middleware = new DispatcherTest_Middleware();
|
||||
|
||||
$dispatcher = new Dispatcher();
|
||||
$response = $dispatcher->dispatch([$middleware], $this->request->reveal(), $this->response->reveal(), $this->next);
|
||||
$this->assertEquals(200, $response->getStatusCode());
|
||||
}
|
||||
|
||||
/**
|
||||
* @expectedException \WellRESTed\Dispatching\DispatchException
|
||||
*/
|
||||
public function testThrowsExceptionWhenUnableToDispatch()
|
||||
{
|
||||
$middleware = null;
|
||||
|
||||
$dispatcher = new Dispatcher();
|
||||
$dispatcher->dispatch($middleware, $this->request->reveal(), $this->response->reveal(), $this->next);
|
||||
}
|
||||
}
|
||||
|
||||
class DispatcherTest_Middleware implements MiddlewareInterface
|
||||
{
|
||||
public function __invoke(ServerRequestInterface $request, ResponseInterface $response, $next)
|
||||
{
|
||||
$response = $response->withStatus(200);
|
||||
return $next($request, $response);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,148 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace WellRESTed\Test\Message;
|
||||
|
||||
use WellRESTed\Message\HeaderCollection;
|
||||
|
||||
/**
|
||||
* @coversDefaultClass WellRESTed\Message\HeaderCollection
|
||||
* @uses WellRESTed\Message\HeaderCollection
|
||||
* @group message
|
||||
*/
|
||||
class HeaderCollectionTest extends \PHPUnit_Framework_TestCase
|
||||
{
|
||||
/**
|
||||
* @covers ::__construct
|
||||
*/
|
||||
public function testCreatesInstance()
|
||||
{
|
||||
$collection = new HeaderCollection();
|
||||
$this->assertNotNull($collection);
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::offsetSet
|
||||
* @covers ::offsetExists
|
||||
*/
|
||||
public function testAddsSingleHeaderAndIndicatesCaseInsensitiveIsset()
|
||||
{
|
||||
$collection = new HeaderCollection();
|
||||
$collection["Content-Type"] = "application/json";
|
||||
$this->assertTrue(isset($collection["content-type"]));
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::offsetSet
|
||||
* @covers ::offsetExists
|
||||
*/
|
||||
public function testAddsMultipleHeadersAndIndicatesCaseInsensitiveIsset()
|
||||
{
|
||||
$collection = new HeaderCollection();
|
||||
$collection["Set-Cookie"] = "cat=Molly";
|
||||
$collection["SET-COOKIE"] = "dog=Bear";
|
||||
$this->assertTrue(isset($collection["set-cookie"]));
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::offsetGet
|
||||
*/
|
||||
public function testReturnsHeadersWithCaseInsensitiveHeaderName()
|
||||
{
|
||||
$collection = new HeaderCollection();
|
||||
$collection["Set-Cookie"] = "cat=Molly";
|
||||
$collection["SET-COOKIE"] = "dog=Bear";
|
||||
|
||||
$headers = $collection["set-cookie"];
|
||||
$this->assertEquals(2, count(array_intersect($headers, ["cat=Molly", "dog=Bear"])));
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::offsetUnset
|
||||
*/
|
||||
public function testRemovesHeadersWithCaseInsensitiveHeaderName()
|
||||
{
|
||||
$collection = new HeaderCollection();
|
||||
$collection["Set-Cookie"] = "cat=Molly";
|
||||
$collection["SET-COOKIE"] = "dog=Bear";
|
||||
unset($collection["set-cookie"]);
|
||||
$this->assertFalse(isset($collection["set-cookie"]));
|
||||
}
|
||||
|
||||
/**
|
||||
* @coversNothing
|
||||
*/
|
||||
public function testCloneMakesDeepCopyOfHeaders()
|
||||
{
|
||||
$collection = new HeaderCollection();
|
||||
$collection["Set-Cookie"] = "cat=Molly";
|
||||
|
||||
$clone = clone $collection;
|
||||
unset($clone["Set-Cookie"]);
|
||||
|
||||
$this->assertTrue(isset($collection["set-cookie"]) && !isset($clone["set-cookie"]));
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::current
|
||||
* @covers ::next
|
||||
* @covers ::key
|
||||
* @covers ::valid
|
||||
* @covers ::rewind
|
||||
*/
|
||||
public function testIteratesWithOriginalKeys()
|
||||
{
|
||||
$collection = new HeaderCollection();
|
||||
$collection["Content-length"] = "100";
|
||||
$collection["Set-Cookie"] = "cat=Molly";
|
||||
$collection["Set-Cookie"] = "dog=Bear";
|
||||
$collection["Content-type"] = "application/json";
|
||||
unset($collection["Content-length"]);
|
||||
|
||||
$headers = [];
|
||||
|
||||
foreach ($collection as $key => $values) {
|
||||
$headers[] = $key;
|
||||
}
|
||||
|
||||
$expected = ["Content-type", "Set-Cookie"];
|
||||
|
||||
$countUnmatched = count(array_diff($expected, $headers)) + count(array_diff($headers, $expected));
|
||||
$this->assertEquals(0, $countUnmatched);
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::current
|
||||
* @covers ::next
|
||||
* @covers ::key
|
||||
* @covers ::valid
|
||||
* @covers ::rewind
|
||||
*/
|
||||
public function testIteratesWithOriginalKeysAndValues()
|
||||
{
|
||||
$collection = new HeaderCollection();
|
||||
$collection["Content-length"] = "100";
|
||||
$collection["Set-Cookie"] = "cat=Molly";
|
||||
$collection["Set-Cookie"] = "dog=Bear";
|
||||
$collection["Content-type"] = "application/json";
|
||||
unset($collection["Content-length"]);
|
||||
|
||||
$headers = [];
|
||||
|
||||
foreach ($collection as $key => $values) {
|
||||
foreach ($values as $value) {
|
||||
if (isset($headers[$key])) {
|
||||
$headers[$key][] = $value;
|
||||
} else {
|
||||
$headers[$key] = [$value];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$expected = [
|
||||
"Set-Cookie" => ["cat=Molly", "dog=Bear"],
|
||||
"Content-type" => ["application/json"]
|
||||
];
|
||||
|
||||
$this->assertEquals($expected, $headers);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,310 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace WellRESTed\Test\Unit\Message;
|
||||
|
||||
/**
|
||||
* @coversDefaultClass WellRESTed\Message\Message
|
||||
* @uses WellRESTed\Message\Message
|
||||
* @uses WellRESTed\Message\HeaderCollection
|
||||
* @group message
|
||||
*/
|
||||
class MessageTest extends \PHPUnit_Framework_TestCase
|
||||
{
|
||||
/**
|
||||
* @covers ::__construct
|
||||
*/
|
||||
public function testCreatesInstance()
|
||||
{
|
||||
$message = $this->getMockForAbstractClass('\WellRESTed\Message\Message');
|
||||
$this->assertNotNull($message);
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::__construct
|
||||
*/
|
||||
public function testSetsHeadersOnConstruction()
|
||||
{
|
||||
$headers = ["X-foo" => ["bar", "baz"]];
|
||||
$body = null;
|
||||
$message = $this->getMockForAbstractClass('\WellRESTed\Message\Message', [$headers, $body]);
|
||||
$this->assertEquals(["bar", "baz"], $message->getHeader("X-foo"));
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::__construct
|
||||
*/
|
||||
public function testSetsBodyOnConstruction()
|
||||
{
|
||||
$headers = null;
|
||||
$body = $this->prophesize('\Psr\Http\Message\StreamInterface');
|
||||
$message = $this->getMockForAbstractClass('\WellRESTed\Message\Message', [$headers, $body->reveal()]);
|
||||
$this->assertSame($body->reveal(), $message->getBody());
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::__clone
|
||||
*/
|
||||
public function testCloneMakesDeepCopyOfHeaders()
|
||||
{
|
||||
$message1 = $this->getMockForAbstractClass('\WellRESTed\Message\Message');
|
||||
$message1 = $message1->withHeader("Content-type", "text/plain");
|
||||
$message2 = $message1->withHeader("Content-type", "application/json");
|
||||
$this->assertNotEquals($message1->getHeader("Content-type"), $message2->getHeader("Content-type"));
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// Protocol Version
|
||||
|
||||
/**
|
||||
* @covers ::getProtocolVersion
|
||||
*/
|
||||
public function testGetProtocolVersionReturnsProtocolVersion1Point1ByDefault()
|
||||
{
|
||||
$message = $this->getMockForAbstractClass('\WellRESTed\Message\Message');
|
||||
$this->assertEquals("1.1", $message->getProtocolVersion());
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::getProtocolVersion
|
||||
*/
|
||||
public function testGetProtocolVersionReturnsProtocolVersion()
|
||||
{
|
||||
$message = $this->getMockForAbstractClass('\WellRESTed\Message\Message');
|
||||
$message = $message->withProtocolVersion("1.0");
|
||||
$this->assertEquals("1.0", $message->getProtocolVersion());
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::withProtocolVersion
|
||||
*/
|
||||
public function testGetProtocolVersionReplacesProtocolVersion()
|
||||
{
|
||||
$message = $this->getMockForAbstractClass('\WellRESTed\Message\Message');
|
||||
$message = $message->withProtocolVersion("1.0");
|
||||
$this->assertEquals("1.0", $message->getProtocolVersion());
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// Headers
|
||||
|
||||
/**
|
||||
* @covers ::withHeader
|
||||
* @covers ::getValidatedHeaders
|
||||
* @dataProvider validHeaderValueProvider
|
||||
*/
|
||||
public function testWithHeaderReplacesHeader($expected, $value)
|
||||
{
|
||||
$message = $this->getMockForAbstractClass('\WellRESTed\Message\Message');
|
||||
$message = $message->withHeader("X-foo", "Original value");
|
||||
$message = $message->withHeader("X-foo", $value);
|
||||
$this->assertEquals($expected, $message->getHeader("X-foo"));
|
||||
}
|
||||
|
||||
public function validHeaderValueProvider()
|
||||
{
|
||||
return [
|
||||
[["0"], 0],
|
||||
[["molly","bear"],["molly","bear"]]
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::withHeader
|
||||
* @covers ::getValidatedHeaders
|
||||
* @expectedException \InvalidArgumentException
|
||||
* @dataProvider invalidHeaderProvider
|
||||
*/
|
||||
public function testWithHeaderThrowsExceptionWithInvalidArgument($name, $value)
|
||||
{
|
||||
$message = $this->getMockForAbstractClass('\WellRESTed\Message\Message');
|
||||
$message->withHeader($name, $value);
|
||||
}
|
||||
|
||||
public function invalidHeaderProvider()
|
||||
{
|
||||
return [
|
||||
[0, 1024],
|
||||
["Content-length", false],
|
||||
["Content-length", [false]]
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::withAddedHeader
|
||||
*/
|
||||
public function testWithAddedHeaderSetsHeader()
|
||||
{
|
||||
$message = $this->getMockForAbstractClass('\WellRESTed\Message\Message');
|
||||
$message = $message->withAddedHeader("Content-type", "application/json");
|
||||
$this->assertEquals(["application/json"], $message->getHeader("Content-type"));
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::withAddedHeader
|
||||
*/
|
||||
public function testWithAddedHeaderAppendsValue()
|
||||
{
|
||||
$message = $this->getMockForAbstractClass('\WellRESTed\Message\Message');
|
||||
$message = $message->withAddedHeader("Set-Cookie", ["cat=Molly"]);
|
||||
$message = $message->withAddedHeader("Set-Cookie", ["dog=Bear"]);
|
||||
$cookies = $message->getHeader("Set-Cookie");
|
||||
$this->assertTrue(in_array("cat=Molly", $cookies) && in_array("dog=Bear", $cookies));
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::withoutHeader
|
||||
*/
|
||||
public function testWithoutHeaderRemovesHeader()
|
||||
{
|
||||
$message = $this->getMockForAbstractClass('\WellRESTed\Message\Message');
|
||||
$message = $message->withHeader("Content-type", "application/json");
|
||||
$message = $message->withoutHeader("Content-type");
|
||||
$this->assertFalse($message->hasHeader("Content-type"));
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::getHeader
|
||||
*/
|
||||
public function testGetHeaderReturnsEmptyArrayForUnsetHeader()
|
||||
{
|
||||
$message = $this->getMockForAbstractClass('\WellRESTed\Message\Message');
|
||||
$this->assertEquals([], $message->getHeader("X-name"));
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::getHeader
|
||||
*/
|
||||
public function testGetHeaderReturnsSingleHeader()
|
||||
{
|
||||
$message = $this->getMockForAbstractClass('\WellRESTed\Message\Message');
|
||||
$message = $message->withAddedHeader("Content-type", "application/json");
|
||||
$this->assertEquals(["application/json"], $message->getHeader("Content-type"));
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::getHeader
|
||||
*/
|
||||
public function testGetHeaderReturnsMultipleValuesForHeader()
|
||||
{
|
||||
$message = $this->getMockForAbstractClass('\WellRESTed\Message\Message');
|
||||
$message = $message->withAddedHeader("X-name", "cat=Molly");
|
||||
$message = $message->withAddedHeader("X-name", "dog=Bear");
|
||||
$this->assertEquals(["cat=Molly", "dog=Bear"], $message->getHeader("X-name"));
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::getHeaderLine
|
||||
*/
|
||||
public function testGetHeaderLineReturnsEmptyStringForUnsetHeader()
|
||||
{
|
||||
$message = $this->getMockForAbstractClass('\WellRESTed\Message\Message');
|
||||
$this->assertSame("", $message->getHeaderLine("X-not-set"));
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::getHeaderLine
|
||||
*/
|
||||
public function testGetHeaderLineReturnsMultipleHeadersJoinedByCommas()
|
||||
{
|
||||
$message = $this->getMockForAbstractClass('\WellRESTed\Message\Message');
|
||||
$message = $message->withAddedHeader("X-name", "cat=Molly");
|
||||
$message = $message->withAddedHeader("X-name", "dog=Bear");
|
||||
$this->assertEquals("cat=Molly, dog=Bear", $message->getHeaderLine("X-name"));
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::hasHeader
|
||||
*/
|
||||
public function testHasHeaderReturnsTrueWhenHeaderIsSet()
|
||||
{
|
||||
$message = $this->getMockForAbstractClass('\WellRESTed\Message\Message');
|
||||
$message = $message->withHeader("Content-type", "application/json");
|
||||
$this->assertTrue($message->hasHeader("Content-type"));
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::hasHeader
|
||||
*/
|
||||
public function testHasHeaderReturnsFalseWhenHeaderIsNotSet()
|
||||
{
|
||||
$message = $this->getMockForAbstractClass('\WellRESTed\Message\Message');
|
||||
$this->assertFalse($message->hasHeader("Content-type"));
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::getHeaders
|
||||
*/
|
||||
public function testGetHeadersReturnOriginalHeaderNamesAsKeys()
|
||||
{
|
||||
$message = $this->getMockForAbstractClass('\WellRESTed\Message\Message');
|
||||
$message = $message->withHeader("Set-Cookie", "cat=Molly");
|
||||
$message = $message->withAddedHeader("Set-Cookie", "dog=Bear");
|
||||
$message = $message->withHeader("Content-type", "application/json");
|
||||
|
||||
$headers = [];
|
||||
foreach ($message->getHeaders() as $key => $values) {
|
||||
$headers[] = $key;
|
||||
}
|
||||
|
||||
$expected = ["Content-type", "Set-Cookie"];
|
||||
$countUnmatched = count(array_diff($expected, $headers)) + count(array_diff($headers, $expected));
|
||||
$this->assertEquals(0, $countUnmatched);
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::getHeaders
|
||||
*/
|
||||
public function testGetHeadersReturnOriginalHeaderNamesAndValues()
|
||||
{
|
||||
$message = $this->getMockForAbstractClass('\WellRESTed\Message\Message');
|
||||
$message = $message->withHeader("Set-Cookie", "cat=Molly");
|
||||
$message = $message->withAddedHeader("Set-Cookie", "dog=Bear");
|
||||
$message = $message->withHeader("Content-type", "application/json");
|
||||
|
||||
$headers = [];
|
||||
|
||||
foreach ($message->getHeaders() as $key => $values) {
|
||||
foreach ($values as $value) {
|
||||
if (isset($headers[$key])) {
|
||||
$headers[$key][] = $value;
|
||||
} else {
|
||||
$headers[$key] = [$value];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$expected = [
|
||||
"Set-Cookie" => ["cat=Molly", "dog=Bear"],
|
||||
"Content-type" => ["application/json"]
|
||||
];
|
||||
|
||||
$this->assertEquals($expected, $headers);
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// Body
|
||||
|
||||
/**
|
||||
* @covers ::getBody
|
||||
* @uses WellRESTed\Message\NullStream
|
||||
*/
|
||||
public function testGetBodyReturnsEmptyStreamByDefault()
|
||||
{
|
||||
$message = $this->getMockForAbstractClass('\WellRESTed\Message\Message');
|
||||
$this->assertEquals("", (string) $message->getBody());
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::getBody
|
||||
* @covers ::withBody
|
||||
*/
|
||||
public function testGetBodyReturnsAttachedStream()
|
||||
{
|
||||
$stream = $this->prophesize('\Psr\Http\Message\StreamInterface');
|
||||
$stream = $stream->reveal();
|
||||
|
||||
$message = $this->getMockForAbstractClass('\WellRESTed\Message\Message');
|
||||
$message = $message->withBody($stream);
|
||||
$this->assertSame($stream, $message->getBody());
|
||||
}
|
||||
}
|
||||
|
|
@ -1,158 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace WellRESTed\Test\Message;
|
||||
|
||||
use WellRESTed\Message\NullStream;
|
||||
|
||||
/**
|
||||
* @coversDefaultClass WellRESTed\Message\NullStream
|
||||
* @uses WellRESTed\Message\NullStream
|
||||
* @group message
|
||||
*/
|
||||
class NullStreamTest extends \PHPUnit_Framework_TestCase
|
||||
{
|
||||
/**
|
||||
* @covers ::__toString()
|
||||
*/
|
||||
public function testCastsToString()
|
||||
{
|
||||
$stream = new NullStream();
|
||||
$this->assertEquals("", (string) $stream);
|
||||
}
|
||||
|
||||
public function testCloseDoesNothing()
|
||||
{
|
||||
$stream = new \WellRESTed\Message\NullStream();
|
||||
$stream->close();
|
||||
$this->assertTrue(true); // Asserting no exception occured.
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::detach()
|
||||
*/
|
||||
public function testDetachReturnsNull()
|
||||
{
|
||||
$stream = new \WellRESTed\Message\NullStream();
|
||||
$this->assertNull($stream->detach());
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::getSize
|
||||
*/
|
||||
public function testSizeReturnsZero()
|
||||
{
|
||||
$stream = new \WellRESTed\Message\NullStream();
|
||||
$this->assertEquals(0, $stream->getSize());
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::tell
|
||||
*/
|
||||
public function testTellReturnsZero()
|
||||
{
|
||||
$stream = new \WellRESTed\Message\NullStream();
|
||||
$this->assertEquals(0, $stream->tell());
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::eof
|
||||
*/
|
||||
public function testEofReturnsTrue()
|
||||
{
|
||||
$stream = new \WellRESTed\Message\NullStream();
|
||||
$this->assertTrue($stream->eof());
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::isSeekable
|
||||
*/
|
||||
public function testIsSeekableReturnsFalse()
|
||||
{
|
||||
$stream = new \WellRESTed\Message\NullStream();
|
||||
$this->assertFalse($stream->isSeekable());
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::seek
|
||||
* @expectedException \RuntimeException
|
||||
*/
|
||||
public function testSeekReturnsFalse()
|
||||
{
|
||||
$stream = new NullStream();
|
||||
$stream->seek(10);
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::rewind
|
||||
* @expectedException \RuntimeException
|
||||
*/
|
||||
public function testRewindThrowsException()
|
||||
{
|
||||
$stream = new \WellRESTed\Message\NullStream();
|
||||
$stream->rewind();
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::isWritable
|
||||
*/
|
||||
public function testIsWritableReturnsFalse()
|
||||
{
|
||||
$stream = new NullStream();
|
||||
$this->assertFalse($stream->isWritable());
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::write
|
||||
* @expectedException \RuntimeException
|
||||
*/
|
||||
public function testWriteThrowsException()
|
||||
{
|
||||
$stream = new \WellRESTed\Message\NullStream();
|
||||
$stream->write("");
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::isReadable
|
||||
*/
|
||||
public function testIsReadableReturnsTrue()
|
||||
{
|
||||
$stream = new \WellRESTed\Message\NullStream();
|
||||
$this->assertTrue($stream->isReadable());
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::read
|
||||
*/
|
||||
public function testReadReturnsEmptyString()
|
||||
{
|
||||
$stream = new \WellRESTed\Message\NullStream();
|
||||
$this->assertEquals("", $stream->read(100));
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::getContents
|
||||
*/
|
||||
public function testGetContentsReturnsEmptyString()
|
||||
{
|
||||
$stream = new NullStream();
|
||||
$this->assertEquals("", $stream->getContents());
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::getMetadata
|
||||
*/
|
||||
public function testGetMetadataReturnsNull()
|
||||
{
|
||||
$stream = new \WellRESTed\Message\NullStream();
|
||||
$this->assertNull($stream->getMetadata());
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::getMetadata
|
||||
*/
|
||||
public function testGetMetadataReturnsNullWithKey()
|
||||
{
|
||||
$stream = new \WellRESTed\Message\NullStream();
|
||||
$this->assertNull($stream->getMetadata("size"));
|
||||
}
|
||||
}
|
||||
|
|
@ -1,285 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace WellRESTed\Test\Unit\Message;
|
||||
|
||||
use WellRESTed\Message\Request;
|
||||
use WellRESTed\Message\Uri;
|
||||
|
||||
/**
|
||||
* @coversDefaultClass WellRESTed\Message\Request
|
||||
* @uses WellRESTed\Message\Request
|
||||
* @uses WellRESTed\Message\Request
|
||||
* @uses WellRESTed\Message\Message
|
||||
* @uses WellRESTed\Message\HeaderCollection
|
||||
* @uses WellRESTed\Message\Uri
|
||||
* @group message
|
||||
*/
|
||||
class RequestTest extends \PHPUnit_Framework_TestCase
|
||||
{
|
||||
// ------------------------------------------------------------------------
|
||||
// Construction
|
||||
|
||||
/**
|
||||
* @covers ::__construct
|
||||
*/
|
||||
public function testCreatesInstance()
|
||||
{
|
||||
$request = new Request();
|
||||
$this->assertNotNull($request);
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::__construct
|
||||
*/
|
||||
public function testCreatesInstanceWithUri()
|
||||
{
|
||||
$uri = $this->prophesize('Psr\Http\Message\UriInterface');
|
||||
$uri = $uri->reveal();
|
||||
$request = new Request($uri);
|
||||
$this->assertSame($uri, $request->getUri());
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::__construct
|
||||
*/
|
||||
public function testCreatesInstanceWithMethod()
|
||||
{
|
||||
$method = "POST";
|
||||
$request = new Request(null, $method);
|
||||
$this->assertSame($method, $request->getMethod());
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::__construct
|
||||
*/
|
||||
public function testSetsHeadersOnConstruction()
|
||||
{
|
||||
$request = new Request(null, null, [
|
||||
"X-foo" => ["bar","baz"]
|
||||
]);
|
||||
$this->assertEquals(["bar","baz"], $request->getHeader("X-foo"));
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::__construct
|
||||
*/
|
||||
public function testSetsBodyOnConstruction()
|
||||
{
|
||||
$body = $this->prophesize('\Psr\Http\Message\StreamInterface');
|
||||
$request = new Request(null, null, [], $body->reveal());
|
||||
$this->assertSame($body->reveal(), $request->getBody());
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// Request Target
|
||||
|
||||
/**
|
||||
* @covers ::getRequestTarget
|
||||
*/
|
||||
public function testGetRequestTargetPrefersExplicitRequestTarget()
|
||||
{
|
||||
$request = new Request();
|
||||
$request = $request->withRequestTarget("*");
|
||||
$this->assertEquals("*", $request->getRequestTarget());
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::getRequestTarget
|
||||
*/
|
||||
public function testGetRequestTargetUsesOriginFormOfUri()
|
||||
{
|
||||
$uri = $this->prophesize('\Psr\Http\Message\UriInterface');
|
||||
$uri->getHost()->willReturn("");
|
||||
$uri->getPath()->willReturn("/my/path");
|
||||
$uri->getQuery()->willReturn("cat=Molly&dog=Bear");
|
||||
|
||||
$request = new Request();
|
||||
$request = $request->withUri($uri->reveal());
|
||||
$this->assertEquals("/my/path?cat=Molly&dog=Bear", $request->getRequestTarget());
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::getRequestTarget
|
||||
*/
|
||||
public function testGetRequestTargetReturnsSlashByDefault()
|
||||
{
|
||||
$request = new Request();
|
||||
$this->assertEquals("/", $request->getRequestTarget());
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::withRequestTarget
|
||||
* @covers ::getRequestTarget
|
||||
*/
|
||||
public function testWithRequestTargetCreatesNewInstance()
|
||||
{
|
||||
$request = new Request();
|
||||
$request = $request->withRequestTarget("*");
|
||||
$this->assertEquals("*", $request->getRequestTarget());
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// Method
|
||||
|
||||
/**
|
||||
* @covers ::getMethod
|
||||
*/
|
||||
public function testGetMethodReturnsGetByDefault()
|
||||
{
|
||||
$request = new Request();
|
||||
$this->assertEquals("GET", $request->getMethod());
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::withMethod
|
||||
* @covers ::getValidatedMethod
|
||||
* @covers ::getMethod
|
||||
*/
|
||||
public function testWithMethodCreatesNewInstance()
|
||||
{
|
||||
$request = new Request();
|
||||
$request = $request->withMethod("POST");
|
||||
$this->assertEquals("POST", $request->getMethod());
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::withMethod
|
||||
* @covers ::getValidatedMethod
|
||||
* @dataProvider invalidMethodProvider
|
||||
* @expectedException \InvalidArgumentException
|
||||
*/
|
||||
public function testWithMethodThrowsExceptionOnInvalidMethod($method)
|
||||
{
|
||||
$request = new Request();
|
||||
$request->withMethod($method);
|
||||
}
|
||||
|
||||
public function invalidMethodProvider()
|
||||
{
|
||||
return [
|
||||
[0],
|
||||
[false],
|
||||
["WITH SPACE"]
|
||||
];
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// Request URI
|
||||
|
||||
/**
|
||||
* @covers ::getUri
|
||||
*/
|
||||
public function testGetUriReturnsEmptyUriByDefault()
|
||||
{
|
||||
$request = new Request();
|
||||
$uri = new Uri();
|
||||
$this->assertEquals($uri, $request->getUri());
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::withUri
|
||||
* @covers ::getUri
|
||||
*/
|
||||
public function testWithUriCreatesNewInstance()
|
||||
{
|
||||
$uri = $this->prophesize('\Psr\Http\Message\UriInterface');
|
||||
$uri = $uri->reveal();
|
||||
|
||||
$request = new Request();
|
||||
$request = $request->withUri($uri);
|
||||
$this->assertSame($uri, $request->getUri());
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::__clone
|
||||
*/
|
||||
public function testWithUriPreservesOriginalRequest()
|
||||
{
|
||||
$uri1 = $this->prophesize('\Psr\Http\Message\UriInterface');
|
||||
$uri1 = $uri1->reveal();
|
||||
|
||||
$uri2 = $this->prophesize('\Psr\Http\Message\UriInterface');
|
||||
$uri2 = $uri2->reveal();
|
||||
|
||||
$request1 = new Request();
|
||||
$request1 = $request1->withUri($uri1);
|
||||
$request1 = $request1->withHeader("Accept", "application/json");
|
||||
|
||||
$request2 = $request1->withUri($uri2);
|
||||
$request2 = $request2->withHeader("Accept", "text/plain");
|
||||
|
||||
$this->assertNotEquals($request1->getHeader("Accept"), $request2->getHeader("Accept"));
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::withUri
|
||||
*/
|
||||
public function testWithUriUpdatesHostHeader()
|
||||
{
|
||||
$hostname = "bar.com";
|
||||
$uri = $this->prophesize('\Psr\Http\Message\UriInterface');
|
||||
$uri->getHost()->willReturn($hostname);
|
||||
|
||||
$request = new Request();
|
||||
$request = $request->withHeader("Host", "foo.com");
|
||||
$request = $request->withUri($uri->reveal());
|
||||
$this->assertSame([$hostname], $request->getHeader("Host"));
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::withUri
|
||||
*/
|
||||
public function testWithUriDoesNotUpdatesHostHeaderWhenUriHasNoHost()
|
||||
{
|
||||
$hostname = "foo.com";
|
||||
$uri = $this->prophesize('\Psr\Http\Message\UriInterface');
|
||||
$uri->getHost()->willReturn("");
|
||||
|
||||
$request = new Request();
|
||||
$request = $request->withHeader("Host", $hostname);
|
||||
$request = $request->withUri($uri->reveal());
|
||||
$this->assertSame([$hostname], $request->getHeader("Host"));
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::withUri
|
||||
*/
|
||||
public function testPreserveHostUpdatesHostHeaderWhenHeaderIsOriginallyMissing()
|
||||
{
|
||||
$hostname = "foo.com";
|
||||
$uri = $this->prophesize('\Psr\Http\Message\UriInterface');
|
||||
$uri->getHost()->willReturn($hostname);
|
||||
|
||||
$request = new Request();
|
||||
$request = $request->withUri($uri->reveal(), true);
|
||||
$this->assertSame([$hostname], $request->getHeader("Host"));
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::withUri
|
||||
*/
|
||||
public function testPreserveHostDoesNotUpdatesWhenBothAreMissingHosts()
|
||||
{
|
||||
$uri = $this->prophesize('\Psr\Http\Message\UriInterface');
|
||||
$uri->getHost()->willReturn("");
|
||||
|
||||
$request = new Request();
|
||||
$request = $request->withUri($uri->reveal(), true);
|
||||
$this->assertSame([], $request->getHeader("Host"));
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::withUri
|
||||
*/
|
||||
public function testPreserveHostDoesNotUpdateHostHeader()
|
||||
{
|
||||
$hostname = "foo.com";
|
||||
$uri = $this->prophesize('\Psr\Http\Message\UriInterface');
|
||||
$uri->getHost()->willReturn("bar.com");
|
||||
|
||||
$request = new Request();
|
||||
$request = $request->withHeader("Host", $hostname);
|
||||
$request = $request->withUri($uri->reveal(), true);
|
||||
$this->assertSame([$hostname], $request->getHeader("Host"));
|
||||
}
|
||||
}
|
||||
|
|
@ -1,137 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace WellRESTed\Test\Unit\Message;
|
||||
|
||||
use WellRESTed\Message\Response;
|
||||
|
||||
/**
|
||||
* @coversDefaultClass WellRESTed\Message\Response
|
||||
* @uses WellRESTed\Message\Response
|
||||
* @uses WellRESTed\Message\Message
|
||||
* @uses WellRESTed\Message\HeaderCollection
|
||||
* @group message
|
||||
*/
|
||||
class ResponseTest extends \PHPUnit_Framework_TestCase
|
||||
{
|
||||
// ------------------------------------------------------------------------
|
||||
// Construction
|
||||
|
||||
/**
|
||||
* @covers ::__construct
|
||||
*/
|
||||
public function testSetsStatusCodeOnConstruction()
|
||||
{
|
||||
$response = new Response(200);
|
||||
$this->assertSame(200, $response->getStatusCode());
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::__construct
|
||||
*/
|
||||
public function testSetsHeadersOnConstruction()
|
||||
{
|
||||
$response = new Response(200, [
|
||||
"X-foo" => ["bar","baz"]
|
||||
]);
|
||||
$this->assertEquals(["bar","baz"], $response->getHeader("X-foo"));
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::__construct
|
||||
*/
|
||||
public function testSetsBodyOnConstruction()
|
||||
{
|
||||
$body = $this->prophesize('\Psr\Http\Message\StreamInterface');
|
||||
$response = new Response(200, [], $body->reveal());
|
||||
$this->assertSame($body->reveal(), $response->getBody());
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// Status and Reason Phrase
|
||||
|
||||
/**
|
||||
* @covers ::withStatus
|
||||
* @covers ::getStatusCode
|
||||
* @covers ::getDefaultReasonPhraseForStatusCode
|
||||
*/
|
||||
public function testCreatesNewInstanceWithStatusCode()
|
||||
{
|
||||
$response = new Response();
|
||||
$copy = $response->withStatus(200);
|
||||
$this->assertEquals(200, $copy->getStatusCode());
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::withStatus
|
||||
* @covers ::getReasonPhrase
|
||||
* @covers ::getDefaultReasonPhraseForStatusCode
|
||||
* @dataProvider statusProvider
|
||||
*/
|
||||
public function testCreatesNewInstanceWithReasonPhrase($code, $reasonPhrase, $expected)
|
||||
{
|
||||
$response = new Response();
|
||||
$copy = $response->withStatus($code, $reasonPhrase);
|
||||
$this->assertEquals($expected, $copy->getReasonPhrase());
|
||||
}
|
||||
|
||||
public function statusProvider()
|
||||
{
|
||||
return [
|
||||
[100, null, "Continue"],
|
||||
[101, null, "Switching Protocols"],
|
||||
[200, null, "OK"],
|
||||
[201, null, "Created"],
|
||||
[202, null, "Accepted"],
|
||||
[203, null, "Non-Authoritative Information"],
|
||||
[204, null, "No Content"],
|
||||
[205, null, "Reset Content"],
|
||||
[206, null, "Partial Content"],
|
||||
[300, null, "Multiple Choices"],
|
||||
[301, null, "Moved Permanently"],
|
||||
[302, null, "Found"],
|
||||
[303, null, "See Other"],
|
||||
[304, null, "Not Modified"],
|
||||
[305, null, "Use Proxy"],
|
||||
[400, null, "Bad Request"],
|
||||
[401, null, "Unauthorized"],
|
||||
[402, null, "Payment Required"],
|
||||
[403, null, "Forbidden"],
|
||||
[404, null, "Not Found"],
|
||||
[405, null, "Method Not Allowed"],
|
||||
[406, null, "Not Acceptable"],
|
||||
[407, null, "Proxy Authentication Required"],
|
||||
[408, null, "Request Timeout"],
|
||||
[409, null, "Conflict"],
|
||||
[410, null, "Gone"],
|
||||
[411, null, "Length Required"],
|
||||
[412, null, "Precondition Failed"],
|
||||
[413, null, "Payload Too Large"],
|
||||
[414, null, "URI Too Long"],
|
||||
[415, null, "Unsupported Media Type"],
|
||||
[500, null, "Internal Server Error"],
|
||||
[501, null, "Not Implemented"],
|
||||
[502, null, "Bad Gateway"],
|
||||
[503, null, "Service Unavailable"],
|
||||
[504, null, "Gateway Timeout"],
|
||||
[505, null, "HTTP Version Not Supported"],
|
||||
[598, null, ""],
|
||||
[599, "Nonstandard", "Nonstandard"]
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::withStatus
|
||||
* @covers ::getStatusCode
|
||||
*/
|
||||
public function testWithStatusCodePreservesOriginalResponse()
|
||||
{
|
||||
$response1 = new Response();
|
||||
$response1 = $response1->withStatus(200);
|
||||
$response1 = $response1->withHeader("Content-type", "application/json");
|
||||
|
||||
$response2 = $response1->withStatus(404);
|
||||
$response2 = $response2->withHeader("Content-type", "text/plain");
|
||||
|
||||
$this->assertNotEquals($response1->getStatusCode(), $response2->getHeader("Content-type"));
|
||||
}
|
||||
}
|
||||
|
|
@ -1,782 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace WellRESTed\Test\Unit\Message;
|
||||
|
||||
use WellRESTed\Message\ServerRequest;
|
||||
use WellRESTed\Message\UploadedFile;
|
||||
use WellRESTed\Message\Uri;
|
||||
|
||||
/**
|
||||
* @coversDefaultClass WellRESTed\Message\ServerRequest
|
||||
* @uses WellRESTed\Message\ServerRequest
|
||||
* @uses WellRESTed\Message\Request
|
||||
* @uses WellRESTed\Message\Message
|
||||
* @uses WellRESTed\Message\HeaderCollection
|
||||
* @uses WellRESTed\Message\Stream
|
||||
* @uses WellRESTed\Message\UploadedFile
|
||||
* @uses WellRESTed\Message\Uri
|
||||
* @group message
|
||||
*/
|
||||
class ServerRequestTest extends \PHPUnit_Framework_TestCase
|
||||
{
|
||||
// ------------------------------------------------------------------------
|
||||
// Construction and Marshalling
|
||||
|
||||
/**
|
||||
* @covers ::__construct
|
||||
*/
|
||||
public function testCreatesInstance()
|
||||
{
|
||||
$request = new ServerRequest();
|
||||
$this->assertNotNull($request);
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::getServerRequest
|
||||
* @covers ::getServerRequestHeaders
|
||||
* @covers ::readFromServerRequest
|
||||
* @covers ::getStreamForBody
|
||||
* @preserveGlobalState disabled
|
||||
*/
|
||||
public function testGetServerRequestReadsFromRequest()
|
||||
{
|
||||
$_SERVER = [
|
||||
"HTTP_HOST" => "localhost",
|
||||
"HTTP_ACCEPT" => "application/json",
|
||||
"HTTP_CONTENT_TYPE" => "application/x-www-form-urlencoded",
|
||||
"QUERY_STRING" => "guinea_pig=Claude&hamster=Fizzgig"
|
||||
];
|
||||
$_COOKIE = [
|
||||
"cat" => "Molly"
|
||||
];
|
||||
$_FILES = [];
|
||||
$_POST = [
|
||||
"dog" => "Bear"
|
||||
];
|
||||
$attributes = ["guinea_pig" => "Claude"];
|
||||
$request = ServerRequest::getServerRequest($attributes);
|
||||
$this->assertNotNull($request);
|
||||
return $request;
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// Marshalling Request Information
|
||||
|
||||
/**
|
||||
* @covers ::readFromServerRequest
|
||||
* @preserveGlobalState disabled
|
||||
* @dataProvider protocolVersionProvider
|
||||
*/
|
||||
public function testGetServerRequestReadsProtocolVersion($expectedProtocol, $serverProtocol)
|
||||
{
|
||||
$_SERVER = [
|
||||
"HTTP_HOST" => "localhost",
|
||||
"SERVER_PROTOCOL" => $serverProtocol,
|
||||
"REQUEST_METHOD" => "GET"
|
||||
];
|
||||
$request = ServerRequest::getServerRequest();
|
||||
$this->assertEquals($expectedProtocol, $request->getProtocolVersion());
|
||||
}
|
||||
|
||||
public function protocolVersionProvider()
|
||||
{
|
||||
return [
|
||||
["1.1", "HTTP/1.1"],
|
||||
["1.0", "HTTP/1.0"],
|
||||
["1.1", null],
|
||||
["1.1", "INVALID"]
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::readFromServerRequest
|
||||
* @preserveGlobalState disabled
|
||||
* @dataProvider methodProvider
|
||||
*/
|
||||
public function testGetServerRequestReadsMethod($exectedMethod, $serverMethod)
|
||||
{
|
||||
$_SERVER = [
|
||||
"HTTP_HOST" => "localhost",
|
||||
"REQUEST_METHOD" => $serverMethod
|
||||
];
|
||||
$request = ServerRequest::getServerRequest();
|
||||
$this->assertEquals($exectedMethod, $request->getMethod());
|
||||
}
|
||||
|
||||
public function methodProvider()
|
||||
{
|
||||
return [
|
||||
["GET", "GET"],
|
||||
["POST", "POST"],
|
||||
["DELETE", "DELETE"],
|
||||
["PUT", "PUT"],
|
||||
["OPTIONS", "OPTIONS"],
|
||||
["GET", null]
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::readFromServerRequest
|
||||
* @preserveGlobalState disabled
|
||||
* @dataProvider requestTargetProvider
|
||||
*/
|
||||
public function testGetServerRequestReadsRequestTargetFromRequest($exectedRequestTarget, $serverRequestUri)
|
||||
{
|
||||
$_SERVER = [
|
||||
"HTTP_HOST" => "localhost",
|
||||
"REQUEST_URI" => $serverRequestUri
|
||||
];
|
||||
$request = ServerRequest::getServerRequest();
|
||||
$this->assertEquals($exectedRequestTarget, $request->getRequestTarget());
|
||||
}
|
||||
|
||||
public function requestTargetProvider()
|
||||
{
|
||||
return [
|
||||
["/", "/"],
|
||||
["/hello", "/hello"],
|
||||
["/my/path.txt", "/my/path.txt"],
|
||||
["/", null]
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::getHeader
|
||||
* @depends testGetServerRequestReadsFromRequest
|
||||
*/
|
||||
public function testGetServerRequestReadsHeaders($request)
|
||||
{
|
||||
/** @var ServerRequest $request */
|
||||
$this->assertEquals(["application/json"], $request->getHeader("Accept"));
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::getBody
|
||||
*/
|
||||
public function testGetServerRequestReadsBody()
|
||||
{
|
||||
$body = $this->prophesize('Psr\Http\Message\StreamInterface');
|
||||
|
||||
$request = $this->getMockBuilder('WellRESTed\Message\ServerRequest')
|
||||
->setMethods(["getStreamForBody"])
|
||||
->getMock();
|
||||
$request->expects($this->any())
|
||||
->method("getStreamForBody")
|
||||
->will($this->returnValue($body->reveal()));
|
||||
|
||||
$called = false;
|
||||
$callReadFromServerRequest = function () use (&$called) {
|
||||
$called = true;
|
||||
$this->readFromServerRequest();
|
||||
};
|
||||
$callReadFromServerRequest = $callReadFromServerRequest->bindTo($request, $request);
|
||||
$callReadFromServerRequest();
|
||||
|
||||
$this->assertSame($body->reveal(), $request->getBody());
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::getServerRequest
|
||||
* @covers ::getServerRequestHeaders
|
||||
* @covers ::readFromServerRequest
|
||||
* @covers ::readUri
|
||||
* @preserveGlobalState disabled
|
||||
* @dataProvider uriProvider
|
||||
*/
|
||||
public function testGetServerRequestReadsUri($expected, $server)
|
||||
{
|
||||
$_SERVER = $server;
|
||||
$request = ServerRequest::getServerRequest();
|
||||
$this->assertEquals($expected, $request->getUri());
|
||||
}
|
||||
|
||||
public function uriProvider()
|
||||
{
|
||||
return [
|
||||
[
|
||||
new Uri("http://localhost/path"),
|
||||
[
|
||||
"HTTPS" => "off",
|
||||
"HTTP_HOST" => "localhost",
|
||||
"REQUEST_URI" => "/path",
|
||||
"QUERY_STRING" => ""
|
||||
]
|
||||
],
|
||||
[
|
||||
new Uri("https://foo.com/path/to/stuff?cat=molly"),
|
||||
[
|
||||
"HTTPS" => "1",
|
||||
"HTTP_HOST" => "foo.com",
|
||||
"REQUEST_URI" => "/path/to/stuff?cat=molly",
|
||||
"QUERY_STRING" => "cat=molly"
|
||||
]
|
||||
],
|
||||
[
|
||||
new Uri("http://foo.com:8080/path/to/stuff?cat=molly"),
|
||||
[
|
||||
"HTTP" => "1",
|
||||
"HTTP_HOST" => "foo.com:8080",
|
||||
"REQUEST_URI" => "/path/to/stuff?cat=molly",
|
||||
"QUERY_STRING" => "cat=molly"
|
||||
]
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// Marshalling ServerRequest Information
|
||||
|
||||
/**
|
||||
* @covers ::getServerParams
|
||||
* @depends testGetServerRequestReadsFromRequest
|
||||
*/
|
||||
public function testGetServerRequestReadsServerParams($request)
|
||||
{
|
||||
/** @var ServerRequest $request */
|
||||
$this->assertEquals("localhost", $request->getServerParams()["HTTP_HOST"]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::getCookieParams
|
||||
* @depends testGetServerRequestReadsFromRequest
|
||||
*/
|
||||
public function testGetServerRequestReadsCookieParams($request)
|
||||
{
|
||||
/** @var ServerRequest $request */
|
||||
$this->assertEquals("Molly", $request->getCookieParams()["cat"]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::getQueryParams
|
||||
* @depends testGetServerRequestReadsFromRequest
|
||||
*/
|
||||
public function testGetServerRequestReadsQueryParams($request)
|
||||
{
|
||||
/** @var ServerRequest $request */
|
||||
$this->assertEquals("Claude", $request->getQueryParams()["guinea_pig"]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::getServerRequest
|
||||
* @covers ::readUploadedFiles
|
||||
* @covers ::getUploadedFiles
|
||||
* @covers ::addUploadedFilesToBranch
|
||||
* @preserveGlobalState disabled
|
||||
* @dataProvider uploadedFileProvider
|
||||
*/
|
||||
public function testGetServerRequestReadsUploadedFiles($file, $path)
|
||||
{
|
||||
$_SERVER = [
|
||||
"HTTP_HOST" => "localhost",
|
||||
"HTTP_ACCEPT" => "application/json",
|
||||
"HTTP_CONTENT_TYPE" => "application/x-www-form-urlencoded"
|
||||
];
|
||||
$_FILES = [
|
||||
"single" => [
|
||||
"name" => "single.txt",
|
||||
"type" => "text/plain",
|
||||
"tmp_name" => "/tmp/php9hNlHe",
|
||||
"error" => UPLOAD_ERR_OK,
|
||||
"size" => 524
|
||||
],
|
||||
"nested" => [
|
||||
"level2" => [
|
||||
"name" => "nested.json",
|
||||
"type" => "application/json",
|
||||
"tmp_name" => "/tmp/phpadhjk",
|
||||
"error" => UPLOAD_ERR_OK,
|
||||
"size" => 1024
|
||||
]
|
||||
],
|
||||
"nestedList" => [
|
||||
"level2" => [
|
||||
"name" => [
|
||||
0 => "nestedList0.jpg",
|
||||
1 => "nestedList1.jpg",
|
||||
2 => ""
|
||||
],
|
||||
"type" => [
|
||||
0 => "image/jpeg",
|
||||
1 => "image/jpeg",
|
||||
2 => ""
|
||||
],
|
||||
"tmp_name" => [
|
||||
0 => "/tmp/phpjpg0",
|
||||
1 => "/tmp/phpjpg1",
|
||||
2 => ""
|
||||
],
|
||||
"error" => [
|
||||
0 => UPLOAD_ERR_OK,
|
||||
1 => UPLOAD_ERR_OK,
|
||||
2 => UPLOAD_ERR_NO_FILE
|
||||
],
|
||||
"size" => [
|
||||
0 => 256,
|
||||
1 => 4096,
|
||||
2 => 0
|
||||
]
|
||||
]
|
||||
],
|
||||
"nestedDictionary" => [
|
||||
"level2" => [
|
||||
"name" => [
|
||||
"file0" => "nestedDictionary0.jpg",
|
||||
"file1" => "nestedDictionary1.jpg"
|
||||
],
|
||||
"type" => [
|
||||
"file0" => "image/png",
|
||||
"file1" => "image/png"
|
||||
],
|
||||
"tmp_name" => [
|
||||
"file0" => "/tmp/phppng0",
|
||||
"file1" => "/tmp/phppng1"
|
||||
],
|
||||
"error" => [
|
||||
"file0" => UPLOAD_ERR_OK,
|
||||
"file1" => UPLOAD_ERR_OK
|
||||
],
|
||||
"size" => [
|
||||
"file0" => 256,
|
||||
"file1" => 4096
|
||||
]
|
||||
]
|
||||
]
|
||||
];
|
||||
$request = ServerRequest::getServerRequest();
|
||||
$current = $request->getUploadedFiles();
|
||||
foreach ($path as $item) {
|
||||
$current = $current[$item];
|
||||
}
|
||||
$this->assertEquals($file, $current);
|
||||
}
|
||||
|
||||
public function uploadedFileProvider()
|
||||
{
|
||||
return [
|
||||
[new UploadedFile("single.txt", "text/plain", 524, "/tmp/php9hNlHe", UPLOAD_ERR_OK), ["single"]],
|
||||
[new UploadedFile("nested.json", "application/json", 1024, "/tmp/phpadhjk", UPLOAD_ERR_OK), ["nested", "level2"]],
|
||||
[new UploadedFile("nestedList0.jpg", "image/jpeg", 256, "/tmp/phpjpg0", UPLOAD_ERR_OK), ["nestedList", "level2", 0]],
|
||||
[new UploadedFile("nestedList1.jpg", "image/jpeg", 4096, "/tmp/phpjpg1", UPLOAD_ERR_OK), ["nestedList", "level2", 1]],
|
||||
[new UploadedFile("", "", 0, "", UPLOAD_ERR_NO_FILE), ["nestedList", "level2", 2]],
|
||||
[new UploadedFile("nestedDictionary0.jpg", "image/png", 256, "/tmp/phppng0", UPLOAD_ERR_OK), ["nestedDictionary", "level2", "file0"]],
|
||||
[new UploadedFile("nestedDictionary1.jpg", "image/png", 4096, "/tmp/phppngg1", UPLOAD_ERR_OK), ["nestedDictionary", "level2", "file1"]]
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::getServerRequest
|
||||
* @covers ::getParsedBody
|
||||
* @preserveGlobalState disabled
|
||||
* @dataProvider formContentTypeProvider
|
||||
*/
|
||||
public function testGetServerRequestParsesFormBody($contentType)
|
||||
{
|
||||
$_SERVER = [
|
||||
"HTTP_HOST" => "localhost",
|
||||
"HTTP_CONTENT_TYPE" => $contentType,
|
||||
];
|
||||
$_COOKIE = [];
|
||||
$_FILES = [];
|
||||
$_POST = [
|
||||
"dog" => "Bear"
|
||||
];
|
||||
$request = ServerRequest::getServerRequest();
|
||||
$this->assertEquals("Bear", $request->getParsedBody()["dog"]);
|
||||
}
|
||||
|
||||
public function formContentTypeProvider()
|
||||
{
|
||||
return [
|
||||
["application/x-www-form-urlencoded"],
|
||||
["multipart/form-data"]
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::getAttribute
|
||||
* @depends testGetServerRequestReadsFromRequest
|
||||
*/
|
||||
public function testGetServerRequestProvidesAttributesIfPassed($request)
|
||||
{
|
||||
/** @var ServerRequest $request */
|
||||
$this->assertEquals("Claude", $request->getAttribute("guinea_pig"));
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// Server Params
|
||||
|
||||
/**
|
||||
* @covers ::getServerParams
|
||||
*/
|
||||
public function testGetServerParamsReturnsEmptyArrayByDefault()
|
||||
{
|
||||
$request = new ServerRequest();
|
||||
$this->assertEquals([], $request->getServerParams());
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// Cookies
|
||||
|
||||
/**
|
||||
* @covers ::getCookieParams
|
||||
*/
|
||||
public function testGetCookieParamsReturnsEmptyArrayByDefault()
|
||||
{
|
||||
$request = new ServerRequest();
|
||||
$this->assertEquals([], $request->getCookieParams());
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::withCookieParams
|
||||
* @depends testGetServerRequestReadsFromRequest
|
||||
*/
|
||||
public function testWithCookieParamsCreatesNewInstance($request1)
|
||||
{
|
||||
/** @var ServerRequest $request1 */
|
||||
$request2 = $request1->withCookieParams([
|
||||
"cat" => "Oscar"
|
||||
]);
|
||||
$this->assertNotEquals($request1->getCookieParams()["cat"], $request2->getCookieParams()["cat"]);
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// Query
|
||||
|
||||
/**
|
||||
* @covers ::getQueryParams
|
||||
*/
|
||||
public function testGetQueryParamsReturnsEmptyArrayByDefault()
|
||||
{
|
||||
$request = new ServerRequest();
|
||||
$this->assertEquals([], $request->getQueryParams());
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::withQueryParams
|
||||
* @depends testGetServerRequestReadsFromRequest
|
||||
*/
|
||||
public function testWithQueryParamsCreatesNewInstance($request1)
|
||||
{
|
||||
/** @var ServerRequest $request1 */
|
||||
$request2 = $request1->withQueryParams([
|
||||
"guinea_pig" => "Clyde"
|
||||
]);
|
||||
$this->assertNotEquals($request1->getQueryParams()["guinea_pig"], $request2->getQueryParams()["guinea_pig"]);
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// Uploaded Files
|
||||
|
||||
/**
|
||||
* @covers ::getUploadedFiles
|
||||
*/
|
||||
public function testGetUploadedFilesReturnsEmptyArrayByDefault()
|
||||
{
|
||||
$request = new ServerRequest();
|
||||
$this->assertEquals([], $request->getUploadedFiles());
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::getUploadedFiles
|
||||
* @preserveGlobalState disabled
|
||||
*/
|
||||
public function testGetUploadedFilesReturnsEmptyArrayWhenNoFilesAreUploaded()
|
||||
{
|
||||
$_SERVER = [
|
||||
"HTTP_HOST" => "localhost",
|
||||
"HTTP_ACCEPT" => "application/json",
|
||||
"HTTP_CONTENT_TYPE" => "application/x-www-form-urlencoded"
|
||||
];
|
||||
$_FILES = [];
|
||||
$request = ServerRequest::getServerRequest();
|
||||
$this->assertSame([], $request->getUploadedFiles());
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::withUploadedFiles
|
||||
* @covers ::isValidUploadedFilesBranch
|
||||
* @covers ::isValidUploadedFilesTree
|
||||
*/
|
||||
public function testWithUploadedFilesCreatesNewInstance()
|
||||
{
|
||||
$uploadedFiles = [
|
||||
"file" => new UploadedFile("index.html", "text/html", 524, "/tmp/php9hNlHe", 0)
|
||||
];
|
||||
$request = new ServerRequest();
|
||||
$request1 = $request->withUploadedFiles([]);
|
||||
$request2 = $request1->withUploadedFiles($uploadedFiles);
|
||||
$this->assertNotSame($request2, $request1);
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::withUploadedFiles
|
||||
* @covers ::isValidUploadedFilesTree
|
||||
* @covers ::isValidUploadedFilesBranch
|
||||
* @dataProvider validUploadedFilesProvider
|
||||
*/
|
||||
public function testWithUploadedFilesStoresPassedUploadedFiles($uploadedFiles)
|
||||
{
|
||||
$request = new ServerRequest();
|
||||
$request = $request->withUploadedFiles($uploadedFiles);
|
||||
$this->assertSame($uploadedFiles, $request->getUploadedFiles());
|
||||
}
|
||||
|
||||
public function validUploadedFilesProvider()
|
||||
{
|
||||
return [
|
||||
[[]],
|
||||
[["files" => new UploadedFile("index.html", "text/html", 524, "/tmp/php9hNlHe", 0)]],
|
||||
[["nested" => [
|
||||
"level2" => new UploadedFile("index.html", "text/html", 524, "/tmp/php9hNlHe", 0)
|
||||
]]],
|
||||
[["nestedList" => [
|
||||
"level2" => [
|
||||
new UploadedFile("file1.html", "text/html", 524, "/tmp/php9hNlHe", 0),
|
||||
new UploadedFile("file2.html", "text/html", 524, "/tmp/php9hNshj", 0)
|
||||
]
|
||||
]]],
|
||||
[["nestedDictionary" => [
|
||||
"level2" => [
|
||||
"file1" => new UploadedFile("file1.html", "text/html", 524, "/tmp/php9hNlHe", 0),
|
||||
"file2" => new UploadedFile("file2.html", "text/html", 524, "/tmp/php9hNshj", 0)
|
||||
]
|
||||
]]]
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::withUploadedFiles
|
||||
* @covers ::isValidUploadedFilesTree
|
||||
* @covers ::isValidUploadedFilesBranch
|
||||
* @expectedException \InvalidArgumentException
|
||||
* @dataProvider invalidUploadedFilesProvider
|
||||
*/
|
||||
public function testWithUploadedFilesThrowsExceptionWithInvalidTree($uploadedFiles)
|
||||
{
|
||||
$request = new ServerRequest();
|
||||
$request->withUploadedFiles($uploadedFiles);
|
||||
}
|
||||
|
||||
public function invalidUploadedFilesProvider()
|
||||
{
|
||||
return [
|
||||
// All keys must be strings
|
||||
[[new UploadedFile("index.html", "text/html", 524, "/tmp/php9hNlHe", 0)]],
|
||||
[
|
||||
[new UploadedFile("index1.html", "text/html", 524, "/tmp/php9hNlHe", 0)],
|
||||
[new UploadedFile("index2.html", "text/html", 524, "/tmp/php9hNlHe", 0)]
|
||||
],
|
||||
[
|
||||
"single" => [
|
||||
"name" => "single.txt",
|
||||
"type" => "text/plain",
|
||||
"tmp_name" => "/tmp/php9hNlHe",
|
||||
"error" => UPLOAD_ERR_OK,
|
||||
"size" => 524
|
||||
],
|
||||
"nested" => [
|
||||
"level2" => [
|
||||
"name" => "nested.json",
|
||||
"type" => "application/json",
|
||||
"tmp_name" => "/tmp/phpadhjk",
|
||||
"error" => UPLOAD_ERR_OK,
|
||||
"size" => 1024
|
||||
]
|
||||
]
|
||||
],
|
||||
[
|
||||
"nestedList" => [
|
||||
"level2" => [
|
||||
"name" => [
|
||||
0 => "nestedList0.jpg",
|
||||
1 => "nestedList1.jpg",
|
||||
2 => ""
|
||||
],
|
||||
"type" => [
|
||||
0 => "image/jpeg",
|
||||
1 => "image/jpeg",
|
||||
2 => ""
|
||||
],
|
||||
"tmp_name" => [
|
||||
0 => "/tmp/phpjpg0",
|
||||
1 => "/tmp/phpjpg1",
|
||||
2 => ""
|
||||
],
|
||||
"error" => [
|
||||
0 => UPLOAD_ERR_OK,
|
||||
1 => UPLOAD_ERR_OK,
|
||||
2 => UPLOAD_ERR_NO_FILE
|
||||
],
|
||||
"size" => [
|
||||
0 => 256,
|
||||
1 => 4096,
|
||||
2 => 0
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// Parsed Body
|
||||
|
||||
/**
|
||||
* @covers ::getParsedBody
|
||||
*/
|
||||
public function testGetParsedBodyReturnsNullByDefault()
|
||||
{
|
||||
$request = new ServerRequest();
|
||||
$this->assertNull($request->getParsedBody());
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::withParsedBody
|
||||
* @depends testGetServerRequestReadsFromRequest
|
||||
*/
|
||||
public function testWithParsedBodyCreatesNewInstance($request1)
|
||||
{
|
||||
/** @var ServerRequest $request1 */
|
||||
$body1 = $request1->getParsedBody();
|
||||
|
||||
$request2 = $request1->withParsedBody([
|
||||
"guinea_pig" => "Clyde"
|
||||
]);
|
||||
$body2 = $request2->getParsedBody();
|
||||
|
||||
$this->assertNotSame($body1, $body2);
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::withParsedBody
|
||||
* @expectedException \InvalidArgumentException
|
||||
* @dataProvider invalidParsedBodyProvider
|
||||
*/
|
||||
public function testWithParsedBodyThrowsExceptionWithInvalidType($body)
|
||||
{
|
||||
$request = new ServerRequest();
|
||||
$request->withParsedBody($body);
|
||||
}
|
||||
|
||||
public function invalidParsedBodyProvider()
|
||||
{
|
||||
return [
|
||||
[false],
|
||||
[1]
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::__clone
|
||||
*/
|
||||
public function testCloneMakesDeepCopiesOfParsedBody()
|
||||
{
|
||||
$body = (object) [
|
||||
"cat" => "Dog"
|
||||
];
|
||||
|
||||
$request1 = new ServerRequest();
|
||||
$request1 = $request1->withParsedBody($body);
|
||||
$request2 = $request1->withHeader("X-extra", "hello world");
|
||||
|
||||
$this->assertTrue(
|
||||
$request1->getParsedBody() == $request2->getParsedBody()
|
||||
&& $request1->getParsedBody() !== $request2->getParsedBody()
|
||||
);
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// Attributes
|
||||
|
||||
/**
|
||||
* @covers ::getAttributes
|
||||
*/
|
||||
public function testGetAttributesReturnsEmptyArrayByDefault()
|
||||
{
|
||||
$request = new ServerRequest();
|
||||
$this->assertEquals([], $request->getAttributes());
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::getAttributes
|
||||
*/
|
||||
public function testGetAttributesReturnsAllAttributes()
|
||||
{
|
||||
$request = new ServerRequest();
|
||||
$request = $request->withAttribute("cat", "Molly");
|
||||
$request = $request->withAttribute("dog", "Bear");
|
||||
$expected = [
|
||||
"cat" => "Molly",
|
||||
"dog" => "Bear"
|
||||
];
|
||||
$this->assertEquals($expected, $request->getAttributes());
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::getAttribute
|
||||
*/
|
||||
public function testGetAttributeReturnsDefaultIfNotSet()
|
||||
{
|
||||
$request = new ServerRequest();
|
||||
$this->assertEquals("Oscar", $request->getAttribute("cat", "Oscar"));
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::withAttribute
|
||||
* @covers ::getAttribute
|
||||
*/
|
||||
public function testWithAttributeCreatesNewInstance()
|
||||
{
|
||||
$request = new ServerRequest();
|
||||
$request = $request->withAttribute("cat", "Molly");
|
||||
$this->assertEquals("Molly", $request->getAttribute("cat"));
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::withAttribute
|
||||
*/
|
||||
public function testWithAttributePreserversOtherAttributes()
|
||||
{
|
||||
$request = new ServerRequest();
|
||||
$request = $request->withAttribute("cat", "Molly");
|
||||
$request = $request->withAttribute("dog", "Bear");
|
||||
$expected = [
|
||||
"cat" => "Molly",
|
||||
"dog" => "Bear"
|
||||
];
|
||||
$this->assertEquals($expected, $request->getAttributes());
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::withoutAttribute
|
||||
*/
|
||||
public function testWithoutAttributeCreatesNewInstance()
|
||||
{
|
||||
$request = new ServerRequest();
|
||||
$request = $request->withAttribute("cat", "Molly");
|
||||
$this->assertNotEquals($request, $request->withoutAttribute("cat"));
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::withoutAttribute
|
||||
*/
|
||||
public function testWithoutAttributeRemovesAttribute()
|
||||
{
|
||||
$request = new ServerRequest();
|
||||
$request = $request->withAttribute("cat", "Molly");
|
||||
$request = $request->withoutAttribute("cat");
|
||||
$this->assertEquals("Oscar", $request->getAttribute("cat", "Oscar"));
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::withoutAttribute
|
||||
*/
|
||||
public function testWithoutAttributePreservesOtherAttributes()
|
||||
{
|
||||
$request = new ServerRequest();
|
||||
$request = $request->withAttribute("cat", "Molly");
|
||||
$request = $request->withAttribute("dog", "Bear");
|
||||
$request = $request->withoutAttribute("cat");
|
||||
$this->assertEquals("Bear", $request->getAttribute("dog"));
|
||||
}
|
||||
}
|
||||
|
|
@ -1,298 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace WellRESTed\Test\Message;
|
||||
|
||||
use WellRESTed\Message\Stream;
|
||||
|
||||
/**
|
||||
* @coversDefaultClass WellRESTed\Message\Stream
|
||||
* @uses WellRESTed\Message\Stream
|
||||
* @group message
|
||||
*/
|
||||
class StreamTest extends \PHPUnit_Framework_TestCase
|
||||
{
|
||||
private $resource;
|
||||
private $content = "Hello, world!";
|
||||
|
||||
public function setUp()
|
||||
{
|
||||
$this->resource = fopen("php://memory", "w+");
|
||||
fwrite($this->resource, $this->content);
|
||||
}
|
||||
|
||||
public function tearDown()
|
||||
{
|
||||
if (is_resource($this->resource)) {
|
||||
fclose($this->resource);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::__construct
|
||||
*/
|
||||
public function testCreatesInstanceWithStreamResource()
|
||||
{
|
||||
$stream = new \WellRESTed\Message\Stream($this->resource);
|
||||
$this->assertNotNull($stream);
|
||||
}
|
||||
|
||||
public function testCreatesInstanceWithString()
|
||||
{
|
||||
$stream = new \WellRESTed\Message\Stream("Hello, world!");
|
||||
$this->assertNotNull($stream);
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::__construct
|
||||
* @expectedException \InvalidArgumentException
|
||||
* @dataProvider invalidResourceProvider
|
||||
*/
|
||||
public function testThrowsExceptiondWithInvalidResource($resource)
|
||||
{
|
||||
new \WellRESTed\Message\Stream($resource);
|
||||
}
|
||||
|
||||
public function invalidResourceProvider()
|
||||
{
|
||||
return [
|
||||
[null],
|
||||
[true],
|
||||
[4],
|
||||
[[]]
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::__toString
|
||||
*/
|
||||
public function testCastsToString()
|
||||
{
|
||||
$stream = new \WellRESTed\Message\Stream($this->resource);
|
||||
$this->assertEquals($this->content, (string) $stream);
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::close
|
||||
*/
|
||||
public function testClosesHandle()
|
||||
{
|
||||
$stream = new Stream($this->resource);
|
||||
$stream->close();
|
||||
$this->assertFalse(is_resource($this->resource));
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::detach
|
||||
*/
|
||||
public function testDetachReturnsHandle()
|
||||
{
|
||||
$stream = new Stream($this->resource);
|
||||
$this->assertSame($this->resource, $stream->detach());
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::detach
|
||||
*/
|
||||
public function testDetachUnsetsInstanceVariable()
|
||||
{
|
||||
$stream = new \WellRESTed\Message\Stream($this->resource);
|
||||
$stream->detach();
|
||||
$this->assertNull($stream->detach());
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::getSize
|
||||
*/
|
||||
public function testReturnsSize()
|
||||
{
|
||||
$stream = new \WellRESTed\Message\Stream($this->resource);
|
||||
$this->assertEquals(strlen($this->content), $stream->getSize());
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::tell
|
||||
*/
|
||||
public function testTellReturnsHandlePosition()
|
||||
{
|
||||
$stream = new \WellRESTed\Message\Stream($this->resource);
|
||||
fseek($this->resource, 10);
|
||||
$this->assertEquals(10, $stream->tell());
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::eof
|
||||
*/
|
||||
public function testReturnsOef()
|
||||
{
|
||||
$stream = new Stream($this->resource);
|
||||
$stream->rewind();
|
||||
$stream->getContents();
|
||||
$this->assertTrue($stream->eof());
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::isSeekable
|
||||
*/
|
||||
public function testReadsSeekableStatusFromMetadata()
|
||||
{
|
||||
$stream = new \WellRESTed\Message\Stream($this->resource);
|
||||
$metadata = stream_get_meta_data($this->resource);
|
||||
$seekable = $metadata["seekable"] == 1;
|
||||
$this->assertEquals($seekable, $stream->isSeekable());
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::seek
|
||||
*/
|
||||
public function testSeeksToPosition()
|
||||
{
|
||||
$stream = new \WellRESTed\Message\Stream($this->resource);
|
||||
$stream->seek(10);
|
||||
$this->assertEquals(10, ftell($this->resource));
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::rewind
|
||||
*/
|
||||
public function testRewindReturnsToBeginning()
|
||||
{
|
||||
$stream = new Stream($this->resource);
|
||||
$stream->seek(10);
|
||||
$stream->rewind();
|
||||
$this->assertEquals(0, ftell($this->resource));
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::write
|
||||
*/
|
||||
public function testWritesToHandle()
|
||||
{
|
||||
$message = "\nThis is a stream.";
|
||||
$stream = new \WellRESTed\Message\Stream($this->resource);
|
||||
$stream->write($message);
|
||||
$this->assertEquals($this->content . $message, (string) $stream);
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::write
|
||||
* @expectedException \RuntimeException
|
||||
*/
|
||||
public function testThrowsExceptionOnErrorWriting()
|
||||
{
|
||||
$filename = tempnam(sys_get_temp_dir(), "php");
|
||||
$handle = fopen($filename, "r");
|
||||
$stream = new \WellRESTed\Message\Stream($handle);
|
||||
$stream->write("Hello, world!");
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::read
|
||||
* @expectedException \RuntimeException
|
||||
*/
|
||||
public function testThrowsExceptionOnErrorReading()
|
||||
{
|
||||
$filename = tempnam(sys_get_temp_dir(), "php");
|
||||
$handle = fopen($filename, "w");
|
||||
$stream = new \WellRESTed\Message\Stream($handle);
|
||||
$stream->read(10);
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::read
|
||||
*/
|
||||
public function testReadsFromStream()
|
||||
{
|
||||
$stream = new \WellRESTed\Message\Stream($this->resource);
|
||||
$stream->seek(7);
|
||||
$string = $stream->read(5);
|
||||
$this->assertEquals("world", $string);
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::getContents
|
||||
* @expectedException \RuntimeException
|
||||
*/
|
||||
public function testThrowsExceptionOnErrorReadingToEnd()
|
||||
{
|
||||
$filename = tempnam(sys_get_temp_dir(), "php");
|
||||
$handle = fopen($filename, "w");
|
||||
$stream = new \WellRESTed\Message\Stream($handle);
|
||||
$stream->getContents();
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::getContents
|
||||
*/
|
||||
public function testReadsToEnd()
|
||||
{
|
||||
$stream = new Stream($this->resource);
|
||||
$stream->seek(7);
|
||||
$string = $stream->getContents();
|
||||
$this->assertEquals("world!", $string);
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::getMetadata
|
||||
*/
|
||||
public function testReturnsMetadataArray()
|
||||
{
|
||||
$stream = new \WellRESTed\Message\Stream($this->resource);
|
||||
$this->assertEquals(stream_get_meta_data($this->resource), $stream->getMetadata());
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::getMetadata
|
||||
*/
|
||||
public function testReturnsMetadataItem()
|
||||
{
|
||||
$stream = new Stream($this->resource);
|
||||
$metadata = stream_get_meta_data($this->resource);
|
||||
$this->assertEquals($metadata["mode"], $stream->getMetadata("mode"));
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::isReadable
|
||||
* @dataProvider modeProvider
|
||||
*/
|
||||
public function testReturnsIsReadableForReadableStreams($mode, $readable, $writeable)
|
||||
{
|
||||
$tmp = tempnam(sys_get_temp_dir(), "php");
|
||||
if ($mode[0] === "x") {
|
||||
unlink($tmp);
|
||||
}
|
||||
$resource = fopen($tmp, $mode);
|
||||
$stream = new Stream($resource);
|
||||
$this->assertEquals($readable, $stream->isReadable());
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::isWritable
|
||||
* @dataProvider modeProvider
|
||||
*/
|
||||
public function testReturnsIsWritableForWritableStreams($mode, $readable, $writeable)
|
||||
{
|
||||
$tmp = tempnam(sys_get_temp_dir(), "php");
|
||||
if ($mode[0] === "x") {
|
||||
unlink($tmp);
|
||||
}
|
||||
$resource = fopen($tmp, $mode);
|
||||
$stream = new \WellRESTed\Message\Stream($resource);
|
||||
$this->assertEquals($writeable, $stream->isWritable());
|
||||
}
|
||||
|
||||
public function modeProvider()
|
||||
{
|
||||
return [
|
||||
["r", true, false],
|
||||
["r+", true, true],
|
||||
["w", false, true],
|
||||
["w+", true, true],
|
||||
["a", false, true],
|
||||
["a+", true, true],
|
||||
["x", false, true],
|
||||
["x+", true, true],
|
||||
["c", false, true],
|
||||
["c+", true, true]
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
@ -1,189 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace WellRESTed\Test\Message;
|
||||
|
||||
use WellRESTed\Message\UploadedFile;
|
||||
use WellRESTed\Message\UploadedFileState;
|
||||
|
||||
// Hides several php core functions for testing.
|
||||
require_once __DIR__ . "/../../../src/UploadedFileState.php";
|
||||
|
||||
/**
|
||||
* @coversDefaultClass WellRESTed\Message\UploadedFile
|
||||
* @uses WellRESTed\Message\UploadedFile
|
||||
* @uses WellRESTed\Message\Stream
|
||||
* @uses WellRESTed\Message\NullStream
|
||||
* @group message
|
||||
*/
|
||||
class UploadedFileTest extends \PHPUnit_Framework_TestCase
|
||||
{
|
||||
private $tmpName;
|
||||
private $movePath;
|
||||
|
||||
public function setUp()
|
||||
{
|
||||
parent::setUp();
|
||||
UploadedFileState::$php_sapi_name = "cli";
|
||||
$this->tmpName = tempnam(sys_get_temp_dir(), "tst");
|
||||
$this->movePath = tempnam(sys_get_temp_dir(), "tst");
|
||||
}
|
||||
|
||||
public function tearDown()
|
||||
{
|
||||
parent::tearDown();
|
||||
if (file_exists($this->tmpName)) {
|
||||
unlink($this->tmpName);
|
||||
}
|
||||
if (file_exists($this->movePath)) {
|
||||
unlink($this->movePath);
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// getStream
|
||||
|
||||
/**
|
||||
* @covers ::__construct
|
||||
* @covers ::getStream
|
||||
*/
|
||||
public function testGetStreamReturnsStreamInterface()
|
||||
{
|
||||
$file = new UploadedFile("", "", 0, "", 0);
|
||||
$this->assertInstanceOf('\Psr\Http\Message\StreamInterface', $file->getStream());
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::__construct
|
||||
* @covers ::getStream
|
||||
*/
|
||||
public function testGetStreamReturnsStreamWrappingUploadedFile()
|
||||
{
|
||||
$content = "Hello, World!";
|
||||
file_put_contents($this->tmpName, $content);
|
||||
$file = new UploadedFile("", "", 0, $this->tmpName, "");
|
||||
$stream = $file->getStream();
|
||||
$this->assertEquals($content, (string) $stream);
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::__construct
|
||||
* @covers ::getStream
|
||||
*/
|
||||
public function testGetStreamReturnsEmptyStreamForNoFile()
|
||||
{
|
||||
$file = new UploadedFile("", "", 0, "", 0);
|
||||
$this->assertTrue($file->getStream()->eof());
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::__construct
|
||||
* @covers ::getStream
|
||||
* @expectedException \RuntimeException
|
||||
*/
|
||||
public function testGetStreamThrowsExceptionAfterMoveTo()
|
||||
{
|
||||
$content = "Hello, World!";
|
||||
file_put_contents($this->tmpName, $content);
|
||||
$file = new UploadedFile("", "", 0, $this->tmpName, "");
|
||||
$file->moveTo($this->movePath);
|
||||
$file->getStream();
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::__construct
|
||||
* @covers ::getStream
|
||||
* @expectedException \RuntimeException
|
||||
*/
|
||||
public function testGetStreamThrowsExceptionForNonUploadedFile()
|
||||
{
|
||||
UploadedFileState::$php_sapi_name = "apache";
|
||||
UploadedFileState::$is_uploaded_file = false;
|
||||
$file = new UploadedFile("", "", 0, "", 0);
|
||||
$file->getStream();
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// moveTo
|
||||
|
||||
/**
|
||||
* @covers ::moveTo
|
||||
*/
|
||||
public function testMoveToSapiRelocatesUploadedFileToDestiationIfExists()
|
||||
{
|
||||
UploadedFileState::$php_sapi_name = "fpm-fcgi";
|
||||
|
||||
$content = "Hello, World!";
|
||||
file_put_contents($this->tmpName, $content);
|
||||
$originalMd5 = md5_file($this->tmpName);
|
||||
|
||||
$file = new UploadedFile("", "", 0, $this->tmpName, "");
|
||||
$file->moveTo($this->movePath);
|
||||
|
||||
$this->assertEquals($originalMd5, md5_file($this->movePath));
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::moveTo
|
||||
*/
|
||||
public function testMoveToNonSapiRelocatesUploadedFileToDestiationIfExists()
|
||||
{
|
||||
$content = "Hello, World!";
|
||||
file_put_contents($this->tmpName, $content);
|
||||
$originalMd5 = md5_file($this->tmpName);
|
||||
|
||||
$file = new UploadedFile("", "", 0, $this->tmpName, "");
|
||||
$file->moveTo($this->movePath);
|
||||
|
||||
$this->assertEquals($originalMd5, md5_file($this->movePath));
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::moveTo
|
||||
* @expectedException \RuntimeException
|
||||
*/
|
||||
public function testMoveToThrowsExcpetionOnSubsequentCall()
|
||||
{
|
||||
$content = "Hello, World!";
|
||||
file_put_contents($this->tmpName, $content);
|
||||
|
||||
$file = new UploadedFile("", "", 0, $this->tmpName, "");
|
||||
$file->moveTo($this->movePath);
|
||||
$file->moveTo($this->movePath);
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// getSize
|
||||
|
||||
public function testGetSizeReturnsSize()
|
||||
{
|
||||
$file = new UploadedFile("", "", 1024, "", 0);
|
||||
$this->assertEquals(1024, $file->getSize());
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// getError
|
||||
|
||||
public function testGetErrorReturnsError()
|
||||
{
|
||||
$file = new UploadedFile("", "", 1024, "", UPLOAD_ERR_INI_SIZE);
|
||||
$this->assertEquals(UPLOAD_ERR_INI_SIZE, $file->getError());
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// clientFilename
|
||||
|
||||
public function testGetClientFilenameReturnsClientFilename()
|
||||
{
|
||||
$file = new UploadedFile("clientFilename", "", 0, "", 0);
|
||||
$this->assertEquals("clientFilename", $file->getClientFilename());
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// clientMediaType
|
||||
|
||||
public function testGetClientMediaTypeReturnsClientMediaType()
|
||||
{
|
||||
$file = new UploadedFile("", "clientMediaType", 0, "", 0);
|
||||
$this->assertEquals("clientMediaType", $file->getClientMediaType());
|
||||
}
|
||||
}
|
||||
|
|
@ -1,704 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace WellRESTed\Test\Message;
|
||||
|
||||
use WellRESTed\Message\Uri;
|
||||
|
||||
/**
|
||||
* @coversDefaultClass WellRESTed\Message\Uri
|
||||
* @uses WellRESTed\Message\Uri
|
||||
* @group message
|
||||
*/
|
||||
class UriTest extends \PHPUnit_Framework_TestCase
|
||||
{
|
||||
// ------------------------------------------------------------------------
|
||||
// Scheme
|
||||
|
||||
/**
|
||||
* @covers ::getScheme
|
||||
*/
|
||||
public function testDefaultSchemeIsEmpty()
|
||||
{
|
||||
$uri = new Uri();
|
||||
$this->assertSame("", $uri->getScheme());
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::withScheme
|
||||
* @dataProvider schemeProvider
|
||||
* @param string $expected The expected result of getScheme
|
||||
* @param string $scheme The scheme to pass to withScheme
|
||||
*/
|
||||
public function testSetsSchemeCaseInsensitively($expected, $scheme)
|
||||
{
|
||||
$uri = new Uri();
|
||||
$uri = $uri->withScheme($scheme);
|
||||
$this->assertSame($expected, $uri->getScheme());
|
||||
}
|
||||
|
||||
public function schemeProvider()
|
||||
{
|
||||
return [
|
||||
["http", "http"],
|
||||
["https", "https"],
|
||||
["http", "HTTP"],
|
||||
["https", "HTTPS"],
|
||||
["", null],
|
||||
["", ""]
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::withScheme
|
||||
* @expectedException \InvalidArgumentException
|
||||
*/
|
||||
public function testInvalidSchemeThrowsException()
|
||||
{
|
||||
$uri = new Uri();
|
||||
$uri->withScheme("gopher");
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// Authority
|
||||
|
||||
/**
|
||||
* @covers ::getAuthority
|
||||
*/
|
||||
public function testDefaultAuthorityIsEmpty()
|
||||
{
|
||||
$uri = new Uri();
|
||||
$this->assertSame("", $uri->getAuthority());
|
||||
}
|
||||
|
||||
public function testRespectsMyAuthoritah()
|
||||
{
|
||||
$this->assertTrue(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::getAuthority
|
||||
* @dataProvider authorityProvider
|
||||
* @param string $expected
|
||||
* @param array $components
|
||||
*/
|
||||
public function testConcatenatesAuthorityFromHostAndUserInfo($expected, $components)
|
||||
{
|
||||
$uri = new Uri();
|
||||
|
||||
if (isset($components["scheme"])) {
|
||||
$uri = $uri->withScheme($components["scheme"]);
|
||||
}
|
||||
|
||||
if (isset($components["user"])) {
|
||||
$user = $components["user"];
|
||||
$password = null;
|
||||
if (isset($components["password"])) {
|
||||
$password = $components["password"];
|
||||
}
|
||||
$uri = $uri->withUserInfo($user, $password);
|
||||
}
|
||||
|
||||
if (isset($components["host"])) {
|
||||
$uri = $uri->withHost($components["host"]);
|
||||
}
|
||||
|
||||
if (isset($components["port"])) {
|
||||
$uri = $uri->withPort($components["port"]);
|
||||
}
|
||||
|
||||
$this->assertEquals($expected, $uri->getAuthority());
|
||||
}
|
||||
|
||||
public function authorityProvider()
|
||||
{
|
||||
return [
|
||||
[
|
||||
"localhost",
|
||||
[
|
||||
"host" => "localhost"
|
||||
]
|
||||
],
|
||||
[
|
||||
"user@localhost",
|
||||
[
|
||||
"host" => "localhost",
|
||||
"user" => "user"
|
||||
]
|
||||
],
|
||||
[
|
||||
"user:password@localhost",
|
||||
[
|
||||
"host" => "localhost",
|
||||
"user" => "user",
|
||||
"password" => "password"
|
||||
]
|
||||
],
|
||||
[
|
||||
"localhost",
|
||||
[
|
||||
"host" => "localhost",
|
||||
"password" => "password"
|
||||
]
|
||||
],
|
||||
[
|
||||
"localhost",
|
||||
[
|
||||
"scheme" => "http",
|
||||
"host" => "localhost",
|
||||
"port" => 80
|
||||
]
|
||||
],
|
||||
[
|
||||
"localhost",
|
||||
[
|
||||
"scheme" => "https",
|
||||
"host" => "localhost",
|
||||
"port" => 443
|
||||
]
|
||||
],
|
||||
[
|
||||
"localhost:4430",
|
||||
[
|
||||
"scheme" => "https",
|
||||
"host" => "localhost",
|
||||
"port" => 4430
|
||||
]
|
||||
],
|
||||
[
|
||||
"localhost:8080",
|
||||
[
|
||||
"scheme" => "http",
|
||||
"host" => "localhost",
|
||||
"port" => 8080
|
||||
]
|
||||
],
|
||||
[
|
||||
"user:password@localhost:4430",
|
||||
[
|
||||
"scheme" => "https",
|
||||
"user" => "user",
|
||||
"password" => "password",
|
||||
"host" => "localhost",
|
||||
"port" => 4430
|
||||
]
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// User Info
|
||||
|
||||
/**
|
||||
* @covers ::getUserInfo
|
||||
*/
|
||||
public function testDefaultUserInfoIsEmpty()
|
||||
{
|
||||
$uri = new Uri();
|
||||
$this->assertSame("", $uri->getUserInfo());
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::getUserInfo
|
||||
* @covers ::withUserInfo
|
||||
* @dataProvider userInfoProvider
|
||||
*
|
||||
* @param string $expected The combined user:password value
|
||||
* @param string $user The username to set
|
||||
* @param string $password The password to set
|
||||
*/
|
||||
public function testSetsUserInfo($expected, $user, $password)
|
||||
{
|
||||
$uri = new Uri();
|
||||
$uri = $uri->withUserInfo($user, $password);
|
||||
$this->assertSame($expected, $uri->getUserInfo());
|
||||
}
|
||||
|
||||
public function userInfoProvider()
|
||||
{
|
||||
return [
|
||||
["user:password", "user", "password"],
|
||||
["user", "user", ""],
|
||||
["user", "user", null],
|
||||
["", "", "password"],
|
||||
["", "", ""]
|
||||
];
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// Host
|
||||
|
||||
/**
|
||||
* @covers ::getHost
|
||||
*/
|
||||
public function testDefaultHostIsEmpty()
|
||||
{
|
||||
$uri = new Uri();
|
||||
$this->assertSame("", $uri->getHost());
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::getHost
|
||||
* @covers ::withHost
|
||||
* @dataProvider hostProvider
|
||||
* @param $expected
|
||||
* @param $host
|
||||
*/
|
||||
public function testSetsHost($expected, $host)
|
||||
{
|
||||
$uri = new Uri();
|
||||
$uri = $uri->withHost($host);
|
||||
$this->assertSame($expected, $uri->getHost());
|
||||
}
|
||||
|
||||
public function hostProvider()
|
||||
{
|
||||
return [
|
||||
["", ""],
|
||||
["localhost", "localhost"],
|
||||
["localhost", "LOCALHOST"],
|
||||
["foo.com", "FOO.com"]
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::withHost
|
||||
* @expectedException \InvalidArgumentException
|
||||
* @dataProvider invalidHostProvider
|
||||
* @param $host
|
||||
*/
|
||||
public function testInvalidHostThrowsException($host)
|
||||
{
|
||||
$uri = new Uri();
|
||||
$uri->withHost($host);
|
||||
}
|
||||
|
||||
public function invalidHostProvider()
|
||||
{
|
||||
return [
|
||||
[null],
|
||||
[false],
|
||||
[0]
|
||||
];
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// Port
|
||||
|
||||
/**
|
||||
* @covers ::getPort
|
||||
*/
|
||||
public function testDefaultPortWithNoSchemeIsNull()
|
||||
{
|
||||
$uri = new Uri();
|
||||
$this->assertNull($uri->getPort());
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::getPort
|
||||
*/
|
||||
public function testDefaultPortForHttpSchemeIs80()
|
||||
{
|
||||
$uri = new Uri();
|
||||
$this->assertSame(80, $uri->withScheme("http")->getPort());
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::getPort
|
||||
*/
|
||||
public function testDefaultPortForHttpsSchemeIs443()
|
||||
{
|
||||
$uri = new Uri();
|
||||
$this->assertSame(443, $uri->withScheme("https")->getPort());
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::getPort
|
||||
* @covers ::withPort
|
||||
* @dataProvider portAndSchemeProvider
|
||||
*
|
||||
* @param int|null $expectedPort
|
||||
* @param string $scheme
|
||||
* @param int|null $port
|
||||
*/
|
||||
public function testReturnsPortWithSchemeDefaults($expectedPort, $scheme, $port)
|
||||
{
|
||||
$uri = new Uri();
|
||||
$uri = $uri->withScheme($scheme)->withPort($port);
|
||||
$this->assertSame($expectedPort, $uri->getPort());
|
||||
}
|
||||
|
||||
public function portAndSchemeProvider()
|
||||
{
|
||||
return [
|
||||
[null, "", null],
|
||||
[80, "http", null],
|
||||
[443, "https", null],
|
||||
[8080, "", 8080],
|
||||
[8080, "http", "8080"],
|
||||
[8080, "https", 8080.0]
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::withPort
|
||||
* @expectedException \InvalidArgumentException
|
||||
* @dataProvider invalidPortProvider
|
||||
* @param int $port
|
||||
*/
|
||||
public function testInvalidPortThrowsException($port)
|
||||
{
|
||||
$uri = new Uri();
|
||||
$uri->withPort($port);
|
||||
}
|
||||
|
||||
public function invalidPortProvider()
|
||||
{
|
||||
return [
|
||||
[true],
|
||||
[-1],
|
||||
[65536],
|
||||
["dog"]
|
||||
];
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// Path
|
||||
|
||||
/**
|
||||
* @covers ::getPath
|
||||
*/
|
||||
public function testDefaultPathIsEmpty()
|
||||
{
|
||||
$uri = new Uri();
|
||||
$this->assertSame("", $uri->getPath());
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::getPath
|
||||
* @covers ::withPath
|
||||
* @covers ::percentEncode
|
||||
* @dataProvider pathProvider
|
||||
* @param $expected
|
||||
* @param $path
|
||||
*/
|
||||
public function testSetsEncodedPath($expected, $path)
|
||||
{
|
||||
$uri = new Uri();
|
||||
$uri = $uri->withPath($path);
|
||||
$this->assertSame($expected, $uri->getPath());
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::getPath
|
||||
* @covers ::withPath
|
||||
* @covers ::percentEncode
|
||||
* @dataProvider pathProvider
|
||||
* @param $expected
|
||||
* @param $path
|
||||
*/
|
||||
public function testDoesNotDoubleEncodePath($expected, $path)
|
||||
{
|
||||
$uri = new Uri();
|
||||
$uri = $uri->withPath($path);
|
||||
$uri = $uri->withPath($uri->getPath());
|
||||
$this->assertSame($expected, $uri->getPath());
|
||||
}
|
||||
|
||||
public function pathProvider()
|
||||
{
|
||||
return [
|
||||
["", ""],
|
||||
["/", "/"],
|
||||
["*", "*"],
|
||||
["/my/path", "/my/path"],
|
||||
["/encoded%2Fslash", "/encoded%2Fslash"],
|
||||
["/percent/%25", "/percent/%"],
|
||||
["/%C3%A1%C3%A9%C3%AD%C3%B3%C3%BA", "/áéíóú"]
|
||||
];
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// Query
|
||||
|
||||
/**
|
||||
* @covers ::getQuery
|
||||
*/
|
||||
public function testDefaultQueryIsEmpty()
|
||||
{
|
||||
$uri = new Uri();
|
||||
$this->assertSame("", $uri->getQuery());
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::getQuery
|
||||
* @covers ::withQuery
|
||||
* @covers ::percentEncode
|
||||
* @dataProvider queryProvider
|
||||
* @param $expected
|
||||
* @param $query
|
||||
*/
|
||||
public function testSetsEncodedQuery($expected, $query)
|
||||
{
|
||||
$uri = new Uri();
|
||||
$uri = $uri->withQuery($query);
|
||||
$this->assertSame($expected, $uri->getQuery());
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::getQuery
|
||||
* @covers ::withQuery
|
||||
* @covers ::percentEncode
|
||||
* @dataProvider queryProvider
|
||||
* @param $expected
|
||||
* @param $query
|
||||
*/
|
||||
public function testDoesNotDoubleEncodeQuery($expected, $query)
|
||||
{
|
||||
$uri = new Uri();
|
||||
$uri = $uri->withQuery($query);
|
||||
$uri = $uri->withQuery($uri->getQuery($query));
|
||||
$this->assertSame($expected, $uri->getQuery());
|
||||
}
|
||||
|
||||
public function queryProvider()
|
||||
{
|
||||
return [
|
||||
["cat=molly", "cat=molly"],
|
||||
["cat=molly&dog=bear", "cat=molly&dog=bear"],
|
||||
["accents=%C3%A1%C3%A9%C3%AD%C3%B3%C3%BA", "accents=áéíóú"]
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::withPath
|
||||
* @expectedException \InvalidArgumentException
|
||||
* @dataProvider invalidPathProvider
|
||||
* @param $path
|
||||
*/
|
||||
public function testInvalidPathThrowsException($path)
|
||||
{
|
||||
$uri = new Uri();
|
||||
$uri->withPath($path);
|
||||
}
|
||||
|
||||
public function invalidPathProvider()
|
||||
{
|
||||
return [
|
||||
[null],
|
||||
[false],
|
||||
[0]
|
||||
];
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// Fragment
|
||||
|
||||
/**
|
||||
* @covers ::getFragment
|
||||
*/
|
||||
public function testDefaultFragmentIsEmpty()
|
||||
{
|
||||
$uri = new Uri();
|
||||
$this->assertSame("", $uri->getFragment());
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::getFragment
|
||||
* @covers ::withFragment
|
||||
* @covers ::percentEncode
|
||||
* @dataProvider fragmentProvider
|
||||
* @param $expected
|
||||
* @param $fragment
|
||||
*/
|
||||
public function testSetsEncodedFragment($expected, $fragment)
|
||||
{
|
||||
$uri = new Uri();
|
||||
$uri = $uri->withFragment($fragment);
|
||||
$this->assertSame($expected, $uri->getFragment());
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::getFragment
|
||||
* @covers ::withFragment
|
||||
* @covers ::percentEncode
|
||||
* @dataProvider fragmentProvider
|
||||
* @param $expected
|
||||
* @param $fragment
|
||||
*/
|
||||
public function testDoesNotDoubleEncodeFragment($expected, $fragment)
|
||||
{
|
||||
$uri = new Uri();
|
||||
$uri = $uri->withFragment($fragment);
|
||||
$uri = $uri->withFragment($uri->getFragment($fragment));
|
||||
$this->assertSame($expected, $uri->getFragment());
|
||||
}
|
||||
|
||||
public function fragmentProvider()
|
||||
{
|
||||
return [
|
||||
["", null],
|
||||
["molly", "molly"],
|
||||
["%C3%A1%C3%A9%C3%AD%C3%B3%C3%BA", "áéíóú"]
|
||||
];
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// Concatenation
|
||||
|
||||
/**
|
||||
* @covers ::__toString
|
||||
* @dataProvider componentProvider
|
||||
* @param string $expected
|
||||
* @param array $components
|
||||
*/
|
||||
public function testConcatenatesComponents($expected, $components)
|
||||
{
|
||||
$uri = new Uri();
|
||||
|
||||
if (isset($components["scheme"])) {
|
||||
$uri = $uri->withScheme($components["scheme"]);
|
||||
}
|
||||
|
||||
if (isset($components["user"])) {
|
||||
$user = $components["user"];
|
||||
$password = null;
|
||||
if (isset($components["password"])) {
|
||||
$password = $components["password"];
|
||||
}
|
||||
$uri = $uri->withUserInfo($user, $password);
|
||||
}
|
||||
|
||||
if (isset($components["host"])) {
|
||||
$uri = $uri->withHost($components["host"]);
|
||||
}
|
||||
|
||||
if (isset($components["port"])) {
|
||||
$uri = $uri->withPort($components["port"]);
|
||||
}
|
||||
|
||||
if (isset($components["path"])) {
|
||||
$uri = $uri->withPath($components["path"]);
|
||||
}
|
||||
|
||||
if (isset($components["query"])) {
|
||||
$uri = $uri->withQuery($components["query"]);
|
||||
}
|
||||
|
||||
if (isset($components["fragment"])) {
|
||||
$uri = $uri->withFragment($components["fragment"]);
|
||||
}
|
||||
|
||||
$this->assertEquals($expected, (string) $uri);
|
||||
}
|
||||
|
||||
public function componentProvider()
|
||||
{
|
||||
return [
|
||||
[
|
||||
"http://localhost/path",
|
||||
[
|
||||
"scheme" => "http",
|
||||
"host" => "localhost",
|
||||
"path" => "/path"
|
||||
]
|
||||
],
|
||||
[
|
||||
"//localhost/path",
|
||||
[
|
||||
"host" => "localhost",
|
||||
"path" => "/path"
|
||||
]
|
||||
],
|
||||
[
|
||||
"/path",
|
||||
[
|
||||
"path" => "/path"
|
||||
]
|
||||
],
|
||||
[
|
||||
"/path?cat=molly&dog=bear",
|
||||
[
|
||||
"path" => "/path",
|
||||
"query" => "cat=molly&dog=bear"
|
||||
]
|
||||
],
|
||||
[
|
||||
"/path?cat=molly&dog=bear#fragment",
|
||||
[
|
||||
"path" => "/path",
|
||||
"query" => "cat=molly&dog=bear",
|
||||
"fragment" => "fragment"
|
||||
]
|
||||
],
|
||||
[
|
||||
"https://user:password@localhost:4430/path?cat=molly&dog=bear#fragment",
|
||||
[
|
||||
"scheme" => "https",
|
||||
"user" => "user",
|
||||
"password" => "password",
|
||||
"host" => "localhost",
|
||||
"port" => 4430,
|
||||
"path" => "/path",
|
||||
"query" => "cat=molly&dog=bear",
|
||||
"fragment" => "fragment"
|
||||
]
|
||||
],
|
||||
// Asterisk Form
|
||||
[
|
||||
"*",
|
||||
[
|
||||
"path" => "*"
|
||||
]
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::__construct()
|
||||
* @covers ::__toString()
|
||||
* @dataProvider stringUriProvider
|
||||
*/
|
||||
public function testUriCreatedFromStringNormalizesString($expected, $input)
|
||||
{
|
||||
$uri = new Uri($input);
|
||||
$this->assertSame($expected, (string) $uri);
|
||||
}
|
||||
|
||||
public function stringUriProvider()
|
||||
{
|
||||
return [
|
||||
[
|
||||
"http://localhost/path",
|
||||
"http://localhost:80/path"
|
||||
],
|
||||
[
|
||||
"https://localhost/path",
|
||||
"https://localhost:443/path"
|
||||
],
|
||||
[
|
||||
"https://my.sub.sub.domain.com/path",
|
||||
"https://my.sub.sub.domain.com/path"
|
||||
],
|
||||
[
|
||||
"https://user:password@localhost:4430/path?cat=molly&dog=bear#fragment",
|
||||
"https://user:password@localhost:4430/path?cat=molly&dog=bear#fragment"
|
||||
],
|
||||
[
|
||||
"/path",
|
||||
"/path"
|
||||
],
|
||||
[
|
||||
"//double/slash",
|
||||
"//double/slash"
|
||||
],
|
||||
[
|
||||
"no/slash",
|
||||
"no/slash"
|
||||
],
|
||||
[
|
||||
"*",
|
||||
"*"
|
||||
]
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
@ -1,300 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace WellRESTed\Test\Unit\Routing;
|
||||
|
||||
use Prophecy\Argument;
|
||||
use WellRESTed\Routing\MethodMap;
|
||||
|
||||
/**
|
||||
* @coversDefaultClass WellRESTed\Routing\MethodMap
|
||||
* @uses WellRESTed\Routing\MethodMap
|
||||
* @uses WellRESTed\Dispatching\Dispatcher
|
||||
* @group routing
|
||||
*/
|
||||
class MethodMapTest extends \PHPUnit_Framework_TestCase
|
||||
{
|
||||
private $dispatcher;
|
||||
private $request;
|
||||
private $response;
|
||||
private $next;
|
||||
private $middleware;
|
||||
|
||||
public function setUp()
|
||||
{
|
||||
$this->request = $this->prophesize('Psr\Http\Message\ServerRequestInterface');
|
||||
$this->response = $this->prophesize('Psr\Http\Message\ResponseInterface');
|
||||
$this->response->withStatus(Argument::any())->willReturn($this->response->reveal());
|
||||
$this->response->withHeader(Argument::cetera())->willReturn($this->response->reveal());
|
||||
$this->next = function ($request, $response) {
|
||||
return $response;
|
||||
};
|
||||
$this->middleware = $this->prophesize('WellRESTed\MiddlewareInterface');
|
||||
$this->middleware->__invoke(Argument::cetera())->willReturn();
|
||||
$this->dispatcher = $this->prophesize('WellRESTed\Dispatching\DispatcherInterface');
|
||||
$this->dispatcher->dispatch(Argument::cetera())->will(
|
||||
function ($args) {
|
||||
list($middleware, $request, $response, $next) = $args;
|
||||
return $middleware($request, $response, $next);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::__construct
|
||||
*/
|
||||
public function testCreatesInstance()
|
||||
{
|
||||
$methodMap = new MethodMap($this->dispatcher->reveal());
|
||||
$this->assertNotNull($methodMap);
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::__invoke
|
||||
* @covers ::dispatchMiddleware
|
||||
* @covers ::register
|
||||
*/
|
||||
public function testDispatchesMiddlewareWithMatchingMethod()
|
||||
{
|
||||
$this->request->getMethod()->willReturn("GET");
|
||||
|
||||
$map = new MethodMap($this->dispatcher->reveal());
|
||||
$map->register("GET", $this->middleware->reveal());
|
||||
$map($this->request->reveal(), $this->response->reveal(), $this->next);
|
||||
|
||||
$this->middleware->__invoke(
|
||||
$this->request->reveal(),
|
||||
$this->response->reveal(),
|
||||
$this->next
|
||||
)->shouldHaveBeenCalled();
|
||||
}
|
||||
|
||||
/**
|
||||
* @coversNothing
|
||||
*/
|
||||
public function testTreatsMethodNamesCaseSensitively()
|
||||
{
|
||||
$this->request->getMethod()->willReturn("get");
|
||||
|
||||
$middlewareUpper = $this->prophesize('WellRESTed\MiddlewareInterface');
|
||||
$middlewareUpper->__invoke(Argument::cetera())->willReturn();
|
||||
|
||||
$middlewareLower = $this->prophesize('WellRESTed\MiddlewareInterface');
|
||||
$middlewareLower->__invoke(Argument::cetera())->willReturn();
|
||||
|
||||
$map = new MethodMap($this->dispatcher->reveal());
|
||||
$map->register("GET", $middlewareUpper->reveal());
|
||||
$map->register("get", $middlewareLower->reveal());
|
||||
$map($this->request->reveal(), $this->response->reveal(), $this->next);
|
||||
|
||||
$middlewareLower->__invoke(
|
||||
$this->request->reveal(),
|
||||
$this->response->reveal(),
|
||||
$this->next
|
||||
)->shouldHaveBeenCalled();
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::__invoke
|
||||
* @covers ::dispatchMiddleware
|
||||
* @covers ::register
|
||||
*/
|
||||
public function testDispatchesWildcardMiddlewareWithNonMatchingMethod()
|
||||
{
|
||||
$this->request->getMethod()->willReturn("GET");
|
||||
|
||||
$map = new MethodMap($this->dispatcher->reveal());
|
||||
$map->register("*", $this->middleware->reveal());
|
||||
$map($this->request->reveal(), $this->response->reveal(), $this->next);
|
||||
|
||||
$this->middleware->__invoke(
|
||||
$this->request->reveal(),
|
||||
$this->response->reveal(),
|
||||
$this->next
|
||||
)->shouldHaveBeenCalled();
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::__invoke
|
||||
*/
|
||||
public function testDispatchesGetMiddlewareForHeadByDefault()
|
||||
{
|
||||
$this->request->getMethod()->willReturn("HEAD");
|
||||
|
||||
$map = new MethodMap($this->dispatcher->reveal());
|
||||
$map->register("GET", $this->middleware->reveal());
|
||||
$map($this->request->reveal(), $this->response->reveal(), $this->next);
|
||||
|
||||
$this->middleware->__invoke(
|
||||
$this->request->reveal(),
|
||||
$this->response->reveal(),
|
||||
$this->next
|
||||
)->shouldHaveBeenCalled();
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::register
|
||||
*/
|
||||
public function testRegistersMiddlewareForMultipleMethods()
|
||||
{
|
||||
$map = new MethodMap($this->dispatcher->reveal());
|
||||
$map->register("GET,POST", $this->middleware->reveal());
|
||||
|
||||
$this->request->getMethod()->willReturn("GET");
|
||||
$map($this->request->reveal(), $this->response->reveal(), $this->next);
|
||||
|
||||
$this->request->getMethod()->willReturn("POST");
|
||||
$map($this->request->reveal(), $this->response->reveal(), $this->next);
|
||||
|
||||
$this->middleware->__invoke(
|
||||
$this->request->reveal(),
|
||||
$this->response->reveal(),
|
||||
$this->next
|
||||
)->shouldHaveBeenCalledTimes(2);
|
||||
}
|
||||
|
||||
public function testSettingNullUnregistersMiddleware()
|
||||
{
|
||||
$this->request->getMethod()->willReturn("POST");
|
||||
|
||||
$map = new MethodMap($this->dispatcher->reveal());
|
||||
$map->register("POST", $this->middleware->reveal());
|
||||
$map->register("POST", null);
|
||||
$map($this->request->reveal(), $this->response->reveal(), $this->next);
|
||||
|
||||
$this->response->withStatus(405)->shouldHaveBeenCalled();
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::__invoke
|
||||
* @covers ::addAllowHeader
|
||||
* @covers ::getAllowedMethods
|
||||
*/
|
||||
public function testSetsStatusTo200ForOptions()
|
||||
{
|
||||
$this->request->getMethod()->willReturn("OPTIONS");
|
||||
|
||||
$map = new MethodMap($this->dispatcher->reveal());
|
||||
$map->register("GET", $this->middleware->reveal());
|
||||
$map($this->request->reveal(), $this->response->reveal(), $this->next);
|
||||
|
||||
$this->response->withStatus(200)->shouldHaveBeenCalled();
|
||||
}
|
||||
|
||||
public function testStopsPropagatingAfterOptions()
|
||||
{
|
||||
$calledNext = false;
|
||||
$next = function ($request, $response) use (&$calledNext) {
|
||||
$calledNext = true;
|
||||
return $response;
|
||||
};
|
||||
|
||||
$this->request->getMethod()->willReturn("OPTIONS");
|
||||
|
||||
$map = new MethodMap($this->dispatcher->reveal());
|
||||
$map->register("GET", $this->middleware->reveal());
|
||||
$map($this->request->reveal(), $this->response->reveal(), $next);
|
||||
|
||||
$this->assertFalse($calledNext);
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::__invoke
|
||||
* @covers ::addAllowHeader
|
||||
* @covers ::getAllowedMethods
|
||||
* @dataProvider allowedMethodProvider
|
||||
*/
|
||||
public function testSetsAllowHeaderForOptions($methodsDeclared, $methodsAllowed)
|
||||
{
|
||||
$this->request->getMethod()->willReturn("OPTIONS");
|
||||
|
||||
$map = new MethodMap($this->dispatcher->reveal());
|
||||
foreach ($methodsDeclared as $method) {
|
||||
$map->register($method, $this->middleware->reveal());
|
||||
}
|
||||
$map($this->request->reveal(), $this->response->reveal(), $this->next);
|
||||
|
||||
$containsAllMethods = function ($headerValue) use ($methodsAllowed) {
|
||||
foreach ($methodsAllowed as $method) {
|
||||
if (strpos($headerValue, $method) === false) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
};
|
||||
$this->response->withHeader("Allow", Argument::that($containsAllMethods))->shouldHaveBeenCalled();
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::__invoke
|
||||
* @covers ::addAllowHeader
|
||||
* @covers ::getAllowedMethods
|
||||
* @dataProvider allowedMethodProvider
|
||||
*/
|
||||
public function testSetsStatusTo405ForBadMethod()
|
||||
{
|
||||
$this->request->getMethod()->willReturn("POST");
|
||||
|
||||
$map = new MethodMap($this->dispatcher->reveal());
|
||||
$map->register("GET", $this->middleware->reveal());
|
||||
$map($this->request->reveal(), $this->response->reveal(), $this->next);
|
||||
|
||||
$this->response->withStatus(405)->shouldHaveBeenCalled();
|
||||
}
|
||||
|
||||
/**
|
||||
* @coversNothing
|
||||
* @dataProvider allowedMethodProvider
|
||||
*/
|
||||
public function testStopsPropagatingAfterBadMethod()
|
||||
{
|
||||
$calledNext = false;
|
||||
$next = function ($request, $response) use (&$calledNext) {
|
||||
$calledNext = true;
|
||||
return $response;
|
||||
};
|
||||
$this->request->getMethod()->willReturn("POST");
|
||||
|
||||
$map = new MethodMap($this->dispatcher->reveal());
|
||||
$map->register("GET", $this->middleware->reveal());
|
||||
$map($this->request->reveal(), $this->response->reveal(), $next);
|
||||
$this->assertFalse($calledNext);
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::__invoke
|
||||
* @covers ::addAllowHeader
|
||||
* @covers ::getAllowedMethods
|
||||
* @dataProvider allowedMethodProvider
|
||||
*/
|
||||
public function testSetsAllowHeaderForBadMethod($methodsDeclared, $methodsAllowed)
|
||||
{
|
||||
$this->request->getMethod()->willReturn("BAD");
|
||||
|
||||
$map = new MethodMap($this->dispatcher->reveal());
|
||||
foreach ($methodsDeclared as $method) {
|
||||
$map->register($method, $this->middleware->reveal());
|
||||
}
|
||||
$map($this->request->reveal(), $this->response->reveal(), $this->next);
|
||||
|
||||
$containsAllMethods = function ($headerValue) use ($methodsAllowed) {
|
||||
foreach ($methodsAllowed as $method) {
|
||||
if (strpos($headerValue, $method) === false) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
};
|
||||
$this->response->withHeader("Allow", Argument::that($containsAllMethods))->shouldHaveBeenCalled();
|
||||
}
|
||||
|
||||
public function allowedMethodProvider()
|
||||
{
|
||||
return [
|
||||
[["GET"], ["GET", "HEAD", "OPTIONS"]],
|
||||
[["GET", "POST"], ["GET", "POST", "HEAD", "OPTIONS"]],
|
||||
[["POST"], ["POST", "OPTIONS"]],
|
||||
[["POST"], ["POST", "OPTIONS"]],
|
||||
[["GET", "PUT,DELETE"], ["GET", "PUT", "DELETE", "HEAD", "OPTIONS"]],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
@ -1,77 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace WellRESTed\Test\Unit\Routing\Route;
|
||||
|
||||
use Prophecy\Argument;
|
||||
use WellRESTed\Routing\Route\PrefixRoute;
|
||||
use WellRESTed\Routing\Route\RouteInterface;
|
||||
|
||||
/**
|
||||
* @coversDefaultClass WellRESTed\Routing\Route\PrefixRoute
|
||||
* @uses WellRESTed\Routing\Route\PrefixRoute
|
||||
* @uses WellRESTed\Routing\Route\Route
|
||||
* @group route
|
||||
* @group routing
|
||||
*/
|
||||
class PrefixRouteTest extends \PHPUnit_Framework_TestCase
|
||||
{
|
||||
/**
|
||||
* @covers ::__construct
|
||||
*/
|
||||
public function testTrimsAsteriskFromEndOfTarget()
|
||||
{
|
||||
$methodMap = $this->prophesize('WellRESTed\Routing\MethodMapInterface');
|
||||
$route = new PrefixRoute("/cats/*", $methodMap->reveal());
|
||||
$this->assertEquals("/cats/", $route->getTarget());
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::getType
|
||||
*/
|
||||
public function testReturnsPrefixType()
|
||||
{
|
||||
$methodMap = $this->prophesize('WellRESTed\Routing\MethodMapInterface');
|
||||
$route = new PrefixRoute("/*", $methodMap->reveal());
|
||||
$this->assertSame(RouteInterface::TYPE_PREFIX, $route->getType());
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::getPathVariables
|
||||
*/
|
||||
public function testReturnsEmptyArrayForPathVariables()
|
||||
{
|
||||
$methodMap = $this->prophesize('WellRESTed\Routing\MethodMapInterface');
|
||||
$route = new PrefixRoute("/*", $methodMap->reveal());
|
||||
$this->assertSame([], $route->getPathVariables());
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::matchesRequestTarget
|
||||
*/
|
||||
public function testMatchesExactRequestTarget()
|
||||
{
|
||||
$methodMap = $this->prophesize('WellRESTed\Routing\MethodMapInterface');
|
||||
$route = new PrefixRoute("/*", $methodMap->reveal());
|
||||
$this->assertTrue($route->matchesRequestTarget("/"));
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::matchesRequestTarget
|
||||
*/
|
||||
public function testMatchesRequestTargetWithSamePrefix()
|
||||
{
|
||||
$methodMap = $this->prophesize('WellRESTed\Routing\MethodMapInterface');
|
||||
$route = new PrefixRoute("/*", $methodMap->reveal());
|
||||
$this->assertTrue($route->matchesRequestTarget("/cats/"));
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::matchesRequestTarget
|
||||
*/
|
||||
public function testDoesNotMatchNonmatchingRequestTarget()
|
||||
{
|
||||
$methodMap = $this->prophesize('WellRESTed\Routing\MethodMapInterface');
|
||||
$route = new PrefixRoute("/animals/cats/", $methodMap->reveal());
|
||||
$this->assertFalse($route->matchesRequestTarget("/animals/dogs/"));
|
||||
}
|
||||
}
|
||||
|
|
@ -1,126 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace WellRESTed\Test\Unit\Routing\Route;
|
||||
|
||||
use Prophecy\Argument;
|
||||
use WellRESTed\Routing\Route\RegexRoute;
|
||||
use WellRESTed\Routing\Route\RouteInterface;
|
||||
|
||||
/**
|
||||
* @coversDefaultClass WellRESTed\Routing\Route\RegexRoute
|
||||
* @uses WellRESTed\Routing\Route\RegexRoute
|
||||
* @uses WellRESTed\Routing\Route\Route
|
||||
* @group route
|
||||
* @group routing
|
||||
*/
|
||||
class RegexRouteTest extends \PHPUnit_Framework_TestCase
|
||||
{
|
||||
private $methodMap;
|
||||
|
||||
public function setUp()
|
||||
{
|
||||
$this->methodMap = $this->prophesize('WellRESTed\Routing\MethodMapInterface');
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::getType
|
||||
*/
|
||||
public function testReturnsPatternType()
|
||||
{
|
||||
$route = new RegexRoute("/", $this->methodMap->reveal());
|
||||
$this->assertSame(RouteInterface::TYPE_PATTERN, $route->getType());
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::matchesRequestTarget
|
||||
* @dataProvider matchingRouteProvider
|
||||
*/
|
||||
public function testMatchesTarget($pattern, $path)
|
||||
{
|
||||
$route = new RegexRoute($pattern, $this->methodMap->reveal());
|
||||
$this->assertTrue($route->matchesRequestTarget($path));
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::matchesRequestTarget
|
||||
* @dataProvider matchingRouteProvider
|
||||
*/
|
||||
public function testMatchesTargetByRegex($pattern, $target)
|
||||
{
|
||||
$route = new RegexRoute($pattern, $this->methodMap->reveal());
|
||||
$this->assertTrue($route->matchesRequestTarget($target));
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::getPathVariables
|
||||
* @dataProvider matchingRouteProvider
|
||||
*/
|
||||
public function testExtractsPathVariablesByRegex($pattern, $target, $expectedCaptures)
|
||||
{
|
||||
$route = new RegexRoute($pattern, $this->methodMap->reveal());
|
||||
$route->matchesRequestTarget($target);
|
||||
$this->assertEquals($expectedCaptures, $route->getPathVariables());
|
||||
}
|
||||
|
||||
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"
|
||||
]]
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::matchesRequestTarget
|
||||
* @dataProvider mismatchingRouteProvider
|
||||
*/
|
||||
public function testDoesNotMatchNonmatchingTarget($pattern, $path)
|
||||
{
|
||||
$route = new RegexRoute($pattern, $this->methodMap->reveal());
|
||||
$this->assertFalse($route->matchesRequestTarget($path));
|
||||
}
|
||||
|
||||
public function mismatchingRouteProvider()
|
||||
{
|
||||
return [
|
||||
["~/cat/[0-9]+~", "/cat/molly"],
|
||||
["~/cat/[0-9]+~", "/dog/bear"],
|
||||
["#/dog/.*#", "/dog"]
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::matchesRequestTarget
|
||||
* @dataProvider invalidRouteProvider
|
||||
* @expectedException \RuntimeException
|
||||
*/
|
||||
public function testThrowsExceptionOnInvalidPattern($pattern)
|
||||
{
|
||||
$route = new RegexRoute($pattern, $this->methodMap->reveal());
|
||||
\PHPUnit_Framework_Error_Warning::$enabled = false;
|
||||
\PHPUnit_Framework_Error_Notice::$enabled = false;
|
||||
$level = error_reporting();
|
||||
error_reporting($level & ~E_WARNING);
|
||||
$route->matchesRequestTarget("/");
|
||||
error_reporting($level);
|
||||
\PHPUnit_Framework_Error_Warning::$enabled = true;
|
||||
\PHPUnit_Framework_Error_Notice::$enabled = true;
|
||||
}
|
||||
|
||||
public function invalidRouteProvider()
|
||||
{
|
||||
return [
|
||||
["~/unterminated"],
|
||||
["/nope"]
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
@ -1,56 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace WellRESTed\Test\Unit\Routing\Route;
|
||||
|
||||
use Prophecy\Argument;
|
||||
use WellRESTed\Routing\Route\RouteFactory;
|
||||
use WellRESTed\Routing\Route\RouteInterface;
|
||||
|
||||
/**
|
||||
* @covers WellRESTed\Routing\Route\RouteFactory
|
||||
* @uses WellRESTed\Routing\Route\TemplateRoute
|
||||
* @uses WellRESTed\Routing\Route\RegexRoute
|
||||
* @uses WellRESTed\Routing\Route\PrefixRoute
|
||||
* @uses WellRESTed\Routing\Route\StaticRoute
|
||||
* @uses WellRESTed\Routing\Route\Route
|
||||
* @uses WellRESTed\Routing\MethodMap
|
||||
* @group route
|
||||
* @group routing
|
||||
*/
|
||||
class RouteFactoryTest extends \PHPUnit_Framework_TestCase
|
||||
{
|
||||
private $dispatcher;
|
||||
|
||||
public function setUp()
|
||||
{
|
||||
$this->dispatcher = $this->prophesize('WellRESTed\Dispatching\DispatcherInterface');
|
||||
}
|
||||
|
||||
public function testCreatesStaticRoute()
|
||||
{
|
||||
$factory = new RouteFactory($this->dispatcher->reveal());
|
||||
$route = $factory->create("/cats/");
|
||||
$this->assertSame(RouteInterface::TYPE_STATIC, $route->getType());
|
||||
}
|
||||
|
||||
public function testCreatesPrefixRoute()
|
||||
{
|
||||
$factory = new RouteFactory($this->dispatcher->reveal());
|
||||
$route = $factory->create("/cats/*");
|
||||
$this->assertSame(RouteInterface::TYPE_PREFIX, $route->getType());
|
||||
}
|
||||
|
||||
public function testCreatesRegexRoute()
|
||||
{
|
||||
$factory = new RouteFactory($this->dispatcher->reveal());
|
||||
$route = $factory->create("~/cat/[0-9]+~");
|
||||
$this->assertSame(RouteInterface::TYPE_PATTERN, $route->getType());
|
||||
}
|
||||
|
||||
public function testCreatesTemplateRoute()
|
||||
{
|
||||
$factory = new RouteFactory($this->dispatcher->reveal());
|
||||
$route = $factory->create("/cat/{id}");
|
||||
$this->assertSame(RouteInterface::TYPE_PATTERN, $route->getType());
|
||||
}
|
||||
}
|
||||
|
|
@ -1,72 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace WellRESTed\Test\Unit\Routing\Route;
|
||||
|
||||
use Prophecy\Argument;
|
||||
|
||||
/**
|
||||
* @coversDefaultClass WellRESTed\Routing\Route\Route
|
||||
* @uses WellRESTed\Routing\Route\Route
|
||||
* @group route
|
||||
* @group routing
|
||||
*/
|
||||
class RouteTest extends \PHPUnit_Framework_TestCase
|
||||
{
|
||||
/**
|
||||
* @covers ::__construct
|
||||
*/
|
||||
public function testCreatesInstance()
|
||||
{
|
||||
$methodMap = $this->prophesize('WellRESTed\Routing\MethodMapInterface');
|
||||
$route = $this->getMockForAbstractClass(
|
||||
'WellRESTed\Routing\Route\Route',
|
||||
["/target", $methodMap->reveal()]);
|
||||
$this->assertNotNull($route);
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::getTarget
|
||||
*/
|
||||
public function testReturnsTarget()
|
||||
{
|
||||
$methodMap = $this->prophesize('WellRESTed\Routing\MethodMapInterface');
|
||||
$route = $this->getMockForAbstractClass(
|
||||
'WellRESTed\Routing\Route\Route',
|
||||
["/target", $methodMap->reveal()]);
|
||||
$this->assertSame("/target", $route->getTarget());
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::getMethodMap
|
||||
*/
|
||||
public function testReturnsMethodMap()
|
||||
{
|
||||
$methodMap = $this->prophesize('WellRESTed\Routing\MethodMapInterface');
|
||||
$route = $this->getMockForAbstractClass(
|
||||
'WellRESTed\Routing\Route\Route',
|
||||
["/target", $methodMap->reveal()]);
|
||||
$this->assertSame($methodMap->reveal(), $route->getMethodMap());
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::__invoke
|
||||
*/
|
||||
public function testDispatchesMethodMap()
|
||||
{
|
||||
$methodMap = $this->prophesize('WellRESTed\Routing\MethodMapInterface');
|
||||
$methodMap->__invoke(Argument::cetera())->willReturn();
|
||||
|
||||
$route = $this->getMockForAbstractClass(
|
||||
'WellRESTed\Routing\Route\Route',
|
||||
["/target", $methodMap->reveal()]);
|
||||
|
||||
$request = $this->prophesize('Psr\Http\Message\ServerRequestInterface')->reveal();
|
||||
$response = $this->prophesize('Psr\Http\Message\ResponseInterface')->reveal();
|
||||
$next = function ($request, $response) {
|
||||
return $response;
|
||||
};
|
||||
$route->__invoke($request, $response, $next);
|
||||
|
||||
$methodMap->__invoke(Argument::cetera())->shouldHaveBeenCalled();
|
||||
}
|
||||
}
|
||||
|
|
@ -1,57 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace WellRESTed\Test\Unit\Routing\Route;
|
||||
|
||||
use Prophecy\Argument;
|
||||
use WellRESTed\Routing\Route\RouteInterface;
|
||||
use WellRESTed\Routing\Route\StaticRoute;
|
||||
|
||||
/**
|
||||
* @coversDefaultClass WellRESTed\Routing\Route\StaticRoute
|
||||
* @uses WellRESTed\Routing\Route\StaticRoute
|
||||
* @uses WellRESTed\Routing\Route\Route
|
||||
* @group route
|
||||
* @group routing
|
||||
*/
|
||||
class StaticRouteTest extends \PHPUnit_Framework_TestCase
|
||||
{
|
||||
/**
|
||||
* @covers ::getType
|
||||
*/
|
||||
public function testReturnsStaticType()
|
||||
{
|
||||
$methodMap = $this->prophesize('WellRESTed\Routing\MethodMapInterface');
|
||||
$route = new StaticRoute("/", $methodMap->reveal());
|
||||
$this->assertSame(RouteInterface::TYPE_STATIC, $route->getType());
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::matchesRequestTarget
|
||||
*/
|
||||
public function testMatchesExactRequestTarget()
|
||||
{
|
||||
$methodMap = $this->prophesize('WellRESTed\Routing\MethodMapInterface');
|
||||
$route = new StaticRoute("/", $methodMap->reveal());
|
||||
$this->assertTrue($route->matchesRequestTarget("/"));
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::getPathVariables
|
||||
*/
|
||||
public function testReturnsEmptyArrayForPathVariables()
|
||||
{
|
||||
$methodMap = $this->prophesize('WellRESTed\Routing\MethodMapInterface');
|
||||
$route = new StaticRoute("/", $methodMap->reveal());
|
||||
$this->assertSame([], $route->getPathVariables());
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::matchesRequestTarget
|
||||
*/
|
||||
public function testDoesNotMatchNonmatchingRequestTarget()
|
||||
{
|
||||
$methodMap = $this->prophesize('WellRESTed\Routing\MethodMapInterface');
|
||||
$route = new StaticRoute("/", $methodMap->reveal());
|
||||
$this->assertFalse($route->matchesRequestTarget("/cats/"));
|
||||
}
|
||||
}
|
||||
|
|
@ -1,298 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace WellRESTed\Test\Unit\Routing\Route;
|
||||
|
||||
use Prophecy\Argument;
|
||||
use WellRESTed\Routing\Route\RouteInterface;
|
||||
use WellRESTed\Routing\Route\TemplateRoute;
|
||||
|
||||
/**
|
||||
* @coversDefaultClass WellRESTed\Routing\Route\TemplateRoute
|
||||
* @uses WellRESTed\Routing\Route\TemplateRoute
|
||||
* @uses WellRESTed\Routing\Route\RegexRoute
|
||||
* @uses WellRESTed\Routing\Route\Route
|
||||
* @group route
|
||||
* @group routing
|
||||
*/
|
||||
class TemplateRouteTest extends \PHPUnit_Framework_TestCase
|
||||
{
|
||||
private $methodMap;
|
||||
|
||||
public function setUp()
|
||||
{
|
||||
$this->methodMap = $this->prophesize('WellRESTed\Routing\MethodMapInterface');
|
||||
}
|
||||
|
||||
private function getExpectedValues($keys)
|
||||
{
|
||||
$expectedValues = [
|
||||
"var" => "value",
|
||||
"hello" => "Hello World!",
|
||||
"x" => "1024",
|
||||
"y" => "768",
|
||||
"path" => "/foo/bar",
|
||||
"who" => "fred",
|
||||
"half" => "50%",
|
||||
"empty" => "",
|
||||
"count" => ["one", "two", "three"],
|
||||
"list" => ["red", "green", "blue"]
|
||||
];
|
||||
return array_intersect_key($expectedValues, array_flip($keys));
|
||||
}
|
||||
|
||||
private function assertArrayHasSameContents($expected, $actual)
|
||||
{
|
||||
ksort($expected);
|
||||
ksort($actual);
|
||||
$this->assertEquals($expected, $actual);
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @covers ::getType
|
||||
*/
|
||||
public function testReturnsPatternType()
|
||||
{
|
||||
$route = new TemplateRoute("/", $this->methodMap->reveal());
|
||||
$this->assertSame(RouteInterface::TYPE_PATTERN, $route->getType());
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// Matching
|
||||
|
||||
/**
|
||||
* @covers ::matchesRequestTarget
|
||||
* @covers ::matchesStartOfRequestTarget
|
||||
* @covers ::getMatchingPattern
|
||||
* @dataProvider nonMatchingTargetProvider
|
||||
* @param string $template
|
||||
* @param string $target
|
||||
*/
|
||||
public function testFailsToMatchNonMatchingTarget($template, $target)
|
||||
{
|
||||
$route = new TemplateRoute($template, $this->methodMap);
|
||||
$this->assertFalse($route->matchesRequestTarget($target));
|
||||
}
|
||||
|
||||
public function nonMatchingTargetProvider()
|
||||
{
|
||||
return [
|
||||
["/foo/{var}", "/bar/12", false, "Mismatch before first template expression"],
|
||||
["/foo/{foo}/bar/{bar}", "/foo/12/13", false, "Mismatch after first template expression"],
|
||||
["/hello/{hello}", "/hello/Hello%20World!", false, "Requires + operator to match reserver characters"]
|
||||
];
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// Matching :: Simple Strings
|
||||
|
||||
/**
|
||||
* @covers ::matchesRequestTarget
|
||||
* @covers ::getMatchingPattern
|
||||
* @covers ::uriVariableReplacementCallback
|
||||
* @dataProvider simpleStringProvider
|
||||
* @param string $template
|
||||
* @param string $target
|
||||
*/
|
||||
public function testMatchesSimpleStrings($template, $target)
|
||||
{
|
||||
$route = new TemplateRoute($template, $this->methodMap);
|
||||
$this->assertTrue($route->matchesRequestTarget($target));
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::getPathVariables
|
||||
* @covers ::processMatches
|
||||
* @covers ::uriVariableReplacementCallback
|
||||
* @dataProvider simpleStringProvider
|
||||
* @param string $template
|
||||
* @param string $target
|
||||
* @param string[] List of variables that should be extracted
|
||||
*/
|
||||
public function testCapturesFromSimpleStrings($template, $target, $variables)
|
||||
{
|
||||
$route = new TemplateRoute($template, $this->methodMap);
|
||||
$route->matchesRequestTarget($target);
|
||||
$this->assertArrayHasSameContents($this->getExpectedValues($variables), $route->getPathVariables());
|
||||
}
|
||||
|
||||
public function simpleStringProvider()
|
||||
{
|
||||
return [
|
||||
["/foo", "/foo", []],
|
||||
["/{var}", "/value", ["var"]],
|
||||
["/{hello}", "/Hello%20World%21", ["hello"]],
|
||||
["/{x,hello,y}", "/1024,Hello%20World%21,768", ["x", "hello", "y"]],
|
||||
["/{x,hello,y}", "/1024,Hello%20World%21,768", ["x", "hello", "y"]],
|
||||
];
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// Matching :: Reservered
|
||||
|
||||
/**
|
||||
* @covers ::matchesRequestTarget
|
||||
* @covers ::getMatchingPattern
|
||||
* @covers ::uriVariableReplacementCallback
|
||||
* @dataProvider reservedStringProvider
|
||||
* @param string $template
|
||||
* @param string $target
|
||||
*/
|
||||
public function testMatchesReserveredStrings($template, $target)
|
||||
{
|
||||
$route = new TemplateRoute($template, $this->methodMap);
|
||||
$this->assertTrue($route->matchesRequestTarget($target));
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::getPathVariables
|
||||
* @covers ::processMatches
|
||||
* @covers ::uriVariableReplacementCallback
|
||||
* @dataProvider reservedStringProvider
|
||||
* @param string $template
|
||||
* @param string $target
|
||||
* @param string[] List of variables that should be extracted
|
||||
*/
|
||||
public function testCapturesFromReservedStrings($template, $target, $variables)
|
||||
{
|
||||
$route = new TemplateRoute($template, $this->methodMap);
|
||||
$route->matchesRequestTarget($target);
|
||||
$this->assertSame($this->getExpectedValues($variables), $route->getPathVariables());
|
||||
}
|
||||
|
||||
public function reservedStringProvider()
|
||||
{
|
||||
return [
|
||||
["/{+var}", "/value", ["var"]],
|
||||
["/{+hello}", "/Hello%20World!", ["hello"]],
|
||||
["{+path}/here", "/foo/bar/here", ["path"]],
|
||||
];
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// Matching :: Label Expansion
|
||||
|
||||
/**
|
||||
* @covers ::matchesRequestTarget
|
||||
* @covers ::getMatchingPattern
|
||||
* @covers ::uriVariableReplacementCallback
|
||||
* @dataProvider labelWithDotPrefixProvider
|
||||
* @param string $template
|
||||
* @param string $target
|
||||
*/
|
||||
public function testMatchesLabelWithDotPrefix($template, $target)
|
||||
{
|
||||
$route = new TemplateRoute($template, $this->methodMap);
|
||||
$this->assertTrue($route->matchesRequestTarget($target));
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::getPathVariables
|
||||
* @covers ::processMatches
|
||||
* @covers ::uriVariableReplacementCallback
|
||||
* @dataProvider labelWithDotPrefixProvider
|
||||
* @param string $template
|
||||
* @param string $target
|
||||
* @param string[] List of variables that should be extracted
|
||||
*/
|
||||
public function testCapturesFromLabelWithDotPrefix($template, $target, $variables)
|
||||
{
|
||||
$route = new TemplateRoute($template, $this->methodMap);
|
||||
$route->matchesRequestTarget($target);
|
||||
$this->assertArrayHasSameContents($this->getExpectedValues($variables), $route->getPathVariables());
|
||||
}
|
||||
|
||||
public function labelWithDotPrefixProvider()
|
||||
{
|
||||
return [
|
||||
["/{.who}", "/.fred", ["who"]],
|
||||
["/{.half,who}", "/.50%25.fred", ["half", "who"]],
|
||||
["/X{.empty}", "/X.", ["empty"]]
|
||||
];
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// Matching :: Path Segments
|
||||
|
||||
/**
|
||||
* @covers ::matchesRequestTarget
|
||||
* @covers ::getMatchingPattern
|
||||
* @covers ::uriVariableReplacementCallback
|
||||
* @dataProvider pathSegmentProvider
|
||||
* @param string $template
|
||||
* @param string $target
|
||||
*/
|
||||
public function testMatchesPathSegments($template, $target)
|
||||
{
|
||||
$route = new TemplateRoute($template, $this->methodMap);
|
||||
$this->assertTrue($route->matchesRequestTarget($target));
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::getPathVariables
|
||||
* @covers ::processMatches
|
||||
* @covers ::uriVariableReplacementCallback
|
||||
* @dataProvider pathSegmentProvider
|
||||
* @param string $template
|
||||
* @param string $target
|
||||
* @param string[] List of variables that should be extracted
|
||||
*/
|
||||
public function testCapturesFromPathSegments($template, $target, $variables)
|
||||
{
|
||||
$route = new TemplateRoute($template, $this->methodMap);
|
||||
$route->matchesRequestTarget($target);
|
||||
$this->assertArrayHasSameContents($this->getExpectedValues($variables), $route->getPathVariables());
|
||||
}
|
||||
|
||||
public function pathSegmentProvider()
|
||||
{
|
||||
return [
|
||||
["{/who}", "/fred", ["who"]],
|
||||
["{/half,who}", "/50%25/fred", ["half", "who"]],
|
||||
["{/var,empty}", "/value/", ["var", "empty"]]
|
||||
];
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// Matching :: Explosion
|
||||
|
||||
/**
|
||||
* @covers ::matchesRequestTarget
|
||||
* @covers ::getMatchingPattern
|
||||
* @covers ::uriVariableReplacementCallback
|
||||
* @dataProvider pathExplosionProvider
|
||||
* @param string $template
|
||||
* @param string $target
|
||||
*/
|
||||
public function testMatchesExplosion($template, $target)
|
||||
{
|
||||
$route = new TemplateRoute($template, $this->methodMap);
|
||||
$this->assertTrue($route->matchesRequestTarget($target));
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::getPathVariables
|
||||
* @covers ::processMatches
|
||||
* @covers ::uriVariableReplacementCallback
|
||||
* @dataProvider pathExplosionProvider
|
||||
* @param string $template
|
||||
* @param string $target
|
||||
* @param string[] List of variables that should be extracted
|
||||
*/
|
||||
public function testCapturesFromExplosion($template, $target, $variables)
|
||||
{
|
||||
$route = new TemplateRoute($template, $this->methodMap);
|
||||
$route->matchesRequestTarget($target);
|
||||
$this->assertArrayHasSameContents($this->getExpectedValues($variables), $route->getPathVariables());
|
||||
}
|
||||
|
||||
public function pathExplosionProvider()
|
||||
{
|
||||
return [
|
||||
["/{count*}", "/one,two,three", ["count"]],
|
||||
["{/count*}", "/one/two/three", ["count"]],
|
||||
["X{.list*}", "X.red.green.blue", ["list"]]
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
@ -1,455 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace WellRESTed\Test\Unit\Routing;
|
||||
|
||||
use Prophecy\Argument;
|
||||
use WellRESTed\Routing\Route\RouteInterface;
|
||||
use WellRESTed\Routing\Router;
|
||||
|
||||
/**
|
||||
* @coversDefaultClass WellRESTed\Routing\Router
|
||||
* @uses WellRESTed\Routing\Router
|
||||
* @group routing
|
||||
*/
|
||||
class RouterTest extends \PHPUnit_Framework_TestCase
|
||||
{
|
||||
private $dispatcher;
|
||||
private $methodMap;
|
||||
private $factory;
|
||||
private $request;
|
||||
private $response;
|
||||
private $route;
|
||||
private $router;
|
||||
private $next;
|
||||
|
||||
public function setUp()
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->methodMap = $this->prophesize('WellRESTed\Routing\MethodMapInterface');
|
||||
$this->methodMap->register(Argument::cetera());
|
||||
|
||||
$this->route = $this->prophesize('WellRESTed\Routing\Route\RouteInterface');
|
||||
$this->route->__invoke(Argument::cetera())->willReturn();
|
||||
$this->route->getMethodMap()->willReturn($this->methodMap->reveal());
|
||||
$this->route->getType()->willReturn(RouteInterface::TYPE_STATIC);
|
||||
$this->route->getTarget()->willReturn("/");
|
||||
$this->route->getPathVariables()->willReturn([]);
|
||||
|
||||
$this->factory = $this->prophesize('WellRESTed\Routing\Route\RouteFactory');
|
||||
$this->factory->create(Argument::any())->willReturn($this->route->reveal());
|
||||
|
||||
$this->request = $this->prophesize('Psr\Http\Message\ServerRequestInterface');
|
||||
$this->request->withAttribute(Argument::cetera())->willReturn($this->request->reveal());
|
||||
|
||||
$this->response = $this->prophesize('Psr\Http\Message\ResponseInterface');
|
||||
$this->next = function ($request, $response) {
|
||||
return $response;
|
||||
};
|
||||
|
||||
$this->dispatcher = $this->prophesize('WellRESTed\Dispatching\DispatcherInterface');
|
||||
$this->dispatcher->dispatch(Argument::cetera())->will(
|
||||
function ($args) {
|
||||
list($middleware, $request, $response, $next) = $args;
|
||||
return $middleware->dispatch($request, $response, $next);
|
||||
}
|
||||
);
|
||||
|
||||
$this->router = $this->getMockBuilder('WellRESTed\Routing\Router')
|
||||
->setMethods(["getRouteFactory"])
|
||||
->disableOriginalConstructor()
|
||||
->getMock();
|
||||
$this->router->expects($this->any())
|
||||
->method("getRouteFactory")
|
||||
->will($this->returnValue($this->factory->reveal()));
|
||||
$this->router->__construct($this->dispatcher->reveal());
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// Construction
|
||||
|
||||
/**
|
||||
* @covers ::__construct
|
||||
* @covers ::getRouteFactory
|
||||
* @uses WellRESTed\Routing\Route\RouteFactory
|
||||
*/
|
||||
public function testCreatesInstance()
|
||||
{
|
||||
$router = new Router($this->dispatcher->reveal());
|
||||
$this->assertNotNull($router);
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// Populating
|
||||
|
||||
/**
|
||||
* @covers ::register
|
||||
* @covers ::getRouteForTarget
|
||||
* @covers ::registerRouteForTarget
|
||||
*/
|
||||
public function testCreatesRouteForTarget()
|
||||
{
|
||||
$this->router->register("GET", "/", "middleware");
|
||||
$this->factory->create("/")->shouldHaveBeenCalled();
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::register
|
||||
* @covers ::getRouteForTarget
|
||||
*/
|
||||
public function testDoesNotRecreateRouteForExistingTarget()
|
||||
{
|
||||
$this->router->register("GET", "/", "middleware");
|
||||
$this->router->register("POST", "/", "middleware");
|
||||
$this->factory->create("/")->shouldHaveBeenCalledTimes(1);
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::register
|
||||
*/
|
||||
public function testPassesMethodAndMiddlewareToMethodMap()
|
||||
{
|
||||
$this->router->register("GET", "/", "middleware");
|
||||
$this->methodMap->register("GET", "middleware")->shouldHaveBeenCalled();
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// Dispatching
|
||||
|
||||
/**
|
||||
* @covers ::__invoke
|
||||
* @covers ::getStaticRoute
|
||||
* @covers ::registerRouteForTarget
|
||||
*/
|
||||
public function testDispatchesStaticRoute()
|
||||
{
|
||||
$target = "/";
|
||||
|
||||
$this->request->getRequestTarget()->willReturn($target);
|
||||
$this->route->getTarget()->willReturn($target);
|
||||
$this->route->getType()->willReturn(RouteInterface::TYPE_STATIC);
|
||||
|
||||
$this->router->register("GET", $target, "middleware");
|
||||
$this->router->__invoke($this->request->reveal(), $this->response->reveal(), $this->next);
|
||||
|
||||
$this->route->__invoke($this->request->reveal(), $this->response->reveal(), $this->next)->shouldHaveBeenCalled();
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::__invoke
|
||||
* @covers ::getPrefixRoute
|
||||
* @covers ::registerRouteForTarget
|
||||
*/
|
||||
public function testDispatchesPrefixRoute()
|
||||
{
|
||||
$target = "/animals/cats/*";
|
||||
$this->request->getRequestTarget()->willReturn("/animals/cats/molly");
|
||||
$this->route->getTarget()->willReturn($target);
|
||||
$this->route->getType()->willReturn(RouteInterface::TYPE_PREFIX);
|
||||
|
||||
$this->router->register("GET", $target, "middleware");
|
||||
$this->router->__invoke($this->request->reveal(), $this->response->reveal(), $this->next);
|
||||
|
||||
$this->route->__invoke($this->request->reveal(), $this->response->reveal(), $this->next)->shouldHaveBeenCalled();
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::__invoke
|
||||
* @covers ::registerRouteForTarget
|
||||
*/
|
||||
public function testDispatchesPatternRoute()
|
||||
{
|
||||
$target = "/";
|
||||
|
||||
$this->request->getRequestTarget()->willReturn($target);
|
||||
$this->route->getTarget()->willReturn($target);
|
||||
$this->route->getType()->willReturn(RouteInterface::TYPE_PATTERN);
|
||||
$this->route->matchesRequestTarget(Argument::cetera())->willReturn(true);
|
||||
|
||||
$this->router->register("GET", $target, "middleware");
|
||||
$this->router->__invoke($this->request->reveal(), $this->response->reveal(), $this->next);
|
||||
|
||||
$this->route->__invoke($this->request->reveal(), $this->response->reveal(), $this->next)->shouldHaveBeenCalled();
|
||||
}
|
||||
|
||||
/**
|
||||
* @coversNothing
|
||||
*/
|
||||
public function testDispatchesStaticRouteBeforePrefixRoute()
|
||||
{
|
||||
$staticRoute = $this->prophesize('WellRESTed\Routing\Route\RouteInterface');
|
||||
$staticRoute->getMethodMap()->willReturn($this->methodMap->reveal());
|
||||
$staticRoute->getTarget()->willReturn("/cats/");
|
||||
$staticRoute->getType()->willReturn(RouteInterface::TYPE_STATIC);
|
||||
$staticRoute->__invoke(Argument::cetera())->willReturn();
|
||||
|
||||
$prefixRoute = $this->prophesize('WellRESTed\Routing\Route\RouteInterface');
|
||||
$prefixRoute->getMethodMap()->willReturn($this->methodMap->reveal());
|
||||
$prefixRoute->getTarget()->willReturn("/cats/*");
|
||||
$prefixRoute->getType()->willReturn(RouteInterface::TYPE_PREFIX);
|
||||
$prefixRoute->__invoke(Argument::cetera())->willReturn();
|
||||
|
||||
$this->request->getRequestTarget()->willReturn("/cats/");
|
||||
|
||||
$this->factory->create("/cats/")->willReturn($staticRoute->reveal());
|
||||
$this->factory->create("/cats/*")->willReturn($prefixRoute->reveal());
|
||||
|
||||
$this->router->register("GET", "/cats/", "middleware");
|
||||
$this->router->register("GET", "/cats/*", "middleware");
|
||||
$this->router->__invoke($this->request->reveal(), $this->response->reveal(), $this->next);
|
||||
|
||||
$staticRoute->__invoke($this->request->reveal(), $this->response->reveal(), $this->next)->shouldHaveBeenCalled();
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::getPrefixRoute
|
||||
*/
|
||||
public function testDispatchesLongestMatchingPrefixRoute()
|
||||
{
|
||||
// Note: The longest route is also good for 2 points in Settlers of Catan.
|
||||
|
||||
$shortRoute = $this->prophesize('WellRESTed\Routing\Route\RouteInterface');
|
||||
$shortRoute->getMethodMap()->willReturn($this->methodMap->reveal());
|
||||
$shortRoute->getTarget()->willReturn("/animals/*");
|
||||
$shortRoute->getType()->willReturn(RouteInterface::TYPE_PREFIX);
|
||||
$shortRoute->__invoke(Argument::cetera())->willReturn();
|
||||
|
||||
$longRoute = $this->prophesize('WellRESTed\Routing\Route\RouteInterface');
|
||||
$longRoute->getMethodMap()->willReturn($this->methodMap->reveal());
|
||||
$longRoute->getTarget()->willReturn("/animals/cats/*");
|
||||
$longRoute->getType()->willReturn(RouteInterface::TYPE_PREFIX);
|
||||
$longRoute->__invoke(Argument::cetera())->willReturn();
|
||||
|
||||
$this->request->getRequestTarget()->willReturn("/animals/cats/molly");
|
||||
|
||||
$this->factory->create("/animals/*")->willReturn($shortRoute->reveal());
|
||||
$this->factory->create("/animals/cats/*")->willReturn($longRoute->reveal());
|
||||
|
||||
$this->router->register("GET", "/animals/*", "middleware");
|
||||
$this->router->register("GET", "/animals/cats/*", "middleware");
|
||||
$this->router->__invoke($this->request->reveal(), $this->response->reveal(), $this->next);
|
||||
|
||||
$longRoute->__invoke($this->request->reveal(), $this->response->reveal(), $this->next)->shouldHaveBeenCalled();
|
||||
}
|
||||
|
||||
/**
|
||||
* @coversNothing
|
||||
*/
|
||||
public function testDispatchesPrefixRouteBeforePatternRoute()
|
||||
{
|
||||
$prefixRoute = $this->prophesize('WellRESTed\Routing\Route\RouteInterface');
|
||||
$prefixRoute->getMethodMap()->willReturn($this->methodMap->reveal());
|
||||
$prefixRoute->getTarget()->willReturn("/cats/*");
|
||||
$prefixRoute->getType()->willReturn(RouteInterface::TYPE_PREFIX);
|
||||
$prefixRoute->__invoke(Argument::cetera())->willReturn();
|
||||
|
||||
$patternRoute = $this->prophesize('WellRESTed\Routing\Route\RouteInterface');
|
||||
$patternRoute->getMethodMap()->willReturn($this->methodMap->reveal());
|
||||
$patternRoute->getTarget()->willReturn("/cats/{id}");
|
||||
$patternRoute->getType()->willReturn(RouteInterface::TYPE_PATTERN);
|
||||
$patternRoute->__invoke(Argument::cetera())->willReturn();
|
||||
|
||||
$this->request->getRequestTarget()->willReturn("/cats/");
|
||||
|
||||
$this->factory->create("/cats/*")->willReturn($prefixRoute->reveal());
|
||||
$this->factory->create("/cats/{id}")->willReturn($patternRoute->reveal());
|
||||
|
||||
$this->router->register("GET", "/cats/*", "middleware");
|
||||
$this->router->register("GET", "/cats/{id}", "middleware");
|
||||
$this->router->__invoke($this->request->reveal(), $this->response->reveal(), $this->next);
|
||||
|
||||
$prefixRoute->__invoke($this->request->reveal(), $this->response->reveal(), $this->next)->shouldHaveBeenCalled();
|
||||
}
|
||||
|
||||
/**
|
||||
* @coversNothing
|
||||
*/
|
||||
public function testDispatchesFirstMatchingPatternRoute()
|
||||
{
|
||||
$patternRoute1 = $this->prophesize('WellRESTed\Routing\Route\RouteInterface');
|
||||
$patternRoute1->getMethodMap()->willReturn($this->methodMap->reveal());
|
||||
$patternRoute1->getTarget()->willReturn("/cats/{id}");
|
||||
$patternRoute1->getType()->willReturn(RouteInterface::TYPE_PATTERN);
|
||||
$patternRoute1->getPathVariables()->willReturn([]);
|
||||
$patternRoute1->matchesRequestTarget(Argument::any())->willReturn(true);
|
||||
$patternRoute1->__invoke(Argument::cetera())->willReturn();
|
||||
|
||||
$patternRoute2 = $this->prophesize('WellRESTed\Routing\Route\RouteInterface');
|
||||
$patternRoute2->getMethodMap()->willReturn($this->methodMap->reveal());
|
||||
$patternRoute2->getTarget()->willReturn("/cats/{name}");
|
||||
$patternRoute2->getType()->willReturn(RouteInterface::TYPE_PATTERN);
|
||||
$patternRoute2->getPathVariables()->willReturn([]);
|
||||
$patternRoute2->matchesRequestTarget(Argument::any())->willReturn(true);
|
||||
$patternRoute2->__invoke(Argument::cetera())->willReturn();
|
||||
|
||||
$this->request->getRequestTarget()->willReturn("/cats/molly");
|
||||
|
||||
$this->factory->create("/cats/{id}")->willReturn($patternRoute1->reveal());
|
||||
$this->factory->create("/cats/{name}")->willReturn($patternRoute2->reveal());
|
||||
|
||||
$this->router->register("GET", "/cats/{id}", "middleware");
|
||||
$this->router->register("GET", "/cats/{name}", "middleware");
|
||||
$this->router->__invoke($this->request->reveal(), $this->response->reveal(), $this->next);
|
||||
|
||||
$patternRoute1->__invoke($this->request->reveal(), $this->response->reveal(), $this->next)->shouldHaveBeenCalled();
|
||||
}
|
||||
|
||||
/**
|
||||
* @coversNothing
|
||||
*/
|
||||
public function testStopsTestingPatternsAfterFirstSuccessfulMatch()
|
||||
{
|
||||
$patternRoute1 = $this->prophesize('WellRESTed\Routing\Route\RouteInterface');
|
||||
$patternRoute1->getMethodMap()->willReturn($this->methodMap->reveal());
|
||||
$patternRoute1->getTarget()->willReturn("/cats/{id}");
|
||||
$patternRoute1->getType()->willReturn(RouteInterface::TYPE_PATTERN);
|
||||
$patternRoute1->getPathVariables()->willReturn([]);
|
||||
$patternRoute1->matchesRequestTarget(Argument::any())->willReturn(true);
|
||||
$patternRoute1->__invoke(Argument::cetera())->willReturn();
|
||||
|
||||
$patternRoute2 = $this->prophesize('WellRESTed\Routing\Route\RouteInterface');
|
||||
$patternRoute2->getMethodMap()->willReturn($this->methodMap->reveal());
|
||||
$patternRoute2->getTarget()->willReturn("/cats/{name}");
|
||||
$patternRoute2->getType()->willReturn(RouteInterface::TYPE_PATTERN);
|
||||
$patternRoute2->getPathVariables()->willReturn([]);
|
||||
$patternRoute2->matchesRequestTarget(Argument::any())->willReturn(true);
|
||||
$patternRoute2->__invoke(Argument::cetera())->willReturn();
|
||||
|
||||
$this->request->getRequestTarget()->willReturn("/cats/molly");
|
||||
|
||||
$this->factory->create("/cats/{id}")->willReturn($patternRoute1->reveal());
|
||||
$this->factory->create("/cats/{name}")->willReturn($patternRoute2->reveal());
|
||||
|
||||
$this->router->register("GET", "/cats/{id}", "middleware");
|
||||
$this->router->register("GET", "/cats/{name}", "middleware");
|
||||
$this->router->__invoke($this->request->reveal(), $this->response->reveal(), $this->next);
|
||||
|
||||
$patternRoute2->matchesRequestTarget(Argument::any())->shouldNotHaveBeenCalled();
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::__invoke
|
||||
* @covers ::registerRouteForTarget
|
||||
*/
|
||||
public function testMatchesPathAgainstRouteWithoutQuery()
|
||||
{
|
||||
$target = "/my/path?cat=molly&dog=bear";
|
||||
|
||||
$this->request->getRequestTarget()->willReturn($target);
|
||||
$this->route->getTarget()->willReturn($target);
|
||||
$this->route->getType()->willReturn(RouteInterface::TYPE_PATTERN);
|
||||
$this->route->matchesRequestTarget(Argument::cetera())->willReturn(true);
|
||||
|
||||
$this->router->register("GET", $target, "middleware");
|
||||
$this->router->__invoke($this->request->reveal(), $this->response->reveal(), $this->next);
|
||||
|
||||
$this->route->matchesRequestTarget("/my/path")->shouldHaveBeenCalled();
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// Path Variables
|
||||
|
||||
/**
|
||||
* @covers ::__invoke
|
||||
* @dataProvider pathVariableProvider
|
||||
*/
|
||||
public function testSetPathVariablesAttributeIndividually($name, $value)
|
||||
{
|
||||
$attributeName = "pathVariables";
|
||||
|
||||
$target = "/";
|
||||
$variables = [
|
||||
"id" => "1024",
|
||||
"name" => "Molly"
|
||||
];
|
||||
|
||||
$this->request->getRequestTarget()->willReturn($target);
|
||||
$this->route->getTarget()->willReturn($target);
|
||||
$this->route->getType()->willReturn(RouteInterface::TYPE_PATTERN);
|
||||
$this->route->matchesRequestTarget(Argument::cetera())->willReturn(true);
|
||||
$this->route->getPathVariables()->willReturn($variables);
|
||||
|
||||
$this->router->__construct($this->dispatcher->reveal());
|
||||
$this->router->register("GET", $target, "middleware");
|
||||
$this->router->__invoke($this->request->reveal(), $this->response->reveal(), $this->next);
|
||||
|
||||
$this->request->withAttribute($name, $value)->shouldHaveBeenCalled();
|
||||
}
|
||||
|
||||
public function pathVariableProvider()
|
||||
{
|
||||
return [
|
||||
["id", "1024"],
|
||||
["name", "Molly"]
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::__invoke
|
||||
*/
|
||||
public function testSetPathVariablesAttributeAsArray()
|
||||
{
|
||||
$attributeName = "pathVariables";
|
||||
|
||||
$target = "/";
|
||||
$variables = [
|
||||
"id" => "1024",
|
||||
"name" => "Molly"
|
||||
];
|
||||
|
||||
$this->request->getRequestTarget()->willReturn($target);
|
||||
$this->route->getTarget()->willReturn($target);
|
||||
$this->route->getType()->willReturn(RouteInterface::TYPE_PATTERN);
|
||||
$this->route->matchesRequestTarget(Argument::cetera())->willReturn(true);
|
||||
$this->route->getPathVariables()->willReturn($variables);
|
||||
|
||||
$this->router->__construct($this->dispatcher->reveal(), $attributeName);
|
||||
$this->router->register("GET", $target, "middleware");
|
||||
$this->router->__invoke($this->request->reveal(), $this->response->reveal(), $this->next);
|
||||
|
||||
$this->request->withAttribute("pathVariables", $variables)->shouldHaveBeenCalled();
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// No Matching Routes
|
||||
|
||||
/**
|
||||
* @covers ::__invoke
|
||||
* @covers ::getStaticRoute
|
||||
* @covers ::getPrefixRoute
|
||||
*/
|
||||
public function testResponds404WhenNoRouteMatches()
|
||||
{
|
||||
$this->request->getRequestTarget()->willReturn("/no/match");
|
||||
$this->response->withStatus(Argument::any())->willReturn($this->response->reveal());
|
||||
$this->router->__invoke($this->request->reveal(), $this->response->reveal(), $this->next);
|
||||
$this->response->withStatus(404)->shouldHaveBeenCalled();
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::__invoke
|
||||
* @covers ::getStaticRoute
|
||||
* @covers ::getPrefixRoute
|
||||
*/
|
||||
public function testStopsPropagatingWhenNoRouteMatches()
|
||||
{
|
||||
$calledNext = false;
|
||||
$next = function ($request, $response) use (&$calledNext) {
|
||||
$calledNext = true;
|
||||
return $response;
|
||||
};
|
||||
|
||||
$this->request->getRequestTarget()->willReturn("/no/match");
|
||||
$this->response->withStatus(Argument::any())->willReturn($this->response->reveal());
|
||||
$this->router->__invoke($this->request->reveal(), $this->response->reveal(), $next);
|
||||
$this->assertFalse($calledNext);
|
||||
}
|
||||
|
||||
public function testRegisterIsFluid()
|
||||
{
|
||||
$router = $this->router
|
||||
->register("GET", "/", "middleware")
|
||||
->register("POST", "/", "middleware");
|
||||
$this->assertSame($this->router, $router);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,191 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace WellRESTed\Test\Unit\Server;
|
||||
|
||||
use Prophecy\Argument;
|
||||
use WellRESTed\Server;
|
||||
|
||||
/**
|
||||
* @coversDefaultClass WellRESTed\Server
|
||||
* @uses WellRESTed\Server
|
||||
*/
|
||||
class ServerTest extends \PHPUnit_Framework_TestCase
|
||||
{
|
||||
private $dispatcher;
|
||||
private $request;
|
||||
private $response;
|
||||
private $transmitter;
|
||||
private $server;
|
||||
|
||||
public function setUp()
|
||||
{
|
||||
parent::setUp();
|
||||
$this->request = $this->prophesize('Psr\Http\Message\ServerRequestInterface');
|
||||
$this->request->withAttribute(Argument::cetera())->willReturn($this->request->reveal());
|
||||
$this->response = $this->prophesize('Psr\Http\Message\ResponseInterface');
|
||||
$this->transmitter = $this->prophesize('WellRESTed\Transmission\TransmitterInterface');
|
||||
$this->transmitter->transmit(Argument::cetera())->willReturn();
|
||||
$this->dispatcher = $this->prophesize('WellRESTed\Dispatching\DispatcherInterface');
|
||||
$this->dispatcher->dispatch(Argument::cetera())->will(
|
||||
function ($args) {
|
||||
list($middleware, $request, $response, $next) = $args;
|
||||
return $next($request, $response);
|
||||
}
|
||||
);
|
||||
|
||||
$this->server = $this->getMockBuilder('WellRESTed\Server')
|
||||
->setMethods(["getDispatcher", "getRequest", "getResponse", "getTransmitter"])
|
||||
->disableOriginalConstructor()
|
||||
->getMock();
|
||||
$this->server->expects($this->any())
|
||||
->method("getDispatcher")
|
||||
->will($this->returnValue($this->dispatcher->reveal()));
|
||||
$this->server->expects($this->any())
|
||||
->method("getRequest")
|
||||
->will($this->returnValue($this->request->reveal()));
|
||||
$this->server->expects($this->any())
|
||||
->method("getResponse")
|
||||
->will($this->returnValue($this->response->reveal()));
|
||||
$this->server->expects($this->any())
|
||||
->method("getTransmitter")
|
||||
->will($this->returnValue($this->transmitter->reveal()));
|
||||
$this->server->__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::__construct
|
||||
* @covers ::getDispatcher
|
||||
* @uses WellRESTed\Dispatching\Dispatcher
|
||||
*/
|
||||
public function testCreatesInstances()
|
||||
{
|
||||
$server = new Server();
|
||||
$this->assertNotNull($server);
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::add
|
||||
*/
|
||||
public function testAddIsFluid()
|
||||
{
|
||||
$server = new Server();
|
||||
$this->assertSame($server, $server->add("middleware"));
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::add
|
||||
* @covers ::dispatch
|
||||
*/
|
||||
public function testDispatchesMiddlewareStack()
|
||||
{
|
||||
$next = function ($request, $response) {
|
||||
return $response;
|
||||
};
|
||||
|
||||
$this->server->add("first");
|
||||
$this->server->add("second");
|
||||
$this->server->add("third");
|
||||
|
||||
$this->server->dispatch($this->request->reveal(), $this->response->reveal(), $next);
|
||||
|
||||
$this->dispatcher->dispatch(
|
||||
["first", "second", "third"],
|
||||
$this->request->reveal(),
|
||||
$this->response->reveal(),
|
||||
$next
|
||||
)->shouldHaveBeenCalled();
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// Respond
|
||||
|
||||
/**
|
||||
* @covers ::respond
|
||||
*/
|
||||
public function testRespondDispatchesRequest()
|
||||
{
|
||||
$this->server->respond();
|
||||
$this->dispatcher->dispatch(
|
||||
Argument::any(),
|
||||
$this->request->reveal(),
|
||||
Argument::any(),
|
||||
Argument::any()
|
||||
)->shouldHaveBeenCalled();
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::respond
|
||||
*/
|
||||
public function testRespondDispatchesResponse()
|
||||
{
|
||||
$this->server->respond();
|
||||
$this->dispatcher->dispatch(
|
||||
Argument::any(),
|
||||
Argument::any(),
|
||||
$this->response->reveal(),
|
||||
Argument::any()
|
||||
)->shouldHaveBeenCalled();
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::respond
|
||||
*/
|
||||
public function testRespondSendsResponseToResponder()
|
||||
{
|
||||
$this->server->respond();
|
||||
$this->transmitter->transmit(
|
||||
$this->request->reveal(),
|
||||
$this->response->reveal()
|
||||
)->shouldHaveBeenCalled();
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// Router
|
||||
|
||||
/**
|
||||
* @covers ::createRouter
|
||||
* @uses WellRESTed\Routing\Router
|
||||
* @uses WellRESTed\Routing\MethodMap
|
||||
* @uses WellRESTed\Routing\Route\RouteFactory
|
||||
* @uses WellRESTed\Routing\Route\Route
|
||||
* @uses WellRESTed\Routing\Route\StaticRoute
|
||||
*/
|
||||
public function testCreatesRouterWithDispatcher()
|
||||
{
|
||||
$this->request->getMethod()->willReturn("GET");
|
||||
$this->request->getRequestTarget()->willReturn("/");
|
||||
|
||||
$next = function ($request, $response) {
|
||||
return $response;
|
||||
};
|
||||
|
||||
$router = $this->server->createRouter();
|
||||
$router->register("GET", "/", "middleware");
|
||||
$router($this->request->reveal(), $this->response->reveal(), $next);
|
||||
|
||||
$this->dispatcher->dispatch(
|
||||
"middleware",
|
||||
$this->request->reveal(),
|
||||
$this->response->reveal(),
|
||||
$next
|
||||
)->shouldHaveBeenCalled();
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// Attributes
|
||||
|
||||
/**
|
||||
* @covers ::respond
|
||||
*/
|
||||
public function testAddsAttributesToRequest()
|
||||
{
|
||||
$attributes = [
|
||||
"name" => "value"
|
||||
];
|
||||
|
||||
$this->server->__construct($attributes);
|
||||
$this->server->respond();
|
||||
$this->request->withAttribute("name", "value")->shouldHaveBeenCalled();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -1,239 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace WellRESTed\Test\Unit\Transmission;
|
||||
|
||||
use Prophecy\Argument;
|
||||
use WellRESTed\Transmission\HeaderStack;
|
||||
use WellRESTed\Transmission\Transmitter;
|
||||
|
||||
require_once __DIR__ . "/../../../src/HeaderStack.php";
|
||||
|
||||
/**
|
||||
* @coversDefaultClass WellRESTed\Transmission\Transmitter
|
||||
* @uses WellRESTed\Transmission\Transmitter
|
||||
* @uses WellRESTed\Dispatching\Dispatcher
|
||||
* @uses WellRESTed\Dispatching\DispatchStack
|
||||
* @group transmission
|
||||
*/
|
||||
class TransmitterTest extends \PHPUnit_Framework_TestCase
|
||||
{
|
||||
private $request;
|
||||
private $response;
|
||||
private $body;
|
||||
|
||||
public function setUp()
|
||||
{
|
||||
HeaderStack::reset();
|
||||
$this->body = $this->prophesize('\Psr\Http\Message\StreamInterface');
|
||||
$this->body->isReadable()->willReturn(false);
|
||||
$this->body->getSize()->willReturn(1024);
|
||||
$this->request = $this->prophesize('\Psr\Http\Message\ServerRequestInterface');
|
||||
$this->request->getMethod()->willReturn("HEAD");
|
||||
$this->response = $this->prophesize('\Psr\Http\Message\ResponseInterface');
|
||||
$this->response->getHeaders()->willReturn([]);
|
||||
$this->response->hasHeader("Content-length")->willReturn(true);
|
||||
$this->response->getHeaderLine("Transfer-encoding")->willReturn("");
|
||||
$this->response->getProtocolVersion()->willReturn("1.1");
|
||||
$this->response->getStatusCode()->willReturn("200");
|
||||
$this->response->getReasonPhrase()->willReturn("Ok");
|
||||
$this->response->getBody()->willReturn($this->body->reveal());
|
||||
$this->response->withHeader(Argument::cetera())->willReturn($this->response->reveal());
|
||||
$this->response->withBody(Argument::any())->willReturn($this->response->reveal());
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::__construct
|
||||
*/
|
||||
public function testCreatesInstance()
|
||||
{
|
||||
$transmitter = new Transmitter();
|
||||
$this->assertNotNull($transmitter);
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::transmit
|
||||
* @covers ::getStatusLine
|
||||
*/
|
||||
public function testSendStatusCodeWithReasonPhrase()
|
||||
{
|
||||
$this->response->getStatusCode()->willReturn("200");
|
||||
$this->response->getReasonPhrase()->willReturn("Ok");
|
||||
|
||||
$transmitter = new Transmitter();
|
||||
$transmitter->transmit($this->request->reveal(), $this->response->reveal());
|
||||
$this->assertContains("HTTP/1.1 200 Ok", HeaderStack::getHeaders());
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::transmit
|
||||
* @covers ::getStatusLine
|
||||
*/
|
||||
public function testSendStatusCodeWithoutReasonPhrase()
|
||||
{
|
||||
$this->response->getStatusCode()->willReturn("999");
|
||||
$this->response->getReasonPhrase()->willReturn(null);
|
||||
|
||||
$transmitter = new Transmitter();
|
||||
$transmitter->transmit($this->request->reveal(), $this->response->reveal());
|
||||
$this->assertContains("HTTP/1.1 999", HeaderStack::getHeaders());
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::transmit
|
||||
* @dataProvider headerProvider
|
||||
*/
|
||||
public function testSendsHeaders($header)
|
||||
{
|
||||
$this->response->getHeaders()->willReturn([
|
||||
"Content-length" => ["2048"],
|
||||
"X-foo" => ["bar", "baz"],
|
||||
]);
|
||||
|
||||
$transmitter = new Transmitter();
|
||||
$transmitter->transmit($this->request->reveal(), $this->response->reveal());
|
||||
$this->assertContains($header, HeaderStack::getHeaders());
|
||||
}
|
||||
|
||||
public function headerProvider()
|
||||
{
|
||||
return [
|
||||
["Content-length: 2048"],
|
||||
["X-foo: bar"],
|
||||
["X-foo: baz"]
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::transmit
|
||||
* @covers ::outputBody
|
||||
*/
|
||||
public function testOutputsBody()
|
||||
{
|
||||
$content = "Hello, world!";
|
||||
|
||||
$this->body->isReadable()->willReturn(true);
|
||||
$this->body->__toString()->willReturn($content);
|
||||
|
||||
$transmitter = new Transmitter();
|
||||
|
||||
ob_start();
|
||||
$transmitter->transmit($this->request->reveal(), $this->response->reveal());
|
||||
$captured = ob_get_contents();
|
||||
ob_end_clean();
|
||||
|
||||
$this->assertEquals($content, $captured);
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::transmit
|
||||
* @covers ::setChunkSize
|
||||
* @covers ::outputBody
|
||||
*/
|
||||
public function testOutputsBodyInChunks()
|
||||
{
|
||||
$content = "Hello, world!";
|
||||
$chunkSize = 3;
|
||||
$position = 0;
|
||||
|
||||
$this->body->isReadable()->willReturn(true);
|
||||
$this->body->rewind()->willReturn(true);
|
||||
$this->body->eof()->willReturn(false);
|
||||
$this->body->read(Argument::any())->will(
|
||||
function ($args) use ($content, &$position) {
|
||||
$chunkSize = $args[0];
|
||||
$chunk = substr($content, $position, $chunkSize);
|
||||
$position += $chunkSize;
|
||||
if ($position >= strlen($content)) {
|
||||
$this->eof()->willReturn(true);
|
||||
}
|
||||
return $chunk;
|
||||
}
|
||||
);
|
||||
|
||||
$transmitter = new Transmitter();
|
||||
$transmitter->setChunkSize($chunkSize);
|
||||
|
||||
ob_start();
|
||||
$transmitter->transmit($this->request->reveal(), $this->response->reveal(), $chunkSize);
|
||||
$captured = ob_get_contents();
|
||||
ob_end_clean();
|
||||
|
||||
$this->assertEquals($content, $captured);
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// Preparation
|
||||
|
||||
/**
|
||||
* @covers ::prepareResponse
|
||||
*/
|
||||
public function testAddContentLengthHeader()
|
||||
{
|
||||
$bodySize = 1024;
|
||||
$this->response->getStatusCode()->willReturn("200");
|
||||
$this->response->getReasonPhrase()->willReturn("Ok");
|
||||
$this->response->hasHeader("Content-length")->willReturn(false);
|
||||
$this->body->isReadable()->willReturn(true);
|
||||
$this->body->__toString()->willReturn("");
|
||||
$this->body->getSize()->willReturn($bodySize);
|
||||
|
||||
$transmitter = new Transmitter();
|
||||
$transmitter->transmit($this->request->reveal(), $this->response->reveal());
|
||||
$this->response->withHeader("Content-length", $bodySize)->shouldHaveBeenCalled();
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::prepareResponse
|
||||
*/
|
||||
public function testDoesNotReplaceContentLengthHeaderWhenContentLenghtIsAlreadySet()
|
||||
{
|
||||
$bodySize = 1024;
|
||||
$this->response->getStatusCode()->willReturn("200");
|
||||
$this->response->getReasonPhrase()->willReturn("Ok");
|
||||
$this->response->hasHeader("Content-length")->willReturn(true);
|
||||
$this->body->isReadable()->willReturn(true);
|
||||
$this->body->__toString()->willReturn("");
|
||||
$this->body->getSize()->willReturn($bodySize);
|
||||
|
||||
$transmitter = new Transmitter();
|
||||
$transmitter->transmit($this->request->reveal(), $this->response->reveal());
|
||||
$this->response->withHeader("Content-length", $bodySize)->shouldNotHaveBeenCalled();
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::prepareResponse
|
||||
*/
|
||||
public function testDoesNotAddContentLengthHeaderWhenTransferEncodingIsChunked()
|
||||
{
|
||||
$bodySize = 1024;
|
||||
$this->response->getStatusCode()->willReturn("200");
|
||||
$this->response->getReasonPhrase()->willReturn("Ok");
|
||||
$this->response->hasHeader("Content-length")->willReturn(false);
|
||||
$this->response->getHeaderLine("Transfer-encoding")->willReturn("CHUNKED");
|
||||
$this->body->isReadable()->willReturn(true);
|
||||
$this->body->__toString()->willReturn("");
|
||||
$this->body->getSize()->willReturn($bodySize);
|
||||
|
||||
$transmitter = new Transmitter();
|
||||
$transmitter->transmit($this->request->reveal(), $this->response->reveal());
|
||||
$this->response->withHeader("Content-length", $bodySize)->shouldNotHaveBeenCalled();
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::prepareResponse
|
||||
*/
|
||||
public function testDoesNotAddContentLengthHeaderWhenBodySizeIsNull()
|
||||
{
|
||||
$this->response->getStatusCode()->willReturn("200");
|
||||
$this->response->getReasonPhrase()->willReturn("Ok");
|
||||
$this->response->hasHeader("Content-length")->willReturn(false);
|
||||
$this->response->getHeaderLine("Transfer-encoding")->willReturn("");
|
||||
$this->body->isReadable()->willReturn(true);
|
||||
$this->body->__toString()->willReturn("");
|
||||
$this->body->getSize()->willReturn(null);
|
||||
|
||||
$transmitter = new Transmitter();
|
||||
$transmitter->transmit($this->request->reveal(), $this->response->reveal());
|
||||
$this->response->withHeader("Content-length", Argument::any())->shouldNotHaveBeenCalled();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,84 @@
|
|||
<?php
|
||||
|
||||
namespace WellRESTed\Dispatching;
|
||||
|
||||
use WellRESTed\Message\Response;
|
||||
use WellRESTed\Message\ServerRequest;
|
||||
use WellRESTed\Test\Doubles\NextMock;
|
||||
use WellRESTed\Test\TestCase;
|
||||
|
||||
class DispatchStackTest extends TestCase
|
||||
{
|
||||
private $request;
|
||||
private $response;
|
||||
private $next;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
$this->request = new ServerRequest();
|
||||
$this->response = new Response();
|
||||
$this->next = new NextMock();
|
||||
}
|
||||
|
||||
public function testDispatchesMiddlewareInOrderAdded(): void
|
||||
{
|
||||
// Each middleware will add its "name" to this array.
|
||||
$callOrder = [];
|
||||
$stack = new DispatchStack(new Dispatcher());
|
||||
$stack->add(function ($request, $response, $next) use (&$callOrder) {
|
||||
$callOrder[] = 'first';
|
||||
return $next($request, $response);
|
||||
});
|
||||
$stack->add(function ($request, $response, $next) use (&$callOrder) {
|
||||
$callOrder[] = 'second';
|
||||
return $next($request, $response);
|
||||
});
|
||||
$stack->add(function ($request, $response, $next) use (&$callOrder) {
|
||||
$callOrder[] = 'third';
|
||||
return $next($request, $response);
|
||||
});
|
||||
$stack($this->request, $this->response, $this->next);
|
||||
$this->assertEquals(['first', 'second', 'third'], $callOrder);
|
||||
}
|
||||
|
||||
public function testCallsNextAfterDispatchingEmptyStack(): void
|
||||
{
|
||||
$stack = new DispatchStack(new Dispatcher());
|
||||
$stack($this->request, $this->response, $this->next);
|
||||
$this->assertTrue($this->next->called);
|
||||
}
|
||||
|
||||
public function testCallsNextAfterDispatchingStack(): void
|
||||
{
|
||||
$middleware = function ($request, $response, $next) use (&$callOrder) {
|
||||
return $next($request, $response);
|
||||
};
|
||||
|
||||
$stack = new DispatchStack(new Dispatcher());
|
||||
$stack->add($middleware);
|
||||
$stack->add($middleware);
|
||||
$stack->add($middleware);
|
||||
|
||||
$stack($this->request, $this->response, $this->next);
|
||||
$this->assertTrue($this->next->called);
|
||||
}
|
||||
|
||||
public function testDoesNotCallNextWhenStackStopsEarly(): void
|
||||
{
|
||||
$middlewareGo = function ($request, $response, $next) use (&$callOrder) {
|
||||
return $next($request, $response);
|
||||
};
|
||||
$middlewareStop = function ($request, $response, $next) use (&$callOrder) {
|
||||
return $response;
|
||||
};
|
||||
|
||||
$stack = new DispatchStack(new Dispatcher());
|
||||
$stack->add($middlewareGo);
|
||||
$stack->add($middlewareStop);
|
||||
$stack->add($middlewareStop);
|
||||
|
||||
$stack($this->request, $this->response, $this->next);
|
||||
$this->assertFalse($this->next->called);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,219 @@
|
|||
<?php
|
||||
|
||||
namespace WellRESTed\Dispatching;
|
||||
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Psr\Http\Server\RequestHandlerInterface;
|
||||
use WellRESTed\Message\Response;
|
||||
use WellRESTed\Message\ServerRequest;
|
||||
use WellRESTed\MiddlewareInterface;
|
||||
use WellRESTed\Test\Doubles\NextMock;
|
||||
use WellRESTed\Test\TestCase;
|
||||
|
||||
class DispatcherTest extends TestCase
|
||||
{
|
||||
/** @var ServerRequestInterface */
|
||||
private $request;
|
||||
/** @var ResponseInterface */
|
||||
private $response;
|
||||
/** @var NextMock */
|
||||
private $next;
|
||||
/** @var ResponseInterface */
|
||||
private $stubResponse;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->request = new ServerRequest();
|
||||
$this->response = new Response();
|
||||
$this->next = new NextMock();
|
||||
$this->stubResponse = new Response();
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatch the provided dispatchable using the class under test and the
|
||||
* ivars $request, $response, and $next. Return the response.
|
||||
* @param $dispatchable
|
||||
* @return ResponseInterface
|
||||
*/
|
||||
private function dispatch($dispatchable): ResponseInterface
|
||||
{
|
||||
$dispatcher = new Dispatcher();
|
||||
return $dispatcher->dispatch(
|
||||
$dispatchable,
|
||||
$this->request,
|
||||
$this->response,
|
||||
$this->next
|
||||
);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// PSR-15 Handler
|
||||
|
||||
public function testDispatchesPsr15Handler(): void
|
||||
{
|
||||
$handler = new HandlerDouble($this->stubResponse);
|
||||
$response = $this->dispatch($handler);
|
||||
$this->assertSame($this->stubResponse, $response);
|
||||
}
|
||||
|
||||
public function testDispatchesPsr15HandlerFromFactory(): void
|
||||
{
|
||||
$factory = function () {
|
||||
return new HandlerDouble($this->stubResponse);
|
||||
};
|
||||
|
||||
$response = $this->dispatch($factory);
|
||||
$this->assertSame($this->stubResponse, $response);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// PSR-15 Middleware
|
||||
|
||||
public function testDispatchesPsr15MiddlewareWithDelegate(): void
|
||||
{
|
||||
$this->next->upstreamResponse = $this->stubResponse;
|
||||
$middleware = new MiddlewareDouble();
|
||||
|
||||
$response = $this->dispatch($middleware);
|
||||
$this->assertSame($this->stubResponse, $response);
|
||||
}
|
||||
|
||||
public function testDispatchesPsr15MiddlewareFromFactoryWithDelegate(): void
|
||||
{
|
||||
$this->next->upstreamResponse = $this->stubResponse;
|
||||
$factory = function () {
|
||||
return new MiddlewareDouble();
|
||||
};
|
||||
|
||||
$response = $this->dispatch($factory);
|
||||
$this->assertSame($this->stubResponse, $response);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Double-Pass Middleware Callable
|
||||
|
||||
public function testDispatchesDoublePassMiddlewareCallable(): void
|
||||
{
|
||||
$doublePass = function ($request, $response, $next) {
|
||||
return $next($request, $this->stubResponse);
|
||||
};
|
||||
|
||||
$response = $this->dispatch($doublePass);
|
||||
$this->assertSame($this->stubResponse, $response);
|
||||
}
|
||||
|
||||
public function testDispatchesDoublePassMiddlewareCallableFromFactory(): void
|
||||
{
|
||||
$factory = function () {
|
||||
return function ($request, $response, $next) {
|
||||
return $next($request, $this->stubResponse);
|
||||
};
|
||||
};
|
||||
|
||||
$response = $this->dispatch($factory);
|
||||
$this->assertSame($this->stubResponse, $response);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Double-Pass Middleware Instance
|
||||
|
||||
public function testDispatchesDoublePassMiddlewareInstance(): void
|
||||
{
|
||||
$doublePass = new DoublePassMiddlewareDouble();
|
||||
$response = $this->dispatch($doublePass);
|
||||
$this->assertEquals(200, $response->getStatusCode());
|
||||
}
|
||||
|
||||
public function testDispatchesDoublePassMiddlewareInstanceFromFactory(): void
|
||||
{
|
||||
$factory = function () {
|
||||
return new DoublePassMiddlewareDouble();
|
||||
};
|
||||
$response = $this->dispatch($factory);
|
||||
$this->assertEquals(200, $response->getStatusCode());
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// String
|
||||
|
||||
public function testDispatchesInstanceFromStringName(): void
|
||||
{
|
||||
$response = $this->dispatch(DoublePassMiddlewareDouble::class);
|
||||
$this->assertEquals(200, $response->getStatusCode());
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Arrays
|
||||
|
||||
public function testDispatchesArrayAsDispatchStack(): void
|
||||
{
|
||||
$doublePass = new DoublePassMiddlewareDouble();
|
||||
$response = $this->dispatch([$doublePass]);
|
||||
$this->assertEquals(200, $response->getStatusCode());
|
||||
}
|
||||
|
||||
public function testThrowsExceptionWhenUnableToDispatch(): void
|
||||
{
|
||||
$this->expectException(DispatchException::class);
|
||||
$this->dispatch(null);
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Doubles
|
||||
|
||||
/**
|
||||
* Double pass middleware that sends a response with a 200 status to $next
|
||||
* and return the response.
|
||||
*
|
||||
* This class has no constructor so that we can test instantiating from string.
|
||||
*/
|
||||
class DoublePassMiddlewareDouble implements MiddlewareInterface
|
||||
{
|
||||
public function __invoke(
|
||||
ServerRequestInterface $request,
|
||||
ResponseInterface $response,
|
||||
$next
|
||||
) {
|
||||
$response = $response->withStatus(200);
|
||||
return $next($request, $response);
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* PSR-15 Handler that returns a ResponseInterface stub
|
||||
*/
|
||||
class HandlerDouble implements RequestHandlerInterface
|
||||
{
|
||||
/** @var ResponseInterface */
|
||||
private $response;
|
||||
|
||||
public function __construct(ResponseInterface $response)
|
||||
{
|
||||
$this->response = $response;
|
||||
}
|
||||
|
||||
public function handle(ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
return $this->response;
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* PSR-15 Middleware that passes the request to the delegate and returns the
|
||||
* delegate's response
|
||||
*/
|
||||
class MiddlewareDouble implements \Psr\Http\Server\MiddlewareInterface
|
||||
{
|
||||
public function process(
|
||||
ServerRequestInterface $request,
|
||||
RequestHandlerInterface $handler
|
||||
): ResponseInterface {
|
||||
return $handler->handle($request);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
<?php
|
||||
|
||||
namespace WellRESTed\Test\Doubles;
|
||||
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use WellRESTed\MiddlewareInterface;
|
||||
|
||||
class MiddlewareMock implements MiddlewareInterface
|
||||
{
|
||||
public $called = false;
|
||||
public $callCount = 0;
|
||||
public $request = null;
|
||||
public $response = null;
|
||||
public $propagate = true;
|
||||
|
||||
public function __invoke(
|
||||
ServerRequestInterface $request,
|
||||
ResponseInterface $response,
|
||||
$next
|
||||
) {
|
||||
$this->called = true;
|
||||
$this->callCount++;
|
||||
$this->request = $request;
|
||||
$this->response = $response;
|
||||
if ($this->propagate) {
|
||||
return $next($request, $response);
|
||||
} else {
|
||||
return $response;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
<?php
|
||||
|
||||
namespace WellRESTed\Test\Doubles;
|
||||
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
|
||||
class NextMock
|
||||
{
|
||||
public $called = false;
|
||||
public $request = null;
|
||||
public $response = null;
|
||||
public $upstreamResponse = null;
|
||||
|
||||
public function __invoke(
|
||||
ServerRequestInterface $request,
|
||||
ResponseInterface $response
|
||||
) {
|
||||
$this->called = true;
|
||||
$this->request = $request;
|
||||
$this->response = $response;
|
||||
if ($this->upstreamResponse) {
|
||||
return $this->upstreamResponse;
|
||||
} else {
|
||||
return $response;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,104 @@
|
|||
<?php
|
||||
|
||||
namespace WellRESTed\Message;
|
||||
|
||||
use WellRESTed\Test\TestCase;
|
||||
|
||||
class HeaderCollectionTest extends TestCase
|
||||
{
|
||||
public function testAddsSingleHeaderAndIndicatesCaseInsensitiveIsset(): void
|
||||
{
|
||||
$collection = new HeaderCollection();
|
||||
$collection['Content-Type'] = 'application/json';
|
||||
$this->assertTrue(isset($collection['content-type']));
|
||||
}
|
||||
|
||||
public function testAddsMultipleHeadersAndIndicatesCaseInsensitiveIsset(): void
|
||||
{
|
||||
$collection = new HeaderCollection();
|
||||
$collection['Set-Cookie'] = 'cat=Molly';
|
||||
$collection['SET-COOKIE'] = 'dog=Bear';
|
||||
$this->assertTrue(isset($collection['set-cookie']));
|
||||
}
|
||||
|
||||
public function testReturnsHeadersWithCaseInsensitiveHeaderName(): void
|
||||
{
|
||||
$collection = new HeaderCollection();
|
||||
$collection['Set-Cookie'] = 'cat=Molly';
|
||||
$collection['SET-COOKIE'] = 'dog=Bear';
|
||||
|
||||
$headers = $collection['set-cookie'];
|
||||
$matched = array_intersect($headers, ['cat=Molly', 'dog=Bear']);
|
||||
$this->assertCount(2, $matched);
|
||||
}
|
||||
|
||||
public function testRemovesHeadersWithCaseInsensitiveHeaderName(): void
|
||||
{
|
||||
$collection = new HeaderCollection();
|
||||
$collection['Set-Cookie'] = 'cat=Molly';
|
||||
$collection['SET-COOKIE'] = 'dog=Bear';
|
||||
unset($collection['set-cookie']);
|
||||
$this->assertFalse(isset($collection['set-cookie']));
|
||||
}
|
||||
|
||||
public function testCloneMakesDeepCopyOfHeaders(): void
|
||||
{
|
||||
$collection = new HeaderCollection();
|
||||
$collection['Set-Cookie'] = 'cat=Molly';
|
||||
|
||||
$clone = clone $collection;
|
||||
unset($clone['Set-Cookie']);
|
||||
|
||||
$this->assertTrue(isset($collection['set-cookie']) && !isset($clone['set-cookie']));
|
||||
}
|
||||
|
||||
public function testIteratesWithOriginalKeys(): void
|
||||
{
|
||||
$collection = new HeaderCollection();
|
||||
$collection['Content-length'] = '100';
|
||||
$collection['Set-Cookie'] = 'cat=Molly';
|
||||
$collection['Set-Cookie'] = 'dog=Bear';
|
||||
$collection['Content-type'] = 'application/json';
|
||||
unset($collection['Content-length']);
|
||||
|
||||
$headers = [];
|
||||
|
||||
foreach ($collection as $key => $values) {
|
||||
$headers[] = $key;
|
||||
}
|
||||
|
||||
$expected = ['Content-type', 'Set-Cookie'];
|
||||
|
||||
$countUnmatched = count(array_diff($expected, $headers)) + count(array_diff($headers, $expected));
|
||||
$this->assertEquals(0, $countUnmatched);
|
||||
}
|
||||
|
||||
public function testIteratesWithOriginalKeysAndValues(): void
|
||||
{
|
||||
$collection = new HeaderCollection();
|
||||
$collection['Content-length'] = '100';
|
||||
$collection['Set-Cookie'] = 'cat=Molly';
|
||||
$collection['Set-Cookie'] = 'dog=Bear';
|
||||
$collection['Content-type'] = 'application/json';
|
||||
unset($collection['Content-length']);
|
||||
|
||||
$headers = [];
|
||||
|
||||
foreach ($collection as $key => $values) {
|
||||
foreach ($values as $value) {
|
||||
if (isset($headers[$key])) {
|
||||
$headers[$key][] = $value;
|
||||
} else {
|
||||
$headers[$key] = [$value];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$expected = [
|
||||
'Set-Cookie' => ['cat=Molly', 'dog=Bear'],
|
||||
'Content-type' => ['application/json']
|
||||
];
|
||||
|
||||
$this->assertEquals($expected, $headers);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,250 @@
|
|||
<?php
|
||||
|
||||
namespace WellRESTed\Message;
|
||||
|
||||
use InvalidArgumentException;
|
||||
use WellRESTed\Test\TestCase;
|
||||
|
||||
class MessageTest extends TestCase
|
||||
{
|
||||
public function testSetsHeadersWithStringValueOnConstruction(): void
|
||||
{
|
||||
$headers = ['X-foo' => 'bar'];
|
||||
$message = new Response(200, $headers);
|
||||
$this->assertEquals(['bar'], $message->getHeader('X-foo'));
|
||||
}
|
||||
|
||||
public function testSetsHeadersWithArrayValueOnConstruction(): void
|
||||
{
|
||||
$headers = ['X-foo' => ['bar', 'baz']];
|
||||
$message = new Response(200, $headers);
|
||||
$this->assertEquals(['bar', 'baz'], $message->getHeader('X-foo'));
|
||||
}
|
||||
|
||||
public function testSetsBodyOnConstruction(): void
|
||||
{
|
||||
$body = new Stream('Hello, world');
|
||||
$message = new Response(200, [], $body);
|
||||
$this->assertSame($body, $message->getBody());
|
||||
}
|
||||
|
||||
public function testCloneMakesDeepCopyOfHeaders(): void
|
||||
{
|
||||
$message1 = (new Response())
|
||||
->withHeader('Content-type', 'text/plain');
|
||||
$message2 = $message1
|
||||
->withHeader('Content-type', 'application/json');
|
||||
|
||||
$this->assertNotEquals(
|
||||
$message1->getHeader('Content-type'),
|
||||
$message2->getHeader('Content-type')
|
||||
);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Protocol Version
|
||||
|
||||
public function testGetProtocolVersionReturnsProtocolVersion1Point1ByDefault(): void
|
||||
{
|
||||
$message = new Response();
|
||||
$this->assertEquals('1.1', $message->getProtocolVersion());
|
||||
}
|
||||
|
||||
public function testGetProtocolVersionReturnsProtocolVersion(): void
|
||||
{
|
||||
$message = (new Response())
|
||||
->withProtocolVersion('1.0');
|
||||
$this->assertEquals('1.0', $message->getProtocolVersion());
|
||||
}
|
||||
|
||||
public function testGetProtocolVersionReplacesProtocolVersion(): void
|
||||
{
|
||||
$message = (new Response())
|
||||
->withProtocolVersion('1.0');
|
||||
$this->assertEquals('1.0', $message->getProtocolVersion());
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Headers
|
||||
|
||||
/**
|
||||
* @dataProvider validHeaderValueProvider
|
||||
* @param array $expected
|
||||
* @param mixed $value
|
||||
*/
|
||||
public function testWithHeaderReplacesHeader(array $expected, $value): void
|
||||
{
|
||||
$message = (new Response())
|
||||
->withHeader('X-foo', 'Original value')
|
||||
->withHeader('X-foo', $value);
|
||||
$this->assertEquals($expected, $message->getHeader('X-foo'));
|
||||
}
|
||||
|
||||
public function validHeaderValueProvider(): array
|
||||
{
|
||||
return [
|
||||
[['0'], 0],
|
||||
[['molly','bear'],['molly','bear']]
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider invalidHeaderProvider
|
||||
* @param mixed $name
|
||||
* @param mixed $value
|
||||
*/
|
||||
public function testWithHeaderThrowsExceptionWithInvalidArgument($name, $value): void
|
||||
{
|
||||
$this->expectException(InvalidArgumentException::class);
|
||||
(new Response())
|
||||
->withHeader($name, $value);
|
||||
}
|
||||
|
||||
public function invalidHeaderProvider(): array
|
||||
{
|
||||
return [
|
||||
[0, 1024],
|
||||
['Content-length', false],
|
||||
['Content-length', [false]]
|
||||
];
|
||||
}
|
||||
|
||||
public function testWithAddedHeaderSetsHeader(): void
|
||||
{
|
||||
$message = (new Response())
|
||||
->withAddedHeader('Content-type', 'application/json');
|
||||
$this->assertEquals(['application/json'], $message->getHeader('Content-type'));
|
||||
}
|
||||
|
||||
public function testWithAddedHeaderAppendsValue(): void
|
||||
{
|
||||
$message = (new Response())
|
||||
->withAddedHeader('Set-Cookie', ['cat=Molly'])
|
||||
->withAddedHeader('Set-Cookie', ['dog=Bear']);
|
||||
$cookies = $message->getHeader('Set-Cookie');
|
||||
$this->assertTrue(
|
||||
in_array('cat=Molly', $cookies) &&
|
||||
in_array('dog=Bear', $cookies)
|
||||
);
|
||||
}
|
||||
|
||||
public function testWithoutHeaderRemovesHeader(): void
|
||||
{
|
||||
$message = (new Response())
|
||||
->withHeader('Content-type', 'application/json')
|
||||
->withoutHeader('Content-type');
|
||||
$this->assertFalse($message->hasHeader('Content-type'));
|
||||
}
|
||||
|
||||
public function testGetHeaderReturnsEmptyArrayForUnsetHeader(): void
|
||||
{
|
||||
$message = new Response();
|
||||
$this->assertEquals([], $message->getHeader('X-name'));
|
||||
}
|
||||
|
||||
public function testGetHeaderReturnsSingleHeader(): void
|
||||
{
|
||||
$message = (new Response())
|
||||
->withAddedHeader('Content-type', 'application/json');
|
||||
$this->assertEquals(['application/json'], $message->getHeader('Content-type'));
|
||||
}
|
||||
|
||||
public function testGetHeaderReturnsMultipleValuesForHeader(): void
|
||||
{
|
||||
$message = (new Response())
|
||||
->withAddedHeader('X-name', 'cat=Molly')
|
||||
->withAddedHeader('X-name', 'dog=Bear');
|
||||
$this->assertEquals(['cat=Molly', 'dog=Bear'], $message->getHeader('X-name'));
|
||||
}
|
||||
|
||||
public function testGetHeaderLineReturnsEmptyStringForUnsetHeader(): void
|
||||
{
|
||||
$message = new Response();
|
||||
$this->assertSame('', $message->getHeaderLine('X-not-set'));
|
||||
}
|
||||
|
||||
public function testGetHeaderLineReturnsMultipleHeadersJoinedByCommas(): void
|
||||
{
|
||||
$message = (new Response())
|
||||
->withAddedHeader('X-name', 'cat=Molly')
|
||||
->withAddedHeader('X-name', 'dog=Bear');
|
||||
$this->assertEquals('cat=Molly, dog=Bear', $message->getHeaderLine('X-name'));
|
||||
}
|
||||
|
||||
public function testHasHeaderReturnsTrueWhenHeaderIsSet(): void
|
||||
{
|
||||
$message = (new Response())
|
||||
->withHeader('Content-type', 'application/json');
|
||||
$this->assertTrue($message->hasHeader('Content-type'));
|
||||
}
|
||||
|
||||
public function testHasHeaderReturnsFalseWhenHeaderIsNotSet(): void
|
||||
{
|
||||
$message = new Response();
|
||||
$this->assertFalse($message->hasHeader('Content-type'));
|
||||
}
|
||||
|
||||
public function testGetHeadersReturnOriginalHeaderNamesAsKeys(): void
|
||||
{
|
||||
$message = (new Response())
|
||||
->withHeader('Set-Cookie', 'cat=Molly')
|
||||
->withAddedHeader('Set-Cookie', 'dog=Bear')
|
||||
->withHeader('Content-type', 'application/json');
|
||||
|
||||
$headers = [];
|
||||
foreach ($message->getHeaders() as $key => $values) {
|
||||
$headers[] = $key;
|
||||
}
|
||||
|
||||
$expected = ['Content-type', 'Set-Cookie'];
|
||||
$countUnmatched
|
||||
= count(array_diff($expected, $headers))
|
||||
+ count(array_diff($headers, $expected));
|
||||
$this->assertEquals(0, $countUnmatched);
|
||||
}
|
||||
|
||||
public function testGetHeadersReturnOriginalHeaderNamesAndValues(): void
|
||||
{
|
||||
$message = (new Response())
|
||||
->withHeader('Set-Cookie', 'cat=Molly')
|
||||
->withAddedHeader('Set-Cookie', 'dog=Bear')
|
||||
->withHeader('Content-type', 'application/json');
|
||||
|
||||
$headers = [];
|
||||
|
||||
foreach ($message->getHeaders() as $key => $values) {
|
||||
foreach ($values as $value) {
|
||||
if (isset($headers[$key])) {
|
||||
$headers[$key][] = $value;
|
||||
} else {
|
||||
$headers[$key] = [$value];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$expected = [
|
||||
'Set-Cookie' => ['cat=Molly', 'dog=Bear'],
|
||||
'Content-type' => ['application/json']
|
||||
];
|
||||
|
||||
$this->assertEquals($expected, $headers);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Body
|
||||
|
||||
public function testGetBodyReturnsEmptyStreamByDefault(): void
|
||||
{
|
||||
$message = new Response();
|
||||
$this->assertEquals('', (string) $message->getBody());
|
||||
}
|
||||
|
||||
public function testGetBodyReturnsAttachedStream(): void
|
||||
{
|
||||
$stream = new Stream('Hello, world!');
|
||||
|
||||
$message = (new Response())
|
||||
->withBody($stream);
|
||||
$this->assertSame($stream, $message->getBody());
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,109 @@
|
|||
<?php
|
||||
|
||||
namespace WellRESTed\Message;
|
||||
|
||||
use RuntimeException;
|
||||
use WellRESTed\Test\TestCase;
|
||||
|
||||
class NullStreamTest extends TestCase
|
||||
{
|
||||
public function testCastsToString(): void
|
||||
{
|
||||
$stream = new NullStream();
|
||||
$this->assertEquals('', (string) $stream);
|
||||
}
|
||||
|
||||
public function testCloseDoesNothing(): void
|
||||
{
|
||||
$stream = new NullStream();
|
||||
$stream->close();
|
||||
$this->assertTrue(true); // Asserting no exception occurred.
|
||||
}
|
||||
|
||||
public function testDetachReturnsNull(): void
|
||||
{
|
||||
$stream = new NullStream();
|
||||
$this->assertNull($stream->detach());
|
||||
}
|
||||
|
||||
public function testSizeReturnsZero(): void
|
||||
{
|
||||
$stream = new NullStream();
|
||||
$this->assertEquals(0, $stream->getSize());
|
||||
}
|
||||
|
||||
public function testTellReturnsZero(): void
|
||||
{
|
||||
$stream = new NullStream();
|
||||
$this->assertEquals(0, $stream->tell());
|
||||
}
|
||||
|
||||
public function testEofReturnsTrue(): void
|
||||
{
|
||||
$stream = new NullStream();
|
||||
$this->assertTrue($stream->eof());
|
||||
}
|
||||
|
||||
public function testIsSeekableReturnsFalse(): void
|
||||
{
|
||||
$stream = new NullStream();
|
||||
$this->assertFalse($stream->isSeekable());
|
||||
}
|
||||
|
||||
public function testSeekReturnsFalse(): void
|
||||
{
|
||||
$this->expectException(RuntimeException::class);
|
||||
$stream = new NullStream();
|
||||
$stream->seek(10);
|
||||
}
|
||||
|
||||
public function testRewindThrowsException(): void
|
||||
{
|
||||
$this->expectException(RuntimeException::class);
|
||||
$stream = new NullStream();
|
||||
$stream->rewind();
|
||||
}
|
||||
|
||||
public function testIsWritableReturnsFalse(): void
|
||||
{
|
||||
$stream = new NullStream();
|
||||
$this->assertFalse($stream->isWritable());
|
||||
}
|
||||
|
||||
public function testWriteThrowsException(): void
|
||||
{
|
||||
$this->expectException(RuntimeException::class);
|
||||
$stream = new NullStream();
|
||||
$stream->write('');
|
||||
}
|
||||
|
||||
public function testIsReadableReturnsTrue(): void
|
||||
{
|
||||
$stream = new NullStream();
|
||||
$this->assertTrue($stream->isReadable());
|
||||
}
|
||||
|
||||
public function testReadReturnsEmptyString(): void
|
||||
{
|
||||
$stream = new NullStream();
|
||||
$this->assertEquals('', $stream->read(100));
|
||||
}
|
||||
|
||||
public function testGetContentsReturnsEmptyString(): void
|
||||
{
|
||||
$stream = new NullStream();
|
||||
$this->assertEquals('', $stream->getContents());
|
||||
}
|
||||
|
||||
public function testGetMetadataReturnsNull(): void
|
||||
{
|
||||
$stream = new NullStream();
|
||||
$this->assertNull($stream->getMetadata());
|
||||
}
|
||||
|
||||
public function testGetMetadataReturnsNullWithKey(): void
|
||||
{
|
||||
$stream = new NullStream();
|
||||
$this->assertNull($stream->getMetadata('size'));
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,206 @@
|
|||
<?php
|
||||
|
||||
namespace WellRESTed\Message;
|
||||
|
||||
use InvalidArgumentException;
|
||||
use WellRESTed\Test\TestCase;
|
||||
|
||||
class RequestTest extends TestCase
|
||||
{
|
||||
// -------------------------------------------------------------------------
|
||||
// Construction
|
||||
|
||||
public function testCreatesInstanceWithNoParameters(): void
|
||||
{
|
||||
$request = new Request();
|
||||
$this->assertNotNull($request);
|
||||
}
|
||||
|
||||
public function testCreatesInstanceWithMethod(): void
|
||||
{
|
||||
$method = 'POST';
|
||||
$request = new Request($method);
|
||||
$this->assertSame($method, $request->getMethod());
|
||||
}
|
||||
|
||||
public function testCreatesInstanceWithUri(): void
|
||||
{
|
||||
$uri = new Uri();
|
||||
$request = new Request('GET', $uri);
|
||||
$this->assertSame($uri, $request->getUri());
|
||||
}
|
||||
|
||||
public function testCreatesInstanceWithStringUri(): void
|
||||
{
|
||||
$uri = 'http://localhost:8080';
|
||||
$request = new Request('GET', $uri);
|
||||
$this->assertSame($uri, (string) $request->getUri());
|
||||
}
|
||||
|
||||
public function testSetsHeadersOnConstruction(): void
|
||||
{
|
||||
$request = new Request('GET', '/', [
|
||||
'X-foo' => ['bar', 'baz']
|
||||
]);
|
||||
$this->assertEquals(['bar', 'baz'], $request->getHeader('X-foo'));
|
||||
}
|
||||
|
||||
public function testSetsBodyOnConstruction(): void
|
||||
{
|
||||
$body = new NullStream();
|
||||
$request = new Request('GET', '/', [], $body);
|
||||
$this->assertSame($body, $request->getBody());
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Request Target
|
||||
|
||||
public function testGetRequestTargetPrefersExplicitRequestTarget(): void
|
||||
{
|
||||
$request = new Request();
|
||||
$request = $request->withRequestTarget('*');
|
||||
$this->assertEquals('*', $request->getRequestTarget());
|
||||
}
|
||||
|
||||
public function testGetRequestTargetUsesOriginFormOfUri(): void
|
||||
{
|
||||
$uri = new Uri('/my/path?cat=Molly&dog=Bear');
|
||||
$request = new Request();
|
||||
$request = $request->withUri($uri);
|
||||
$this->assertEquals('/my/path?cat=Molly&dog=Bear', $request->getRequestTarget());
|
||||
}
|
||||
|
||||
public function testGetRequestTargetReturnsSlashByDefault(): void
|
||||
{
|
||||
$request = new Request();
|
||||
$this->assertEquals('/', $request->getRequestTarget());
|
||||
}
|
||||
|
||||
public function testWithRequestTargetCreatesNewInstance(): void
|
||||
{
|
||||
$request = new Request();
|
||||
$request = $request->withRequestTarget('*');
|
||||
$this->assertEquals('*', $request->getRequestTarget());
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Method
|
||||
|
||||
public function testGetMethodReturnsGetByDefault(): void
|
||||
{
|
||||
$request = new Request();
|
||||
$this->assertEquals('GET', $request->getMethod());
|
||||
}
|
||||
|
||||
public function testWithMethodCreatesNewInstance(): void
|
||||
{
|
||||
$request = new Request();
|
||||
$request = $request->withMethod('POST');
|
||||
$this->assertEquals('POST', $request->getMethod());
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider invalidMethodProvider
|
||||
* @param mixed $method
|
||||
*/
|
||||
public function testWithMethodThrowsExceptionOnInvalidMethod($method): void
|
||||
{
|
||||
$this->expectException(InvalidArgumentException::class);
|
||||
$request = new Request();
|
||||
$request->withMethod($method);
|
||||
}
|
||||
|
||||
public function invalidMethodProvider(): array
|
||||
{
|
||||
return [
|
||||
[0],
|
||||
[false],
|
||||
['WITH SPACE']
|
||||
];
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Request URI
|
||||
|
||||
public function testGetUriReturnsEmptyUriByDefault(): void
|
||||
{
|
||||
$request = new Request();
|
||||
$uri = new Uri();
|
||||
$this->assertEquals($uri, $request->getUri());
|
||||
}
|
||||
|
||||
public function testWithUriCreatesNewInstance(): void
|
||||
{
|
||||
$uri = new Uri();
|
||||
$request = new Request();
|
||||
$request = $request->withUri($uri);
|
||||
$this->assertSame($uri, $request->getUri());
|
||||
}
|
||||
|
||||
public function testWithUriPreservesOriginalRequest(): void
|
||||
{
|
||||
$uri1 = new Uri();
|
||||
$uri2 = new Uri();
|
||||
|
||||
$request1 = new Request();
|
||||
$request1 = $request1->withUri($uri1);
|
||||
$request1 = $request1->withHeader('Accept', 'application/json');
|
||||
|
||||
$request2 = $request1->withUri($uri2);
|
||||
$request2 = $request2->withHeader('Accept', 'text/plain');
|
||||
|
||||
$this->assertNotEquals($request1->getHeader('Accept'), $request2->getHeader('Accept'));
|
||||
}
|
||||
|
||||
public function testWithUriUpdatesHostHeader(): void
|
||||
{
|
||||
$hostname = 'bar.com';
|
||||
$uri = new uri("http://$hostname");
|
||||
|
||||
$request = new Request();
|
||||
$request = $request->withHeader('Host', 'foo.com');
|
||||
$request = $request->withUri($uri);
|
||||
$this->assertSame([$hostname], $request->getHeader('Host'));
|
||||
}
|
||||
|
||||
public function testWithUriDoesNotUpdatesHostHeaderWhenUriHasNoHost(): void
|
||||
{
|
||||
$hostname = 'foo.com';
|
||||
$uri = new Uri();
|
||||
|
||||
$request = new Request();
|
||||
$request = $request->withHeader('Host', $hostname);
|
||||
$request = $request->withUri($uri);
|
||||
$this->assertSame([$hostname], $request->getHeader('Host'));
|
||||
}
|
||||
|
||||
public function testPreserveHostUpdatesHostHeaderWhenHeaderIsOriginallyMissing(): void
|
||||
{
|
||||
$hostname = 'foo.com';
|
||||
$uri = new uri("http://$hostname");
|
||||
|
||||
$request = new Request();
|
||||
$request = $request->withUri($uri, true);
|
||||
$this->assertSame([$hostname], $request->getHeader('Host'));
|
||||
}
|
||||
|
||||
public function testPreserveHostDoesNotUpdatesWhenBothAreMissingHosts(): void
|
||||
{
|
||||
$uri = new Uri();
|
||||
|
||||
$request = new Request();
|
||||
$request = $request->withUri($uri, true);
|
||||
$this->assertSame([], $request->getHeader('Host'));
|
||||
}
|
||||
|
||||
public function testPreserveHostDoesNotUpdateHostHeader(): void
|
||||
{
|
||||
$hostname = 'foo.com';
|
||||
$uri = new uri('http://bar.com');
|
||||
|
||||
$request = new Request();
|
||||
$request = $request->withHeader('Host', $hostname);
|
||||
$request = $request->withUri($uri, true);
|
||||
$this->assertSame([$hostname], $request->getHeader('Host'));
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
<?php
|
||||
|
||||
namespace WellRESTed\Message;
|
||||
|
||||
use WellRESTed\Test\TestCase;
|
||||
|
||||
class ResponseFactoryTest extends TestCase
|
||||
{
|
||||
public function testCreatesResponseWithStatusCode200ByDefault(): void
|
||||
{
|
||||
$statusCode = 200;
|
||||
$reasonPhrase = 'OK';
|
||||
|
||||
$factory = new ResponseFactory();
|
||||
$response = $factory->createResponse();
|
||||
|
||||
$this->assertEquals($statusCode, $response->getStatusCode());
|
||||
$this->assertEquals($reasonPhrase, $response->getReasonPhrase());
|
||||
}
|
||||
|
||||
public function testCreateResponseWithStatusCode(): void
|
||||
{
|
||||
$statusCode = 201;
|
||||
$reasonPhrase = 'Created';
|
||||
|
||||
$factory = new ResponseFactory();
|
||||
$response = $factory->createResponse($statusCode);
|
||||
|
||||
$this->assertEquals($statusCode, $response->getStatusCode());
|
||||
$this->assertEquals($reasonPhrase, $response->getReasonPhrase());
|
||||
}
|
||||
|
||||
public function testCreateResponseWithStatusCodeAndCustomReasonPhrase(): void
|
||||
{
|
||||
$statusCode = 512;
|
||||
$reasonPhrase = 'Shortage of Chairs';
|
||||
|
||||
$factory = new ResponseFactory();
|
||||
$response = $factory->createResponse($statusCode, $reasonPhrase);
|
||||
|
||||
$this->assertEquals($statusCode, $response->getStatusCode());
|
||||
$this->assertEquals($reasonPhrase, $response->getReasonPhrase());
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,115 @@
|
|||
<?php
|
||||
|
||||
namespace WellRESTed\Message;
|
||||
|
||||
use WellRESTed\Test\TestCase;
|
||||
|
||||
class ResponseTest extends TestCase
|
||||
{
|
||||
// -------------------------------------------------------------------------
|
||||
// Construction
|
||||
|
||||
public function testSetsStatusCodeOnConstruction(): void
|
||||
{
|
||||
$response = new Response(200);
|
||||
$this->assertSame(200, $response->getStatusCode());
|
||||
}
|
||||
|
||||
public function testSetsHeadersOnConstruction(): void
|
||||
{
|
||||
$response = new Response(200, [
|
||||
'X-foo' => ['bar','baz']
|
||||
]);
|
||||
$this->assertEquals(['bar','baz'], $response->getHeader('X-foo'));
|
||||
}
|
||||
|
||||
public function testSetsBodyOnConstruction(): void
|
||||
{
|
||||
$body = new NullStream();
|
||||
$response = new Response(200, [], $body);
|
||||
$this->assertSame($body, $response->getBody());
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Status and Reason Phrase
|
||||
|
||||
public function testCreatesNewInstanceWithStatusCode(): void
|
||||
{
|
||||
$response = new Response();
|
||||
$copy = $response->withStatus(200);
|
||||
$this->assertEquals(200, $copy->getStatusCode());
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider statusProvider
|
||||
* @param int $code
|
||||
* @param string|null $reasonPhrase
|
||||
* @param string $expected
|
||||
*/
|
||||
public function testCreatesNewInstanceWithReasonPhrase(
|
||||
int $code,
|
||||
?string $reasonPhrase,
|
||||
string $expected
|
||||
): void {
|
||||
$response = new Response();
|
||||
$copy = $response->withStatus($code, $reasonPhrase);
|
||||
$this->assertEquals($expected, $copy->getReasonPhrase());
|
||||
}
|
||||
|
||||
public function statusProvider(): array
|
||||
{
|
||||
return [
|
||||
[100, null, 'Continue'],
|
||||
[101, null, 'Switching Protocols'],
|
||||
[200, null, 'OK'],
|
||||
[201, null, 'Created'],
|
||||
[202, null, 'Accepted'],
|
||||
[203, null, 'Non-Authoritative Information'],
|
||||
[204, null, 'No Content'],
|
||||
[205, null, 'Reset Content'],
|
||||
[206, null, 'Partial Content'],
|
||||
[300, null, 'Multiple Choices'],
|
||||
[301, null, 'Moved Permanently'],
|
||||
[302, null, 'Found'],
|
||||
[303, null, 'See Other'],
|
||||
[304, null, 'Not Modified'],
|
||||
[305, null, 'Use Proxy'],
|
||||
[400, null, 'Bad Request'],
|
||||
[401, null, 'Unauthorized'],
|
||||
[402, null, 'Payment Required'],
|
||||
[403, null, 'Forbidden'],
|
||||
[404, null, 'Not Found'],
|
||||
[405, null, 'Method Not Allowed'],
|
||||
[406, null, 'Not Acceptable'],
|
||||
[407, null, 'Proxy Authentication Required'],
|
||||
[408, null, 'Request Timeout'],
|
||||
[409, null, 'Conflict'],
|
||||
[410, null, 'Gone'],
|
||||
[411, null, 'Length Required'],
|
||||
[412, null, 'Precondition Failed'],
|
||||
[413, null, 'Payload Too Large'],
|
||||
[414, null, 'URI Too Long'],
|
||||
[415, null, 'Unsupported Media Type'],
|
||||
[500, null, 'Internal Server Error'],
|
||||
[501, null, 'Not Implemented'],
|
||||
[502, null, 'Bad Gateway'],
|
||||
[503, null, 'Service Unavailable'],
|
||||
[504, null, 'Gateway Timeout'],
|
||||
[505, null, 'HTTP Version Not Supported'],
|
||||
[598, null, ''],
|
||||
[599, 'Nonstandard', 'Nonstandard']
|
||||
];
|
||||
}
|
||||
|
||||
public function testWithStatusCodePreservesOriginalResponse(): void
|
||||
{
|
||||
$response1 = new Response();
|
||||
$response1 = $response1->withStatus(200);
|
||||
$response1 = $response1->withHeader('Content-type', 'application/json');
|
||||
|
||||
$response2 = $response1->withStatus(404);
|
||||
$response2 = $response2->withHeader('Content-type', 'text/plain');
|
||||
|
||||
$this->assertNotEquals($response1->getStatusCode(), $response2->getHeader('Content-type'));
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,400 @@
|
|||
<?php
|
||||
|
||||
namespace WellRESTed\Message;
|
||||
|
||||
use Psr\Http\Message\UploadedFileInterface;
|
||||
use Psr\Http\Message\UriInterface;
|
||||
use WellRESTed\Test\TestCase;
|
||||
|
||||
/** @backupGlobals enabled */
|
||||
class ServerRequestMarshallerTest extends TestCase
|
||||
{
|
||||
/** @var ServerRequestMarshaller */
|
||||
private $marshaller;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$_SERVER = [
|
||||
'HTTP_HOST' => 'localhost',
|
||||
'HTTP_ACCEPT' => 'application/json',
|
||||
'HTTP_CONTENT_TYPE' => 'application/x-www-form-urlencoded',
|
||||
'QUERY_STRING' => 'cat=molly&kitten=aggie'
|
||||
];
|
||||
|
||||
$_COOKIE = [
|
||||
'dog' => 'Bear',
|
||||
'hamster' => 'Dusty'
|
||||
];
|
||||
|
||||
FopenHelper::$inputTempFile = null;
|
||||
|
||||
$this->marshaller = new ServerRequestMarshaller();
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Psr\Http\Message\MessageInterface
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Protocol Version
|
||||
|
||||
/**
|
||||
* @dataProvider protocolVersionProvider
|
||||
* @param $expectedProtocol
|
||||
* @param $actualProtocol
|
||||
*/
|
||||
public function testProvidesProtocolVersion(string $expectedProtocol, ?string $actualProtocol): void
|
||||
{
|
||||
$_SERVER['SERVER_PROTOCOL'] = $actualProtocol;
|
||||
$request = $this->marshaller->getServerRequest();
|
||||
$this->assertEquals($expectedProtocol, $request->getProtocolVersion());
|
||||
}
|
||||
|
||||
public function protocolVersionProvider(): array
|
||||
{
|
||||
return [
|
||||
['1.1', 'HTTP/1.1'],
|
||||
['1.0', 'HTTP/1.0'],
|
||||
['1.1', null],
|
||||
['1.1', 'INVALID']
|
||||
];
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Headers
|
||||
|
||||
public function testProvidesHeadersFromHttpFields(): void
|
||||
{
|
||||
$_SERVER = [
|
||||
'HTTP_ACCEPT' => 'application/json',
|
||||
'HTTP_CONTENT_TYPE' => 'application/x-www-form-urlencoded'
|
||||
];
|
||||
$request = $this->marshaller->getServerRequest();
|
||||
$this->assertEquals(['application/json'], $request->getHeader('Accept'));
|
||||
$this->assertEquals(['application/x-www-form-urlencoded'], $request->getHeader('Content-type'));
|
||||
}
|
||||
|
||||
public function testProvidesApacheContentHeaders(): void
|
||||
{
|
||||
$_SERVER = [
|
||||
'CONTENT_LENGTH' => '1024',
|
||||
'CONTENT_TYPE' => 'application/json'
|
||||
];
|
||||
$request = $this->marshaller->getServerRequest();
|
||||
$this->assertEquals('1024', $request->getHeaderLine('Content-length'));
|
||||
$this->assertEquals('application/json', $request->getHeaderLine('Content-type'));
|
||||
}
|
||||
|
||||
public function testDoesNotProvideEmptyApacheContentHeaders(): void
|
||||
{
|
||||
$_SERVER = [
|
||||
'CONTENT_LENGTH' => '',
|
||||
'CONTENT_TYPE' => ' '
|
||||
];
|
||||
$request = $this->marshaller->getServerRequest();
|
||||
$this->assertFalse($request->hasHeader('Content-length'));
|
||||
$this->assertFalse($request->hasHeader('Content-type'));
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Body
|
||||
|
||||
public function testProvidesBodyFromInputStream(): void
|
||||
{
|
||||
$tempFilePath = tempnam(sys_get_temp_dir(), 'test');
|
||||
$content = 'Body content';
|
||||
file_put_contents($tempFilePath, $content);
|
||||
FopenHelper::$inputTempFile = $tempFilePath;
|
||||
|
||||
$request = $this->marshaller->getServerRequest();
|
||||
unlink($tempFilePath);
|
||||
|
||||
$this->assertEquals($content, (string) $request->getBody());
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Psr\Http\Message\RequestInterface
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Request Target
|
||||
|
||||
/**
|
||||
* @dataProvider requestTargetProvider
|
||||
* @param $expectedRequestTarget
|
||||
* @param $actualRequestUri
|
||||
*/
|
||||
public function testProvidesRequestTarget(string $expectedRequestTarget, ?string $actualRequestUri): void
|
||||
{
|
||||
$_SERVER['REQUEST_URI'] = $actualRequestUri;
|
||||
$request = $this->marshaller->getServerRequest();
|
||||
$this->assertEquals($expectedRequestTarget, $request->getRequestTarget());
|
||||
}
|
||||
|
||||
public function requestTargetProvider(): array
|
||||
{
|
||||
return [
|
||||
['/', '/'],
|
||||
['/hello', '/hello'],
|
||||
['/my/path.txt', '/my/path.txt'],
|
||||
['/', null]
|
||||
];
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Method
|
||||
|
||||
/**
|
||||
* @dataProvider methodProvider
|
||||
* @param $expectedMethod
|
||||
* @param $serverMethod
|
||||
*/
|
||||
public function testProvidesMethod($expectedMethod, $serverMethod)
|
||||
{
|
||||
$_SERVER['REQUEST_METHOD'] = $serverMethod;
|
||||
$request = $this->marshaller->getServerRequest();
|
||||
$this->assertEquals($expectedMethod, $request->getMethod());
|
||||
}
|
||||
|
||||
public function methodProvider()
|
||||
{
|
||||
return [
|
||||
['GET', 'GET'],
|
||||
['POST', 'POST'],
|
||||
['DELETE', 'DELETE'],
|
||||
['PUT', 'PUT'],
|
||||
['OPTIONS', 'OPTIONS'],
|
||||
['GET', null]
|
||||
];
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// URI
|
||||
|
||||
/**
|
||||
* @dataProvider uriProvider
|
||||
* @param UriInterface $expected
|
||||
* @param array $serverParams
|
||||
*/
|
||||
public function testProvidesUri(UriInterface $expected, array $serverParams): void
|
||||
{
|
||||
$_SERVER = $serverParams;
|
||||
$request = $this->marshaller->getServerRequest();
|
||||
$this->assertEquals($expected, $request->getUri());
|
||||
}
|
||||
|
||||
public function uriProvider()
|
||||
{
|
||||
return [
|
||||
[
|
||||
new Uri('http://localhost/path'),
|
||||
[
|
||||
'HTTPS' => 'off',
|
||||
'HTTP_HOST' => 'localhost',
|
||||
'REQUEST_URI' => '/path',
|
||||
'QUERY_STRING' => ''
|
||||
]
|
||||
],
|
||||
[
|
||||
new Uri('https://foo.com/path/to/stuff?cat=molly'),
|
||||
[
|
||||
'HTTPS' => '1',
|
||||
'HTTP_HOST' => 'foo.com',
|
||||
'REQUEST_URI' => '/path/to/stuff?cat=molly',
|
||||
'QUERY_STRING' => 'cat=molly'
|
||||
]
|
||||
],
|
||||
[
|
||||
new Uri('http://foo.com:8080/path/to/stuff?cat=molly'),
|
||||
[
|
||||
'HTTP' => '1',
|
||||
'HTTP_HOST' => 'foo.com:8080',
|
||||
'REQUEST_URI' => '/path/to/stuff?cat=molly',
|
||||
'QUERY_STRING' => 'cat=molly'
|
||||
]
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Psr\Http\Message\ServerRequestInterface
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Server Params
|
||||
|
||||
public function testProvidesServerParams(): void
|
||||
{
|
||||
$request = $this->marshaller->getServerRequest();
|
||||
$this->assertEquals($_SERVER, $request->getServerParams());
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Cookies
|
||||
|
||||
public function testProvidesCookieParams(): void
|
||||
{
|
||||
$request = $this->marshaller->getServerRequest();
|
||||
$this->assertEquals($_COOKIE, $request->getCookieParams());
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Query
|
||||
|
||||
public function testProvidesQueryParams(): void
|
||||
{
|
||||
$request = $this->marshaller->getServerRequest();
|
||||
$query = $request->getQueryParams();
|
||||
$this->assertCount(2, $query);
|
||||
$this->assertEquals('molly', $query['cat']);
|
||||
$this->assertEquals('aggie', $query['kitten']);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Uploaded Files
|
||||
|
||||
/**
|
||||
* @dataProvider uploadedFileProvider
|
||||
* @param UploadedFileInterface $file
|
||||
* @param array $path
|
||||
*/
|
||||
public function testGetServerRequestReadsUploadedFiles(UploadedFileInterface $file, array $path): void
|
||||
{
|
||||
$_FILES = [
|
||||
'single' => [
|
||||
'name' => 'single.txt',
|
||||
'type' => 'text/plain',
|
||||
'tmp_name' => '/tmp/php9hNlHe',
|
||||
'error' => UPLOAD_ERR_OK,
|
||||
'size' => 524
|
||||
],
|
||||
'nested' => [
|
||||
'level2' => [
|
||||
'name' => 'nested.json',
|
||||
'type' => 'application/json',
|
||||
'tmp_name' => '/tmp/phpadhjk',
|
||||
'error' => UPLOAD_ERR_OK,
|
||||
'size' => 1024
|
||||
]
|
||||
],
|
||||
'nestedList' => [
|
||||
'level2' => [
|
||||
'name' => [
|
||||
0 => 'nestedList0.jpg',
|
||||
1 => 'nestedList1.jpg',
|
||||
2 => ''
|
||||
],
|
||||
'type' => [
|
||||
0 => 'image/jpeg',
|
||||
1 => 'image/jpeg',
|
||||
2 => ''
|
||||
],
|
||||
'tmp_name' => [
|
||||
0 => '/tmp/phpjpg0',
|
||||
1 => '/tmp/phpjpg1',
|
||||
2 => ''
|
||||
],
|
||||
'error' => [
|
||||
0 => UPLOAD_ERR_OK,
|
||||
1 => UPLOAD_ERR_OK,
|
||||
2 => UPLOAD_ERR_NO_FILE
|
||||
],
|
||||
'size' => [
|
||||
0 => 256,
|
||||
1 => 4096,
|
||||
2 => 0
|
||||
]
|
||||
]
|
||||
],
|
||||
'nestedDictionary' => [
|
||||
'level2' => [
|
||||
'name' => [
|
||||
'file0' => 'nestedDictionary0.jpg',
|
||||
'file1' => 'nestedDictionary1.jpg'
|
||||
],
|
||||
'type' => [
|
||||
'file0' => 'image/png',
|
||||
'file1' => 'image/png'
|
||||
],
|
||||
'tmp_name' => [
|
||||
'file0' => '/tmp/phppng0',
|
||||
'file1' => '/tmp/phppng1'
|
||||
],
|
||||
'error' => [
|
||||
'file0' => UPLOAD_ERR_OK,
|
||||
'file1' => UPLOAD_ERR_OK
|
||||
],
|
||||
'size' => [
|
||||
'file0' => 256,
|
||||
'file1' => 4096
|
||||
]
|
||||
]
|
||||
]
|
||||
];
|
||||
$request = $this->marshaller->getServerRequest();
|
||||
$current = $request->getUploadedFiles();
|
||||
foreach ($path as $item) {
|
||||
$current = $current[$item];
|
||||
}
|
||||
$this->assertEquals($file, $current);
|
||||
}
|
||||
|
||||
public function uploadedFileProvider(): array
|
||||
{
|
||||
return [
|
||||
[new UploadedFile('single.txt', 'text/plain', 524, '/tmp/php9hNlHe', UPLOAD_ERR_OK), ['single']],
|
||||
[new UploadedFile('nested.json', 'application/json', 1024, '/tmp/phpadhjk', UPLOAD_ERR_OK), ['nested', 'level2']],
|
||||
[new UploadedFile('nestedList0.jpg', 'image/jpeg', 256, '/tmp/phpjpg0', UPLOAD_ERR_OK), ['nestedList', 'level2', 0]],
|
||||
[new UploadedFile('nestedList1.jpg', 'image/jpeg', 4096, '/tmp/phpjpg1', UPLOAD_ERR_OK), ['nestedList', 'level2', 1]],
|
||||
[new UploadedFile('', '', 0, '', UPLOAD_ERR_NO_FILE), ['nestedList', 'level2', 2]],
|
||||
[new UploadedFile('nestedDictionary0.jpg', 'image/png', 256, '/tmp/phppng0', UPLOAD_ERR_OK), ['nestedDictionary', 'level2', 'file0']],
|
||||
[new UploadedFile('nestedDictionary1.jpg', 'image/png', 4096, '/tmp/phppngg1', UPLOAD_ERR_OK), ['nestedDictionary', 'level2', 'file1']]
|
||||
];
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Parsed Body
|
||||
|
||||
/**
|
||||
* @dataProvider formContentTypeProvider
|
||||
* @param string $contentType
|
||||
*/
|
||||
public function testProvidesParsedBodyForForms(string $contentType): void
|
||||
{
|
||||
$_SERVER['HTTP_CONTENT_TYPE'] = $contentType;
|
||||
$_POST = [
|
||||
'dog' => 'Bear'
|
||||
];
|
||||
$request = $this->marshaller->getServerRequest();
|
||||
$this->assertEquals('Bear', $request->getParsedBody()['dog']);
|
||||
}
|
||||
|
||||
public function formContentTypeProvider(): array
|
||||
{
|
||||
return [
|
||||
['application/x-www-form-urlencoded'],
|
||||
['multipart/form-data']
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
// Declare fopen function in this namespace so the class under test will use
|
||||
// this instead of the internal global functions during testing.
|
||||
|
||||
class FopenHelper
|
||||
{
|
||||
/**
|
||||
* @var string Path to temp file to read in place of 'php://input'
|
||||
*/
|
||||
public static $inputTempFile;
|
||||
}
|
||||
|
||||
function fopen($filename, $mode)
|
||||
{
|
||||
if (FopenHelper::$inputTempFile && $filename === 'php://input') {
|
||||
$filename = FopenHelper::$inputTempFile;
|
||||
}
|
||||
|
||||
return \fopen($filename, $mode);
|
||||
}
|
||||
|
|
@ -0,0 +1,315 @@
|
|||
<?php
|
||||
|
||||
namespace WellRESTed\Message;
|
||||
|
||||
use InvalidArgumentException;
|
||||
use WellRESTed\Test\TestCase;
|
||||
|
||||
class ServerRequestTest extends TestCase
|
||||
{
|
||||
// -------------------------------------------------------------------------
|
||||
// Server Params
|
||||
|
||||
public function testGetServerParamsReturnsEmptyArrayByDefault(): void
|
||||
{
|
||||
$request = new ServerRequest();
|
||||
$this->assertEquals([], $request->getServerParams());
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Cookies
|
||||
|
||||
public function testGetCookieParamsReturnsEmptyArrayByDefault(): void
|
||||
{
|
||||
$request = new ServerRequest();
|
||||
$this->assertEquals([], $request->getCookieParams());
|
||||
}
|
||||
|
||||
public function testWithCookieParamsCreatesNewInstanceWithCookies(): void
|
||||
{
|
||||
$cookies = [
|
||||
'cat' => 'Oscar'
|
||||
];
|
||||
|
||||
$request1 = new ServerRequest();
|
||||
$request2 = $request1->withCookieParams($cookies);
|
||||
|
||||
$this->assertEquals($cookies, $request2->getCookieParams());
|
||||
$this->assertNotSame($request2, $request1);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Query
|
||||
|
||||
public function testGetQueryParamsReturnsEmptyArrayByDefault(): void
|
||||
{
|
||||
$request = new ServerRequest();
|
||||
$this->assertEquals([], $request->getQueryParams());
|
||||
}
|
||||
|
||||
public function testWithQueryParamsCreatesNewInstance(): void
|
||||
{
|
||||
$query = [
|
||||
'cat' => 'Aggie'
|
||||
];
|
||||
|
||||
$request1 = new ServerRequest();
|
||||
$request2 = $request1->withQueryParams($query);
|
||||
|
||||
$this->assertEquals($query, $request2->getQueryParams());
|
||||
$this->assertNotSame($request2, $request1);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Uploaded Files
|
||||
|
||||
public function testGetUploadedFilesReturnsEmptyArrayByDefault(): void
|
||||
{
|
||||
$request = new ServerRequest();
|
||||
$this->assertEquals([], $request->getUploadedFiles());
|
||||
}
|
||||
|
||||
public function testWithUploadedFilesCreatesNewInstance(): void
|
||||
{
|
||||
$uploadedFiles = [
|
||||
'file' => new UploadedFile('index.html', 'text/html', 524, '/tmp/php9hNlHe', 0)
|
||||
];
|
||||
$request = new ServerRequest();
|
||||
$request1 = $request->withUploadedFiles([]);
|
||||
$request2 = $request1->withUploadedFiles($uploadedFiles);
|
||||
$this->assertNotSame($request2, $request1);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider validUploadedFilesProvider
|
||||
* @param array $uploadedFiles
|
||||
*/
|
||||
public function testWithUploadedFilesStoresPassedUploadedFiles(array $uploadedFiles): void
|
||||
{
|
||||
$request = new ServerRequest();
|
||||
$request = $request->withUploadedFiles($uploadedFiles);
|
||||
$this->assertSame($uploadedFiles, $request->getUploadedFiles());
|
||||
}
|
||||
|
||||
public function validUploadedFilesProvider(): array
|
||||
{
|
||||
return [
|
||||
[[]],
|
||||
[['files' => new UploadedFile('index.html', 'text/html', 524, '/tmp/php9hNlHe', 0)]],
|
||||
[['nested' => [
|
||||
'level2' => new UploadedFile('index.html', 'text/html', 524, '/tmp/php9hNlHe', 0)
|
||||
]]],
|
||||
[['nestedList' => [
|
||||
'level2' => [
|
||||
new UploadedFile('file1.html', 'text/html', 524, '/tmp/php9hNlHe', 0),
|
||||
new UploadedFile('file2.html', 'text/html', 524, '/tmp/php9hNshj', 0)
|
||||
]
|
||||
]]],
|
||||
[['nestedDictionary' => [
|
||||
'level2' => [
|
||||
'file1' => new UploadedFile('file1.html', 'text/html', 524, '/tmp/php9hNlHe', 0),
|
||||
'file2' => new UploadedFile('file2.html', 'text/html', 524, '/tmp/php9hNshj', 0)
|
||||
]
|
||||
]]]
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider invalidUploadedFilesProvider
|
||||
* @param array $uploadedFiles
|
||||
*/
|
||||
public function testWithUploadedFilesThrowsExceptionWithInvalidTree(array $uploadedFiles): void
|
||||
{
|
||||
$this->expectException(InvalidArgumentException::class);
|
||||
$request = new ServerRequest();
|
||||
$request->withUploadedFiles($uploadedFiles);
|
||||
}
|
||||
|
||||
public function invalidUploadedFilesProvider()
|
||||
{
|
||||
return [
|
||||
// All keys must be strings
|
||||
[[new UploadedFile('index.html', 'text/html', 524, '/tmp/php9hNlHe', 0)]],
|
||||
[
|
||||
[new UploadedFile('index1.html', 'text/html', 524, '/tmp/php9hNlHe', 0)],
|
||||
[new UploadedFile('index2.html', 'text/html', 524, '/tmp/php9hNlHe', 0)]
|
||||
],
|
||||
[
|
||||
'single' => [
|
||||
'name' => 'single.txt',
|
||||
'type' => 'text/plain',
|
||||
'tmp_name' => '/tmp/php9hNlHe',
|
||||
'error' => UPLOAD_ERR_OK,
|
||||
'size' => 524
|
||||
],
|
||||
'nested' => [
|
||||
'level2' => [
|
||||
'name' => 'nested.json',
|
||||
'type' => 'application/json',
|
||||
'tmp_name' => '/tmp/phpadhjk',
|
||||
'error' => UPLOAD_ERR_OK,
|
||||
'size' => 1024
|
||||
]
|
||||
]
|
||||
],
|
||||
[
|
||||
'nestedList' => [
|
||||
'level2' => [
|
||||
'name' => [
|
||||
0 => 'nestedList0.jpg',
|
||||
1 => 'nestedList1.jpg',
|
||||
2 => ''
|
||||
],
|
||||
'type' => [
|
||||
0 => 'image/jpeg',
|
||||
1 => 'image/jpeg',
|
||||
2 => ''
|
||||
],
|
||||
'tmp_name' => [
|
||||
0 => '/tmp/phpjpg0',
|
||||
1 => '/tmp/phpjpg1',
|
||||
2 => ''
|
||||
],
|
||||
'error' => [
|
||||
0 => UPLOAD_ERR_OK,
|
||||
1 => UPLOAD_ERR_OK,
|
||||
2 => UPLOAD_ERR_NO_FILE
|
||||
],
|
||||
'size' => [
|
||||
0 => 256,
|
||||
1 => 4096,
|
||||
2 => 0
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Parsed Body
|
||||
|
||||
public function testGetParsedBodyReturnsNullByDefault(): void
|
||||
{
|
||||
$request = new ServerRequest();
|
||||
$this->assertNull($request->getParsedBody());
|
||||
}
|
||||
|
||||
public function testWithParsedBodyCreatesNewInstance(): void
|
||||
{
|
||||
$body = [
|
||||
'guinea_pig' => 'Clyde'
|
||||
];
|
||||
|
||||
$request1 = new ServerRequest();
|
||||
$request2 = $request1->withParsedBody($body);
|
||||
|
||||
$this->assertEquals($body, $request2->getParsedBody());
|
||||
$this->assertNotSame($request2, $request1);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider invalidParsedBodyProvider
|
||||
* @param mixed $body
|
||||
*/
|
||||
public function testWithParsedBodyThrowsExceptionWithInvalidType($body): void
|
||||
{
|
||||
$this->expectException(InvalidArgumentException::class);
|
||||
$request = new ServerRequest();
|
||||
$request->withParsedBody($body);
|
||||
}
|
||||
|
||||
public function invalidParsedBodyProvider()
|
||||
{
|
||||
return [
|
||||
[false],
|
||||
[1]
|
||||
];
|
||||
}
|
||||
|
||||
public function testCloneMakesDeepCopiesOfParsedBody(): void
|
||||
{
|
||||
$body = (object) [
|
||||
'cat' => 'Dog'
|
||||
];
|
||||
|
||||
$request1 = new ServerRequest();
|
||||
$request1 = $request1->withParsedBody($body);
|
||||
$request2 = $request1->withHeader('X-extra', 'hello world');
|
||||
|
||||
$this->assertTrue(
|
||||
$request1->getParsedBody() == $request2->getParsedBody()
|
||||
&& $request1->getParsedBody() !== $request2->getParsedBody()
|
||||
);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Attributes
|
||||
|
||||
public function testGetAttributesReturnsEmptyArrayByDefault(): void
|
||||
{
|
||||
$request = new ServerRequest();
|
||||
$this->assertEquals([], $request->getAttributes());
|
||||
}
|
||||
|
||||
public function testGetAttributesReturnsAllAttributes(): void
|
||||
{
|
||||
$request = new ServerRequest();
|
||||
$request = $request->withAttribute('cat', 'Molly');
|
||||
$request = $request->withAttribute('dog', 'Bear');
|
||||
$expected = [
|
||||
'cat' => 'Molly',
|
||||
'dog' => 'Bear'
|
||||
];
|
||||
$this->assertEquals($expected, $request->getAttributes());
|
||||
}
|
||||
|
||||
public function testGetAttributeReturnsDefaultIfNotSet(): void
|
||||
{
|
||||
$request = new ServerRequest();
|
||||
$this->assertEquals('Oscar', $request->getAttribute('cat', 'Oscar'));
|
||||
}
|
||||
|
||||
public function testWithAttributeCreatesNewInstance(): void
|
||||
{
|
||||
$request = new ServerRequest();
|
||||
$request = $request->withAttribute('cat', 'Molly');
|
||||
$this->assertEquals('Molly', $request->getAttribute('cat'));
|
||||
}
|
||||
|
||||
public function testWithAttributePreserversOtherAttributes(): void
|
||||
{
|
||||
$request = new ServerRequest();
|
||||
$request = $request->withAttribute('cat', 'Molly');
|
||||
$request = $request->withAttribute('dog', 'Bear');
|
||||
$expected = [
|
||||
'cat' => 'Molly',
|
||||
'dog' => 'Bear'
|
||||
];
|
||||
$this->assertEquals($expected, $request->getAttributes());
|
||||
}
|
||||
|
||||
public function testWithoutAttributeCreatesNewInstance(): void
|
||||
{
|
||||
$request = new ServerRequest();
|
||||
$request = $request->withAttribute('cat', 'Molly');
|
||||
$this->assertNotEquals($request, $request->withoutAttribute('cat'));
|
||||
}
|
||||
|
||||
public function testWithoutAttributeRemovesAttribute(): void
|
||||
{
|
||||
$request = new ServerRequest();
|
||||
$request = $request->withAttribute('cat', 'Molly');
|
||||
$request = $request->withoutAttribute('cat');
|
||||
$this->assertEquals('Oscar', $request->getAttribute('cat', 'Oscar'));
|
||||
}
|
||||
|
||||
public function testWithoutAttributePreservesOtherAttributes(): void
|
||||
{
|
||||
$request = new ServerRequest();
|
||||
$request = $request->withAttribute('cat', 'Molly');
|
||||
$request = $request->withAttribute('dog', 'Bear');
|
||||
$request = $request->withoutAttribute('cat');
|
||||
$this->assertEquals('Bear', $request->getAttribute('dog'));
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,95 @@
|
|||
<?php
|
||||
|
||||
namespace WellRESTed\Message;
|
||||
|
||||
use RuntimeException;
|
||||
use WellRESTed\Test\TestCase;
|
||||
|
||||
class StreamFactoryTest extends TestCase
|
||||
{
|
||||
private const CONTENT = 'Stream content';
|
||||
|
||||
/** @var string $tempPath */
|
||||
private $tempPath;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
$this->tempPath = tempnam(sys_get_temp_dir(), 'test');
|
||||
file_put_contents($this->tempPath, self::CONTENT);
|
||||
}
|
||||
|
||||
protected function tearDown(): void
|
||||
{
|
||||
parent::tearDown();
|
||||
unlink($this->tempPath);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
public function testCreatesStreamFromString(): void
|
||||
{
|
||||
$factory = new StreamFactory();
|
||||
$stream = $factory->createStream(self::CONTENT);
|
||||
|
||||
$this->assertEquals(self::CONTENT, (string) $stream);
|
||||
}
|
||||
|
||||
public function testCreatesStreamFromFile(): void
|
||||
{
|
||||
$factory = new StreamFactory();
|
||||
$stream = $factory->createStreamFromFile($this->tempPath);
|
||||
|
||||
$this->assertEquals(self::CONTENT, (string) $stream);
|
||||
}
|
||||
|
||||
public function testCreatesStreamFromFileWithModeRByDefault(): void
|
||||
{
|
||||
$factory = new StreamFactory();
|
||||
$stream = $factory->createStreamFromFile($this->tempPath);
|
||||
|
||||
$mode = $stream->getMetadata('mode');
|
||||
$this->assertEquals('r', $mode);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider modeProvider
|
||||
* @param string $mode
|
||||
*/
|
||||
public function testCreatesStreamFromFileWithPassedMode(string $mode): void
|
||||
{
|
||||
$factory = new StreamFactory();
|
||||
$stream = $factory->createStreamFromFile($this->tempPath, $mode);
|
||||
|
||||
$actual = $stream->getMetadata('mode');
|
||||
$this->assertEquals($mode, $actual);
|
||||
}
|
||||
|
||||
public function modeProvider(): array
|
||||
{
|
||||
return [
|
||||
['r'],
|
||||
['r+'],
|
||||
['w'],
|
||||
['w+']
|
||||
];
|
||||
}
|
||||
|
||||
public function testCreateStreamFromFileThrowsRuntimeExceptionWhenUnableToOpenFile(): void
|
||||
{
|
||||
$this->expectException(RuntimeException::class);
|
||||
|
||||
$factory = new StreamFactory();
|
||||
@$factory->createStreamFromFile('/dev/null/not-a-file', 'w');
|
||||
}
|
||||
|
||||
public function testCreatesStreamFromResource(): void
|
||||
{
|
||||
$f = fopen($this->tempPath, 'r');
|
||||
|
||||
$factory = new StreamFactory();
|
||||
$stream = $factory->createStreamFromResource($f);
|
||||
|
||||
$this->assertEquals(self::CONTENT, (string) $stream);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,424 @@
|
|||
<?php
|
||||
|
||||
namespace WellRESTed\Message;
|
||||
|
||||
use InvalidArgumentException;
|
||||
use RuntimeException;
|
||||
use WellRESTed\Test\TestCase;
|
||||
|
||||
class StreamTest extends TestCase
|
||||
{
|
||||
private $resource;
|
||||
private $resourceDevNull;
|
||||
private $content = 'Hello, world!';
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->resource = fopen('php://memory', 'w+');
|
||||
$this->resourceDevNull = fopen('/dev/zero', 'r');
|
||||
fwrite($this->resource, $this->content);
|
||||
StreamHelper::$fail = false;
|
||||
}
|
||||
|
||||
protected function tearDown(): void
|
||||
{
|
||||
if (is_resource($this->resource)) {
|
||||
fclose($this->resource);
|
||||
}
|
||||
}
|
||||
|
||||
public function testCreatesInstanceWithStreamResource(): void
|
||||
{
|
||||
$stream = new Stream($this->resource);
|
||||
$this->assertNotNull($stream);
|
||||
}
|
||||
|
||||
public function testCreatesInstanceWithString(): void
|
||||
{
|
||||
$stream = new Stream('Hello, world!');
|
||||
$this->assertNotNull($stream);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider invalidResourceProvider
|
||||
* @param mixed $resource
|
||||
*/
|
||||
public function testThrowsExceptionWithInvalidResource($resource): void
|
||||
{
|
||||
$this->expectException(InvalidArgumentException::class);
|
||||
new Stream($resource);
|
||||
}
|
||||
|
||||
public function invalidResourceProvider(): array
|
||||
{
|
||||
return [
|
||||
[null],
|
||||
[true],
|
||||
[4],
|
||||
[[]]
|
||||
];
|
||||
}
|
||||
|
||||
public function testCastsToString(): void
|
||||
{
|
||||
$stream = new Stream($this->resource);
|
||||
$this->assertEquals($this->content, (string) $stream);
|
||||
}
|
||||
|
||||
public function testClosesHandle(): void
|
||||
{
|
||||
$stream = new Stream($this->resource);
|
||||
$stream->close();
|
||||
$this->assertFalse(is_resource($this->resource));
|
||||
}
|
||||
|
||||
public function testDetachReturnsHandle(): void
|
||||
{
|
||||
$stream = new Stream($this->resource);
|
||||
$this->assertSame($this->resource, $stream->detach());
|
||||
}
|
||||
|
||||
public function testDetachUnsetsInstanceVariable(): void
|
||||
{
|
||||
$stream = new Stream($this->resource);
|
||||
$stream->detach();
|
||||
$this->assertNull($stream->detach());
|
||||
}
|
||||
|
||||
public function testReturnsSize(): void
|
||||
{
|
||||
$stream = new Stream($this->resource);
|
||||
$this->assertEquals(strlen($this->content), $stream->getSize());
|
||||
}
|
||||
|
||||
public function testReturnsNullForSizeWhenUnableToReadFromFstat(): void
|
||||
{
|
||||
$stream = new Stream($this->resourceDevNull);
|
||||
$this->assertNull($stream->getSize());
|
||||
}
|
||||
|
||||
public function testTellReturnsHandlePosition(): void
|
||||
{
|
||||
$stream = new Stream($this->resource);
|
||||
fseek($this->resource, 10);
|
||||
$this->assertEquals(10, $stream->tell());
|
||||
}
|
||||
|
||||
public function testTellThrowsRuntimeExceptionWhenUnableToReadStreamPosition(): void
|
||||
{
|
||||
StreamHelper::$fail = true;
|
||||
$stream = new Stream($this->resource);
|
||||
$this->expectException(RuntimeException::class);
|
||||
$stream->tell();
|
||||
}
|
||||
|
||||
public function testReturnsOef(): void
|
||||
{
|
||||
$stream = new Stream($this->resource);
|
||||
$stream->rewind();
|
||||
$stream->getContents();
|
||||
$this->assertTrue($stream->eof());
|
||||
}
|
||||
|
||||
public function testReadsSeekableStatusFromMetadata(): void
|
||||
{
|
||||
$stream = new Stream($this->resource);
|
||||
$metadata = stream_get_meta_data($this->resource);
|
||||
$seekable = $metadata['seekable'] == 1;
|
||||
$this->assertEquals($seekable, $stream->isSeekable());
|
||||
}
|
||||
|
||||
public function testSeeksToPosition(): void
|
||||
{
|
||||
$stream = new Stream($this->resource);
|
||||
$stream->seek(10);
|
||||
$this->assertEquals(10, ftell($this->resource));
|
||||
}
|
||||
|
||||
public function testSeekThrowsRuntimeExceptionWhenUnableToSeek(): void
|
||||
{
|
||||
StreamHelper::$fail = true;
|
||||
$stream = new Stream($this->resource);
|
||||
$this->expectException(RuntimeException::class);
|
||||
$stream->seek(10);
|
||||
}
|
||||
|
||||
public function testRewindReturnsToBeginning(): void
|
||||
{
|
||||
$stream = new Stream($this->resource);
|
||||
$stream->seek(10);
|
||||
$stream->rewind();
|
||||
$this->assertEquals(0, ftell($this->resource));
|
||||
}
|
||||
|
||||
public function testRewindThrowsRuntimeExceptionWhenUnableToRewind(): void
|
||||
{
|
||||
StreamHelper::$fail = true;
|
||||
$stream = new Stream($this->resource);
|
||||
$this->expectException(RuntimeException::class);
|
||||
$stream->rewind();
|
||||
}
|
||||
|
||||
public function testWritesToHandle(): void
|
||||
{
|
||||
$message = "\nThis is a stream.";
|
||||
$stream = new Stream($this->resource);
|
||||
$stream->write($message);
|
||||
$this->assertEquals($this->content . $message, (string) $stream);
|
||||
}
|
||||
|
||||
public function testThrowsExceptionOnErrorWriting(): void
|
||||
{
|
||||
$this->expectException(RuntimeException::class);
|
||||
$filename = tempnam(sys_get_temp_dir(), 'php');
|
||||
$handle = fopen($filename, 'r');
|
||||
$stream = new Stream($handle);
|
||||
$stream->write('Hello, world!');
|
||||
}
|
||||
|
||||
public function testThrowsExceptionOnErrorReading(): void
|
||||
{
|
||||
$this->expectException(RuntimeException::class);
|
||||
$filename = tempnam(sys_get_temp_dir(), 'php');
|
||||
$handle = fopen($filename, 'w');
|
||||
$stream = new Stream($handle);
|
||||
$stream->read(10);
|
||||
}
|
||||
|
||||
public function testReadsFromStream(): void
|
||||
{
|
||||
$stream = new Stream($this->resource);
|
||||
$stream->seek(7);
|
||||
$string = $stream->read(5);
|
||||
$this->assertEquals('world', $string);
|
||||
}
|
||||
|
||||
public function testThrowsExceptionOnErrorReadingToEnd(): void
|
||||
{
|
||||
$this->expectException(RuntimeException::class);
|
||||
$filename = tempnam(sys_get_temp_dir(), 'php');
|
||||
$handle = fopen($filename, 'w');
|
||||
$stream = new Stream($handle);
|
||||
$stream->getContents();
|
||||
}
|
||||
|
||||
public function testReadsToEnd(): void
|
||||
{
|
||||
$stream = new Stream($this->resource);
|
||||
$stream->seek(7);
|
||||
$string = $stream->getContents();
|
||||
$this->assertEquals('world!', $string);
|
||||
}
|
||||
|
||||
public function testReturnsMetadataArray(): void
|
||||
{
|
||||
$stream = new Stream($this->resource);
|
||||
$this->assertEquals(stream_get_meta_data($this->resource), $stream->getMetadata());
|
||||
}
|
||||
|
||||
public function testReturnsMetadataItem(): void
|
||||
{
|
||||
$stream = new Stream($this->resource);
|
||||
$metadata = stream_get_meta_data($this->resource);
|
||||
$this->assertEquals($metadata['mode'], $stream->getMetadata('mode'));
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider modeProvider
|
||||
* @param string $mode Access type used to open the stream
|
||||
* @param bool $readable The stream should be readable
|
||||
* @param bool $writable The stream should be writeable
|
||||
*/
|
||||
public function testReturnsIsReadableForReadableStreams(string $mode, bool $readable, bool $writable): void
|
||||
{
|
||||
$tmp = tempnam(sys_get_temp_dir(), 'php');
|
||||
if ($mode[0] === 'x') {
|
||||
unlink($tmp);
|
||||
}
|
||||
$resource = fopen($tmp, $mode);
|
||||
$stream = new Stream($resource);
|
||||
$this->assertEquals($readable, $stream->isReadable());
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider modeProvider
|
||||
* @param string $mode Access type used to open the stream
|
||||
* @param bool $readable The stream should be readable
|
||||
* @param bool $writable The stream should be writeable
|
||||
*/
|
||||
public function testReturnsIsWritableForWritableStreams(string $mode, bool $readable, bool $writable): void
|
||||
{
|
||||
$tmp = tempnam(sys_get_temp_dir(), 'php');
|
||||
if ($mode[0] === 'x') {
|
||||
unlink($tmp);
|
||||
}
|
||||
$resource = fopen($tmp, $mode);
|
||||
$stream = new Stream($resource);
|
||||
$this->assertEquals($writable, $stream->isWritable());
|
||||
}
|
||||
|
||||
public function modeProvider(): array
|
||||
{
|
||||
return [
|
||||
['r', true, false],
|
||||
['r+', true, true],
|
||||
['w', false, true],
|
||||
['w+', true, true],
|
||||
['a', false, true],
|
||||
['a+', true, true],
|
||||
['x', false, true],
|
||||
['x+', true, true],
|
||||
['c', false, true],
|
||||
['c+', true, true]
|
||||
];
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// After Detach
|
||||
|
||||
public function testAfterDetachToStringReturnsEmptyString(): void
|
||||
{
|
||||
$stream = new Stream($this->resource);
|
||||
$stream->detach();
|
||||
$this->assertEquals('', (string) $stream);
|
||||
}
|
||||
|
||||
public function testAfterDetachCloseDoesNothing(): void
|
||||
{
|
||||
$stream = new Stream($this->resource);
|
||||
$stream->detach();
|
||||
$stream->close();
|
||||
$this->assertTrue(true);
|
||||
}
|
||||
|
||||
public function testAfterDetachDetachReturnsNull(): void
|
||||
{
|
||||
$stream = new Stream($this->resource);
|
||||
$stream->detach();
|
||||
$this->assertNull($stream->detach());
|
||||
}
|
||||
|
||||
public function testAfterDetachGetSizeReturnsNull(): void
|
||||
{
|
||||
$stream = new Stream($this->resource);
|
||||
$stream->detach();
|
||||
$this->assertNull($stream->getSize());
|
||||
}
|
||||
|
||||
public function testAfterDetachTellThrowsRuntimeException(): void
|
||||
{
|
||||
$stream = new Stream($this->resource);
|
||||
$stream->detach();
|
||||
$this->expectException(RuntimeException::class);
|
||||
$stream->tell();
|
||||
}
|
||||
|
||||
public function testAfterDetachEofReturnsTrue(): void
|
||||
{
|
||||
$stream = new Stream($this->resource);
|
||||
$stream->detach();
|
||||
$this->assertTrue($stream->eof());
|
||||
}
|
||||
|
||||
public function testAfterDetachIsSeekableReturnsFalse(): void
|
||||
{
|
||||
$stream = new Stream($this->resource);
|
||||
$stream->detach();
|
||||
$this->assertFalse($stream->isSeekable());
|
||||
}
|
||||
|
||||
public function testAfterDetachSeekThrowsRuntimeException(): void
|
||||
{
|
||||
$stream = new Stream($this->resource);
|
||||
$stream->detach();
|
||||
$this->expectException(RuntimeException::class);
|
||||
$stream->seek(0);
|
||||
}
|
||||
|
||||
public function testAfterDetachRewindThrowsRuntimeException(): void
|
||||
{
|
||||
$stream = new Stream($this->resource);
|
||||
$stream->detach();
|
||||
$this->expectException(RuntimeException::class);
|
||||
$stream->rewind();
|
||||
}
|
||||
|
||||
public function testAfterDetachIsWritableReturnsFalse(): void
|
||||
{
|
||||
$stream = new Stream($this->resource);
|
||||
$stream->detach();
|
||||
$this->assertFalse($stream->isWritable());
|
||||
}
|
||||
|
||||
public function testAfterDetachWriteThrowsRuntimeException(): void
|
||||
{
|
||||
$stream = new Stream($this->resource);
|
||||
$stream->detach();
|
||||
$this->expectException(RuntimeException::class);
|
||||
$stream->write('bork');
|
||||
}
|
||||
|
||||
public function testAfterDetachIsReadableReturnsFalse(): void
|
||||
{
|
||||
$stream = new Stream($this->resource);
|
||||
$stream->detach();
|
||||
$this->assertFalse($stream->isReadable());
|
||||
}
|
||||
|
||||
public function testAfterDetachReadThrowsRuntimeException(): void
|
||||
{
|
||||
$stream = new Stream($this->resource);
|
||||
$stream->detach();
|
||||
$this->expectException(RuntimeException::class);
|
||||
$stream->read(10);
|
||||
}
|
||||
|
||||
public function testAfterDetachGetContentsThrowsRuntimeException(): void
|
||||
{
|
||||
$stream = new Stream($this->resource);
|
||||
$stream->detach();
|
||||
$this->expectException(RuntimeException::class);
|
||||
$stream->getContents();
|
||||
}
|
||||
|
||||
public function testAfterDetachGetMetadataReturnsNull(): void
|
||||
{
|
||||
$stream = new Stream($this->resource);
|
||||
$stream->detach();
|
||||
$this->assertNull($stream->getMetadata());
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
// Declare functions in this namespace so the class under test will use these
|
||||
// instead of the internal global functions during testing.
|
||||
|
||||
class StreamHelper
|
||||
{
|
||||
public static $fail = false;
|
||||
}
|
||||
|
||||
function fseek($resource, $offset, $whence = SEEK_SET)
|
||||
{
|
||||
if (StreamHelper::$fail) {
|
||||
return -1;
|
||||
}
|
||||
return \fseek($resource, $offset, $whence);
|
||||
}
|
||||
|
||||
function ftell($resource)
|
||||
{
|
||||
if (StreamHelper::$fail) {
|
||||
return false;
|
||||
}
|
||||
return \ftell($resource);
|
||||
}
|
||||
|
||||
function rewind($resource)
|
||||
{
|
||||
if (StreamHelper::$fail) {
|
||||
return false;
|
||||
}
|
||||
return \rewind($resource);
|
||||
}
|
||||
|
|
@ -0,0 +1,179 @@
|
|||
<?php
|
||||
|
||||
namespace WellRESTed\Message;
|
||||
|
||||
use Psr\Http\Message\StreamInterface;
|
||||
use RuntimeException;
|
||||
use WellRESTed\Test\TestCase;
|
||||
|
||||
class UploadedFileTest extends TestCase
|
||||
{
|
||||
private $tmpName;
|
||||
private $movePath;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
UploadedFileState::$php_sapi_name = 'cli';
|
||||
$this->tmpName = tempnam(sys_get_temp_dir(), 'tst');
|
||||
$this->movePath = tempnam(sys_get_temp_dir(), 'tst');
|
||||
}
|
||||
|
||||
protected function tearDown(): void
|
||||
{
|
||||
parent::tearDown();
|
||||
if (file_exists($this->tmpName)) {
|
||||
unlink($this->tmpName);
|
||||
}
|
||||
if (file_exists($this->movePath)) {
|
||||
unlink($this->movePath);
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// getStream
|
||||
|
||||
public function testGetStreamReturnsStreamInterface(): void
|
||||
{
|
||||
$file = new UploadedFile('', '', 0, $this->tmpName, 0);
|
||||
$this->assertInstanceOf(StreamInterface::class, $file->getStream());
|
||||
}
|
||||
|
||||
public function testGetStreamReturnsStreamWrappingUploadedFile(): void
|
||||
{
|
||||
$content = 'Hello, World!';
|
||||
file_put_contents($this->tmpName, $content);
|
||||
$file = new UploadedFile('', '', 0, $this->tmpName, '');
|
||||
$stream = $file->getStream();
|
||||
$this->assertEquals($content, (string) $stream);
|
||||
}
|
||||
|
||||
public function testGetStreamThrowsRuntimeExceptionForNoFile(): void
|
||||
{
|
||||
$file = new UploadedFile('', '', 0, '', 0);
|
||||
$this->expectException(RuntimeException::class);
|
||||
$file->getStream();
|
||||
}
|
||||
|
||||
public function testGetStreamThrowsExceptionAfterMoveTo(): void
|
||||
{
|
||||
$this->expectException(RuntimeException::class);
|
||||
$content = 'Hello, World!';
|
||||
file_put_contents($this->tmpName, $content);
|
||||
$file = new UploadedFile('', '', 0, $this->tmpName, '');
|
||||
$file->moveTo($this->movePath);
|
||||
$file->getStream();
|
||||
}
|
||||
|
||||
public function testGetStreamThrowsExceptionForNonUploadedFile(): void
|
||||
{
|
||||
$this->expectException(RuntimeException::class);
|
||||
UploadedFileState::$php_sapi_name = 'apache';
|
||||
UploadedFileState::$is_uploaded_file = false;
|
||||
$file = new UploadedFile('', '', 0, $this->tmpName, 0);
|
||||
$file->getStream();
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// moveTo
|
||||
|
||||
public function testMoveToSapiRelocatesUploadedFileToDestinationIfExists(): void
|
||||
{
|
||||
UploadedFileState::$php_sapi_name = 'fpm-fcgi';
|
||||
|
||||
$content = 'Hello, World!';
|
||||
file_put_contents($this->tmpName, $content);
|
||||
$originalMd5 = md5_file($this->tmpName);
|
||||
|
||||
$file = new UploadedFile('', '', 0, $this->tmpName, '');
|
||||
$file->moveTo($this->movePath);
|
||||
|
||||
$this->assertEquals($originalMd5, md5_file($this->movePath));
|
||||
}
|
||||
|
||||
public function testMoveToNonSapiRelocatesUploadedFileToDestinationIfExists(): void
|
||||
{
|
||||
$content = 'Hello, World!';
|
||||
file_put_contents($this->tmpName, $content);
|
||||
$originalMd5 = md5_file($this->tmpName);
|
||||
|
||||
$file = new UploadedFile('', '', 0, $this->tmpName, '');
|
||||
$file->moveTo($this->movePath);
|
||||
|
||||
$this->assertEquals($originalMd5, md5_file($this->movePath));
|
||||
}
|
||||
|
||||
public function testMoveToThrowsExceptionOnSubsequentCall(): void
|
||||
{
|
||||
$this->expectException(RuntimeException::class);
|
||||
|
||||
$content = 'Hello, World!';
|
||||
file_put_contents($this->tmpName, $content);
|
||||
|
||||
$file = new UploadedFile('', '', 0, $this->tmpName, '');
|
||||
$file->moveTo($this->movePath);
|
||||
$file->moveTo($this->movePath);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// getSize
|
||||
|
||||
public function testGetSizeReturnsSize(): void
|
||||
{
|
||||
$file = new UploadedFile('', '', 1024, '', 0);
|
||||
$this->assertEquals(1024, $file->getSize());
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// getError
|
||||
|
||||
public function testGetErrorReturnsError(): void
|
||||
{
|
||||
$file = new UploadedFile('', '', 1024, '', UPLOAD_ERR_INI_SIZE);
|
||||
$this->assertEquals(UPLOAD_ERR_INI_SIZE, $file->getError());
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// clientFilename
|
||||
|
||||
public function testGetClientFilenameReturnsClientFilename(): void
|
||||
{
|
||||
$file = new UploadedFile('clientFilename', '', 0, '', 0);
|
||||
$this->assertEquals('clientFilename', $file->getClientFilename());
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// clientMediaType
|
||||
|
||||
public function testGetClientMediaTypeReturnsClientMediaType(): void
|
||||
{
|
||||
$file = new UploadedFile('', 'clientMediaType', 0, '', 0);
|
||||
$this->assertEquals('clientMediaType', $file->getClientMediaType());
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
// Declare functions in this namespace so the class under test will use these
|
||||
// instead of the internal global functions during testing.
|
||||
|
||||
class UploadedFileState
|
||||
{
|
||||
public static $php_sapi_name;
|
||||
public static $is_uploaded_file;
|
||||
}
|
||||
|
||||
function php_sapi_name()
|
||||
{
|
||||
return UploadedFileState::$php_sapi_name;
|
||||
}
|
||||
|
||||
function move_uploaded_file($source, $target)
|
||||
{
|
||||
return rename($source, $target);
|
||||
}
|
||||
|
||||
function is_uploaded_file($file)
|
||||
{
|
||||
return UploadedFileState::$is_uploaded_file;
|
||||
}
|
||||
|
|
@ -0,0 +1,638 @@
|
|||
<?php
|
||||
|
||||
namespace WellRESTed\Message;
|
||||
|
||||
use InvalidArgumentException;
|
||||
use WellRESTed\Test\TestCase;
|
||||
|
||||
class UriTest extends TestCase
|
||||
{
|
||||
// -------------------------------------------------------------------------
|
||||
// Scheme
|
||||
|
||||
public function testDefaultSchemeIsEmpty(): void
|
||||
{
|
||||
$uri = new Uri();
|
||||
$this->assertSame('', $uri->getScheme());
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider schemeProvider
|
||||
* @param $expected
|
||||
* @param $scheme
|
||||
*/
|
||||
public function testSetsSchemeCaseInsensitively($expected, $scheme): void
|
||||
{
|
||||
$uri = new Uri();
|
||||
$uri = $uri->withScheme($scheme);
|
||||
$this->assertSame($expected, $uri->getScheme());
|
||||
}
|
||||
|
||||
public function schemeProvider(): array
|
||||
{
|
||||
return [
|
||||
['http', 'http'],
|
||||
['https', 'https'],
|
||||
['http', 'HTTP'],
|
||||
['https', 'HTTPS'],
|
||||
['', null],
|
||||
['', '']
|
||||
];
|
||||
}
|
||||
|
||||
public function testInvalidSchemeThrowsException(): void
|
||||
{
|
||||
$this->expectException(InvalidArgumentException::class);
|
||||
$uri = new Uri();
|
||||
$uri->withScheme('gopher');
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Authority
|
||||
|
||||
public function testDefaultAuthorityIsEmpty(): void
|
||||
{
|
||||
$uri = new Uri();
|
||||
$this->assertSame('', $uri->getAuthority());
|
||||
}
|
||||
|
||||
public function testRespectsMyAuthoritah(): void
|
||||
{
|
||||
$this->assertTrue(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider authorityProvider
|
||||
* @param string $expected
|
||||
* @param array $components
|
||||
*/
|
||||
public function testConcatenatesAuthorityFromHostAndUserInfo(
|
||||
string $expected,
|
||||
array $components
|
||||
): void {
|
||||
$uri = new Uri();
|
||||
|
||||
if (isset($components['scheme'])) {
|
||||
$uri = $uri->withScheme($components['scheme']);
|
||||
}
|
||||
|
||||
if (isset($components['user'])) {
|
||||
$user = $components['user'];
|
||||
$password = null;
|
||||
if (isset($components['password'])) {
|
||||
$password = $components['password'];
|
||||
}
|
||||
$uri = $uri->withUserInfo($user, $password);
|
||||
}
|
||||
|
||||
if (isset($components['host'])) {
|
||||
$uri = $uri->withHost($components['host']);
|
||||
}
|
||||
|
||||
if (isset($components['port'])) {
|
||||
$uri = $uri->withPort($components['port']);
|
||||
}
|
||||
|
||||
$this->assertEquals($expected, $uri->getAuthority());
|
||||
}
|
||||
|
||||
public function authorityProvider()
|
||||
{
|
||||
return [
|
||||
[
|
||||
'localhost',
|
||||
[
|
||||
'host' => 'localhost'
|
||||
]
|
||||
],
|
||||
[
|
||||
'user@localhost',
|
||||
[
|
||||
'host' => 'localhost',
|
||||
'user' => 'user'
|
||||
]
|
||||
],
|
||||
[
|
||||
'user:password@localhost',
|
||||
[
|
||||
'host' => 'localhost',
|
||||
'user' => 'user',
|
||||
'password' => 'password'
|
||||
]
|
||||
],
|
||||
[
|
||||
'localhost',
|
||||
[
|
||||
'host' => 'localhost',
|
||||
'password' => 'password'
|
||||
]
|
||||
],
|
||||
[
|
||||
'localhost',
|
||||
[
|
||||
'scheme' => 'http',
|
||||
'host' => 'localhost',
|
||||
'port' => 80
|
||||
]
|
||||
],
|
||||
[
|
||||
'localhost',
|
||||
[
|
||||
'scheme' => 'https',
|
||||
'host' => 'localhost',
|
||||
'port' => 443
|
||||
]
|
||||
],
|
||||
[
|
||||
'localhost:4430',
|
||||
[
|
||||
'scheme' => 'https',
|
||||
'host' => 'localhost',
|
||||
'port' => 4430
|
||||
]
|
||||
],
|
||||
[
|
||||
'localhost:8080',
|
||||
[
|
||||
'scheme' => 'http',
|
||||
'host' => 'localhost',
|
||||
'port' => 8080
|
||||
]
|
||||
],
|
||||
[
|
||||
'user:password@localhost:4430',
|
||||
[
|
||||
'scheme' => 'https',
|
||||
'user' => 'user',
|
||||
'password' => 'password',
|
||||
'host' => 'localhost',
|
||||
'port' => 4430
|
||||
]
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// User Info
|
||||
|
||||
public function testDefaultUserInfoIsEmpty(): void
|
||||
{
|
||||
$uri = new Uri();
|
||||
$this->assertSame('', $uri->getUserInfo());
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider userInfoProvider
|
||||
*
|
||||
* @param string $expected The combined user:password value
|
||||
* @param string $user The username to set
|
||||
* @param string|null $password The password to set
|
||||
*/
|
||||
public function testSetsUserInfo(string $expected, string $user, ?string $password): void
|
||||
{
|
||||
$uri = new Uri();
|
||||
$uri = $uri->withUserInfo($user, $password);
|
||||
$this->assertSame($expected, $uri->getUserInfo());
|
||||
}
|
||||
|
||||
public function userInfoProvider(): array
|
||||
{
|
||||
return [
|
||||
['user:password', 'user', 'password'],
|
||||
['user', 'user', ''],
|
||||
['user', 'user', null],
|
||||
['', '', 'password'],
|
||||
['', '', '']
|
||||
];
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Host
|
||||
|
||||
public function testDefaultHostIsEmpty(): void
|
||||
{
|
||||
$uri = new Uri();
|
||||
$this->assertSame('', $uri->getHost());
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider hostProvider
|
||||
* @param string $expected
|
||||
* @param string $host
|
||||
*/
|
||||
public function testSetsHost(string $expected, string $host): void
|
||||
{
|
||||
$uri = new Uri();
|
||||
$uri = $uri->withHost($host);
|
||||
$this->assertSame($expected, $uri->getHost());
|
||||
}
|
||||
|
||||
public function hostProvider(): array
|
||||
{
|
||||
return [
|
||||
['', ''],
|
||||
['localhost', 'localhost'],
|
||||
['localhost', 'LOCALHOST'],
|
||||
['foo.com', 'FOO.com']
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider invalidHostProvider
|
||||
* @param mixed $host
|
||||
*/
|
||||
public function testInvalidHostThrowsException($host): void
|
||||
{
|
||||
$this->expectException(InvalidArgumentException::class);
|
||||
$uri = new Uri();
|
||||
$uri->withHost($host);
|
||||
}
|
||||
|
||||
public function invalidHostProvider(): array
|
||||
{
|
||||
return [
|
||||
[null],
|
||||
[false],
|
||||
[0]
|
||||
];
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Port
|
||||
|
||||
public function testDefaultPortWithNoSchemeIsNull(): void
|
||||
{
|
||||
$uri = new Uri();
|
||||
$this->assertNull($uri->getPort());
|
||||
}
|
||||
|
||||
public function testDefaultPortForHttpSchemeIs80(): void
|
||||
{
|
||||
$uri = new Uri();
|
||||
$this->assertSame(80, $uri->withScheme('http')->getPort());
|
||||
}
|
||||
|
||||
public function testDefaultPortForHttpsSchemeIs443(): void
|
||||
{
|
||||
$uri = new Uri();
|
||||
$this->assertSame(443, $uri->withScheme('https')->getPort());
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider portAndSchemeProvider
|
||||
* @param mixed $expectedPort
|
||||
* @param mixed $scheme
|
||||
* @param mixed $port
|
||||
*/
|
||||
public function testReturnsPortWithSchemeDefaults($expectedPort, $scheme, $port): void
|
||||
{
|
||||
$uri = new Uri();
|
||||
$uri = $uri->withScheme($scheme)->withPort($port);
|
||||
$this->assertSame($expectedPort, $uri->getPort());
|
||||
}
|
||||
|
||||
public function portAndSchemeProvider(): array
|
||||
{
|
||||
return [
|
||||
[null, '', null],
|
||||
[80, 'http', null],
|
||||
[443, 'https', null],
|
||||
[8080, '', 8080],
|
||||
[8080, 'http', '8080'],
|
||||
[8080, 'https', 8080.0]
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider invalidPortProvider
|
||||
* @param mixed $port
|
||||
*/
|
||||
public function testInvalidPortThrowsException($port): void
|
||||
{
|
||||
$this->expectException(InvalidArgumentException::class);
|
||||
$uri = new Uri();
|
||||
$uri->withPort($port);
|
||||
}
|
||||
|
||||
public function invalidPortProvider(): array
|
||||
{
|
||||
return [
|
||||
[true],
|
||||
[-1],
|
||||
[65536],
|
||||
['dog']
|
||||
];
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Path
|
||||
|
||||
public function testDefaultPathIsEmpty(): void
|
||||
{
|
||||
$uri = new Uri();
|
||||
$this->assertSame('', $uri->getPath());
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider pathProvider
|
||||
* @param string $expected
|
||||
* @param string $path
|
||||
*/
|
||||
public function testSetsEncodedPath(string $expected, string $path): void
|
||||
{
|
||||
$uri = new Uri();
|
||||
$uri = $uri->withPath($path);
|
||||
$this->assertSame($expected, $uri->getPath());
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider pathProvider
|
||||
* @param string $expected
|
||||
* @param string $path
|
||||
*/
|
||||
public function testDoesNotDoubleEncodePath(string $expected, string $path): void
|
||||
{
|
||||
$uri = new Uri();
|
||||
$uri = $uri->withPath($path);
|
||||
$uri = $uri->withPath($uri->getPath());
|
||||
$this->assertSame($expected, $uri->getPath());
|
||||
}
|
||||
|
||||
public function pathProvider()
|
||||
{
|
||||
return [
|
||||
['', ''],
|
||||
['/', '/'],
|
||||
['*', '*'],
|
||||
['/my/path', '/my/path'],
|
||||
['/encoded%2Fslash', '/encoded%2Fslash'],
|
||||
['/percent/%25', '/percent/%'],
|
||||
['/%C3%A1%C3%A9%C3%AD%C3%B3%C3%BA', '/áéíóú']
|
||||
];
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Query
|
||||
|
||||
public function testDefaultQueryIsEmpty(): void
|
||||
{
|
||||
$uri = new Uri();
|
||||
$this->assertSame('', $uri->getQuery());
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider queryProvider
|
||||
* @param string $expected
|
||||
* @param string $query
|
||||
*/
|
||||
public function testSetsEncodedQuery(string $expected, string $query): void
|
||||
{
|
||||
$uri = new Uri();
|
||||
$uri = $uri->withQuery($query);
|
||||
$this->assertSame($expected, $uri->getQuery());
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider queryProvider
|
||||
* @param string $expected
|
||||
* @param string $query
|
||||
*/
|
||||
public function testDoesNotDoubleEncodeQuery(string $expected, string $query): void
|
||||
{
|
||||
$uri = new Uri();
|
||||
$uri = $uri->withQuery($query);
|
||||
$uri = $uri->withQuery($uri->getQuery());
|
||||
$this->assertSame($expected, $uri->getQuery());
|
||||
}
|
||||
|
||||
public function queryProvider(): array
|
||||
{
|
||||
return [
|
||||
['cat=molly', 'cat=molly'],
|
||||
['cat=molly&dog=bear', 'cat=molly&dog=bear'],
|
||||
['accents=%C3%A1%C3%A9%C3%AD%C3%B3%C3%BA', 'accents=áéíóú']
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider invalidPathProvider
|
||||
* @param mixed $path
|
||||
*/
|
||||
public function testInvalidPathThrowsException($path): void
|
||||
{
|
||||
$this->expectException(InvalidArgumentException::class);
|
||||
$uri = new Uri();
|
||||
$uri->withPath($path);
|
||||
}
|
||||
|
||||
public function invalidPathProvider(): array
|
||||
{
|
||||
return [
|
||||
[null],
|
||||
[false],
|
||||
[0]
|
||||
];
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Fragment
|
||||
|
||||
public function testDefaultFragmentIsEmpty(): void
|
||||
{
|
||||
$uri = new Uri();
|
||||
$this->assertSame('', $uri->getFragment());
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider fragmentProvider
|
||||
* @param string $expected
|
||||
* @param string|null $fragment
|
||||
*/
|
||||
public function testSetsEncodedFragment(string $expected, ?string $fragment): void
|
||||
{
|
||||
$uri = new Uri();
|
||||
$uri = $uri->withFragment($fragment);
|
||||
$this->assertSame($expected, $uri->getFragment());
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider fragmentProvider
|
||||
* @param string $expected
|
||||
* @param string|null $fragment
|
||||
*/
|
||||
public function testDoesNotDoubleEncodeFragment(string $expected, ?string $fragment): void
|
||||
{
|
||||
$uri = new Uri();
|
||||
$uri = $uri->withFragment($fragment);
|
||||
$uri = $uri->withFragment($uri->getFragment());
|
||||
$this->assertSame($expected, $uri->getFragment());
|
||||
}
|
||||
|
||||
public function fragmentProvider(): array
|
||||
{
|
||||
return [
|
||||
['', null],
|
||||
['molly', 'molly'],
|
||||
['%C3%A1%C3%A9%C3%AD%C3%B3%C3%BA', 'áéíóú']
|
||||
];
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Concatenation
|
||||
|
||||
/**
|
||||
* @dataProvider componentProvider
|
||||
* @param string $expected
|
||||
* @param array $components
|
||||
*/
|
||||
public function testConcatenatesComponents(string $expected, array $components): void
|
||||
{
|
||||
$uri = new Uri();
|
||||
|
||||
if (isset($components['scheme'])) {
|
||||
$uri = $uri->withScheme($components['scheme']);
|
||||
}
|
||||
|
||||
if (isset($components['user'])) {
|
||||
$user = $components['user'];
|
||||
$password = null;
|
||||
if (isset($components['password'])) {
|
||||
$password = $components['password'];
|
||||
}
|
||||
$uri = $uri->withUserInfo($user, $password);
|
||||
}
|
||||
|
||||
if (isset($components['host'])) {
|
||||
$uri = $uri->withHost($components['host']);
|
||||
}
|
||||
|
||||
if (isset($components['port'])) {
|
||||
$uri = $uri->withPort($components['port']);
|
||||
}
|
||||
|
||||
if (isset($components['path'])) {
|
||||
$uri = $uri->withPath($components['path']);
|
||||
}
|
||||
|
||||
if (isset($components['query'])) {
|
||||
$uri = $uri->withQuery($components['query']);
|
||||
}
|
||||
|
||||
if (isset($components['fragment'])) {
|
||||
$uri = $uri->withFragment($components['fragment']);
|
||||
}
|
||||
|
||||
$this->assertEquals($expected, (string) $uri);
|
||||
}
|
||||
|
||||
public function componentProvider()
|
||||
{
|
||||
return [
|
||||
[
|
||||
'http://localhost/path',
|
||||
[
|
||||
'scheme' => 'http',
|
||||
'host' => 'localhost',
|
||||
'path' => '/path'
|
||||
]
|
||||
],
|
||||
[
|
||||
'//localhost/path',
|
||||
[
|
||||
'host' => 'localhost',
|
||||
'path' => '/path'
|
||||
]
|
||||
],
|
||||
[
|
||||
'/path',
|
||||
[
|
||||
'path' => '/path'
|
||||
]
|
||||
],
|
||||
[
|
||||
'/path?cat=molly&dog=bear',
|
||||
[
|
||||
'path' => '/path',
|
||||
'query' => 'cat=molly&dog=bear'
|
||||
]
|
||||
],
|
||||
[
|
||||
'/path?cat=molly&dog=bear#fragment',
|
||||
[
|
||||
'path' => '/path',
|
||||
'query' => 'cat=molly&dog=bear',
|
||||
'fragment' => 'fragment'
|
||||
]
|
||||
],
|
||||
[
|
||||
'https://user:password@localhost:4430/path?cat=molly&dog=bear#fragment',
|
||||
[
|
||||
'scheme' => 'https',
|
||||
'user' => 'user',
|
||||
'password' => 'password',
|
||||
'host' => 'localhost',
|
||||
'port' => 4430,
|
||||
'path' => '/path',
|
||||
'query' => 'cat=molly&dog=bear',
|
||||
'fragment' => 'fragment'
|
||||
]
|
||||
],
|
||||
// Asterisk Form
|
||||
[
|
||||
'*',
|
||||
[
|
||||
'path' => '*'
|
||||
]
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider stringUriProvider
|
||||
* @param string $expected
|
||||
* @param string $input
|
||||
*/
|
||||
public function testUriCreatedFromStringNormalizesString(string $expected, string $input): void
|
||||
{
|
||||
$uri = new Uri($input);
|
||||
$this->assertSame($expected, (string) $uri);
|
||||
}
|
||||
|
||||
public function stringUriProvider(): array
|
||||
{
|
||||
return [
|
||||
[
|
||||
'http://localhost/path',
|
||||
'http://localhost:80/path'
|
||||
],
|
||||
[
|
||||
'https://localhost/path',
|
||||
'https://localhost:443/path'
|
||||
],
|
||||
[
|
||||
'https://my.sub.sub.domain.com/path',
|
||||
'https://my.sub.sub.domain.com/path'
|
||||
],
|
||||
[
|
||||
'https://user:password@localhost:4430/path?cat=molly&dog=bear#fragment',
|
||||
'https://user:password@localhost:4430/path?cat=molly&dog=bear#fragment'
|
||||
],
|
||||
[
|
||||
'/path',
|
||||
'/path'
|
||||
],
|
||||
[
|
||||
'//double/slash',
|
||||
'//double/slash'
|
||||
],
|
||||
[
|
||||
'no/slash',
|
||||
'no/slash'
|
||||
],
|
||||
[
|
||||
'*',
|
||||
'*'
|
||||
]
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,209 @@
|
|||
<?php
|
||||
|
||||
namespace WellRESTed\Routing\Route;
|
||||
|
||||
use WellRESTed\Dispatching\Dispatcher;
|
||||
use WellRESTed\Message\Response;
|
||||
use WellRESTed\Message\ServerRequest;
|
||||
use WellRESTed\Test\Doubles\MiddlewareMock;
|
||||
use WellRESTed\Test\Doubles\NextMock;
|
||||
use WellRESTed\Test\TestCase;
|
||||
|
||||
class MethodMapTest extends TestCase
|
||||
{
|
||||
private $dispatcher;
|
||||
private $request;
|
||||
private $response;
|
||||
private $next;
|
||||
private $middleware;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->request = new ServerRequest();
|
||||
$this->response = new Response();
|
||||
$this->next = new NextMock();
|
||||
$this->middleware = new MiddlewareMock();
|
||||
$this->dispatcher = new Dispatcher();
|
||||
}
|
||||
|
||||
private function getMethodMap(): MethodMap
|
||||
{
|
||||
return new MethodMap($this->dispatcher);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
public function testDispatchesMiddlewareWithMatchingMethod(): void
|
||||
{
|
||||
$this->request = $this->request->withMethod('GET');
|
||||
|
||||
$map = $this->getMethodMap();
|
||||
$map->register('GET', $this->middleware);
|
||||
$map($this->request, $this->response, $this->next);
|
||||
|
||||
$this->assertTrue($this->middleware->called);
|
||||
}
|
||||
|
||||
public function testTreatsMethodNamesCaseSensitively(): void
|
||||
{
|
||||
$this->request = $this->request->withMethod('get');
|
||||
|
||||
$middlewareUpper = new MiddlewareMock();
|
||||
$middlewareLower = new MiddlewareMock();
|
||||
|
||||
$map = $this->getMethodMap();
|
||||
$map->register('GET', $middlewareUpper);
|
||||
$map->register('get', $middlewareLower);
|
||||
$map($this->request, $this->response, $this->next);
|
||||
|
||||
$this->assertTrue($middlewareLower->called);
|
||||
}
|
||||
|
||||
public function testDispatchesWildcardMiddlewareWithNonMatchingMethod(): void
|
||||
{
|
||||
$this->request = $this->request->withMethod('GET');
|
||||
|
||||
$map = $this->getMethodMap();
|
||||
$map->register('*', $this->middleware);
|
||||
$map($this->request, $this->response, $this->next);
|
||||
|
||||
$this->assertTrue($this->middleware->called);
|
||||
}
|
||||
|
||||
public function testDispatchesGetMiddlewareForHeadByDefault(): void
|
||||
{
|
||||
$this->request = $this->request->withMethod('HEAD');
|
||||
|
||||
$map = $this->getMethodMap();
|
||||
$map->register('GET', $this->middleware);
|
||||
$map($this->request, $this->response, $this->next);
|
||||
|
||||
$this->assertTrue($this->middleware->called);
|
||||
}
|
||||
|
||||
public function testRegistersMiddlewareForMultipleMethods(): void
|
||||
{
|
||||
$map = $this->getMethodMap();
|
||||
$map->register('GET,POST', $this->middleware);
|
||||
|
||||
$this->request = $this->request->withMethod('GET');
|
||||
$map($this->request, $this->response, $this->next);
|
||||
|
||||
$this->request = $this->request->withMethod('POST');
|
||||
$map($this->request, $this->response, $this->next);
|
||||
|
||||
$this->assertEquals(2, $this->middleware->callCount);
|
||||
}
|
||||
|
||||
public function testSettingNullUnregistersMiddleware(): void
|
||||
{
|
||||
$this->request = $this->request->withMethod('POST');
|
||||
|
||||
$map = $this->getMethodMap();
|
||||
$map->register('POST', $this->middleware);
|
||||
$map->register('POST', null);
|
||||
$response = $map($this->request, $this->response, $this->next);
|
||||
|
||||
$this->assertEquals(405, $response->getStatusCode());
|
||||
}
|
||||
|
||||
public function testSetsStatusTo200ForOptions(): void
|
||||
{
|
||||
$this->request = $this->request->withMethod('OPTIONS');
|
||||
|
||||
$map = $this->getMethodMap();
|
||||
$map->register('GET', $this->middleware);
|
||||
$response = $map($this->request, $this->response, $this->next);
|
||||
|
||||
$this->assertEquals(200, $response->getStatusCode());
|
||||
}
|
||||
|
||||
public function testStopsPropagatingAfterOptions(): void
|
||||
{
|
||||
$this->request = $this->request->withMethod('OPTIONS');
|
||||
|
||||
$map = $this->getMethodMap();
|
||||
$map->register('GET', $this->middleware);
|
||||
$map($this->request, $this->response, $this->next);
|
||||
|
||||
$this->assertFalse($this->next->called);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider allowedMethodProvider
|
||||
* @param string[] $methodsDeclared
|
||||
* @param string[] $methodsAllowed
|
||||
*/
|
||||
public function testSetsAllowHeaderForOptions(array $methodsDeclared, array $methodsAllowed): void
|
||||
{
|
||||
$this->request = $this->request->withMethod('OPTIONS');
|
||||
|
||||
$map = $this->getMethodMap();
|
||||
foreach ($methodsDeclared as $method) {
|
||||
$map->register($method, $this->middleware);
|
||||
}
|
||||
$response = $map($this->request, $this->response, $this->next);
|
||||
|
||||
$this->assertContainsEach($methodsAllowed, $response->getHeaderLine('Allow'));
|
||||
}
|
||||
|
||||
public function testSetsStatusTo405ForBadMethod(): void
|
||||
{
|
||||
$this->request = $this->request->withMethod('POST');
|
||||
|
||||
$map = $this->getMethodMap();
|
||||
$map->register('GET', $this->middleware);
|
||||
$response = $map($this->request, $this->response, $this->next);
|
||||
|
||||
$this->assertEquals(405, $response->getStatusCode());
|
||||
}
|
||||
|
||||
public function testStopsPropagatingAfterBadMethod(): void
|
||||
{
|
||||
$this->request = $this->request->withMethod('POST');
|
||||
|
||||
$map = $this->getMethodMap();
|
||||
$map->register('GET', $this->middleware);
|
||||
$map($this->request, $this->response, $this->next);
|
||||
$this->assertFalse($this->next->called);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider allowedMethodProvider
|
||||
* @param string[] $methodsDeclared
|
||||
* @param string[] $methodsAllowed
|
||||
*/
|
||||
public function testSetsAllowHeaderForBadMethod(array $methodsDeclared, array $methodsAllowed): void
|
||||
{
|
||||
$this->request = $this->request->withMethod('BAD');
|
||||
|
||||
$map = $this->getMethodMap();
|
||||
foreach ($methodsDeclared as $method) {
|
||||
$map->register($method, $this->middleware);
|
||||
}
|
||||
$response = $map($this->request, $this->response, $this->next);
|
||||
|
||||
$this->assertContainsEach($methodsAllowed, $response->getHeaderLine('Allow'));
|
||||
}
|
||||
|
||||
public function allowedMethodProvider(): array
|
||||
{
|
||||
return [
|
||||
[['GET'], ['GET', 'HEAD', 'OPTIONS']],
|
||||
[['GET', 'POST'], ['GET', 'POST', 'HEAD', 'OPTIONS']],
|
||||
[['POST'], ['POST', 'OPTIONS']],
|
||||
[['POST'], ['POST', 'OPTIONS']],
|
||||
[['GET', 'PUT,DELETE'], ['GET', 'PUT', 'DELETE', 'HEAD', 'OPTIONS']],
|
||||
];
|
||||
}
|
||||
|
||||
private function assertContainsEach($expectedList, $actual): void
|
||||
{
|
||||
foreach ($expectedList as $expected) {
|
||||
if (strpos($actual, $expected) === false) {
|
||||
$this->assertTrue(false, "'$actual' does not contain expected '$expected'");
|
||||
}
|
||||
}
|
||||
$this->assertTrue(true);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
<?php
|
||||
|
||||
namespace WellRESTed\Routing\Route;
|
||||
|
||||
use Prophecy\PhpUnit\ProphecyTrait;
|
||||
use WellRESTed\Test\TestCase;
|
||||
|
||||
class PrefixRouteTest extends TestCase
|
||||
{
|
||||
use ProphecyTrait;
|
||||
|
||||
public function testTrimsAsteriskFromEndOfTarget(): void
|
||||
{
|
||||
$methodMap = $this->prophesize(MethodMap::class);
|
||||
$route = new PrefixRoute('/cats/*', $methodMap->reveal());
|
||||
$this->assertEquals('/cats/', $route->getTarget());
|
||||
}
|
||||
|
||||
public function testReturnsPrefixType(): void
|
||||
{
|
||||
$methodMap = $this->prophesize(MethodMap::class);
|
||||
$route = new PrefixRoute('/*', $methodMap->reveal());
|
||||
$this->assertSame(Route::TYPE_PREFIX, $route->getType());
|
||||
}
|
||||
|
||||
public function testReturnsEmptyArrayForPathVariables(): void
|
||||
{
|
||||
$methodMap = $this->prophesize(MethodMap::class);
|
||||
$route = new PrefixRoute('/*', $methodMap->reveal());
|
||||
$this->assertSame([], $route->getPathVariables());
|
||||
}
|
||||
|
||||
public function testMatchesExactRequestTarget(): void
|
||||
{
|
||||
$methodMap = $this->prophesize(MethodMap::class);
|
||||
$route = new PrefixRoute('/*', $methodMap->reveal());
|
||||
$this->assertTrue($route->matchesRequestTarget('/'));
|
||||
}
|
||||
|
||||
public function testMatchesRequestTargetWithSamePrefix(): void
|
||||
{
|
||||
$methodMap = $this->prophesize(MethodMap::class);
|
||||
$route = new PrefixRoute('/*', $methodMap->reveal());
|
||||
$this->assertTrue($route->matchesRequestTarget('/cats/'));
|
||||
}
|
||||
|
||||
public function testDoesNotMatchNonMatchingRequestTarget(): void
|
||||
{
|
||||
$methodMap = $this->prophesize(MethodMap::class);
|
||||
$route = new PrefixRoute('/animals/cats/', $methodMap->reveal());
|
||||
$this->assertFalse($route->matchesRequestTarget('/animals/dogs/'));
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue