Compare commits
71 Commits
| Author | SHA1 | Date |
|---|---|---|
|
|
ca3bb2cb0a | |
|
|
c5f49214b5 | |
|
|
c153bc0028 | |
|
|
e22d5889b0 | |
|
|
fb2b2ab527 | |
|
|
00ea49ded6 | |
|
|
4c4b6104e4 | |
|
|
6b31620314 | |
|
|
e6bb814a76 | |
|
|
19c03b9b8b | |
|
|
b94b01453a | |
|
|
9db267c427 | |
|
|
8379dd69a0 | |
|
|
9a4b78b84a | |
|
|
2c80da2f79 | |
|
|
a15b5396e9 | |
|
|
aeb9d733cc | |
|
|
c74a468f3e | |
|
|
84b4bce04f | |
|
|
bba0602122 | |
|
|
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 |
|
|
@ -1,11 +1,15 @@
|
||||||
/.env export-ignore
|
/.env export-ignore
|
||||||
/.gitattributes export-ignore
|
/.gitattributes export-ignore
|
||||||
/.gitignore export-ignore
|
/.gitignore export-ignore
|
||||||
|
/.php_cs* export-ignore
|
||||||
/.travis.yml export-ignore
|
/.travis.yml export-ignore
|
||||||
/composer.lock export-ignore
|
/composer.lock export-ignore
|
||||||
|
/coverage export-ignore
|
||||||
/docker export-ignore
|
/docker export-ignore
|
||||||
/docker-compose.yml export-ignore
|
/docker-compose* export-ignore
|
||||||
/docs export-ignore
|
/docs export-ignore
|
||||||
/test export-ignore
|
/tests export-ignore
|
||||||
/phpunit.xml.dist export-ignore
|
/phpunit.xml* export-ignore
|
||||||
|
/psalm.xml export-ignore
|
||||||
/public export-ignore
|
/public export-ignore
|
||||||
|
/vendor export-ignore
|
||||||
|
|
@ -8,6 +8,9 @@ phpdoc/
|
||||||
coverage/
|
coverage/
|
||||||
report/
|
report/
|
||||||
|
|
||||||
|
# Cache
|
||||||
|
.php_cs.cache
|
||||||
|
|
||||||
# Sphinx Documentation
|
# Sphinx Documentation
|
||||||
docs/build
|
docs/build
|
||||||
|
|
||||||
|
|
@ -18,9 +21,10 @@ preview
|
||||||
# PhpStorm
|
# PhpStorm
|
||||||
workspace.xml
|
workspace.xml
|
||||||
|
|
||||||
# Vagrant
|
# Local scratch files
|
||||||
.vagrant/
|
notes
|
||||||
|
|
||||||
# Vagrant sandbox site files.
|
# Local overrides
|
||||||
/htdocs/
|
.env
|
||||||
/autoload/
|
docker-compose.override.yml
|
||||||
|
phpunit.xml
|
||||||
12
.travis.yml
12
.travis.yml
|
|
@ -1,12 +0,0 @@
|
||||||
language: php
|
|
||||||
php:
|
|
||||||
- "7.2"
|
|
||||||
- "7.3"
|
|
||||||
- "7.4"
|
|
||||||
|
|
||||||
before_script:
|
|
||||||
- composer selfupdate
|
|
||||||
- composer install --prefer-source
|
|
||||||
|
|
||||||
script:
|
|
||||||
- vendor/bin/phpunit
|
|
||||||
40
README.md
40
README.md
|
|
@ -1,12 +1,20 @@
|
||||||
WellRESTed
|
WellRESTed
|
||||||
==========
|
==========
|
||||||
|
|
||||||
[](https://php.net/)
|
[](https://php.net/)
|
||||||
[](https://travis-ci.org/wellrestedphp/wellrested)
|
|
||||||
[](http://wellrested.readthedocs.org/en/latest/)
|
[](http://wellrested.readthedocs.org/en/latest/)
|
||||||
[](https://insight.sensiolabs.com/projects/b0a2efcb-49f8-4a90-a5bd-0c14e409f59e)
|
|
||||||
|
|
||||||
WellRESTed is a library for creating RESTful Web services and websites 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.
|
||||||
|
|
||||||
|
This fork (basemaster/wellrested) is back to php 7.2 release.
|
||||||
|
|
||||||
|
### 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
|
Install
|
||||||
-------
|
-------
|
||||||
|
|
@ -16,7 +24,7 @@ Add an entry for "wellrested/wellrested" to your composer.json file's `require`
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"require": {
|
"require": {
|
||||||
"wellrested/wellrested": "^4"
|
"wellrested/wellrested": "^5"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
@ -55,14 +63,14 @@ class HomePageHandler implements RequestHandlerInterface
|
||||||
|
|
||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
|
|
||||||
// Create a new server.
|
// Create a new Server instance.
|
||||||
$server = new Server();
|
$server = new Server();
|
||||||
|
|
||||||
// Add a router to the server to map methods and endpoints to handlers.
|
// Add a router to the server to map methods and endpoints to handlers.
|
||||||
$router = $server->createRouter();
|
$router = $server->createRouter();
|
||||||
$router->register('GET', '/', new HomePageHandler());
|
// 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);
|
$server->add($router);
|
||||||
|
|
||||||
// Read the request from the client, dispatch a handler, and output.
|
// Read the request from the client, dispatch a handler, and output.
|
||||||
$server->respond();
|
$server->respond();
|
||||||
```
|
```
|
||||||
|
|
@ -85,6 +93,18 @@ To run PHPUnit tests, use the `php` service:
|
||||||
docker-compose run --rm php phpunit
|
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:
|
To generate documentation, use the `docs` service:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|
@ -104,5 +124,5 @@ The runs a site you can access at [http://localhost:8080](http://localhost:8080)
|
||||||
|
|
||||||
Copyright and License
|
Copyright and License
|
||||||
---------------------
|
---------------------
|
||||||
Copyright © 2018 by PJ Dietz
|
Copyright © 2020 by PJ Dietz
|
||||||
Licensed under the [MIT license](http://opensource.org/licenses/MIT)
|
Licensed under the [MIT license](http://opensource.org/licenses/MIT)
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,7 @@
|
||||||
{
|
{
|
||||||
"name": "wellrested/wellrested",
|
"name": "basemaster/wellrested",
|
||||||
"description": "Simple PHP Library for RESTful APIs",
|
"description": "Clone for Simple PHP Library for RESTful APIs (wellrested.org)",
|
||||||
"keywords": ["rest", "restful", "api", "http", "psr7", "psr-7", "psr15", "psr-15"],
|
"keywords": ["rest", "restful", "api", "http", "psr7", "psr-7", "psr15", "psr-15", "psr17", "psr-17"],
|
||||||
"homepage": "https://www.wellrested.org",
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"type": "library",
|
"type": "library",
|
||||||
"authors": [
|
"authors": [
|
||||||
|
|
@ -12,13 +11,15 @@
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"require": {
|
"require": {
|
||||||
"php": ">=7.0",
|
"php": ">=7.2",
|
||||||
|
"psr/http-factory": "~1.0",
|
||||||
"psr/http-message": "~1.0",
|
"psr/http-message": "~1.0",
|
||||||
"psr/http-server-handler": "~1.0",
|
"psr/http-server-handler": "~1.0",
|
||||||
"psr/http-server-middleware": "~1.0"
|
"psr/http-server-middleware": "~1.0"
|
||||||
},
|
},
|
||||||
"require-dev": {
|
"provide": {
|
||||||
"phpunit/phpunit": "^8"
|
"psr/http-message-implementation": "1.0",
|
||||||
|
"psr/http-factory-implementation": "1.0"
|
||||||
},
|
},
|
||||||
"autoload": {
|
"autoload": {
|
||||||
"psr-4": {
|
"psr-4": {
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -1,32 +0,0 @@
|
||||||
version: '3.7'
|
|
||||||
services:
|
|
||||||
|
|
||||||
# PHPUnit and Composer
|
|
||||||
php:
|
|
||||||
build:
|
|
||||||
context: .
|
|
||||||
dockerfile: ./docker/php/Dockerfile
|
|
||||||
volumes:
|
|
||||||
- .:/usr/local/src/wellrested
|
|
||||||
|
|
||||||
# Documentation generator
|
|
||||||
docs:
|
|
||||||
build:
|
|
||||||
context: .
|
|
||||||
dockerfile: ./docker/docs/Dockerfile
|
|
||||||
volumes:
|
|
||||||
- .:/usr/local/src/wellrested
|
|
||||||
|
|
||||||
# Local development site
|
|
||||||
nginx:
|
|
||||||
image: nginx:1.15
|
|
||||||
ports:
|
|
||||||
- ${PORT}:80
|
|
||||||
volumes:
|
|
||||||
- .:/usr/local/src/wellrested
|
|
||||||
- ./docker/nginx/site.conf:/etc/nginx/conf.d/default.conf
|
|
||||||
|
|
||||||
php-fpm:
|
|
||||||
image: php:7.4-fpm
|
|
||||||
volumes:
|
|
||||||
- .:/usr/local/src/wellrested
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
FROM python:3-jessie
|
|
||||||
|
|
||||||
RUN pip install sphinx sphinx_rtd_theme
|
|
||||||
|
|
||||||
WORKDIR /usr/local/src/wellrested
|
|
||||||
|
|
||||||
CMD ["make", "html", "-C", "docs"]
|
|
||||||
|
|
@ -1,31 +0,0 @@
|
||||||
server {
|
|
||||||
listen 80;
|
|
||||||
|
|
||||||
root /usr/local/src/wellrested/public;
|
|
||||||
index index.php index.html;
|
|
||||||
charset utf-8;
|
|
||||||
|
|
||||||
access_log /var/log/nginx/access.log;
|
|
||||||
error_log /var/log/nginx/error.log;
|
|
||||||
|
|
||||||
# Front Controller
|
|
||||||
location / {
|
|
||||||
try_files $uri $uri/ /index.php?$args;
|
|
||||||
}
|
|
||||||
|
|
||||||
# Generated Documentation
|
|
||||||
location /docs {
|
|
||||||
alias /usr/local/src/wellrested/docs/build/html;
|
|
||||||
}
|
|
||||||
|
|
||||||
# PHP
|
|
||||||
location ~ \.php$ {
|
|
||||||
include fastcgi_params;
|
|
||||||
fastcgi_pass php-fpm:9000;
|
|
||||||
fastcgi_index index.php;
|
|
||||||
fastcgi_buffers 8 8k;
|
|
||||||
fastcgi_buffer_size 16k;
|
|
||||||
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
|
|
||||||
fastcgi_param SCRIPT_NAME index.php;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,44 +0,0 @@
|
||||||
FROM php:7.4-cli
|
|
||||||
|
|
||||||
RUN DEBIAN_FRONTEND=noninteractive \
|
|
||||||
apt-get update && \
|
|
||||||
apt-get -y install \
|
|
||||||
gettext \
|
|
||||||
libssl-dev \
|
|
||||||
unzip \
|
|
||||||
wget \
|
|
||||||
zip \
|
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
|
||||||
|
|
||||||
# Install Xdebug
|
|
||||||
RUN yes | pecl install xdebug \
|
|
||||||
&& echo "zend_extension=$(find /usr/local/lib/php/extensions/ -name xdebug.so)" > /usr/local/etc/php/conf.d/xdebug.ini
|
|
||||||
|
|
||||||
# Install Composer.
|
|
||||||
RUN curl -sS https://getcomposer.org/installer | php -- \
|
|
||||||
--filename=composer --install-dir=/usr/local/bin
|
|
||||||
|
|
||||||
# Install dumb-init.
|
|
||||||
RUN wget -O /usr/local/bin/dumb-init https://github.com/Yelp/dumb-init/releases/download/v1.2.1/dumb-init_1.2.1_amd64
|
|
||||||
RUN chmod +x /usr/local/bin/dumb-init
|
|
||||||
|
|
||||||
# Create a directory for project sources and user's home directory
|
|
||||||
RUN mkdir /usr/local/src/wellrested && \
|
|
||||||
chown -R www-data:www-data /usr/local/src/wellrested && \
|
|
||||||
chown -R www-data:www-data /var/www
|
|
||||||
|
|
||||||
COPY ./src /usr/local/src/wellrested/src
|
|
||||||
COPY ./test /usr/local/src/wellrested/test
|
|
||||||
COPY ./composer.* /usr/local/src/wellrested/
|
|
||||||
COPY ./phpunit.xml.dist /usr/local/src/wellrested/
|
|
||||||
|
|
||||||
# Add symlink for phpunit for easier running
|
|
||||||
RUN ln -s /usr/local/src/wellrested/vendor/bin/phpunit /usr/local/bin/phpunit
|
|
||||||
|
|
||||||
WORKDIR /usr/local/src/wellrested
|
|
||||||
|
|
||||||
USER www-data
|
|
||||||
|
|
||||||
ENTRYPOINT ["dumb-init", "--"]
|
|
||||||
|
|
||||||
RUN composer install
|
|
||||||
|
|
@ -1,16 +0,0 @@
|
||||||
#!/usr/bin/env bash
|
|
||||||
|
|
||||||
EXPECTED_SIGNATURE=$(wget https://composer.github.io/installer.sig -O - -q)
|
|
||||||
php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');"
|
|
||||||
ACTUAL_SIGNATURE=$(php -r "echo hash_file('SHA384', 'composer-setup.php');")
|
|
||||||
|
|
||||||
if [ "$EXPECTED_SIGNATURE" != "$ACTUAL_SIGNATURE" ]; then
|
|
||||||
>&2 echo 'ERROR: Invalid installer signature'
|
|
||||||
rm composer-setup.php
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
php composer-setup.php --quiet --filename=composer --install-dir=/usr/local/bin
|
|
||||||
RESULT=$?
|
|
||||||
rm composer-setup.php
|
|
||||||
exit $RESULT
|
|
||||||
|
|
@ -1,16 +0,0 @@
|
||||||
Changes from Version 3
|
|
||||||
======================
|
|
||||||
|
|
||||||
If your project uses WellRESTed version 3, you can most likely upgrade to to version 4 without making any changes to your code. However, there are a few changes that may affect some users, particularly users who have subclassed ``WellRESTed\Server`` or used custom implementations of other ``WellRESTed`` classes.
|
|
||||||
|
|
||||||
Server Configuration
|
|
||||||
^^^^^^^^^^^^^^^^^^^^
|
|
||||||
|
|
||||||
Version 4 allows for easier customization of the server than version 3. Previously, to customize the Server, you would need to subclass Server and override protected methods that provided a default request, response, transmitter, etc. The Server in version 4 now provides the following setters for providing custom behavior:
|
|
||||||
|
|
||||||
- ``setAttributes(array $attributes)``
|
|
||||||
- ``setDispatcher(DispatcherInterface $dispatcher)``
|
|
||||||
- ``setPathVariablesAttributeName(string $name)``
|
|
||||||
- ``setRequest(ServerRequestInterface $request)``
|
|
||||||
- ``setResponse(ResponseInterface $response)``
|
|
||||||
- ``setTransmitter(TransmitterInterface $transmitter)``
|
|
||||||
|
|
@ -25,9 +25,9 @@ master_doc = 'index'
|
||||||
|
|
||||||
# General information about the project.
|
# General information about the project.
|
||||||
project = u'WellRESTed'
|
project = u'WellRESTed'
|
||||||
copyright = u'2018, PJ Dietz'
|
copyright = u'2021, PJ Dietz'
|
||||||
version = '4.0.0'
|
version = '5.0.0'
|
||||||
release = '4.0.0'
|
release = '5.0.0'
|
||||||
|
|
||||||
# List of patterns, relative to source directory, that match files and
|
# List of patterns, relative to source directory, that match files and
|
||||||
# directories to ignore when looking for source files.
|
# directories to ignore when looking for source files.
|
||||||
|
|
|
||||||
|
|
@ -93,18 +93,9 @@ To use this dispatcher, create an instance implementing ``WellRESTed\Dispatching
|
||||||
$server = new WellRESTed\Server();
|
$server = new WellRESTed\Server();
|
||||||
$server->setDispatcher(new MyApi\CustomDispatcher());
|
$server->setDispatcher(new MyApi\CustomDispatcher());
|
||||||
|
|
||||||
Message Customization
|
.. warning::
|
||||||
---------------------
|
|
||||||
|
|
||||||
In the example above, we passed a custom dispatcher to the server. You can also customize your server in other ways. For example, when the server reaches these end of its stack of middleware and has not had the response handled, it returns a blank 404 error response. You can customize this by passing a response to the server's ``setUnhandledResponse`` method.
|
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``.
|
||||||
|
|
||||||
.. code-block:: php
|
|
||||||
|
|
||||||
$unhandled = (new Response(404))
|
|
||||||
->withHeader('text/html')
|
|
||||||
->withBody($fancy404message);
|
|
||||||
|
|
||||||
$server->setUnhandledResponse($unhandled);
|
|
||||||
|
|
||||||
.. _PSR-7: https://www.php-fig.org/psr/psr-7/
|
.. _PSR-7: https://www.php-fig.org/psr/psr-7/
|
||||||
.. _Handlers and Middleware: handlers-and-middleware.html
|
.. _Handlers and Middleware: handlers-and-middleware.html
|
||||||
|
|
|
||||||
|
|
@ -138,7 +138,6 @@ Contents
|
||||||
dependency-injection
|
dependency-injection
|
||||||
additional
|
additional
|
||||||
web-server-configuration
|
web-server-configuration
|
||||||
changes-from-version-3
|
|
||||||
|
|
||||||
.. _PSR-7: https://www.php-fig.org/psr/psr-7/
|
.. _PSR-7: https://www.php-fig.org/psr/psr-7/
|
||||||
.. _PSR-15: https://www.php-fig.org/psr/psr-15/
|
.. _PSR-15: https://www.php-fig.org/psr/psr-15/
|
||||||
|
|
|
||||||
|
|
@ -10,14 +10,14 @@ The recommended method for installing WellRESTed is to use Composer_. Add an ent
|
||||||
|
|
||||||
{
|
{
|
||||||
"require": {
|
"require": {
|
||||||
"wellrested/wellrested": "^4"
|
"wellrested/wellrested": "^5"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Requirements
|
Requirements
|
||||||
^^^^^^^^^^^^
|
^^^^^^^^^^^^
|
||||||
|
|
||||||
- PHP 7.0
|
- PHP 7.3
|
||||||
|
|
||||||
License
|
License
|
||||||
^^^^^^^
|
^^^^^^^
|
||||||
|
|
@ -26,7 +26,7 @@ Licensed using the `MIT license <http://opensource.org/licenses/MIT>`_.
|
||||||
|
|
||||||
The MIT License (MIT)
|
The MIT License (MIT)
|
||||||
|
|
||||||
Copyright (c) 2018 PJ Dietz
|
Copyright (c) 2021 PJ Dietz
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
|
|
||||||
|
|
@ -1,28 +0,0 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<phpunit
|
|
||||||
bootstrap="test/bootstrap.php"
|
|
||||||
colors="true"
|
|
||||||
convertErrorsToExceptions="true"
|
|
||||||
convertNoticesToExceptions="true"
|
|
||||||
convertWarningsToExceptions="true"
|
|
||||||
processIsolation="false"
|
|
||||||
stopOnFailure="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="./public/coverage" />
|
|
||||||
</logging>
|
|
||||||
</phpunit>
|
|
||||||
|
|
@ -47,36 +47,13 @@ HTML;
|
||||||
|
|
||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
// Create a new Server instance.
|
||||||
$server = new Server();
|
$server = new Server();
|
||||||
|
|
||||||
// Add a router to the server to map methods and endpoints to handlers.
|
// Add a router to the server to map methods and endpoints to handlers.
|
||||||
$router = $server->createRouter();
|
$router = $server->createRouter();
|
||||||
$router->register("GET", "/", new HomePageHandler());
|
// 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);
|
$server->add($router);
|
||||||
|
|
||||||
|
|
||||||
$server->add($server->createRouter()
|
|
||||||
->register('GET, POST', '/cat', function ($rqst, $resp, $next) {
|
|
||||||
|
|
||||||
$resp = $resp
|
|
||||||
->withStatus(200)
|
|
||||||
->withHeader('Content-type', 'text/html')
|
|
||||||
->withBody(new Stream('Molly'));
|
|
||||||
return $next($rqst, $resp);
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
$server->add($server->createRouter()
|
|
||||||
->register('GET', '/cat', function ($rqst, $resp) {
|
|
||||||
|
|
||||||
$body = (string) $resp->getBody();
|
|
||||||
|
|
||||||
|
|
||||||
return (new Response(200))
|
|
||||||
->withHeader('Content-type', 'text/html')
|
|
||||||
->withBody(new Stream($body . ' Oscar'));
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
// Read the request from the client, dispatch a handler, and output.
|
// Read the request from the client, dispatch a handler, and output.
|
||||||
$server->respond();
|
$server->respond();
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,8 @@
|
||||||
|
|
||||||
namespace WellRESTed\Dispatching;
|
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
|
class DispatchStack implements DispatchStackInterface
|
||||||
{
|
{
|
||||||
|
/** @var mixed[] */
|
||||||
private $stack;
|
private $stack;
|
||||||
|
/** @var DispatcherInterface */
|
||||||
private $dispatcher;
|
private $dispatcher;
|
||||||
|
|
||||||
/**
|
|
||||||
* @param DispatcherInterface $dispatcher
|
|
||||||
*/
|
|
||||||
public function __construct(DispatcherInterface $dispatcher)
|
public function __construct(DispatcherInterface $dispatcher)
|
||||||
{
|
{
|
||||||
$this->dispatcher = $dispatcher;
|
$this->dispatcher = $dispatcher;
|
||||||
|
|
@ -26,7 +25,7 @@ class DispatchStack implements DispatchStackInterface
|
||||||
* Push a new middleware onto the stack.
|
* Push a new middleware onto the stack.
|
||||||
*
|
*
|
||||||
* @param mixed $middleware Middleware to dispatch in sequence
|
* @param mixed $middleware Middleware to dispatch in sequence
|
||||||
* @return self
|
* @return static
|
||||||
*/
|
*/
|
||||||
public function add($middleware)
|
public function add($middleware)
|
||||||
{
|
{
|
||||||
|
|
@ -40,7 +39,7 @@ class DispatchStack implements DispatchStackInterface
|
||||||
* The first middleware that was added is dispatched first.
|
* The first middleware that was added is dispatched first.
|
||||||
*
|
*
|
||||||
* Each middleware, when dispatched, receives a $next callable that, when
|
* 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
|
* 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
|
* call the $next argument they were passed, this method will call the
|
||||||
|
|
@ -66,7 +65,10 @@ class DispatchStack implements DispatchStackInterface
|
||||||
|
|
||||||
// The final middleware's $next returns $response unchanged and sets
|
// The final middleware's $next returns $response unchanged and sets
|
||||||
// the $stackCompleted flag to indicate the stack has completed.
|
// 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;
|
$stackCompleted = true;
|
||||||
return $response;
|
return $response;
|
||||||
};
|
};
|
||||||
|
|
@ -77,8 +79,16 @@ class DispatchStack implements DispatchStackInterface
|
||||||
// contain a dispatcher, the associated middleware, and a $next function
|
// contain a dispatcher, the associated middleware, and a $next function
|
||||||
// that serves as the link to the next middleware in the chain.
|
// that serves as the link to the next middleware in the chain.
|
||||||
foreach (array_reverse($this->stack) as $middleware) {
|
foreach (array_reverse($this->stack) as $middleware) {
|
||||||
$chain = function ($request, $response) use ($dispatcher, $middleware, $chain) {
|
$chain = function (
|
||||||
return $dispatcher->dispatch($middleware, $request, $response, $chain);
|
ServerRequestInterface $request,
|
||||||
|
ResponseInterface $response
|
||||||
|
) use ($dispatcher, $middleware, $chain): ResponseInterface {
|
||||||
|
return $dispatcher->dispatch(
|
||||||
|
$middleware,
|
||||||
|
$request,
|
||||||
|
$response,
|
||||||
|
$chain
|
||||||
|
);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,12 +8,12 @@ use Psr\Http\Server\MiddlewareInterface;
|
||||||
use Psr\Http\Server\RequestHandlerInterface;
|
use Psr\Http\Server\RequestHandlerInterface;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Dispatches handlers and middleware
|
* Runs a handler or middleware with a request and return the response.
|
||||||
*/
|
*/
|
||||||
class Dispatcher implements DispatcherInterface
|
class Dispatcher implements DispatcherInterface
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* Dispatch a handler or middleware and return the response.
|
* Run a handler or middleware with a request and return the response.
|
||||||
*
|
*
|
||||||
* Dispatcher can dispatch any of the following:
|
* Dispatcher can dispatch any of the following:
|
||||||
* - An instance implementing one of these interfaces:
|
* - An instance implementing one of these interfaces:
|
||||||
|
|
@ -63,11 +63,15 @@ class Dispatcher implements DispatcherInterface
|
||||||
} elseif ($dispatchable instanceof ResponseInterface) {
|
} elseif ($dispatchable instanceof ResponseInterface) {
|
||||||
return $dispatchable;
|
return $dispatchable;
|
||||||
} else {
|
} else {
|
||||||
throw new DispatchException('Unable to dispatch middleware.');
|
throw new DispatchException('Unable to dispatch handler.');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function getDispatchStack($dispatchables)
|
/**
|
||||||
|
* @param mixed[] $dispatchables
|
||||||
|
* @return DispatchStack
|
||||||
|
*/
|
||||||
|
private function getDispatchStack($dispatchables)
|
||||||
{
|
{
|
||||||
$stack = new DispatchStack($this);
|
$stack = new DispatchStack($this);
|
||||||
foreach ($dispatchables as $dispatchable) {
|
foreach ($dispatchables as $dispatchable) {
|
||||||
|
|
|
||||||
|
|
@ -6,12 +6,12 @@ use Psr\Http\Message\ResponseInterface;
|
||||||
use Psr\Http\Message\ServerRequestInterface;
|
use Psr\Http\Message\ServerRequestInterface;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Dispatches handlers and middleware
|
* Runs a handler or middleware with a request and return the response.
|
||||||
*/
|
*/
|
||||||
interface DispatcherInterface
|
interface DispatcherInterface
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* Dispatch a handler or middleware and return the response.
|
* Run a handler or middleware with a request and return the response.
|
||||||
*
|
*
|
||||||
* Dispatchables (middleware and handlers) comes in a number of varieties
|
* Dispatchables (middleware and handlers) comes in a number of varieties
|
||||||
* (e.g., instance, string, callable). DispatcherInterface interface unpacks
|
* (e.g., instance, string, callable). DispatcherInterface interface unpacks
|
||||||
|
|
|
||||||
|
|
@ -8,57 +8,50 @@ use Iterator;
|
||||||
/**
|
/**
|
||||||
* HeaderCollection provides case-insensitive access to lists of header values.
|
* HeaderCollection provides case-insensitive 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 preserves the cases of keys as they are set, but treats key
|
* HeaderCollection preserves the cases of keys as they are set, but treats key
|
||||||
* access case insensitively.
|
* access case insensitively.
|
||||||
*
|
*
|
||||||
* Any values added to HeaderCollection are added to list arrays. Subsequent
|
* 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
|
* calls to add a value for a given key will append the new value to the list
|
||||||
* array of values for that key.
|
* 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
|
class HeaderCollection implements ArrayAccess, Iterator
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* @var array
|
|
||||||
*
|
|
||||||
* Hash array mapping lowercase header names to original case header names.
|
* 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[]
|
* 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 array of lowercase header names.
|
||||||
|
*
|
||||||
|
* @var string[]
|
||||||
*/
|
*/
|
||||||
private $keys;
|
private $keys = [];
|
||||||
|
|
||||||
/** @var int */
|
/** @var int */
|
||||||
private $position = 0;
|
private $position = 0;
|
||||||
|
|
||||||
public function __construct()
|
// -------------------------------------------------------------------------
|
||||||
{
|
|
||||||
$this->keys = [];
|
|
||||||
$this->fields = [];
|
|
||||||
$this->values = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
// ------------------------------------------------------------------------
|
|
||||||
// ArrayAccess
|
// ArrayAccess
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param string $offset
|
* @param string $offset
|
||||||
* @return bool
|
* @return bool
|
||||||
*/
|
*/
|
||||||
public function offsetExists($offset)
|
public function offsetExists($offset): bool
|
||||||
{
|
{
|
||||||
return isset($this->values[strtolower($offset)]);
|
return isset($this->values[strtolower($offset)]);
|
||||||
}
|
}
|
||||||
|
|
@ -67,7 +60,7 @@ class HeaderCollection implements ArrayAccess, Iterator
|
||||||
* @param mixed $offset
|
* @param mixed $offset
|
||||||
* @return string[]
|
* @return string[]
|
||||||
*/
|
*/
|
||||||
public function offsetGet($offset)
|
public function offsetGet($offset): array
|
||||||
{
|
{
|
||||||
return $this->values[strtolower($offset)];
|
return $this->values[strtolower($offset)];
|
||||||
}
|
}
|
||||||
|
|
@ -76,7 +69,7 @@ class HeaderCollection implements ArrayAccess, Iterator
|
||||||
* @param string $offset
|
* @param string $offset
|
||||||
* @param string $value
|
* @param string $value
|
||||||
*/
|
*/
|
||||||
public function offsetSet($offset, $value)
|
public function offsetSet($offset, $value): void
|
||||||
{
|
{
|
||||||
$normalized = strtolower($offset);
|
$normalized = strtolower($offset);
|
||||||
|
|
||||||
|
|
@ -99,7 +92,7 @@ class HeaderCollection implements ArrayAccess, Iterator
|
||||||
/**
|
/**
|
||||||
* @param string $offset
|
* @param string $offset
|
||||||
*/
|
*/
|
||||||
public function offsetUnset($offset)
|
public function offsetUnset($offset): void
|
||||||
{
|
{
|
||||||
$normalized = strtolower($offset);
|
$normalized = strtolower($offset);
|
||||||
unset($this->fields[$normalized]);
|
unset($this->fields[$normalized]);
|
||||||
|
|
@ -111,30 +104,33 @@ class HeaderCollection implements ArrayAccess, Iterator
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
// Iterator
|
// Iterator
|
||||||
|
|
||||||
public function current()
|
/**
|
||||||
|
* @return string[]
|
||||||
|
*/
|
||||||
|
public function current(): array
|
||||||
{
|
{
|
||||||
return $this->values[$this->keys[$this->position]];
|
return $this->values[$this->keys[$this->position]];
|
||||||
}
|
}
|
||||||
|
|
||||||
public function next()
|
public function next(): void
|
||||||
{
|
{
|
||||||
++$this->position;
|
++$this->position;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function key()
|
public function key(): string
|
||||||
{
|
{
|
||||||
return $this->fields[$this->keys[$this->position]];
|
return $this->fields[$this->keys[$this->position]];
|
||||||
}
|
}
|
||||||
|
|
||||||
public function valid()
|
public function valid(): bool
|
||||||
{
|
{
|
||||||
return isset($this->keys[$this->position]);
|
return isset($this->keys[$this->position]);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function rewind()
|
public function rewind(): void
|
||||||
{
|
{
|
||||||
$this->position = 0;
|
$this->position = 0;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
namespace WellRESTed\Message;
|
namespace WellRESTed\Message;
|
||||||
|
|
||||||
|
use InvalidArgumentException;
|
||||||
use Psr\Http\Message\MessageInterface;
|
use Psr\Http\Message\MessageInterface;
|
||||||
use Psr\Http\Message\StreamInterface;
|
use Psr\Http\Message\StreamInterface;
|
||||||
|
|
||||||
|
|
@ -21,32 +22,32 @@ abstract class Message implements MessageInterface
|
||||||
* Create a new Message, optionally with headers and a body.
|
* Create a new Message, optionally with headers and a body.
|
||||||
*
|
*
|
||||||
* $headers is an optional associative array with header field names as
|
* $headers is an optional associative array with header field names as
|
||||||
* (string) keys and lists of header field values (string[]) as values.
|
* string keys and values as either string or string[].
|
||||||
*
|
*
|
||||||
* If no StreamInterface is provided for $body, the instance will create
|
* If no StreamInterface is provided for $body, the instance will create
|
||||||
* a NullStream instance for the message body.
|
* a NullStream instance for the message body.
|
||||||
*
|
*
|
||||||
* @param array $headers Associative array with header field names as
|
* @param array $headers Associative array with header field names as
|
||||||
* (string) keys and lists of header field values (string[]) as values.
|
* keys and values as string|string[]
|
||||||
* @param StreamInterface $body A stream representation of the message
|
* @param StreamInterface|null $body A stream representation of the message
|
||||||
* entity body
|
* entity body
|
||||||
*/
|
*/
|
||||||
public function __construct(array $headers = null, StreamInterface $body = null)
|
public function __construct(
|
||||||
{
|
array $headers = [],
|
||||||
|
?StreamInterface $body = null
|
||||||
|
) {
|
||||||
$this->headers = new HeaderCollection();
|
$this->headers = new HeaderCollection();
|
||||||
if ($headers) {
|
|
||||||
foreach ($headers as $name => $values) {
|
foreach ($headers as $name => $values) {
|
||||||
|
if (is_string($values)) {
|
||||||
|
$values = [$values];
|
||||||
|
}
|
||||||
foreach ($values as $value) {
|
foreach ($values as $value) {
|
||||||
$this->headers[$name] = $value;
|
$this->headers[$name] = $value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if ($body !== null) {
|
$this->body = $body ?? new Stream('');
|
||||||
$this->body = $body;
|
|
||||||
} else {
|
|
||||||
$this->body = new Stream('');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function __clone()
|
public function __clone()
|
||||||
|
|
@ -54,7 +55,7 @@ abstract class Message implements MessageInterface
|
||||||
$this->headers = clone $this->headers;
|
$this->headers = clone $this->headers;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
// Psr\Http\Message\MessageInterface
|
// Psr\Http\Message\MessageInterface
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -101,7 +102,7 @@ abstract class Message implements MessageInterface
|
||||||
* While header names are not case-sensitive, getHeaders() will preserve the
|
* While header names are not case-sensitive, getHeaders() will preserve the
|
||||||
* exact case in which headers were originally specified.
|
* 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()
|
public function getHeaders()
|
||||||
{
|
{
|
||||||
|
|
@ -187,7 +188,7 @@ abstract class Message implements MessageInterface
|
||||||
* @param string $name Case-insensitive header field name.
|
* @param string $name Case-insensitive header field name.
|
||||||
* @param string|string[] $value Header value(s).
|
* @param string|string[] $value Header value(s).
|
||||||
* @return static
|
* @return static
|
||||||
* @throws \InvalidArgumentException for invalid header names or values.
|
* @throws InvalidArgumentException for invalid header names or values.
|
||||||
*/
|
*/
|
||||||
public function withHeader($name, $value)
|
public function withHeader($name, $value)
|
||||||
{
|
{
|
||||||
|
|
@ -211,7 +212,7 @@ abstract class Message implements MessageInterface
|
||||||
* @param string $name Case-insensitive header field name to add.
|
* @param string $name Case-insensitive header field name to add.
|
||||||
* @param string|string[] $value Header value(s).
|
* @param string|string[] $value Header value(s).
|
||||||
* @return static
|
* @return static
|
||||||
* @throws \InvalidArgumentException for invalid header names or values.
|
* @throws InvalidArgumentException for invalid header names or values.
|
||||||
*/
|
*/
|
||||||
public function withAddedHeader($name, $value)
|
public function withAddedHeader($name, $value)
|
||||||
{
|
{
|
||||||
|
|
@ -254,7 +255,7 @@ abstract class Message implements MessageInterface
|
||||||
*
|
*
|
||||||
* @param StreamInterface $body Body.
|
* @param StreamInterface $body Body.
|
||||||
* @return static
|
* @return static
|
||||||
* @throws \InvalidArgumentException When the body is not valid.
|
* @throws InvalidArgumentException When the body is not valid.
|
||||||
*/
|
*/
|
||||||
public function withBody(StreamInterface $body)
|
public function withBody(StreamInterface $body)
|
||||||
{
|
{
|
||||||
|
|
@ -263,24 +264,34 @@ abstract class Message implements MessageInterface
|
||||||
return $message;
|
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) {
|
if (!is_string($name)) {
|
||||||
return is_string($item) || is_numeric($item);
|
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)) {
|
$invalid = array_filter($values, $isNotStringOrNumber);
|
||||||
throw new \InvalidArgumentException('Header name must be a string');
|
if ($invalid) {
|
||||||
|
throw new InvalidArgumentException('Header values must be a string or string[]');
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($is_allowed($value)) {
|
return array_map('strval', $values);
|
||||||
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[]');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
namespace WellRESTed\Message;
|
namespace WellRESTed\Message;
|
||||||
|
|
||||||
use Psr\Http\Message\StreamInterface;
|
use Psr\Http\Message\StreamInterface;
|
||||||
|
use RuntimeException;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* NullStream is a minimal, always-empty, non-writable stream.
|
* NullStream is a minimal, always-empty, non-writable stream.
|
||||||
|
|
@ -18,7 +19,7 @@ class NullStream implements StreamInterface
|
||||||
*/
|
*/
|
||||||
public function __toString()
|
public function __toString()
|
||||||
{
|
{
|
||||||
return "";
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -51,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()
|
public function tell()
|
||||||
{
|
{
|
||||||
|
|
@ -90,11 +92,12 @@ class NullStream implements StreamInterface
|
||||||
* PHP $whence values for `fseek()`. SEEK_SET: Set position equal to
|
* PHP $whence values for `fseek()`. SEEK_SET: Set position equal to
|
||||||
* offset bytes SEEK_CUR: Set position to current location plus offset
|
* offset bytes SEEK_CUR: Set position to current location plus offset
|
||||||
* SEEK_END: Set position to end-of-stream 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)
|
public function seek($offset, $whence = SEEK_SET)
|
||||||
{
|
{
|
||||||
throw new \RuntimeException("Unable to seek to position.");
|
throw new RuntimeException('Unable to seek to position.');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -102,11 +105,12 @@ class NullStream implements StreamInterface
|
||||||
*
|
*
|
||||||
* @see seek()
|
* @see seek()
|
||||||
* @link http://www.php.net/manual/en/function.fseek.php
|
* @link http://www.php.net/manual/en/function.fseek.php
|
||||||
* @throws \RuntimeException on failure.
|
* @return void
|
||||||
|
* @throws RuntimeException on failure.
|
||||||
*/
|
*/
|
||||||
public function rewind()
|
public function rewind()
|
||||||
{
|
{
|
||||||
throw new \RuntimeException("Unable to rewind stream.");
|
throw new RuntimeException('Unable to rewind stream.');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -124,11 +128,11 @@ class NullStream implements StreamInterface
|
||||||
*
|
*
|
||||||
* @param string $string The string that is to be written.
|
* @param string $string The string that is to be written.
|
||||||
* @return int Returns the number of bytes written to the stream.
|
* @return int Returns the number of bytes written to the stream.
|
||||||
* @throws \RuntimeException on failure.
|
* @throws RuntimeException on failure.
|
||||||
*/
|
*/
|
||||||
public function write($string)
|
public function write($string)
|
||||||
{
|
{
|
||||||
throw new \RuntimeException("Unable to write to stream.");
|
throw new RuntimeException('Unable to write to stream.');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -149,23 +153,23 @@ class NullStream implements StreamInterface
|
||||||
* call returns fewer bytes.
|
* call returns fewer bytes.
|
||||||
* @return string Returns the data read from the stream, or an empty string
|
* @return string Returns the data read from the stream, or an empty string
|
||||||
* if no bytes are available.
|
* if no bytes are available.
|
||||||
* @throws \RuntimeException if an error occurs.
|
* @throws RuntimeException if an error occurs.
|
||||||
*/
|
*/
|
||||||
public function read($length)
|
public function read($length)
|
||||||
{
|
{
|
||||||
return "";
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the remaining contents in a string
|
* Returns the remaining contents in a string
|
||||||
*
|
*
|
||||||
* @return 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.
|
* reading.
|
||||||
*/
|
*/
|
||||||
public function getContents()
|
public function getContents()
|
||||||
{
|
{
|
||||||
return "";
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
namespace WellRESTed\Message;
|
namespace WellRESTed\Message;
|
||||||
|
|
||||||
|
use InvalidArgumentException;
|
||||||
use Psr\Http\Message\RequestInterface;
|
use Psr\Http\Message\RequestInterface;
|
||||||
use Psr\Http\Message\StreamInterface;
|
use Psr\Http\Message\StreamInterface;
|
||||||
use Psr\Http\Message\UriInterface;
|
use Psr\Http\Message\UriInterface;
|
||||||
|
|
@ -22,37 +23,42 @@ class Request extends Message implements RequestInterface
|
||||||
{
|
{
|
||||||
/** @var string */
|
/** @var string */
|
||||||
protected $method;
|
protected $method;
|
||||||
/** @var string */
|
/** @var string|null */
|
||||||
protected $requestTarget;
|
protected $requestTarget;
|
||||||
/** @var UriInterface */
|
/** @var UriInterface */
|
||||||
protected $uri;
|
protected $uri;
|
||||||
|
|
||||||
// ------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a new Request.
|
* Create a new Request.
|
||||||
*
|
*
|
||||||
* @see \WellRESTed\Message\Message
|
* $headers is an optional associative array with header field names as
|
||||||
* @param UriInterface $uri
|
* string keys and values as either string or string[].
|
||||||
|
*
|
||||||
|
* If no StreamInterface is provided for $body, the instance will create
|
||||||
|
* a NullStream instance for the message body.
|
||||||
|
*
|
||||||
* @param string $method
|
* @param string $method
|
||||||
* @param array $headers
|
* @param string|UriInterface $uri
|
||||||
* @param StreamInterface $body
|
* @param array $headers Associative array with header field names as
|
||||||
|
* keys and values as string|string[]
|
||||||
|
* @param StreamInterface|null $body A stream representation of the message
|
||||||
|
* entity body
|
||||||
*/
|
*/
|
||||||
public function __construct(
|
public function __construct(
|
||||||
UriInterface $uri = null,
|
string $method = 'GET',
|
||||||
$method = "GET",
|
$uri = '',
|
||||||
array $headers = null,
|
array $headers = [],
|
||||||
StreamInterface $body = null
|
?StreamInterface $body = null
|
||||||
) {
|
) {
|
||||||
parent::__construct($headers, $body);
|
parent::__construct($headers, $body);
|
||||||
|
|
||||||
if ($uri !== null) {
|
|
||||||
$this->uri = $uri;
|
|
||||||
} else {
|
|
||||||
$this->uri = new Uri();
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->method = $method;
|
$this->method = $method;
|
||||||
|
if (!($uri instanceof UriInterface)) {
|
||||||
|
$uri = new Uri($uri);
|
||||||
|
}
|
||||||
|
$this->uri = $uri;
|
||||||
|
$this->requestTarget = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function __clone()
|
public function __clone()
|
||||||
|
|
@ -61,7 +67,7 @@ class Request extends Message implements RequestInterface
|
||||||
parent::__clone();
|
parent::__clone();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
// Psr\Http\Message\RequestInterface
|
// Psr\Http\Message\RequestInterface
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -83,7 +89,7 @@ class Request extends Message implements RequestInterface
|
||||||
public function getRequestTarget()
|
public function getRequestTarget()
|
||||||
{
|
{
|
||||||
// Use the explicitly set request target first.
|
// Use the explicitly set request target first.
|
||||||
if (isset($this->requestTarget)) {
|
if ($this->requestTarget !== null) {
|
||||||
return $this->requestTarget;
|
return $this->requestTarget;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -91,11 +97,11 @@ class Request extends Message implements RequestInterface
|
||||||
$target = $this->uri->getPath();
|
$target = $this->uri->getPath();
|
||||||
$query = $this->uri->getQuery();
|
$query = $this->uri->getQuery();
|
||||||
if ($query) {
|
if ($query) {
|
||||||
$target .= "?" . $query;
|
$target .= '?' . $query;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return "/" if the origin form is empty.
|
// Return "/" if the origin form is empty.
|
||||||
return $target ?: "/";
|
return $target ?: '/';
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -137,7 +143,7 @@ class Request extends Message implements RequestInterface
|
||||||
*
|
*
|
||||||
* @param string $method Case-insensitive method.
|
* @param string $method Case-insensitive method.
|
||||||
* @return static
|
* @return static
|
||||||
* @throws \InvalidArgumentException for invalid HTTP methods.
|
* @throws InvalidArgumentException for invalid HTTP methods.
|
||||||
*/
|
*/
|
||||||
public function withMethod($method)
|
public function withMethod($method)
|
||||||
{
|
{
|
||||||
|
|
@ -189,18 +195,18 @@ class Request extends Message implements RequestInterface
|
||||||
$request = clone $this;
|
$request = clone $this;
|
||||||
|
|
||||||
$newHost = $uri->getHost();
|
$newHost = $uri->getHost();
|
||||||
$oldHost = isset($request->headers["Host"]) ? $request->headers["Host"] : "";
|
$oldHost = $request->headers['Host'] ?? '';
|
||||||
|
|
||||||
if ($preserveHost === false) {
|
if ($preserveHost === false) {
|
||||||
// Update Host
|
// Update Host
|
||||||
if ($newHost && $newHost !== $oldHost) {
|
if ($newHost && $newHost !== $oldHost) {
|
||||||
unset($request->headers["Host"]);
|
unset($request->headers['Host']);
|
||||||
$request->headers["Host"] = $newHost;
|
$request->headers['Host'] = $newHost;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Preserve Host
|
// Preserve Host
|
||||||
if (!$oldHost && $newHost) {
|
if (!$oldHost && $newHost) {
|
||||||
$request->headers["Host"] = $newHost;
|
$request->headers['Host'] = $newHost;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -208,21 +214,21 @@ class Request extends Message implements RequestInterface
|
||||||
return $request;
|
return $request;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param string $method
|
* @param mixed $method
|
||||||
* @return static
|
* @return string
|
||||||
* @throws \InvalidArgumentException
|
* @throws InvalidArgumentException
|
||||||
*/
|
*/
|
||||||
private function getValidatedMethod($method)
|
private function getValidatedMethod($method)
|
||||||
{
|
{
|
||||||
if (!is_string($method)) {
|
if (!is_string($method)) {
|
||||||
throw new \InvalidArgumentException("Method must be a string.");
|
throw new InvalidArgumentException('Method must be a string.');
|
||||||
}
|
}
|
||||||
$method = trim($method);
|
$method = trim($method);
|
||||||
if (strpos($method, " ") !== false) {
|
if (strpos($method, ' ') !== false) {
|
||||||
throw new \InvalidArgumentException("Method cannot contain spaces.");
|
throw new InvalidArgumentException('Method cannot contain spaces.');
|
||||||
}
|
}
|
||||||
return $method;
|
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;
|
namespace WellRESTed\Message;
|
||||||
|
|
||||||
|
use InvalidArgumentException;
|
||||||
use Psr\Http\Message\ResponseInterface;
|
use Psr\Http\Message\ResponseInterface;
|
||||||
use Psr\Http\Message\StreamInterface;
|
use Psr\Http\Message\StreamInterface;
|
||||||
|
|
||||||
|
|
@ -34,18 +35,22 @@ class Response extends Message implements ResponseInterface
|
||||||
* a NullStream instance for the message body.
|
* a NullStream instance for the message body.
|
||||||
*
|
*
|
||||||
* @see \WellRESTed\Message\Message
|
* @see \WellRESTed\Message\Message
|
||||||
|
*
|
||||||
* @param int $statusCode
|
* @param int $statusCode
|
||||||
* @param array $headers
|
* @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);
|
parent::__construct($headers, $body);
|
||||||
$this->statusCode = $statusCode;
|
$this->statusCode = $statusCode;
|
||||||
$this->reasonPhrase = $this->getDefaultReasonPhraseForStatusCode($statusCode);
|
$this->reasonPhrase = $this->getDefaultReasonPhraseForStatusCode($statusCode);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
// Psr\Http\Message\ResponseInterface
|
// 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
|
* The status code is a 3-digit integer result code of the server's attempt
|
||||||
* to understand and satisfy the request.
|
* to understand and satisfy the request.
|
||||||
*
|
*
|
||||||
* @return integer Status code.
|
* @return int Status code.
|
||||||
*/
|
*/
|
||||||
public function getStatusCode()
|
public function getStatusCode()
|
||||||
{
|
{
|
||||||
|
|
@ -74,9 +79,9 @@ class Response extends Message implements ResponseInterface
|
||||||
* provided status code; if none is provided, implementations MAY
|
* provided status code; if none is provided, implementations MAY
|
||||||
* use the defaults as suggested in the HTTP specification.
|
* use the defaults as suggested in the HTTP specification.
|
||||||
* @return static
|
* @return static
|
||||||
* @throws \InvalidArgumentException For invalid status code arguments.
|
* @throws InvalidArgumentException For invalid status code arguments.
|
||||||
*/
|
*/
|
||||||
public function withStatus($code, $reasonPhrase = "")
|
public function withStatus($code, $reasonPhrase = '')
|
||||||
{
|
{
|
||||||
$response = clone $this;
|
$response = clone $this;
|
||||||
$response->statusCode = $code;
|
$response->statusCode = $code;
|
||||||
|
|
@ -103,73 +108,70 @@ class Response extends Message implements ResponseInterface
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param int $statusCode
|
* @param int $statusCode
|
||||||
* @return string|null
|
* @return string
|
||||||
*/
|
*/
|
||||||
private function getDefaultReasonPhraseForStatusCode($statusCode)
|
private function getDefaultReasonPhraseForStatusCode($statusCode)
|
||||||
{
|
{
|
||||||
$reasonPhraseLookup = [
|
$reasonPhraseLookup = [
|
||||||
100 => "Continue",
|
100 => 'Continue',
|
||||||
101 => "Switching Protocols",
|
101 => 'Switching Protocols',
|
||||||
102 => "Processing",
|
102 => 'Processing',
|
||||||
200 => "OK",
|
200 => 'OK',
|
||||||
201 => "Created",
|
201 => 'Created',
|
||||||
202 => "Accepted",
|
202 => 'Accepted',
|
||||||
203 => "Non-Authoritative Information",
|
203 => 'Non-Authoritative Information',
|
||||||
204 => "No Content",
|
204 => 'No Content',
|
||||||
205 => "Reset Content",
|
205 => 'Reset Content',
|
||||||
206 => "Partial Content",
|
206 => 'Partial Content',
|
||||||
207 => "Multi-Status",
|
207 => 'Multi-Status',
|
||||||
208 => "Already Reported",
|
208 => 'Already Reported',
|
||||||
226 => "IM Used",
|
226 => 'IM Used',
|
||||||
300 => "Multiple Choices",
|
300 => 'Multiple Choices',
|
||||||
301 => "Moved Permanently",
|
301 => 'Moved Permanently',
|
||||||
302 => "Found",
|
302 => 'Found',
|
||||||
303 => "See Other",
|
303 => 'See Other',
|
||||||
304 => "Not Modified",
|
304 => 'Not Modified',
|
||||||
305 => "Use Proxy",
|
305 => 'Use Proxy',
|
||||||
307 => "Temporary Redirect",
|
307 => 'Temporary Redirect',
|
||||||
308 => "Permanent Redirect",
|
308 => 'Permanent Redirect',
|
||||||
400 => "Bad Request",
|
400 => 'Bad Request',
|
||||||
401 => "Unauthorized",
|
401 => 'Unauthorized',
|
||||||
402 => "Payment Required",
|
402 => 'Payment Required',
|
||||||
403 => "Forbidden",
|
403 => 'Forbidden',
|
||||||
404 => "Not Found",
|
404 => 'Not Found',
|
||||||
405 => "Method Not Allowed",
|
405 => 'Method Not Allowed',
|
||||||
406 => "Not Acceptable",
|
406 => 'Not Acceptable',
|
||||||
407 => "Proxy Authentication Required",
|
407 => 'Proxy Authentication Required',
|
||||||
408 => "Request Timeout",
|
408 => 'Request Timeout',
|
||||||
409 => "Conflict",
|
409 => 'Conflict',
|
||||||
410 => "Gone",
|
410 => 'Gone',
|
||||||
411 => "Length Required",
|
411 => 'Length Required',
|
||||||
412 => "Precondition Failed",
|
412 => 'Precondition Failed',
|
||||||
413 => "Payload Too Large",
|
413 => 'Payload Too Large',
|
||||||
414 => "URI Too Long",
|
414 => 'URI Too Long',
|
||||||
415 => "Unsupported Media Type",
|
415 => 'Unsupported Media Type',
|
||||||
416 => "Range Not Satisfiable",
|
416 => 'Range Not Satisfiable',
|
||||||
417 => "Expectation Failed",
|
417 => 'Expectation Failed',
|
||||||
421 => "Misdirected Request",
|
421 => 'Misdirected Request',
|
||||||
422 => "Unprocessable Entity",
|
422 => 'Unprocessable Entity',
|
||||||
423 => "Locked",
|
423 => 'Locked',
|
||||||
424 => "Failed Dependency",
|
424 => 'Failed Dependency',
|
||||||
426 => "Upgrade Required",
|
426 => 'Upgrade Required',
|
||||||
428 => "Precondition Required",
|
428 => 'Precondition Required',
|
||||||
429 => "Too Many Requests",
|
429 => 'Too Many Requests',
|
||||||
431 => "Request Header Fields Too Large",
|
431 => 'Request Header Fields Too Large',
|
||||||
500 => "Internal Server Error",
|
500 => 'Internal Server Error',
|
||||||
501 => "Not Implemented",
|
501 => 'Not Implemented',
|
||||||
502 => "Bad Gateway",
|
502 => 'Bad Gateway',
|
||||||
503 => "Service Unavailable",
|
503 => 'Service Unavailable',
|
||||||
504 => "Gateway Timeout",
|
504 => 'Gateway Timeout',
|
||||||
505 => "HTTP Version Not Supported",
|
505 => 'HTTP Version Not Supported',
|
||||||
506 => "Variant Also Negotiates",
|
506 => 'Variant Also Negotiates',
|
||||||
507 => "Insufficient Storage",
|
507 => 'Insufficient Storage',
|
||||||
508 => "Loop Detected",
|
508 => 'Loop Detected',
|
||||||
510 => "Not Extended",
|
510 => 'Not Extended',
|
||||||
511 => "Network Authentication Required"
|
511 => 'Network Authentication Required'
|
||||||
];
|
];
|
||||||
if (isset($reasonPhraseLookup[$statusCode])) {
|
return $reasonPhraseLookup[$statusCode] ?? '';
|
||||||
return $reasonPhraseLookup[$statusCode];
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
namespace WellRESTed\Message;
|
||||||
|
|
||||||
|
use InvalidArgumentException;
|
||||||
use Psr\Http\Message\ServerRequestInterface;
|
use Psr\Http\Message\ServerRequestInterface;
|
||||||
use Psr\Http\Message\StreamInterface;
|
use Psr\Http\Message\StreamInterface;
|
||||||
use Psr\Http\Message\UploadedFileInterface;
|
use Psr\Http\Message\UploadedFileInterface;
|
||||||
|
use Psr\Http\Message\UriInterface;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Representation of an incoming, server-side HTTP request.
|
* Representation of an incoming, server-side HTTP request.
|
||||||
|
|
@ -49,24 +51,37 @@ class ServerRequest extends Request implements ServerRequestInterface
|
||||||
/** @var array */
|
/** @var array */
|
||||||
private $uploadedFiles;
|
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
|
* $headers is an optional associative array with header field names as
|
||||||
* instantiating the request, use the factory method
|
* string keys and values as either string or string[].
|
||||||
* ServerRequest::getServerRequest
|
|
||||||
*
|
*
|
||||||
* @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()
|
public function __construct(
|
||||||
{
|
string $method = 'GET',
|
||||||
parent::__construct();
|
$uri = '',
|
||||||
$this->attributes = [];
|
array $headers = [],
|
||||||
|
?StreamInterface $body = null,
|
||||||
|
array $serverParams = []
|
||||||
|
) {
|
||||||
|
parent::__construct($method, $uri, $headers, $body);
|
||||||
|
$this->serverParams = $serverParams;
|
||||||
$this->cookieParams = [];
|
$this->cookieParams = [];
|
||||||
$this->queryParams = [];
|
$this->queryParams = [];
|
||||||
$this->serverParams = [];
|
$this->attributes = [];
|
||||||
$this->uploadedFiles = [];
|
$this->uploadedFiles = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -78,7 +93,7 @@ class ServerRequest extends Request implements ServerRequestInterface
|
||||||
parent::__clone();
|
parent::__clone();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
// Psr\Http\Message\ServerRequestInterface
|
// Psr\Http\Message\ServerRequestInterface
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -188,13 +203,14 @@ class ServerRequest extends Request implements ServerRequestInterface
|
||||||
*
|
*
|
||||||
* @param array $uploadedFiles An array tree of UploadedFileInterface instances.
|
* @param array $uploadedFiles An array tree of UploadedFileInterface instances.
|
||||||
* @return static
|
* @return static
|
||||||
* @throws \InvalidArgumentException if an invalid structure is provided.
|
* @throws InvalidArgumentException if an invalid structure is provided.
|
||||||
*/
|
*/
|
||||||
public function withUploadedFiles(array $uploadedFiles)
|
public function withUploadedFiles(array $uploadedFiles)
|
||||||
{
|
{
|
||||||
if (!$this->isValidUploadedFilesTree($uploadedFiles)) {
|
if (!$this->isValidUploadedFilesTree($uploadedFiles)) {
|
||||||
throw new \InvalidArgumentException(
|
throw new InvalidArgumentException(
|
||||||
"withUploadedFiles expects an array tree with UploadedFileInterface leaves.");
|
'withUploadedFiles expects an array tree with UploadedFileInterface leaves.'
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
$request = clone $this;
|
$request = clone $this;
|
||||||
|
|
@ -247,7 +263,7 @@ class ServerRequest extends Request implements ServerRequestInterface
|
||||||
public function withParsedBody($data)
|
public function withParsedBody($data)
|
||||||
{
|
{
|
||||||
if (!(is_null($data) || is_array($data) || is_object($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;
|
$request = clone $this;
|
||||||
|
|
@ -334,185 +350,7 @@ class ServerRequest extends Request implements ServerRequestInterface
|
||||||
return $request;
|
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->protocolVersion = "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->getHeaderLine("Content-type");
|
|
||||||
if (strpos($contentType, "application/x-www-form-urlencoded") !== false
|
|
||||||
|| strpos($contentType, "multipart/form-data") !== false) {
|
|
||||||
$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 static
|
|
||||||
* @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()
|
|
||||||
{
|
|
||||||
$input = fopen("php://input", "rb");
|
|
||||||
$temp = fopen("php://temp", "wb+");
|
|
||||||
stream_copy_to_stream($input, $temp);
|
|
||||||
rewind($temp);
|
|
||||||
return new Stream($temp);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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_") {
|
|
||||||
$name = $this->normalizeHeaderName(substr($name, 5));
|
|
||||||
$headers[$name] = $value;
|
|
||||||
} elseif ($this->isContentHeader($name, $value)) {
|
|
||||||
$name = $this->normalizeHeaderName($name);
|
|
||||||
$headers[$name] = $value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return $headers;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param $name
|
|
||||||
* @param $value
|
|
||||||
* @return bool
|
|
||||||
*/
|
|
||||||
private function isContentHeader($name, $value)
|
|
||||||
{
|
|
||||||
return ($name === "CONTENT_LENGTH" || $name === "CONTENT_TYPE")
|
|
||||||
&& trim($value);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param string $name
|
|
||||||
* @return string
|
|
||||||
*/
|
|
||||||
private function normalizeHeaderName($name)
|
|
||||||
{
|
|
||||||
$name = ucwords(strtolower(str_replace("_", " ", $name)));
|
|
||||||
return str_replace(" ", "-", $name);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param array $root
|
* @param array $root
|
||||||
|
|
@ -527,7 +365,7 @@ class ServerRequest extends Request implements ServerRequestInterface
|
||||||
|
|
||||||
// If not empty, the array MUST have all string keys.
|
// If not empty, the array MUST have all string keys.
|
||||||
$keys = array_keys($root);
|
$keys = array_keys($root);
|
||||||
if (count($keys) !== count(array_filter($keys, "is_string"))) {
|
if (count($keys) !== count(array_filter($keys, 'is_string'))) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -540,7 +378,11 @@ class ServerRequest extends Request implements ServerRequestInterface
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private function isValidUploadedFilesBranch($branch)
|
/**
|
||||||
|
* @param UploadedFileInterface|array $branch
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
private function isValidUploadedFilesBranch($branch): bool
|
||||||
{
|
{
|
||||||
if (is_array($branch)) {
|
if (is_array($branch)) {
|
||||||
// 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;
|
namespace WellRESTed\Message;
|
||||||
|
|
||||||
|
use Exception;
|
||||||
|
use InvalidArgumentException;
|
||||||
use Psr\Http\Message\StreamInterface;
|
use Psr\Http\Message\StreamInterface;
|
||||||
|
use RuntimeException;
|
||||||
|
|
||||||
class Stream implements StreamInterface
|
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;
|
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.
|
* from fopen) or a string.
|
||||||
*
|
*
|
||||||
* If $resource is a string, the Stream will open a php://temp stream,
|
* 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
|
* @param resource|string $resource A file system pointer resource or
|
||||||
* string
|
* 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;
|
$this->resource = $resource;
|
||||||
} elseif (is_string($resource)) {
|
} elseif (is_string($resource)) {
|
||||||
$this->resource = fopen("php://temp", "wb+");
|
$this->resource = fopen('php://temp', 'wb+');
|
||||||
if ($resource !== "") {
|
if ($resource !== '') {
|
||||||
$this->write($resource);
|
$this->write($resource);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
throw new \InvalidArgumentException("Expected a resource handler.");
|
throw new InvalidArgumentException('Expected resource or string.');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -46,18 +52,16 @@ class Stream implements StreamInterface
|
||||||
*/
|
*/
|
||||||
public function __toString()
|
public function __toString()
|
||||||
{
|
{
|
||||||
$string = "";
|
|
||||||
try {
|
try {
|
||||||
if ($this->isSeekable()) {
|
if ($this->isSeekable()) {
|
||||||
rewind($this->resource);
|
$this->rewind();
|
||||||
}
|
}
|
||||||
$string = $this->getContents();
|
return $this->getContents();
|
||||||
// @codeCoverageIgnoreStart
|
} catch (Exception $e) {
|
||||||
} catch (\Exception $e) {
|
// Silence exceptions in order to conform with PHP's string casting
|
||||||
// @codeCoverageIgnoreEnd
|
// operations.
|
||||||
// Silence exceptions in order to conform with PHP's string casting operations.
|
return '';
|
||||||
}
|
}
|
||||||
return $string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -67,7 +71,13 @@ class Stream implements StreamInterface
|
||||||
*/
|
*/
|
||||||
public function close()
|
public function close()
|
||||||
{
|
{
|
||||||
fclose($this->resource);
|
if ($this->resource === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$resource = $this->resource;
|
||||||
|
fclose($resource);
|
||||||
|
$this->resource = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -79,9 +89,9 @@ class Stream implements StreamInterface
|
||||||
*/
|
*/
|
||||||
public function detach()
|
public function detach()
|
||||||
{
|
{
|
||||||
$stream = $this->resource;
|
$resource = $this->resource;
|
||||||
$this->resource = null;
|
$this->resource = null;
|
||||||
return $stream;
|
return $resource;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -91,9 +101,13 @@ class Stream implements StreamInterface
|
||||||
*/
|
*/
|
||||||
public function getSize()
|
public function getSize()
|
||||||
{
|
{
|
||||||
|
if ($this->resource === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
$statistics = fstat($this->resource);
|
$statistics = fstat($this->resource);
|
||||||
if ($statistics && $statistics["size"]) {
|
if ($statistics && $statistics['size']) {
|
||||||
return $statistics["size"];
|
return $statistics['size'];
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
@ -102,13 +116,17 @@ class Stream implements StreamInterface
|
||||||
* Returns the current position of the file read/write pointer
|
* Returns the current position of the file read/write pointer
|
||||||
*
|
*
|
||||||
* @return int Position of the file pointer
|
* @return int Position of the file pointer
|
||||||
* @throws \RuntimeException on error.
|
* @throws RuntimeException on error.
|
||||||
*/
|
*/
|
||||||
public function tell()
|
public function tell()
|
||||||
{
|
{
|
||||||
|
if ($this->resource === null) {
|
||||||
|
throw new RuntimeException('Unable to retrieve current position of detached stream.');
|
||||||
|
}
|
||||||
|
|
||||||
$position = ftell($this->resource);
|
$position = ftell($this->resource);
|
||||||
if ($position === false) {
|
if ($position === false) {
|
||||||
throw new \RuntimeException("Unable to retrieve current position of file pointer.");
|
throw new RuntimeException('Unable to retrieve current position of file pointer.');
|
||||||
}
|
}
|
||||||
return $position;
|
return $position;
|
||||||
}
|
}
|
||||||
|
|
@ -120,6 +138,10 @@ class Stream implements StreamInterface
|
||||||
*/
|
*/
|
||||||
public function eof()
|
public function eof()
|
||||||
{
|
{
|
||||||
|
if ($this->resource === null) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
return feof($this->resource);
|
return feof($this->resource);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -130,7 +152,11 @@ class Stream implements StreamInterface
|
||||||
*/
|
*/
|
||||||
public function isSeekable()
|
public function isSeekable()
|
||||||
{
|
{
|
||||||
return $this->getMetadata("seekable") == 1;
|
if ($this->resource === null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->getMetadata('seekable') == 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -143,16 +169,21 @@ class Stream implements StreamInterface
|
||||||
* PHP $whence values for `fseek()`. SEEK_SET: Set position equal to
|
* PHP $whence values for `fseek()`. SEEK_SET: Set position equal to
|
||||||
* offset bytes SEEK_CUR: Set position to current location plus offset
|
* offset bytes SEEK_CUR: Set position to current location plus offset
|
||||||
* SEEK_END: Set position to end-of-stream 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)
|
public function seek($offset, $whence = SEEK_SET)
|
||||||
{
|
{
|
||||||
|
if ($this->resource === null) {
|
||||||
|
throw new RuntimeException('Unable to seek detached stream.');
|
||||||
|
}
|
||||||
|
|
||||||
$result = -1;
|
$result = -1;
|
||||||
if ($this->isSeekable()) {
|
if ($this->isSeekable()) {
|
||||||
$result = fseek($this->resource, $offset, $whence);
|
$result = fseek($this->resource, $offset, $whence);
|
||||||
}
|
}
|
||||||
if ($result === -1) {
|
if ($result === -1) {
|
||||||
throw new \RuntimeException("Unable to seek to position.");
|
throw new RuntimeException('Unable to seek to position.');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -164,16 +195,21 @@ class Stream implements StreamInterface
|
||||||
*
|
*
|
||||||
* @see seek()
|
* @see seek()
|
||||||
* @link http://www.php.net/manual/en/function.fseek.php
|
* @link http://www.php.net/manual/en/function.fseek.php
|
||||||
* @throws \RuntimeException on failure.
|
* @return void
|
||||||
|
* @throws RuntimeException on failure.
|
||||||
*/
|
*/
|
||||||
public function rewind()
|
public function rewind()
|
||||||
{
|
{
|
||||||
|
if ($this->resource === null) {
|
||||||
|
throw new RuntimeException('Unable to seek detached stream.');
|
||||||
|
}
|
||||||
|
|
||||||
$result = false;
|
$result = false;
|
||||||
if ($this->isSeekable()) {
|
if ($this->isSeekable()) {
|
||||||
$result = rewind($this->resource);
|
$result = rewind($this->resource);
|
||||||
}
|
}
|
||||||
if ($result === false) {
|
if ($result === false) {
|
||||||
throw new \RuntimeException("Unable to seek to position.");
|
throw new RuntimeException('Unable to rewind.');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -184,8 +220,12 @@ class Stream implements StreamInterface
|
||||||
*/
|
*/
|
||||||
public function isWritable()
|
public function isWritable()
|
||||||
{
|
{
|
||||||
$mode = $this->getMetadata("mode");
|
if ($this->resource === null) {
|
||||||
return $mode[0] !== "r" || strpos($mode, "+") !== false;
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$mode = $this->getBasicMode();
|
||||||
|
return in_array($mode, self::WRITABLE_MODES);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -193,16 +233,20 @@ class Stream implements StreamInterface
|
||||||
*
|
*
|
||||||
* @param string $string The string that is to be written.
|
* @param string $string The string that is to be written.
|
||||||
* @return int Returns the number of bytes written to the stream.
|
* @return int Returns the number of bytes written to the stream.
|
||||||
* @throws \RuntimeException on failure.
|
* @throws RuntimeException on failure.
|
||||||
*/
|
*/
|
||||||
public function write($string)
|
public function write($string)
|
||||||
{
|
{
|
||||||
|
if ($this->resource === null) {
|
||||||
|
throw new RuntimeException('Unable to write to detached stream.');
|
||||||
|
}
|
||||||
|
|
||||||
$result = false;
|
$result = false;
|
||||||
if ($this->isWritable()) {
|
if ($this->isWritable()) {
|
||||||
$result = fwrite($this->resource, $string);
|
$result = fwrite($this->resource, $string);
|
||||||
}
|
}
|
||||||
if ($result === false) {
|
if ($result === false) {
|
||||||
throw new \RuntimeException("Unable to write to stream.");
|
throw new RuntimeException('Unable to write to stream.');
|
||||||
}
|
}
|
||||||
return $result;
|
return $result;
|
||||||
}
|
}
|
||||||
|
|
@ -214,8 +258,12 @@ class Stream implements StreamInterface
|
||||||
*/
|
*/
|
||||||
public function isReadable()
|
public function isReadable()
|
||||||
{
|
{
|
||||||
$mode = $this->getMetadata("mode");
|
if ($this->resource === null) {
|
||||||
return strpos($mode, "r") !== false || strpos($mode, "+") !== false;
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$mode = $this->getBasicMode();
|
||||||
|
return in_array($mode, self::READABLE_MODES);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -226,16 +274,20 @@ class Stream implements StreamInterface
|
||||||
* call returns fewer bytes.
|
* call returns fewer bytes.
|
||||||
* @return string Returns the data read from the stream, or an empty string
|
* @return string Returns the data read from the stream, or an empty string
|
||||||
* if no bytes are available.
|
* if no bytes are available.
|
||||||
* @throws \RuntimeException if an error occurs.
|
* @throws RuntimeException if an error occurs.
|
||||||
*/
|
*/
|
||||||
public function read($length)
|
public function read($length)
|
||||||
{
|
{
|
||||||
|
if ($this->resource === null) {
|
||||||
|
throw new RuntimeException('Unable to read to detached stream.');
|
||||||
|
}
|
||||||
|
|
||||||
$result = false;
|
$result = false;
|
||||||
if ($this->isReadable()) {
|
if ($this->isReadable()) {
|
||||||
$result = fread($this->resource, $length);
|
$result = fread($this->resource, $length);
|
||||||
}
|
}
|
||||||
if ($result === false) {
|
if ($result === false) {
|
||||||
throw new \RuntimeException("Unable to read from stream.");
|
throw new RuntimeException('Unable to read from stream.');
|
||||||
}
|
}
|
||||||
return $result;
|
return $result;
|
||||||
}
|
}
|
||||||
|
|
@ -244,17 +296,21 @@ class Stream implements StreamInterface
|
||||||
* Returns the remaining contents in a string
|
* Returns the remaining contents in a string
|
||||||
*
|
*
|
||||||
* @return 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.
|
* reading.
|
||||||
*/
|
*/
|
||||||
public function getContents()
|
public function getContents()
|
||||||
{
|
{
|
||||||
|
if ($this->resource === null) {
|
||||||
|
throw new RuntimeException('Unable to read to detached stream.');
|
||||||
|
}
|
||||||
|
|
||||||
$result = false;
|
$result = false;
|
||||||
if ($this->isReadable()) {
|
if ($this->isReadable()) {
|
||||||
$result = stream_get_contents($this->resource);
|
$result = stream_get_contents($this->resource);
|
||||||
}
|
}
|
||||||
if ($result === false) {
|
if ($result === false) {
|
||||||
throw new \RuntimeException("Unable to read from stream.");
|
throw new RuntimeException('Unable to read from stream.');
|
||||||
}
|
}
|
||||||
return $result;
|
return $result;
|
||||||
}
|
}
|
||||||
|
|
@ -266,13 +322,17 @@ class Stream implements StreamInterface
|
||||||
* stream_get_meta_data() function.
|
* stream_get_meta_data() function.
|
||||||
*
|
*
|
||||||
* @link http://php.net/manual/en/function.stream-get-meta-data.php
|
* @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
|
* @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
|
* provided. Returns a specific key value if a key is provided and the
|
||||||
* value is found, or null if the key is not found.
|
* value is found, or null if the key is not found.
|
||||||
*/
|
*/
|
||||||
public function getMetadata($key = null)
|
public function getMetadata($key = null)
|
||||||
{
|
{
|
||||||
|
if ($this->resource === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
$metadata = stream_get_meta_data($this->resource);
|
$metadata = stream_get_meta_data($this->resource);
|
||||||
if ($key === null) {
|
if ($key === null) {
|
||||||
return $metadata;
|
return $metadata;
|
||||||
|
|
@ -280,4 +340,14 @@ class Stream implements StreamInterface
|
||||||
return $metadata[$key];
|
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,24 +2,33 @@
|
||||||
|
|
||||||
namespace WellRESTed\Message;
|
namespace WellRESTed\Message;
|
||||||
|
|
||||||
|
use InvalidArgumentException;
|
||||||
use Psr\Http\Message\StreamInterface;
|
use Psr\Http\Message\StreamInterface;
|
||||||
use Psr\Http\Message\UploadedFileInterface;
|
use Psr\Http\Message\UploadedFileInterface;
|
||||||
|
use RuntimeException;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Value object representing a file uploaded through an HTTP request.
|
* Value object representing a file uploaded through an HTTP request.
|
||||||
*/
|
*/
|
||||||
class UploadedFile implements UploadedFileInterface
|
class UploadedFile implements UploadedFileInterface
|
||||||
{
|
{
|
||||||
|
/** @var string */
|
||||||
private $clientFilename;
|
private $clientFilename;
|
||||||
|
/** @var string */
|
||||||
private $clientMediaType;
|
private $clientMediaType;
|
||||||
|
/** @var int */
|
||||||
private $error;
|
private $error;
|
||||||
|
/** @var bool */
|
||||||
private $moved = false;
|
private $moved = false;
|
||||||
|
/** @var int */
|
||||||
private $size;
|
private $size;
|
||||||
|
/** @var StreamInterface */
|
||||||
private $stream;
|
private $stream;
|
||||||
|
/** @var string|null */
|
||||||
private $tmpName;
|
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:
|
* provided by $_FILES. For example, given this structure for $_FILES:
|
||||||
*
|
*
|
||||||
* array(
|
* array(
|
||||||
|
|
@ -57,10 +66,11 @@ class UploadedFile implements UploadedFileInterface
|
||||||
$this->size = $size;
|
$this->size = $size;
|
||||||
|
|
||||||
if (file_exists($tmpName)) {
|
if (file_exists($tmpName)) {
|
||||||
|
$this->stream = new Stream(fopen($tmpName, 'rb'));
|
||||||
$this->tmpName = $tmpName;
|
$this->tmpName = $tmpName;
|
||||||
$this->stream = new Stream(fopen($tmpName, "r"));
|
|
||||||
} else {
|
} else {
|
||||||
$this->stream = new NullStream();
|
$this->stream = new NullStream();
|
||||||
|
$this->tmpName = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -77,16 +87,19 @@ class UploadedFile implements UploadedFileInterface
|
||||||
* raise an exception.
|
* raise an exception.
|
||||||
*
|
*
|
||||||
* @return StreamInterface Stream representation of the uploaded file.
|
* @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.
|
* be created.
|
||||||
*/
|
*/
|
||||||
public function getStream()
|
public function getStream()
|
||||||
{
|
{
|
||||||
if ($this->moved) {
|
if ($this->tmpName === null) {
|
||||||
throw new \RuntimeException("File has already been moved");
|
throw new RuntimeException('Unable to read uploaded file.');
|
||||||
}
|
}
|
||||||
if (php_sapi_name() !== "cli" && !is_uploaded_file($this->tmpName)) {
|
if ($this->moved) {
|
||||||
throw new \RuntimeException("File is not an uploaded file.");
|
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;
|
return $this->stream;
|
||||||
}
|
}
|
||||||
|
|
@ -104,20 +117,21 @@ class UploadedFile implements UploadedFileInterface
|
||||||
*
|
*
|
||||||
* @see http://php.net/is_uploaded_file
|
* @see http://php.net/is_uploaded_file
|
||||||
* @see http://php.net/move_uploaded_file
|
* @see http://php.net/move_uploaded_file
|
||||||
* @param string $path Path to which to move the uploaded file.
|
* @param string $targetPath Path to which to move the uploaded file.
|
||||||
* @throws \InvalidArgumentException if the $path specified is invalid.
|
* @return void
|
||||||
* @throws \RuntimeException on any error during the move operation, or on
|
* @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.
|
* the second or subsequent call to the method.
|
||||||
*/
|
*/
|
||||||
public function moveTo($path)
|
public function moveTo($targetPath)
|
||||||
{
|
{
|
||||||
if ($this->tmpName === null || !file_exists($this->tmpName)) {
|
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") {
|
if (php_sapi_name() === 'cli') {
|
||||||
rename($this->tmpName, $path);
|
rename($this->tmpName, $targetPath);
|
||||||
} else {
|
} else {
|
||||||
move_uploaded_file($this->tmpName, $path);
|
move_uploaded_file($this->tmpName, $targetPath);
|
||||||
}
|
}
|
||||||
$this->moved = true;
|
$this->moved = true;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
namespace WellRESTed\Message;
|
namespace WellRESTed\Message;
|
||||||
|
|
||||||
|
use InvalidArgumentException;
|
||||||
use Psr\Http\Message\UriInterface;
|
use Psr\Http\Message\UriInterface;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -17,60 +18,41 @@ use Psr\Http\Message\UriInterface;
|
||||||
*/
|
*/
|
||||||
class Uri implements UriInterface
|
class Uri implements UriInterface
|
||||||
{
|
{
|
||||||
const MIN_PORT = 0;
|
private const MIN_PORT = 0;
|
||||||
const MAX_PORT = 65535;
|
private const MAX_PORT = 65535;
|
||||||
|
|
||||||
/** @var string */
|
/** @var string */
|
||||||
private $scheme = "";
|
private $scheme;
|
||||||
|
/** @var string */
|
||||||
|
private $user;
|
||||||
/** @var string */
|
/** @var string */
|
||||||
private $user = "";
|
|
||||||
/** @var string|null */
|
|
||||||
private $password;
|
private $password;
|
||||||
/** @var string */
|
/** @var string */
|
||||||
private $host = "";
|
private $host;
|
||||||
/** @var int|null */
|
/** @var int|null */
|
||||||
private $port;
|
private $port;
|
||||||
/** @var string */
|
/** @var string */
|
||||||
private $path = "";
|
private $path;
|
||||||
/** @var string */
|
/** @var string */
|
||||||
private $query = "";
|
private $query;
|
||||||
/** @var string */
|
/** @var string */
|
||||||
private $fragment = "";
|
private $fragment;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param string $uri A string representation of a URI.
|
* @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);
|
$parsed = parse_url($uri);
|
||||||
if ($parsed !== false) {
|
|
||||||
if (isset($parsed["scheme"])) {
|
$this->scheme = $parsed['scheme'] ?? '';
|
||||||
$this->scheme = $parsed["scheme"];
|
$this->user = $parsed['user'] ?? '';
|
||||||
}
|
$this->password = $parsed['pass'] ?? '';
|
||||||
if (isset($parsed["host"])) {
|
$this->host = strtolower($parsed['host'] ?? '');
|
||||||
$this->host = strtolower($parsed["host"]);
|
$this->port = $parsed['port'] ?? null;
|
||||||
}
|
$this->path = $parsed['path'] ?? '';
|
||||||
if (isset($parsed["port"])) {
|
$this->query = $parsed['query'] ?? '';
|
||||||
$this->port = $parsed["port"];
|
$this->fragment = $parsed['fragment'] ?? '';
|
||||||
}
|
|
||||||
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"];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -112,33 +94,38 @@ class Uri implements UriInterface
|
||||||
*/
|
*/
|
||||||
public function getAuthority()
|
public function getAuthority()
|
||||||
{
|
{
|
||||||
$authority = "";
|
|
||||||
|
|
||||||
$host = $this->getHost();
|
$host = $this->getHost();
|
||||||
if ($host !== "") {
|
if (!$host) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
$authority = '';
|
||||||
|
|
||||||
// User Info
|
// User Info
|
||||||
$userInfo = $this->getUserInfo();
|
$userInfo = $this->getUserInfo();
|
||||||
if ($userInfo !== "") {
|
if ($userInfo) {
|
||||||
$authority .= $userInfo . "@";
|
$authority .= $userInfo . '@';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Host
|
// Host
|
||||||
$authority .= $host;
|
$authority .= $host;
|
||||||
|
|
||||||
// Port: Include only if set AND non-standard.
|
// Port: Include only if non-standard
|
||||||
$port = $this->getPort();
|
if ($this->nonStandardPort()) {
|
||||||
if ($port !== null) {
|
$authority .= ':' . $this->getPort();
|
||||||
$scheme = $this->getScheme();
|
|
||||||
if (($scheme === "http" && $port !== 80 ) || ($scheme === "https" && $port !== 443)) {
|
|
||||||
$authority .= ":" . $port;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return $authority;
|
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.
|
* Retrieve the user information component of the URI.
|
||||||
*
|
*
|
||||||
|
|
@ -158,7 +145,7 @@ class Uri implements UriInterface
|
||||||
{
|
{
|
||||||
$userInfo = $this->user;
|
$userInfo = $this->user;
|
||||||
if ($userInfo && $this->password) {
|
if ($userInfo && $this->password) {
|
||||||
$userInfo .= ":" . $this->password;
|
$userInfo .= ':' . $this->password;
|
||||||
}
|
}
|
||||||
return $userInfo;
|
return $userInfo;
|
||||||
}
|
}
|
||||||
|
|
@ -198,9 +185,9 @@ class Uri implements UriInterface
|
||||||
{
|
{
|
||||||
if ($this->port === null) {
|
if ($this->port === null) {
|
||||||
switch ($this->scheme) {
|
switch ($this->scheme) {
|
||||||
case "http":
|
case 'http':
|
||||||
return 80;
|
return 80;
|
||||||
case "https":
|
case 'https':
|
||||||
return 443;
|
return 443;
|
||||||
default:
|
default:
|
||||||
return null;
|
return null;
|
||||||
|
|
@ -236,7 +223,7 @@ class Uri implements UriInterface
|
||||||
*/
|
*/
|
||||||
public function getPath()
|
public function getPath()
|
||||||
{
|
{
|
||||||
if ($this->path === "*") {
|
if ($this->path === '*') {
|
||||||
return $this->path;
|
return $this->path;
|
||||||
}
|
}
|
||||||
return $this->percentEncode($this->path);
|
return $this->percentEncode($this->path);
|
||||||
|
|
@ -301,13 +288,13 @@ class Uri implements UriInterface
|
||||||
*
|
*
|
||||||
* @param string $scheme The scheme to use with the new instance.
|
* @param string $scheme The scheme to use with the new instance.
|
||||||
* @return static A new instance with the specified scheme.
|
* @return static A new instance with the specified scheme.
|
||||||
* @throws \InvalidArgumentException for invalid or unsupported schemes.
|
* @throws InvalidArgumentException for invalid or unsupported schemes.
|
||||||
*/
|
*/
|
||||||
public function withScheme($scheme)
|
public function withScheme($scheme)
|
||||||
{
|
{
|
||||||
$scheme = $scheme ? strtolower($scheme) : "";
|
$scheme = strtolower($scheme ?? '');
|
||||||
if (!in_array($scheme, ["", "http", "https"])) {
|
if (!in_array($scheme, ['', 'http', 'https'])) {
|
||||||
throw new \InvalidArgumentException("Scheme must be http, https, or empty.");
|
throw new InvalidArgumentException('Scheme must be http, https, or empty.');
|
||||||
}
|
}
|
||||||
$uri = clone $this;
|
$uri = clone $this;
|
||||||
$uri->scheme = $scheme;
|
$uri->scheme = $scheme;
|
||||||
|
|
@ -332,7 +319,7 @@ class Uri implements UriInterface
|
||||||
{
|
{
|
||||||
$uri = clone $this;
|
$uri = clone $this;
|
||||||
$uri->user = $user;
|
$uri->user = $user;
|
||||||
$uri->password = $password;
|
$uri->password = $password ?? '';
|
||||||
return $uri;
|
return $uri;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -346,12 +333,12 @@ class Uri implements UriInterface
|
||||||
*
|
*
|
||||||
* @param string $host The hostname to use with the new instance.
|
* @param string $host The hostname to use with the new instance.
|
||||||
* @return static A new instance with the specified host.
|
* @return static A new instance with the specified host.
|
||||||
* @throws \InvalidArgumentException for invalid hostnames.
|
* @throws InvalidArgumentException for invalid hostnames.
|
||||||
*/
|
*/
|
||||||
public function withHost($host)
|
public function withHost($host)
|
||||||
{
|
{
|
||||||
if (!is_string($host)) {
|
if (!is_string($host)) {
|
||||||
throw new \InvalidArgumentException("Host must be a string.");
|
throw new InvalidArgumentException('Host must be a string.');
|
||||||
}
|
}
|
||||||
|
|
||||||
$uri = clone $this;
|
$uri = clone $this;
|
||||||
|
|
@ -374,18 +361,18 @@ class Uri implements UriInterface
|
||||||
* @param null|int $port The port to use with the new instance; a null value
|
* @param null|int $port The port to use with the new instance; a null value
|
||||||
* removes the port information.
|
* removes the port information.
|
||||||
* @return static A new instance with the specified port.
|
* @return static A new instance with the specified port.
|
||||||
* @throws \InvalidArgumentException for invalid ports.
|
* @throws InvalidArgumentException for invalid ports.
|
||||||
*/
|
*/
|
||||||
public function withPort($port)
|
public function withPort($port)
|
||||||
{
|
{
|
||||||
if (is_numeric($port)) {
|
if (is_numeric($port)) {
|
||||||
if ($port < self::MIN_PORT || $port > self::MAX_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);
|
$message = sprintf('Port must be between %s and %s.', self::MIN_PORT, self::MAX_PORT);
|
||||||
throw new \InvalidArgumentException($message);
|
throw new InvalidArgumentException($message);
|
||||||
}
|
}
|
||||||
$port = (int) $port;
|
$port = (int) $port;
|
||||||
} elseif ($port !== null) {
|
} 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;
|
$uri = clone $this;
|
||||||
|
|
@ -408,12 +395,12 @@ class Uri implements UriInterface
|
||||||
*
|
*
|
||||||
* @param string $path The path to use with the new instance.
|
* @param string $path The path to use with the new instance.
|
||||||
* @return static A new instance with the specified path.
|
* @return static A new instance with the specified path.
|
||||||
* @throws \InvalidArgumentException for invalid paths.
|
* @throws InvalidArgumentException for invalid paths.
|
||||||
*/
|
*/
|
||||||
public function withPath($path)
|
public function withPath($path)
|
||||||
{
|
{
|
||||||
if (!is_string($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 = clone $this;
|
||||||
$uri->path = $path;
|
$uri->path = $path;
|
||||||
|
|
@ -433,7 +420,7 @@ class Uri implements UriInterface
|
||||||
*
|
*
|
||||||
* @param string $query The query string to use with the new instance.
|
* @param string $query The query string to use with the new instance.
|
||||||
* @return static A new instance with the specified query string.
|
* @return static A new instance with the specified query string.
|
||||||
* @throws \InvalidArgumentException for invalid query strings.
|
* @throws InvalidArgumentException for invalid query strings.
|
||||||
*/
|
*/
|
||||||
public function withQuery($query)
|
public function withQuery($query)
|
||||||
{
|
{
|
||||||
|
|
@ -459,7 +446,7 @@ class Uri implements UriInterface
|
||||||
public function withFragment($fragment)
|
public function withFragment($fragment)
|
||||||
{
|
{
|
||||||
$uri = clone $this;
|
$uri = clone $this;
|
||||||
$uri->fragment = $fragment;
|
$uri->fragment = $fragment ?? '';
|
||||||
return $uri;
|
return $uri;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -488,29 +475,29 @@ class Uri implements UriInterface
|
||||||
*/
|
*/
|
||||||
public function __toString()
|
public function __toString()
|
||||||
{
|
{
|
||||||
$string = "";
|
$string = '';
|
||||||
|
|
||||||
$authority = $this->getAuthority();
|
$authority = $this->getAuthority();
|
||||||
if ($authority !== "") {
|
if ($authority !== '') {
|
||||||
$scheme = $this->getScheme();
|
$scheme = $this->getScheme();
|
||||||
if ($scheme !== "") {
|
if ($scheme !== '') {
|
||||||
$string = $scheme . ":";
|
$string = $scheme . ':';
|
||||||
}
|
}
|
||||||
$string .= "//$authority";
|
$string .= "//$authority";
|
||||||
}
|
}
|
||||||
|
|
||||||
$path = $this->getPath();
|
$path = $this->getPath();
|
||||||
if ($path !== "") {
|
if ($path !== '') {
|
||||||
$string .= $path;
|
$string .= $path;
|
||||||
}
|
}
|
||||||
|
|
||||||
$query = $this->getQuery();
|
$query = $this->getQuery();
|
||||||
if ($query !== "") {
|
if ($query !== '') {
|
||||||
$string .= "?$query";
|
$string .= "?$query";
|
||||||
}
|
}
|
||||||
|
|
||||||
$fragment = $this->getFragment();
|
$fragment = $this->getFragment();
|
||||||
if ($fragment !== "") {
|
if ($fragment !== '') {
|
||||||
$string .= "#$fragment";
|
$string .= "#$fragment";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -533,12 +520,12 @@ class Uri implements UriInterface
|
||||||
* @param string $subject
|
* @param string $subject
|
||||||
* @return string
|
* @return string
|
||||||
*/
|
*/
|
||||||
private function percentEncode($subject)
|
private function percentEncode(string $subject)
|
||||||
{
|
{
|
||||||
$reserved = ':/?#[]@!$&\'()*+,;=';
|
$reserved = ':/?#[]@!$&\'()*+,;=';
|
||||||
$reserved = preg_quote($reserved);
|
$reserved = preg_quote($reserved);
|
||||||
$pattern = '~(?:%(?![a-fA-F0-9]{2}))|(?:[^%a-zA-Z0-9\-\.\_\~' . $reserved . ']{1})~';
|
$pattern = '~(?:%(?![a-fA-F0-9]{2}))|(?:[^%a-zA-Z0-9\-\.\_\~' . $reserved . ']{1})~';
|
||||||
$callback = function ($matches) {
|
$callback = function (array $matches): string {
|
||||||
return urlencode($matches[0]);
|
return urlencode($matches[0]);
|
||||||
};
|
};
|
||||||
return preg_replace_callback($pattern, $callback, $subject);
|
return preg_replace_callback($pattern, $callback, $subject);
|
||||||
|
|
|
||||||
|
|
@ -5,13 +5,19 @@ namespace WellRESTed\Routing\Route;
|
||||||
use Psr\Http\Message\ResponseInterface;
|
use Psr\Http\Message\ResponseInterface;
|
||||||
use Psr\Http\Message\ServerRequestInterface;
|
use Psr\Http\Message\ServerRequestInterface;
|
||||||
use WellRESTed\Dispatching\DispatcherInterface;
|
use WellRESTed\Dispatching\DispatcherInterface;
|
||||||
|
use WellRESTed\MiddlewareInterface;
|
||||||
|
|
||||||
class MethodMap
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
class MethodMap implements MiddlewareInterface
|
||||||
{
|
{
|
||||||
|
/** @var DispatcherInterface */
|
||||||
private $dispatcher;
|
private $dispatcher;
|
||||||
|
/** @var array */
|
||||||
private $map;
|
private $map;
|
||||||
|
|
||||||
// ------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
|
|
||||||
public function __construct(DispatcherInterface $dispatcher)
|
public function __construct(DispatcherInterface $dispatcher)
|
||||||
{
|
{
|
||||||
|
|
@ -19,11 +25,8 @@ class MethodMap
|
||||||
$this->dispatcher = $dispatcher;
|
$this->dispatcher = $dispatcher;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ------------------------------------------------------------------------
|
|
||||||
// MethodMapInterface
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Register a dispatchable (handler or middleware) with a method.
|
* Register a dispatchable (e.g.m handler or middleware) with a method.
|
||||||
*
|
*
|
||||||
* $method may be:
|
* $method may be:
|
||||||
* - A single verb ("GET"),
|
* - A single verb ("GET"),
|
||||||
|
|
@ -39,16 +42,16 @@ class MethodMap
|
||||||
* @param string $method
|
* @param string $method
|
||||||
* @param mixed $dispatchable
|
* @param mixed $dispatchable
|
||||||
*/
|
*/
|
||||||
public function register($method, $dispatchable)
|
public function register(string $method, $dispatchable): void
|
||||||
{
|
{
|
||||||
$methods = explode(",", $method);
|
$methods = explode(',', $method);
|
||||||
$methods = array_map("trim", $methods);
|
$methods = array_map('trim', $methods);
|
||||||
foreach ($methods as $method) {
|
foreach ($methods as $method) {
|
||||||
$this->map[$method] = $dispatchable;
|
$this->map[$method] = $dispatchable;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
// MiddlewareInterface
|
// MiddlewareInterface
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -69,18 +72,18 @@ class MethodMap
|
||||||
return $this->dispatchMiddleware($middleware, $request, $response, $next);
|
return $this->dispatchMiddleware($middleware, $request, $response, $next);
|
||||||
}
|
}
|
||||||
// For HEAD, dispatch GET by default.
|
// For HEAD, dispatch GET by default.
|
||||||
if ($method === "HEAD" && isset($this->map["GET"])) {
|
if ($method === 'HEAD' && isset($this->map['GET'])) {
|
||||||
$middleware = $this->map["GET"];
|
$middleware = $this->map['GET'];
|
||||||
return $this->dispatchMiddleware($middleware, $request, $response, $next);
|
return $this->dispatchMiddleware($middleware, $request, $response, $next);
|
||||||
}
|
}
|
||||||
// Dispatch * middleware, if registered.
|
// Dispatch * middleware, if registered.
|
||||||
if (isset($this->map["*"])) {
|
if (isset($this->map['*'])) {
|
||||||
$middleware = $this->map["*"];
|
$middleware = $this->map['*'];
|
||||||
return $this->dispatchMiddleware($middleware, $request, $response, $next);
|
return $this->dispatchMiddleware($middleware, $request, $response, $next);
|
||||||
}
|
}
|
||||||
// Respond describing the allowed methods, either as a 405 response or
|
// Respond describing the allowed methods, either as a 405 response or
|
||||||
// in response to an OPTIONS request.
|
// in response to an OPTIONS request.
|
||||||
if ($method === "OPTIONS") {
|
if ($method === 'OPTIONS') {
|
||||||
$response = $response->withStatus(200);
|
$response = $response->withStatus(200);
|
||||||
} else {
|
} else {
|
||||||
$response = $response->withStatus(405);
|
$response = $response->withStatus(405);
|
||||||
|
|
@ -88,39 +91,42 @@ class MethodMap
|
||||||
return $this->addAllowHeader($response);
|
return $this->addAllowHeader($response);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
|
|
||||||
private function addAllowHeader(ResponseInterface $response)
|
private function addAllowHeader(ResponseInterface $response): ResponseInterface
|
||||||
{
|
{
|
||||||
$methods = join(",", $this->getAllowedMethods());
|
$methods = join(',', $this->getAllowedMethods());
|
||||||
return $response->withHeader("Allow", $methods);
|
return $response->withHeader('Allow', $methods);
|
||||||
}
|
}
|
||||||
|
|
||||||
private function getAllowedMethods()
|
/**
|
||||||
|
* @return string[]
|
||||||
|
*/
|
||||||
|
private function getAllowedMethods(): array
|
||||||
{
|
{
|
||||||
$methods = array_keys($this->map);
|
$methods = array_keys($this->map);
|
||||||
// Add HEAD if GET is allowed and HEAD is not present.
|
// Add HEAD if GET is allowed and HEAD is not present.
|
||||||
if (in_array("GET", $methods) && !in_array("HEAD", $methods)) {
|
if (in_array('GET', $methods) && !in_array('HEAD', $methods)) {
|
||||||
$methods[] = "HEAD";
|
$methods[] = 'HEAD';
|
||||||
}
|
}
|
||||||
// Add OPTIONS if not already present.
|
// Add OPTIONS if not already present.
|
||||||
if (!in_array("OPTIONS", $methods)) {
|
if (!in_array('OPTIONS', $methods)) {
|
||||||
$methods[] = "OPTIONS";
|
$methods[] = 'OPTIONS';
|
||||||
}
|
}
|
||||||
return $methods;
|
return $methods;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param $middleware
|
* @param mixed $middleware
|
||||||
* @param ServerRequestInterface $request
|
* @param ServerRequestInterface $request
|
||||||
* @param ResponseInterface $response
|
* @param ResponseInterface $response
|
||||||
* @param $next
|
* @param callable $next
|
||||||
* @return ResponseInterface
|
* @return ResponseInterface
|
||||||
*/
|
*/
|
||||||
private function dispatchMiddleware(
|
private function dispatchMiddleware(
|
||||||
$middleware,
|
$middleware,
|
||||||
ServerRequestInterface $request,
|
ServerRequestInterface $request,
|
||||||
ResponseInterface &$response,
|
ResponseInterface $response,
|
||||||
$next
|
$next
|
||||||
) {
|
) {
|
||||||
return $this->dispatcher->dispatch($middleware, $request, $response, $next);
|
return $this->dispatcher->dispatch($middleware, $request, $response, $next);
|
||||||
|
|
|
||||||
|
|
@ -2,25 +2,28 @@
|
||||||
|
|
||||||
namespace WellRESTed\Routing\Route;
|
namespace WellRESTed\Routing\Route;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
class PrefixRoute extends Route
|
class PrefixRoute extends Route
|
||||||
{
|
{
|
||||||
public function __construct($target, $methodMap)
|
public function __construct(string $target, MethodMap $methodMap)
|
||||||
{
|
{
|
||||||
parent::__construct(rtrim($target, "*"), $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.
|
* Examines a request target to see if it is a match for the route.
|
||||||
*
|
*
|
||||||
* @param string $requestTarget
|
* @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;
|
return strrpos($requestTarget, $this->target, -strlen($requestTarget)) !== false;
|
||||||
}
|
}
|
||||||
|
|
@ -28,7 +31,7 @@ class PrefixRoute extends Route
|
||||||
/**
|
/**
|
||||||
* Always returns an empty array.
|
* Always returns an empty array.
|
||||||
*/
|
*/
|
||||||
public function getPathVariables()
|
public function getPathVariables(): array
|
||||||
{
|
{
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,22 +2,28 @@
|
||||||
|
|
||||||
namespace WellRESTed\Routing\Route;
|
namespace WellRESTed\Routing\Route;
|
||||||
|
|
||||||
|
use RuntimeException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
class RegexRoute extends Route
|
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.
|
* Examines a request target to see if it is a match for the route.
|
||||||
*
|
*
|
||||||
* @param string $requestTarget
|
* @param string $requestTarget
|
||||||
* @return boolean
|
* @return bool
|
||||||
*/
|
*/
|
||||||
public function matchesRequestTarget($requestTarget)
|
public function matchesRequestTarget(string $requestTarget): bool
|
||||||
{
|
{
|
||||||
$this->captures = [];
|
$this->captures = [];
|
||||||
$matched = preg_match($this->getTarget(), $requestTarget, $captures);
|
$matched = preg_match($this->getTarget(), $requestTarget, $captures);
|
||||||
|
|
@ -25,7 +31,7 @@ class RegexRoute extends Route
|
||||||
$this->captures = $captures;
|
$this->captures = $captures;
|
||||||
return true;
|
return true;
|
||||||
} elseif ($matched === false) {
|
} elseif ($matched === false) {
|
||||||
throw new \RuntimeException("Invalid regular expression: " . $this->getTarget());
|
throw new RuntimeException('Invalid regular expression: ' . $this->getTarget());
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
@ -36,7 +42,7 @@ class RegexRoute extends Route
|
||||||
* @see \preg_match
|
* @see \preg_match
|
||||||
* @return array
|
* @return array
|
||||||
*/
|
*/
|
||||||
public function getPathVariables()
|
public function getPathVariables(): array
|
||||||
{
|
{
|
||||||
return $this->captures;
|
return $this->captures;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,24 +4,77 @@ namespace WellRESTed\Routing\Route;
|
||||||
|
|
||||||
use Psr\Http\Message\ResponseInterface;
|
use Psr\Http\Message\ResponseInterface;
|
||||||
use Psr\Http\Message\ServerRequestInterface;
|
use Psr\Http\Message\ServerRequestInterface;
|
||||||
|
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 */
|
/** @var string */
|
||||||
protected $target;
|
protected $target;
|
||||||
/** @var MethodMap */
|
/** @var MethodMap */
|
||||||
protected $methodMap;
|
protected $methodMap;
|
||||||
|
|
||||||
public function __construct($target, $methodMap)
|
public function __construct(string $target, MethodMap $methodMap)
|
||||||
{
|
{
|
||||||
$this->target = $target;
|
$this->target = $target;
|
||||||
$this->methodMap = $methodMap;
|
$this->methodMap = $methodMap;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* Return the Route::TYPE_ constants that identifies the type.
|
||||||
|
*
|
||||||
|
* TYPE_STATIC indicates the route MUST match only when the path is an
|
||||||
|
* exact match to the route's entire target. This route type SHOULD NOT
|
||||||
|
* provide path variables.
|
||||||
|
*
|
||||||
|
* TYPE_PREFIX indicates the route MUST match when the route's target
|
||||||
|
* appears in its entirety at the beginning of the path.
|
||||||
|
*
|
||||||
|
* TYPE_PATTERN indicates that matchesRequestTarget MUST be used
|
||||||
|
* to determine a match against a given path. This route type SHOULD
|
||||||
|
* provide path variables.
|
||||||
|
*
|
||||||
|
* @return int One of the Route::TYPE_ constants.
|
||||||
|
*/
|
||||||
|
abstract public function getType(): int;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return an array of variables extracted from the path most recently
|
||||||
|
* passed to matchesRequestTarget.
|
||||||
|
*
|
||||||
|
* If the path does not contain variables, or if matchesRequestTarget
|
||||||
|
* has not yet been called, this method MUST return an empty array.
|
||||||
|
*
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
abstract public function getPathVariables(): array;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Examines a request target to see if it is a match for the route.
|
||||||
|
*
|
||||||
|
* @param string $requestTarget
|
||||||
|
* @return bool
|
||||||
|
* @throws RuntimeException Error occurred testing the target such as an
|
||||||
|
* invalid regular expression
|
||||||
|
*/
|
||||||
|
abstract public function matchesRequestTarget(string $requestTarget): bool;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Path, partial path, or pattern to match request paths against.
|
||||||
|
*
|
||||||
* @return string
|
* @return string
|
||||||
*/
|
*/
|
||||||
public function getTarget()
|
public function getTarget(): string
|
||||||
{
|
{
|
||||||
return $this->target;
|
return $this->target;
|
||||||
}
|
}
|
||||||
|
|
@ -40,11 +93,17 @@ abstract class Route implements RouteInterface
|
||||||
* @param string $method
|
* @param string $method
|
||||||
* @param mixed $dispatchable
|
* @param mixed $dispatchable
|
||||||
*/
|
*/
|
||||||
public function register($method, $dispatchable)
|
public function register(string $method, $dispatchable): void
|
||||||
{
|
{
|
||||||
$this->methodMap->register($method, $dispatchable);
|
$this->methodMap->register($method, $dispatchable);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @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)
|
||||||
{
|
{
|
||||||
$map = $this->methodMap;
|
$map = $this->methodMap;
|
||||||
|
|
|
||||||
|
|
@ -5,9 +5,9 @@ namespace WellRESTed\Routing\Route;
|
||||||
use WellRESTed\Dispatching\DispatcherInterface;
|
use WellRESTed\Dispatching\DispatcherInterface;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class for creating routes
|
* @internal
|
||||||
*/
|
*/
|
||||||
class RouteFactory implements RouteFactoryInterface
|
class RouteFactory
|
||||||
{
|
{
|
||||||
private $dispatcher;
|
private $dispatcher;
|
||||||
|
|
||||||
|
|
@ -25,16 +25,16 @@ class RouteFactory implements RouteFactoryInterface
|
||||||
* - Regular expressions will create RegexRoutes
|
* - Regular expressions will create RegexRoutes
|
||||||
*
|
*
|
||||||
* @param string $target Route target or target pattern
|
* @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
|
// Possible static, prefix, or template
|
||||||
|
|
||||||
// PrefixRoutes end with *
|
// PrefixRoutes end with *
|
||||||
if (substr($target, -1) === "*") {
|
if (substr($target, -1) === '*') {
|
||||||
return new PrefixRoute($target, new MethodMap($this->dispatcher));
|
return new PrefixRoute($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 expressions will create RegexRoutes
|
|
||||||
*
|
|
||||||
* @param string $target Route target or target pattern
|
|
||||||
* @return RouteInterface
|
|
||||||
*/
|
|
||||||
public function create($target);
|
|
||||||
}
|
|
||||||
|
|
@ -1,75 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace WellRESTed\Routing\Route;
|
|
||||||
|
|
||||||
use WellRESTed\MiddlewareInterface;
|
|
||||||
|
|
||||||
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_ constants 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();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Examines a request target to see if it is a match for the route.
|
|
||||||
*
|
|
||||||
* @param string $requestTarget
|
|
||||||
* @return boolean
|
|
||||||
* @throw \RuntimeException Error occurred testing the target such as an
|
|
||||||
* invalid regular expression
|
|
||||||
*/
|
|
||||||
public function matchesRequestTarget($requestTarget);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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($method, $dispatchable);
|
|
||||||
}
|
|
||||||
|
|
@ -2,20 +2,23 @@
|
||||||
|
|
||||||
namespace WellRESTed\Routing\Route;
|
namespace WellRESTed\Routing\Route;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
class StaticRoute extends Route
|
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.
|
* Examines a request target to see if it is a match for the route.
|
||||||
*
|
*
|
||||||
* @param string $requestTarget
|
* @param string $requestTarget
|
||||||
* @return boolean
|
* @return bool
|
||||||
*/
|
*/
|
||||||
public function matchesRequestTarget($requestTarget)
|
public function matchesRequestTarget(string $requestTarget): bool
|
||||||
{
|
{
|
||||||
return $requestTarget === $this->getTarget();
|
return $requestTarget === $this->getTarget();
|
||||||
}
|
}
|
||||||
|
|
@ -23,7 +26,7 @@ class StaticRoute extends Route
|
||||||
/**
|
/**
|
||||||
* Always returns an empty array.
|
* Always returns an empty array.
|
||||||
*/
|
*/
|
||||||
public function getPathVariables()
|
public function getPathVariables(): array
|
||||||
{
|
{
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,36 +2,41 @@
|
||||||
|
|
||||||
namespace WellRESTed\Routing\Route;
|
namespace WellRESTed\Routing\Route;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
class TemplateRoute extends Route
|
class TemplateRoute extends Route
|
||||||
{
|
{
|
||||||
private $pathVariables;
|
/** Regular expression matching a URI template variable (e.g., {id}) */
|
||||||
private $explosions;
|
public const URI_TEMPLATE_EXPRESSION_RE = '/{([+.\/]?[a-zA-Z0-9_,]+\*?)}/';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Regular expression matching 1 or more unreserved characters.
|
* Regular expression matching 1 or more unreserved characters.
|
||||||
* ALPHA / DIGIT / "-" / "." / "_" / "~"
|
* ALPHA / DIGIT / "-" / "." / "_" / "~"
|
||||||
*/
|
*/
|
||||||
const RE_UNRESERVED = '[0-9a-zA-Z\-._\~%]*';
|
private 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_,]+\*?)}/';
|
|
||||||
|
|
||||||
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.
|
* Examines a request target to see if it is a match for the route.
|
||||||
*
|
*
|
||||||
* @param string $requestTarget
|
* @param string $requestTarget
|
||||||
* @return boolean
|
* @return bool
|
||||||
*/
|
*/
|
||||||
public function matchesRequestTarget($requestTarget)
|
public function matchesRequestTarget(string $requestTarget): bool
|
||||||
{
|
{
|
||||||
$this->pathVariables = [];
|
$this->pathVariables = [];
|
||||||
$this->explosions = [];
|
$this->explosions = [];
|
||||||
|
|
@ -49,54 +54,55 @@ class TemplateRoute extends Route
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private function matchesStartOfRequestTarget($requestTarget)
|
private function matchesStartOfRequestTarget(string $requestTarget): bool
|
||||||
{
|
{
|
||||||
$firstVarPos = strpos($this->target, "{");
|
$firstVarPos = strpos($this->target, '{');
|
||||||
return (substr($requestTarget, 0, $firstVarPos) === substr($this->target, 0, $firstVarPos));
|
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 = [];
|
$variables = [];
|
||||||
|
|
||||||
// Isolate the named captures.
|
// 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.
|
// Store named captures to the variables.
|
||||||
foreach ($keys as $key) {
|
foreach ($keys as $key) {
|
||||||
|
|
||||||
$value = $matches[$key];
|
$value = $matches[$key];
|
||||||
|
|
||||||
if (isset($this->explosions[$key])) {
|
if (isset($this->explosions[$key])) {
|
||||||
$values = explode($this->explosions[$key], $value);
|
$values = explode($this->explosions[$key], $value);
|
||||||
$variables[$key] = array_map("urldecode", $values);
|
$variables[$key] = array_map('urldecode', $values);
|
||||||
} else {
|
} else {
|
||||||
$value = urldecode($value);
|
$value = urldecode($value);
|
||||||
$variables[$key] = $value;
|
$variables[$key] = $value;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return $variables;
|
return $variables;
|
||||||
}
|
}
|
||||||
|
|
||||||
private function getMatchingPattern()
|
private function getMatchingPattern(): string
|
||||||
{
|
{
|
||||||
// Convert the template into the pattern
|
// Convert the template into the pattern
|
||||||
$pattern = $this->target;
|
$pattern = $this->target;
|
||||||
|
|
||||||
// Escape allowable characters with regex meaning.
|
// Escape allowable characters with regex meaning.
|
||||||
$escape = [
|
$escape = [
|
||||||
"." => "\\.",
|
'.' => '\\.',
|
||||||
"-" => "\\-",
|
'-' => '\\-',
|
||||||
"+" => "\\+",
|
'+' => '\\+',
|
||||||
"*" => "\\*"
|
'*' => '\\*'
|
||||||
];
|
];
|
||||||
$pattern = str_replace(array_keys($escape), array_values($escape), $pattern);
|
$pattern = str_replace(array_keys($escape), array_values($escape), $pattern);
|
||||||
$unescape = [
|
$unescape = [
|
||||||
"{\\+" => "{+",
|
'{\\+' => '{+',
|
||||||
"{\\." => "{.",
|
'{\\.' => '{.',
|
||||||
"\\*}" => "*}"
|
'\\*}' => '*}'
|
||||||
];
|
];
|
||||||
$pattern = str_replace(array_keys($unescape), array_values($unescape), $pattern);
|
$pattern = str_replace(array_keys($unescape), array_values($unescape), $pattern);
|
||||||
|
|
||||||
|
|
@ -105,47 +111,47 @@ class TemplateRoute extends Route
|
||||||
|
|
||||||
$pattern = preg_replace_callback(
|
$pattern = preg_replace_callback(
|
||||||
self::URI_TEMPLATE_EXPRESSION_RE,
|
self::URI_TEMPLATE_EXPRESSION_RE,
|
||||||
[$this, "uriVariableReplacementCallback"],
|
[$this, 'uriVariableReplacementCallback'],
|
||||||
$pattern
|
$pattern
|
||||||
);
|
);
|
||||||
|
|
||||||
return $pattern;
|
return $pattern;
|
||||||
}
|
}
|
||||||
|
|
||||||
private function uriVariableReplacementCallback($matches)
|
private function uriVariableReplacementCallback(array $matches): string
|
||||||
{
|
{
|
||||||
$name = $matches[1];
|
$name = $matches[1];
|
||||||
$pattern = self::RE_UNRESERVED;
|
$pattern = self::RE_UNRESERVED;
|
||||||
|
|
||||||
$prefix = "";
|
$prefix = '';
|
||||||
$delimiter = ",";
|
$delimiter = ',';
|
||||||
$explodeDelimiter = ",";
|
$explodeDelimiter = ',';
|
||||||
|
|
||||||
// Read the first character as an operator. This determines which
|
// Read the first character as an operator. This determines which
|
||||||
// characters to allow in the match.
|
// characters to allow in the match.
|
||||||
$operator = $name[0];
|
$operator = $name[0];
|
||||||
|
|
||||||
// Read the last character as the modifier.
|
// Read the last character as the modifier.
|
||||||
$explosion = (substr($name, -1, 1) === "*");
|
$explosion = (substr($name, -1, 1) === '*');
|
||||||
|
|
||||||
switch ($operator) {
|
switch ($operator) {
|
||||||
case "+":
|
case '+':
|
||||||
$name = substr($name, 1);
|
$name = substr($name, 1);
|
||||||
$pattern = ".*";
|
$pattern = '.*';
|
||||||
break;
|
break;
|
||||||
case ".":
|
case '.':
|
||||||
$name = substr($name, 1);
|
$name = substr($name, 1);
|
||||||
$prefix = "\\.";
|
$prefix = '\\.';
|
||||||
$delimiter = "\\.";
|
$delimiter = '\\.';
|
||||||
$explodeDelimiter = ".";
|
$explodeDelimiter = '.';
|
||||||
break;
|
break;
|
||||||
case "/":
|
case '/':
|
||||||
$name = substr($name, 1);
|
$name = substr($name, 1);
|
||||||
$prefix = "\\/";
|
$prefix = '\\/';
|
||||||
$delimiter = "\\/";
|
$delimiter = '\\/';
|
||||||
if ($explosion) {
|
if ($explosion) {
|
||||||
$pattern = '[0-9a-zA-Z\-._\~%,\/]*'; // Unreserved + "," and "/"
|
$pattern = '[0-9a-zA-Z\-._\~%,\/]*'; // Unreserved + "," and "/"
|
||||||
$explodeDelimiter = "/";
|
$explodeDelimiter = '/';
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
@ -159,7 +165,7 @@ class TemplateRoute extends Route
|
||||||
$this->explosions[$name] = $explodeDelimiter;
|
$this->explosions[$name] = $explodeDelimiter;
|
||||||
}
|
}
|
||||||
|
|
||||||
$names = explode(",", $name);
|
$names = explode(',', $name);
|
||||||
$results = [];
|
$results = [];
|
||||||
foreach ($names as $name) {
|
foreach ($names as $name) {
|
||||||
$results[] = "(?<{$name}>{$pattern})";
|
$results[] = "(?<{$name}>{$pattern})";
|
||||||
|
|
|
||||||
|
|
@ -6,25 +6,25 @@ use Psr\Http\Message\ResponseInterface;
|
||||||
use Psr\Http\Message\ServerRequestInterface;
|
use Psr\Http\Message\ServerRequestInterface;
|
||||||
use WellRESTed\Dispatching\Dispatcher;
|
use WellRESTed\Dispatching\Dispatcher;
|
||||||
use WellRESTed\Dispatching\DispatcherInterface;
|
use WellRESTed\Dispatching\DispatcherInterface;
|
||||||
|
use WellRESTed\MiddlewareInterface;
|
||||||
|
use WellRESTed\Routing\Route\Route;
|
||||||
use WellRESTed\Routing\Route\RouteFactory;
|
use WellRESTed\Routing\Route\RouteFactory;
|
||||||
use WellRESTed\Routing\Route\RouteFactoryInterface;
|
|
||||||
use WellRESTed\Routing\Route\RouteInterface;
|
|
||||||
|
|
||||||
class Router
|
class Router implements MiddlewareInterface
|
||||||
{
|
{
|
||||||
/** @var string attribute name for matched path variables */
|
/** @var string|null Attribute name for matched path variables */
|
||||||
private $pathVariablesAttributeName;
|
private $pathVariablesAttributeName;
|
||||||
/** @var DispatcherInterface */
|
/** @var DispatcherInterface */
|
||||||
private $dispatcher;
|
private $dispatcher;
|
||||||
/** @var RouteFactoryInterface */
|
/** @var RouteFactory */
|
||||||
private $factory;
|
private $factory;
|
||||||
/** @var RouteInterface[] Array of Route objects */
|
/** @var Route[] Array of Route objects */
|
||||||
private $routes;
|
private $routes;
|
||||||
/** @var RouteInterface[] Hash array mapping exact paths to routes */
|
/** @var Route[] Hash array mapping exact paths to routes */
|
||||||
private $staticRoutes;
|
private $staticRoutes;
|
||||||
/** @var RouteInterface[] Hash array mapping path prefixes to routes */
|
/** @var Route[] Hash array mapping path prefixes to routes */
|
||||||
private $prefixRoutes;
|
private $prefixRoutes;
|
||||||
/** @var RouteInterface[] Hash array mapping path prefixes to routes */
|
/** @var Route[] Hash array mapping path prefixes to routes */
|
||||||
private $patternRoutes;
|
private $patternRoutes;
|
||||||
/** @var mixed[] List array of middleware */
|
/** @var mixed[] List array of middleware */
|
||||||
private $stack;
|
private $stack;
|
||||||
|
|
@ -42,17 +42,24 @@ class Router
|
||||||
* stored with the name. The value will be an array containing all of the
|
* stored with the name. The value will be an array containing all of the
|
||||||
* path variables.
|
* path variables.
|
||||||
*
|
*
|
||||||
* @param DispatcherInterface $dispatcher
|
* Use Server->createRouter to instantiate a new Router rather than calling
|
||||||
* Instance to use for dispatching middleware and handlers.
|
* this constructor manually.
|
||||||
|
*
|
||||||
* @param string|null $pathVariablesAttributeName
|
* @param string|null $pathVariablesAttributeName
|
||||||
* Attribute name for matched path variables. A null value sets
|
* Attribute name for matched path variables. A null value sets
|
||||||
* attributes directly.
|
* attributes directly.
|
||||||
|
* @param DispatcherInterface|null $dispatcher
|
||||||
|
* Instance to use for dispatching middleware and handlers.
|
||||||
|
* @param RouteFactory|null $routeFactory
|
||||||
*/
|
*/
|
||||||
public function __construct($dispatcher = null, $pathVariablesAttributeName = null)
|
public function __construct(
|
||||||
{
|
?string $pathVariablesAttributeName = null,
|
||||||
$this->dispatcher = $dispatcher ?: $this->getDefaultDispatcher();
|
?DispatcherInterface $dispatcher = null,
|
||||||
|
?RouteFactory $routeFactory = null
|
||||||
|
) {
|
||||||
$this->pathVariablesAttributeName = $pathVariablesAttributeName;
|
$this->pathVariablesAttributeName = $pathVariablesAttributeName;
|
||||||
$this->factory = $this->getRouteFactory($this->dispatcher);
|
$this->dispatcher = $dispatcher ?? new Dispatcher();
|
||||||
|
$this->factory = $routeFactory ?? new RouteFactory($this->dispatcher);
|
||||||
$this->routes = [];
|
$this->routes = [];
|
||||||
$this->staticRoutes = [];
|
$this->staticRoutes = [];
|
||||||
$this->prefixRoutes = [];
|
$this->prefixRoutes = [];
|
||||||
|
|
@ -60,27 +67,32 @@ class Router
|
||||||
$this->stack = [];
|
$this->stack = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param ServerRequestInterface $request
|
||||||
|
* @param ResponseInterface $response
|
||||||
|
* @param callable $next
|
||||||
|
* @return ResponseInterface
|
||||||
|
*/
|
||||||
public function __invoke(
|
public function __invoke(
|
||||||
ServerRequestInterface $request,
|
ServerRequestInterface $request,
|
||||||
ResponseInterface $response,
|
ResponseInterface $response,
|
||||||
$next
|
$next
|
||||||
) {
|
): ResponseInterface {
|
||||||
// Use only the path for routing.
|
$path = $this->getPath($request->getRequestTarget());
|
||||||
$requestTarget = parse_url($request->getRequestTarget(), PHP_URL_PATH);
|
|
||||||
|
|
||||||
$route = $this->getStaticRoute($requestTarget);
|
$route = $this->getStaticRoute($path);
|
||||||
if ($route) {
|
if ($route) {
|
||||||
return $this->dispatch($route, $request, $response, $next);
|
return $this->dispatch($route, $request, $response, $next);
|
||||||
}
|
}
|
||||||
|
|
||||||
$route = $this->getPrefixRoute($requestTarget);
|
$route = $this->getPrefixRoute($path);
|
||||||
if ($route) {
|
if ($route) {
|
||||||
return $this->dispatch($route, $request, $response, $next);
|
return $this->dispatch($route, $request, $response, $next);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try each of the routes.
|
// Try each of the routes.
|
||||||
foreach ($this->patternRoutes as $route) {
|
foreach ($this->patternRoutes as $route) {
|
||||||
if ($route->matchesRequestTarget($requestTarget)) {
|
if ($route->matchesRequestTarget($path)) {
|
||||||
$pathVariables = $route->getPathVariables();
|
$pathVariables = $route->getPathVariables();
|
||||||
if ($this->pathVariablesAttributeName) {
|
if ($this->pathVariablesAttributeName) {
|
||||||
$request = $request->withAttribute($this->pathVariablesAttributeName, $pathVariables);
|
$request = $request->withAttribute($this->pathVariablesAttributeName, $pathVariables);
|
||||||
|
|
@ -100,18 +112,30 @@ class Router
|
||||||
return $next($request, $response);
|
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(
|
private function dispatch(
|
||||||
$route,
|
callable $route,
|
||||||
ServerRequestInterface $request,
|
ServerRequestInterface $request,
|
||||||
ResponseInterface $response,
|
ResponseInterface $response,
|
||||||
$next
|
callable $next
|
||||||
) {
|
): ResponseInterface {
|
||||||
if (!$this->stack) {
|
if (!$this->stack) {
|
||||||
return $route($request, $response, $next);
|
return $route($request, $response, $next);
|
||||||
}
|
}
|
||||||
$stack = array_merge($this->stack, [$route]);
|
$stack = array_merge($this->stack, [$route]);
|
||||||
return $this->dispatcher->dispatch(
|
return $this->dispatcher->dispatch(
|
||||||
$stack, $request, $response, $next
|
$stack,
|
||||||
|
$request,
|
||||||
|
$response,
|
||||||
|
$next
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -145,12 +169,12 @@ class Router
|
||||||
* - An array containing any of the items in this list.
|
* - An array containing any of the items in this list.
|
||||||
* @see DispatchedInterface::dispatch
|
* @see DispatchedInterface::dispatch
|
||||||
*
|
*
|
||||||
* @param string $target Request target or pattern to match
|
|
||||||
* @param string $method HTTP method(s) to match
|
* @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
|
* @param mixed $dispatchable Handler or middleware to dispatch
|
||||||
* @return static
|
* @return static
|
||||||
*/
|
*/
|
||||||
public function register($method, $target, $dispatchable)
|
public function register(string $method, string $target, $dispatchable): Router
|
||||||
{
|
{
|
||||||
$route = $this->getRouteForTarget($target);
|
$route = $this->getRouteForTarget($target);
|
||||||
$route->register($method, $dispatchable);
|
$route->register($method, $dispatchable);
|
||||||
|
|
@ -174,7 +198,7 @@ class Router
|
||||||
* @param mixed $middleware Middleware to dispatch in sequence
|
* @param mixed $middleware Middleware to dispatch in sequence
|
||||||
* @return static
|
* @return static
|
||||||
*/
|
*/
|
||||||
public function add($middleware)
|
public function add($middleware): Router
|
||||||
{
|
{
|
||||||
$this->stack[] = $middleware;
|
$this->stack[] = $middleware;
|
||||||
return $this;
|
return $this;
|
||||||
|
|
@ -186,38 +210,13 @@ class Router
|
||||||
*
|
*
|
||||||
* @return static
|
* @return static
|
||||||
*/
|
*/
|
||||||
public function continueOnNotFound()
|
public function continueOnNotFound(): Router
|
||||||
{
|
{
|
||||||
$this->continueOnNotFound = true;
|
$this->continueOnNotFound = true;
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
private function getRouteForTarget(string $target): Route
|
||||||
* Return an instance to dispatch middleware.
|
|
||||||
*
|
|
||||||
* @return DispatcherInterface
|
|
||||||
*/
|
|
||||||
protected function getDefaultDispatcher()
|
|
||||||
{
|
|
||||||
return new Dispatcher();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param DispatcherInterface
|
|
||||||
* @return RouteFactoryInterface
|
|
||||||
*/
|
|
||||||
protected function getRouteFactory($dispatcher)
|
|
||||||
{
|
|
||||||
return new RouteFactory($dispatcher);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return the route for a given target.
|
|
||||||
*
|
|
||||||
* @param $target
|
|
||||||
* @return RouteInterface
|
|
||||||
*/
|
|
||||||
private function getRouteForTarget($target)
|
|
||||||
{
|
{
|
||||||
if (isset($this->routes[$target])) {
|
if (isset($this->routes[$target])) {
|
||||||
$route = $this->routes[$target];
|
$route = $this->routes[$target];
|
||||||
|
|
@ -228,26 +227,26 @@ class Router
|
||||||
return $route;
|
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.
|
// Store the route to the hash indexed by original target.
|
||||||
$this->routes[$target] = $route;
|
$this->routes[$target] = $route;
|
||||||
|
|
||||||
// Store the route to the array of routes for its type.
|
// Store the route to the array of routes for its type.
|
||||||
switch ($route->getType()) {
|
switch ($route->getType()) {
|
||||||
case RouteInterface::TYPE_STATIC:
|
case Route::TYPE_STATIC:
|
||||||
$this->staticRoutes[$route->getTarget()] = $route;
|
$this->staticRoutes[$route->getTarget()] = $route;
|
||||||
break;
|
break;
|
||||||
case RouteInterface::TYPE_PREFIX:
|
case Route::TYPE_PREFIX:
|
||||||
$this->prefixRoutes[rtrim($route->getTarget(), "*")] = $route;
|
$this->prefixRoutes[rtrim($route->getTarget(), '*')] = $route;
|
||||||
break;
|
break;
|
||||||
case RouteInterface::TYPE_PATTERN:
|
case Route::TYPE_PATTERN:
|
||||||
$this->patternRoutes[] = $route;
|
$this->patternRoutes[] = $route;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private function getStaticRoute($requestTarget)
|
private function getStaticRoute(string $requestTarget): ?Route
|
||||||
{
|
{
|
||||||
if (isset($this->staticRoutes[$requestTarget])) {
|
if (isset($this->staticRoutes[$requestTarget])) {
|
||||||
return $this->staticRoutes[$requestTarget];
|
return $this->staticRoutes[$requestTarget];
|
||||||
|
|
@ -255,7 +254,7 @@ class Router
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private function getPrefixRoute($requestTarget)
|
private function getPrefixRoute(string $requestTarget): ?Route
|
||||||
{
|
{
|
||||||
// Find all prefixes that match the start of this path.
|
// Find all prefixes that match the start of this path.
|
||||||
$prefixes = array_keys($this->prefixRoutes);
|
$prefixes = array_keys($this->prefixRoutes);
|
||||||
|
|
@ -266,26 +265,26 @@ class Router
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
if ($matches) {
|
if (!$matches) {
|
||||||
if (count($matches) > 0) {
|
return null;
|
||||||
// If there are multiple matches, sort them to find the one with
|
}
|
||||||
// the longest string length.
|
|
||||||
$compareByLength = function ($a, $b) {
|
// 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);
|
return strlen($b) - strlen($a);
|
||||||
};
|
};
|
||||||
usort($matches, $compareByLength);
|
usort($matches, $compareByLength);
|
||||||
}
|
}
|
||||||
/** @var string $bestMatch */
|
|
||||||
$bestMatch = $matches[0];
|
$bestMatch = array_values($matches)[0];
|
||||||
$route = $this->prefixRoutes[$bestMatch];
|
return $this->prefixRoutes[$bestMatch];
|
||||||
return $route;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private function startsWith($haystack, $needle)
|
private function startsWith(string $haystack, string $needle): bool
|
||||||
{
|
{
|
||||||
$length = strlen($needle);
|
$length = strlen($needle);
|
||||||
return (substr($haystack, 0, $length) === $needle);
|
return substr($haystack, 0, $length) === $needle;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,21 +7,21 @@ use Psr\Http\Message\ServerRequestInterface;
|
||||||
use WellRESTed\Dispatching\Dispatcher;
|
use WellRESTed\Dispatching\Dispatcher;
|
||||||
use WellRESTed\Dispatching\DispatcherInterface;
|
use WellRESTed\Dispatching\DispatcherInterface;
|
||||||
use WellRESTed\Message\Response;
|
use WellRESTed\Message\Response;
|
||||||
use WellRESTed\Message\ServerRequest;
|
use WellRESTed\Message\ServerRequestMarshaller;
|
||||||
use WellRESTed\Routing\Router;
|
use WellRESTed\Routing\Router;
|
||||||
use WellRESTed\Transmission\Transmitter;
|
use WellRESTed\Transmission\Transmitter;
|
||||||
use WellRESTed\Transmission\TransmitterInterface;
|
use WellRESTed\Transmission\TransmitterInterface;
|
||||||
|
|
||||||
class Server
|
class Server
|
||||||
{
|
{
|
||||||
/** @var array */
|
/** @var mixed[] */
|
||||||
protected $attributes;
|
private $attributes = [];
|
||||||
/** @var DispatcherInterface */
|
/** @var DispatcherInterface */
|
||||||
private $dispatcher;
|
private $dispatcher;
|
||||||
/** @var string ServerRequestInterface attribute name for matched path variables */
|
/** @var string|null attribute name for matched path variables */
|
||||||
private $pathVariablesAttributeName;
|
private $pathVariablesAttributeName = null;
|
||||||
/** @var ServerRequestInterface */
|
/** @var ServerRequestInterface|null */
|
||||||
private $request;
|
private $request = null;
|
||||||
/** @var ResponseInterface */
|
/** @var ResponseInterface */
|
||||||
private $response;
|
private $response;
|
||||||
/** @var TransmitterInterface */
|
/** @var TransmitterInterface */
|
||||||
|
|
@ -29,8 +29,12 @@ class Server
|
||||||
/** @var mixed[] List array of middleware */
|
/** @var mixed[] List array of middleware */
|
||||||
private $stack;
|
private $stack;
|
||||||
|
|
||||||
public function __construct() {
|
public function __construct()
|
||||||
|
{
|
||||||
$this->stack = [];
|
$this->stack = [];
|
||||||
|
$this->response = new Response();
|
||||||
|
$this->dispatcher = new Dispatcher();
|
||||||
|
$this->transmitter = new Transmitter();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -39,22 +43,22 @@ class Server
|
||||||
* @param mixed $middleware Middleware to dispatch in sequence
|
* @param mixed $middleware Middleware to dispatch in sequence
|
||||||
* @return Server
|
* @return Server
|
||||||
*/
|
*/
|
||||||
public function add($middleware)
|
public function add($middleware): Server
|
||||||
{
|
{
|
||||||
$this->stack[] = $middleware;
|
$this->stack[] = $middleware;
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return a new Router that uses the server's dispatcher.
|
* Return a new Router that uses the server's configuration.
|
||||||
*
|
*
|
||||||
* @return Router
|
* @return Router
|
||||||
*/
|
*/
|
||||||
public function createRouter()
|
public function createRouter(): Router
|
||||||
{
|
{
|
||||||
return new Router(
|
return new Router(
|
||||||
$this->getDispatcher(),
|
$this->pathVariablesAttributeName,
|
||||||
$this->pathVariablesAttributeName
|
$this->dispatcher
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -64,25 +68,30 @@ class Server
|
||||||
* This method reads a server request, dispatches the request through the
|
* This method reads a server request, dispatches the request through the
|
||||||
* server's stack of middleware, and outputs the response via a Transmitter.
|
* server's stack of middleware, and outputs the response via a Transmitter.
|
||||||
*/
|
*/
|
||||||
public function respond()
|
public function respond(): void
|
||||||
{
|
{
|
||||||
$request = $this->getRequest();
|
$request = $this->getRequest();
|
||||||
foreach ($this->getAttributes() as $name => $value) {
|
foreach ($this->attributes as $name => $value) {
|
||||||
$request = $request->withAttribute($name, $value);
|
$request = $request->withAttribute($name, $value);
|
||||||
}
|
}
|
||||||
|
|
||||||
$response = $this->getResponse();
|
$next = function (
|
||||||
|
ServerRequestInterface $rqst,
|
||||||
$next = function ($rqst, $resp) {
|
ResponseInterface $resp
|
||||||
|
): ResponseInterface {
|
||||||
return $resp;
|
return $resp;
|
||||||
};
|
};
|
||||||
|
|
||||||
$dispatcher = $this->getDispatcher();
|
$response = $this->response;
|
||||||
$response = $dispatcher->dispatch(
|
|
||||||
$this->stack, $request, $response, $next);
|
|
||||||
|
|
||||||
$transmitter = $this->getTransmitter();
|
$response = $this->dispatcher->dispatch(
|
||||||
$transmitter->transmit($request, $response);
|
$this->stack,
|
||||||
|
$request,
|
||||||
|
$response,
|
||||||
|
$next
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->transmitter->transmit($request, $response);
|
||||||
}
|
}
|
||||||
|
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
|
|
@ -112,7 +121,8 @@ class Server
|
||||||
* @param string $name
|
* @param string $name
|
||||||
* @return Server
|
* @return Server
|
||||||
*/
|
*/
|
||||||
public function setPathVariablesAttributeName(string $name): Server {
|
public function setPathVariablesAttributeName(string $name): Server
|
||||||
|
{
|
||||||
$this->pathVariablesAttributeName = $name;
|
$this->pathVariablesAttributeName = $name;
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
@ -150,43 +160,12 @@ class Server
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
/* Defaults */
|
/* Defaults */
|
||||||
|
|
||||||
private function getAttributes()
|
private function getRequest(): ServerRequestInterface
|
||||||
{
|
|
||||||
if (!$this->attributes) {
|
|
||||||
$this->attributes = [];
|
|
||||||
}
|
|
||||||
return $this->attributes;
|
|
||||||
}
|
|
||||||
|
|
||||||
private function getDispatcher()
|
|
||||||
{
|
|
||||||
if (!$this->dispatcher) {
|
|
||||||
$this->dispatcher = new Dispatcher();
|
|
||||||
}
|
|
||||||
return $this->dispatcher;
|
|
||||||
}
|
|
||||||
|
|
||||||
private function getRequest()
|
|
||||||
{
|
{
|
||||||
if (!$this->request) {
|
if (!$this->request) {
|
||||||
$this->request = ServerRequest::getServerRequest();
|
$marshaller = new ServerRequestMarshaller();
|
||||||
|
return $marshaller->getServerRequest();
|
||||||
}
|
}
|
||||||
return $this->request;
|
return $this->request;
|
||||||
}
|
}
|
||||||
|
|
||||||
private function getResponse()
|
|
||||||
{
|
|
||||||
if (!$this->response) {
|
|
||||||
$this->response = new Response();
|
|
||||||
}
|
|
||||||
return $this->response;
|
|
||||||
}
|
|
||||||
|
|
||||||
private function getTransmitter()
|
|
||||||
{
|
|
||||||
if (!$this->transmitter) {
|
|
||||||
$this->transmitter = new Transmitter();
|
|
||||||
}
|
|
||||||
return $this->transmitter;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,7 @@ class Transmitter implements TransmitterInterface
|
||||||
public function transmit(
|
public function transmit(
|
||||||
ServerRequestInterface $request,
|
ServerRequestInterface $request,
|
||||||
ResponseInterface $response
|
ResponseInterface $response
|
||||||
) {
|
): void {
|
||||||
// Prepare the response for output.
|
// Prepare the response for output.
|
||||||
$response = $this->prepareResponse($request, $response);
|
$response = $this->prepareResponse($request, $response);
|
||||||
|
|
||||||
|
|
@ -48,36 +48,35 @@ class Transmitter implements TransmitterInterface
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
public function setChunkSize(int $chunkSize): void
|
||||||
* @param int $chunkSize
|
|
||||||
*/
|
|
||||||
public function setChunkSize($chunkSize)
|
|
||||||
{
|
{
|
||||||
$this->chunkSize = $chunkSize;
|
$this->chunkSize = $chunkSize;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function prepareResponse(
|
private function prepareResponse(
|
||||||
ServerRequestInterface $request,
|
ServerRequestInterface $request,
|
||||||
ResponseInterface $response
|
ResponseInterface $response
|
||||||
) {
|
): ResponseInterface {
|
||||||
// Add a Content-length header to the response when all of these are true:
|
|
||||||
|
// 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 Content-length header
|
||||||
// - Response does not have a Transfer-encoding: chunked header
|
// - Response does not have a Transfer-encoding: chunked header
|
||||||
// - Response body stream is readable and reports a non-null size
|
// - Response body stream is readable and reports a non-null size
|
||||||
//
|
//
|
||||||
if (!$response->hasHeader("Content-length")
|
$contentLengthMissing = !$response->hasHeader('Content-length');
|
||||||
&& !(strtolower($response->getHeaderLine("Transfer-encoding")) === "chunked")
|
$notChunked = strtolower($response->getHeaderLine('Transfer-encoding'))
|
||||||
) {
|
!== 'chunked';
|
||||||
$size = $response->getBody()->getSize();
|
$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;
|
return $response;
|
||||||
}
|
}
|
||||||
|
|
||||||
private function getStatusLine(ResponseInterface $response)
|
private function getStatusLine(ResponseInterface $response): string
|
||||||
{
|
{
|
||||||
$protocol = $response->getProtocolVersion();
|
$protocol = $response->getProtocolVersion();
|
||||||
$statusCode = $response->getStatusCode();
|
$statusCode = $response->getStatusCode();
|
||||||
|
|
@ -89,7 +88,7 @@ class Transmitter implements TransmitterInterface
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private function outputBody(StreamInterface $body)
|
private function outputBody(StreamInterface $body): void
|
||||||
{
|
{
|
||||||
if ($this->chunkSize > 0) {
|
if ($this->chunkSize > 0) {
|
||||||
if ($body->isSeekable()) {
|
if ($body->isSeekable()) {
|
||||||
|
|
|
||||||
|
|
@ -18,5 +18,5 @@ interface TransmitterInterface
|
||||||
* @param ServerRequestInterface $request
|
* @param ServerRequestInterface $request
|
||||||
* @param ResponseInterface $response Response to output
|
* @param ResponseInterface $response Response to output
|
||||||
*/
|
*/
|
||||||
public function transmit(ServerRequestInterface $request, ResponseInterface $response);
|
public function transmit(ServerRequestInterface $request, ResponseInterface $response): void;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
error_reporting(E_ALL);
|
|
||||||
|
|
||||||
$loader = require __DIR__ . '/../vendor/autoload.php';
|
|
||||||
$loader->addPsr4('WellRESTed\\Test\\', __DIR__ . '/src');
|
|
||||||
|
|
@ -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,35 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace WellRESTed\Message;
|
|
||||||
|
|
||||||
// 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);
|
|
||||||
}
|
|
||||||
|
|
@ -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,105 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace WellRESTed\Test\Unit\Message;
|
|
||||||
|
|
||||||
use WellRESTed\Message\HeaderCollection;
|
|
||||||
use WellRESTed\Test\TestCase;
|
|
||||||
|
|
||||||
class HeaderCollectionTest extends TestCase
|
|
||||||
{
|
|
||||||
public function testAddsSingleHeaderAndIndicatesCaseInsensitiveIsset()
|
|
||||||
{
|
|
||||||
$collection = new HeaderCollection();
|
|
||||||
$collection["Content-Type"] = "application/json";
|
|
||||||
$this->assertTrue(isset($collection["content-type"]));
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testAddsMultipleHeadersAndIndicatesCaseInsensitiveIsset()
|
|
||||||
{
|
|
||||||
$collection = new HeaderCollection();
|
|
||||||
$collection["Set-Cookie"] = "cat=Molly";
|
|
||||||
$collection["SET-COOKIE"] = "dog=Bear";
|
|
||||||
$this->assertTrue(isset($collection["set-cookie"]));
|
|
||||||
}
|
|
||||||
|
|
||||||
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"])));
|
|
||||||
}
|
|
||||||
|
|
||||||
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"]));
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
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,201 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace WellRESTed\Test\Unit\Message;
|
|
||||||
|
|
||||||
use InvalidArgumentException;
|
|
||||||
use WellRESTed\Message\NullStream;
|
|
||||||
use WellRESTed\Message\Request;
|
|
||||||
use WellRESTed\Message\Uri;
|
|
||||||
use WellRESTed\Test\TestCase;
|
|
||||||
|
|
||||||
class RequestTest extends TestCase
|
|
||||||
{
|
|
||||||
// ------------------------------------------------------------------------
|
|
||||||
// Construction
|
|
||||||
|
|
||||||
public function testCreatesInstanceWithNoParameters()
|
|
||||||
{
|
|
||||||
$request = new Request();
|
|
||||||
$this->assertNotNull($request);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testCreatesInstanceWithUri()
|
|
||||||
{
|
|
||||||
$uri = new Uri();
|
|
||||||
$request = new Request($uri);
|
|
||||||
$this->assertSame($uri, $request->getUri());
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testCreatesInstanceWithMethod()
|
|
||||||
{
|
|
||||||
$method = "POST";
|
|
||||||
$request = new Request(null, $method);
|
|
||||||
$this->assertSame($method, $request->getMethod());
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testSetsHeadersOnConstruction()
|
|
||||||
{
|
|
||||||
$request = new Request(null, null, [
|
|
||||||
"X-foo" => ["bar","baz"]
|
|
||||||
]);
|
|
||||||
$this->assertEquals(["bar","baz"], $request->getHeader("X-foo"));
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testSetsBodyOnConstruction()
|
|
||||||
{
|
|
||||||
$body = new NullStream();
|
|
||||||
$request = new Request(null, null, [], $body);
|
|
||||||
$this->assertSame($body, $request->getBody());
|
|
||||||
}
|
|
||||||
|
|
||||||
// ------------------------------------------------------------------------
|
|
||||||
// Request Target
|
|
||||||
|
|
||||||
public function testGetRequestTargetPrefersExplicitRequestTarget()
|
|
||||||
{
|
|
||||||
$request = new Request();
|
|
||||||
$request = $request->withRequestTarget("*");
|
|
||||||
$this->assertEquals("*", $request->getRequestTarget());
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testGetRequestTargetUsesOriginFormOfUri()
|
|
||||||
{
|
|
||||||
$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()
|
|
||||||
{
|
|
||||||
$request = new Request();
|
|
||||||
$this->assertEquals("/", $request->getRequestTarget());
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testWithRequestTargetCreatesNewInstance()
|
|
||||||
{
|
|
||||||
$request = new Request();
|
|
||||||
$request = $request->withRequestTarget("*");
|
|
||||||
$this->assertEquals("*", $request->getRequestTarget());
|
|
||||||
}
|
|
||||||
|
|
||||||
// ------------------------------------------------------------------------
|
|
||||||
// Method
|
|
||||||
|
|
||||||
public function testGetMethodReturnsGetByDefault()
|
|
||||||
{
|
|
||||||
$request = new Request();
|
|
||||||
$this->assertEquals("GET", $request->getMethod());
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testWithMethodCreatesNewInstance()
|
|
||||||
{
|
|
||||||
$request = new Request();
|
|
||||||
$request = $request->withMethod("POST");
|
|
||||||
$this->assertEquals("POST", $request->getMethod());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @dataProvider invalidMethodProvider
|
|
||||||
*/
|
|
||||||
public function testWithMethodThrowsExceptionOnInvalidMethod($method)
|
|
||||||
{
|
|
||||||
$this->expectException(InvalidArgumentException::class);
|
|
||||||
$request = new Request();
|
|
||||||
$request->withMethod($method);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function invalidMethodProvider()
|
|
||||||
{
|
|
||||||
return [
|
|
||||||
[0],
|
|
||||||
[false],
|
|
||||||
["WITH SPACE"]
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
// ------------------------------------------------------------------------
|
|
||||||
// Request URI
|
|
||||||
|
|
||||||
public function testGetUriReturnsEmptyUriByDefault()
|
|
||||||
{
|
|
||||||
$request = new Request();
|
|
||||||
$uri = new Uri();
|
|
||||||
$this->assertEquals($uri, $request->getUri());
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testWithUriCreatesNewInstance()
|
|
||||||
{
|
|
||||||
$uri = new Uri();
|
|
||||||
$request = new Request();
|
|
||||||
$request = $request->withUri($uri);
|
|
||||||
$this->assertSame($uri, $request->getUri());
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testWithUriPreservesOriginalRequest()
|
|
||||||
{
|
|
||||||
$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()
|
|
||||||
{
|
|
||||||
$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()
|
|
||||||
{
|
|
||||||
$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()
|
|
||||||
{
|
|
||||||
$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()
|
|
||||||
{
|
|
||||||
$uri = new Uri();
|
|
||||||
|
|
||||||
$request = new Request();
|
|
||||||
$request = $request->withUri($uri, true);
|
|
||||||
$this->assertSame([], $request->getHeader("Host"));
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testPreserveHostDoesNotUpdateHostHeader()
|
|
||||||
{
|
|
||||||
$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"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,109 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace WellRESTed\Test\Unit\Message;
|
|
||||||
|
|
||||||
use WellRESTed\Message\NullStream;
|
|
||||||
use WellRESTed\Message\Response;
|
|
||||||
use WellRESTed\Test\TestCase;
|
|
||||||
|
|
||||||
class ResponseTest extends TestCase
|
|
||||||
{
|
|
||||||
// ------------------------------------------------------------------------
|
|
||||||
// Construction
|
|
||||||
|
|
||||||
public function testSetsStatusCodeOnConstruction()
|
|
||||||
{
|
|
||||||
$response = new Response(200);
|
|
||||||
$this->assertSame(200, $response->getStatusCode());
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testSetsHeadersOnConstruction()
|
|
||||||
{
|
|
||||||
$response = new Response(200, [
|
|
||||||
"X-foo" => ["bar","baz"]
|
|
||||||
]);
|
|
||||||
$this->assertEquals(["bar","baz"], $response->getHeader("X-foo"));
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testSetsBodyOnConstruction()
|
|
||||||
{
|
|
||||||
$body = new NullStream();
|
|
||||||
$response = new Response(200, [], $body);
|
|
||||||
$this->assertSame($body, $response->getBody());
|
|
||||||
}
|
|
||||||
|
|
||||||
// ------------------------------------------------------------------------
|
|
||||||
// Status and Reason Phrase
|
|
||||||
|
|
||||||
public function testCreatesNewInstanceWithStatusCode()
|
|
||||||
{
|
|
||||||
$response = new Response();
|
|
||||||
$copy = $response->withStatus(200);
|
|
||||||
$this->assertEquals(200, $copy->getStatusCode());
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @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"]
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
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,686 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace WellRESTed\Test\Unit\Message;
|
|
||||||
|
|
||||||
use InvalidArgumentException;
|
|
||||||
use WellRESTed\Message\NullStream;
|
|
||||||
use WellRESTed\Message\ServerRequest;
|
|
||||||
use WellRESTed\Message\UploadedFile;
|
|
||||||
use WellRESTed\Message\Uri;
|
|
||||||
use WellRESTed\Test\TestCase;
|
|
||||||
|
|
||||||
class ServerRequestTest extends TestCase
|
|
||||||
{
|
|
||||||
// ------------------------------------------------------------------------
|
|
||||||
// Construction and Marshalling
|
|
||||||
|
|
||||||
/** @backupGlobals enabled */
|
|
||||||
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
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @backupGlobals enabled
|
|
||||||
* @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"]
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @backupGlobals enabled
|
|
||||||
* @dataProvider methodProvider
|
|
||||||
*/
|
|
||||||
public function testGetServerRequestReadsMethod($expectedMethod, $serverMethod)
|
|
||||||
{
|
|
||||||
$_SERVER = [
|
|
||||||
"HTTP_HOST" => "localhost",
|
|
||||||
"REQUEST_METHOD" => $serverMethod
|
|
||||||
];
|
|
||||||
$request = ServerRequest::getServerRequest();
|
|
||||||
$this->assertEquals($expectedMethod, $request->getMethod());
|
|
||||||
}
|
|
||||||
|
|
||||||
public function methodProvider()
|
|
||||||
{
|
|
||||||
return [
|
|
||||||
["GET", "GET"],
|
|
||||||
["POST", "POST"],
|
|
||||||
["DELETE", "DELETE"],
|
|
||||||
["PUT", "PUT"],
|
|
||||||
["OPTIONS", "OPTIONS"],
|
|
||||||
["GET", null]
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @backupGlobals enabled
|
|
||||||
* @dataProvider requestTargetProvider
|
|
||||||
*/
|
|
||||||
public function testGetServerRequestReadsRequestTargetFromRequest($expectedRequestTarget, $serverRequestUri)
|
|
||||||
{
|
|
||||||
$_SERVER = [
|
|
||||||
"HTTP_HOST" => "localhost",
|
|
||||||
"REQUEST_URI" => $serverRequestUri
|
|
||||||
];
|
|
||||||
$request = ServerRequest::getServerRequest();
|
|
||||||
$this->assertEquals($expectedRequestTarget, $request->getRequestTarget());
|
|
||||||
}
|
|
||||||
|
|
||||||
public function requestTargetProvider()
|
|
||||||
{
|
|
||||||
return [
|
|
||||||
["/", "/"],
|
|
||||||
["/hello", "/hello"],
|
|
||||||
["/my/path.txt", "/my/path.txt"],
|
|
||||||
["/", null]
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @depends testGetServerRequestReadsFromRequest */
|
|
||||||
public function testGetServerRequestReadsHeaders($request)
|
|
||||||
{
|
|
||||||
/** @var ServerRequest $request */
|
|
||||||
$this->assertEquals(["application/json"], $request->getHeader("Accept"));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @backupGlobals enabled
|
|
||||||
*/
|
|
||||||
public function testGetServerRequestReadsContentHeaders()
|
|
||||||
{
|
|
||||||
$_SERVER = [
|
|
||||||
"CONTENT_LENGTH" => "1024",
|
|
||||||
"CONTENT_TYPE" => "application/json"
|
|
||||||
];
|
|
||||||
$request = ServerRequest::getServerRequest();
|
|
||||||
$this->assertEquals("1024", $request->getHeaderLine("Content-length"));
|
|
||||||
$this->assertEquals("application/json", $request->getHeaderLine("Content-type"));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @backupGlobals enabled
|
|
||||||
*/
|
|
||||||
public function testGetServerRequestDoesNotReadEmptyContentHeaders()
|
|
||||||
{
|
|
||||||
$_SERVER = [
|
|
||||||
"CONTENT_LENGTH" => "",
|
|
||||||
"CONTENT_TYPE" => " "
|
|
||||||
];
|
|
||||||
$request = ServerRequest::getServerRequest();
|
|
||||||
$this->assertFalse($request->hasHeader("Content-length"));
|
|
||||||
$this->assertFalse($request->hasHeader("Content-type"));
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testGetServerRequestReadsBody()
|
|
||||||
{
|
|
||||||
$body = new NullStream();
|
|
||||||
$request = $this->getMockBuilder('WellRESTed\Message\ServerRequest')
|
|
||||||
->setMethods(["getStreamForBody"])
|
|
||||||
->getMock();
|
|
||||||
$request->expects($this->any())
|
|
||||||
->method("getStreamForBody")
|
|
||||||
->will($this->returnValue($body));
|
|
||||||
|
|
||||||
$called = false;
|
|
||||||
$callReadFromServerRequest = function () use (&$called) {
|
|
||||||
$called = true;
|
|
||||||
$this->readFromServerRequest();
|
|
||||||
};
|
|
||||||
$callReadFromServerRequest = $callReadFromServerRequest->bindTo($request, $request);
|
|
||||||
$callReadFromServerRequest();
|
|
||||||
|
|
||||||
$this->assertSame($body, $request->getBody());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @backupGlobals enabled
|
|
||||||
* @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
|
|
||||||
|
|
||||||
/** @depends testGetServerRequestReadsFromRequest */
|
|
||||||
public function testGetServerRequestReadsServerParams($request)
|
|
||||||
{
|
|
||||||
/** @var ServerRequest $request */
|
|
||||||
$this->assertEquals("localhost", $request->getServerParams()["HTTP_HOST"]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @depends testGetServerRequestReadsFromRequest */
|
|
||||||
public function testGetServerRequestReadsCookieParams($request)
|
|
||||||
{
|
|
||||||
/** @var ServerRequest $request */
|
|
||||||
$this->assertEquals("Molly", $request->getCookieParams()["cat"]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @depends testGetServerRequestReadsFromRequest */
|
|
||||||
public function testGetServerRequestReadsQueryParams($request)
|
|
||||||
{
|
|
||||||
/** @var ServerRequest $request */
|
|
||||||
$this->assertEquals("Claude", $request->getQueryParams()["guinea_pig"]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @backupGlobals enabled
|
|
||||||
* @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"]]
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @backupGlobals enabled
|
|
||||||
* @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"]
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @depends testGetServerRequestReadsFromRequest */
|
|
||||||
public function testGetServerRequestProvidesAttributesIfPassed($request)
|
|
||||||
{
|
|
||||||
/** @var ServerRequest $request */
|
|
||||||
$this->assertEquals("Claude", $request->getAttribute("guinea_pig"));
|
|
||||||
}
|
|
||||||
|
|
||||||
// ------------------------------------------------------------------------
|
|
||||||
// Server Params
|
|
||||||
|
|
||||||
public function testGetServerParamsReturnsEmptyArrayByDefault()
|
|
||||||
{
|
|
||||||
$request = new ServerRequest();
|
|
||||||
$this->assertEquals([], $request->getServerParams());
|
|
||||||
}
|
|
||||||
|
|
||||||
// ------------------------------------------------------------------------
|
|
||||||
// Cookies
|
|
||||||
|
|
||||||
public function testGetCookieParamsReturnsEmptyArrayByDefault()
|
|
||||||
{
|
|
||||||
$request = new ServerRequest();
|
|
||||||
$this->assertEquals([], $request->getCookieParams());
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @depends testGetServerRequestReadsFromRequest */
|
|
||||||
public function testWithCookieParamsCreatesNewInstance($request1)
|
|
||||||
{
|
|
||||||
/** @var ServerRequest $request1 */
|
|
||||||
$request2 = $request1->withCookieParams([
|
|
||||||
"cat" => "Oscar"
|
|
||||||
]);
|
|
||||||
$this->assertNotEquals($request1->getCookieParams()["cat"], $request2->getCookieParams()["cat"]);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ------------------------------------------------------------------------
|
|
||||||
// Query
|
|
||||||
|
|
||||||
public function testGetQueryParamsReturnsEmptyArrayByDefault()
|
|
||||||
{
|
|
||||||
$request = new ServerRequest();
|
|
||||||
$this->assertEquals([], $request->getQueryParams());
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @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
|
|
||||||
|
|
||||||
public function testGetUploadedFilesReturnsEmptyArrayByDefault()
|
|
||||||
{
|
|
||||||
$request = new ServerRequest();
|
|
||||||
$this->assertEquals([], $request->getUploadedFiles());
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @backupGlobals enabled */
|
|
||||||
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());
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @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)
|
|
||||||
]
|
|
||||||
]]]
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @dataProvider invalidUploadedFilesProvider
|
|
||||||
*/
|
|
||||||
public function testWithUploadedFilesThrowsExceptionWithInvalidTree($uploadedFiles)
|
|
||||||
{
|
|
||||||
$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()
|
|
||||||
{
|
|
||||||
$request = new ServerRequest();
|
|
||||||
$this->assertNull($request->getParsedBody());
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @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);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @dataProvider invalidParsedBodyProvider
|
|
||||||
*/
|
|
||||||
public function testWithParsedBodyThrowsExceptionWithInvalidType($body)
|
|
||||||
{
|
|
||||||
$this->expectException(InvalidArgumentException::class);
|
|
||||||
$request = new ServerRequest();
|
|
||||||
$request->withParsedBody($body);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function invalidParsedBodyProvider()
|
|
||||||
{
|
|
||||||
return [
|
|
||||||
[false],
|
|
||||||
[1]
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
public function testGetAttributesReturnsEmptyArrayByDefault()
|
|
||||||
{
|
|
||||||
$request = new ServerRequest();
|
|
||||||
$this->assertEquals([], $request->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());
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testGetAttributeReturnsDefaultIfNotSet()
|
|
||||||
{
|
|
||||||
$request = new ServerRequest();
|
|
||||||
$this->assertEquals("Oscar", $request->getAttribute("cat", "Oscar"));
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testWithAttributeCreatesNewInstance()
|
|
||||||
{
|
|
||||||
$request = new ServerRequest();
|
|
||||||
$request = $request->withAttribute("cat", "Molly");
|
|
||||||
$this->assertEquals("Molly", $request->getAttribute("cat"));
|
|
||||||
}
|
|
||||||
|
|
||||||
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());
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testWithoutAttributeCreatesNewInstance()
|
|
||||||
{
|
|
||||||
$request = new ServerRequest();
|
|
||||||
$request = $request->withAttribute("cat", "Molly");
|
|
||||||
$this->assertNotEquals($request, $request->withoutAttribute("cat"));
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testWithoutAttributeRemovesAttribute()
|
|
||||||
{
|
|
||||||
$request = new ServerRequest();
|
|
||||||
$request = $request->withAttribute("cat", "Molly");
|
|
||||||
$request = $request->withoutAttribute("cat");
|
|
||||||
$this->assertEquals("Oscar", $request->getAttribute("cat", "Oscar"));
|
|
||||||
}
|
|
||||||
|
|
||||||
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,279 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace WellRESTed\Test\Unit\Message;
|
|
||||||
|
|
||||||
use InvalidArgumentException;
|
|
||||||
use RuntimeException;
|
|
||||||
use WellRESTed\Message\Stream;
|
|
||||||
use WellRESTed\Message\StreamHelper;
|
|
||||||
use WellRESTed\Test\TestCase;
|
|
||||||
|
|
||||||
require_once __DIR__ . "/../../../src/StreamHelper.php";
|
|
||||||
|
|
||||||
class StreamTest extends TestCase
|
|
||||||
{
|
|
||||||
private $resource;
|
|
||||||
private $resourceDevNull;
|
|
||||||
private $content = "Hello, world!";
|
|
||||||
|
|
||||||
public function setUp(): void
|
|
||||||
{
|
|
||||||
$this->resource = fopen("php://memory", "w+");
|
|
||||||
$this->resourceDevNull = fopen("/dev/null", "r");
|
|
||||||
fwrite($this->resource, $this->content);
|
|
||||||
StreamHelper::$fail = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function tearDown(): void
|
|
||||||
{
|
|
||||||
if (is_resource($this->resource)) {
|
|
||||||
fclose($this->resource);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testCreatesInstanceWithStreamResource()
|
|
||||||
{
|
|
||||||
$stream = new Stream($this->resource);
|
|
||||||
$this->assertNotNull($stream);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testCreatesInstanceWithString()
|
|
||||||
{
|
|
||||||
$stream = new Stream("Hello, world!");
|
|
||||||
$this->assertNotNull($stream);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @dataProvider invalidResourceProvider
|
|
||||||
* @param mixed $resource
|
|
||||||
*/
|
|
||||||
public function testThrowsExceptionWithInvalidResource($resource)
|
|
||||||
{
|
|
||||||
$this->expectException(InvalidArgumentException::class);
|
|
||||||
new Stream($resource);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function invalidResourceProvider()
|
|
||||||
{
|
|
||||||
return [
|
|
||||||
[null],
|
|
||||||
[true],
|
|
||||||
[4],
|
|
||||||
[[]]
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testCastsToString()
|
|
||||||
{
|
|
||||||
$stream = new Stream($this->resource);
|
|
||||||
$this->assertEquals($this->content, (string) $stream);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testClosesHandle()
|
|
||||||
{
|
|
||||||
$stream = new Stream($this->resource);
|
|
||||||
$stream->close();
|
|
||||||
$this->assertFalse(is_resource($this->resource));
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testDetachReturnsHandle()
|
|
||||||
{
|
|
||||||
$stream = new Stream($this->resource);
|
|
||||||
$this->assertSame($this->resource, $stream->detach());
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testDetachUnsetsInstanceVariable()
|
|
||||||
{
|
|
||||||
$stream = new Stream($this->resource);
|
|
||||||
$stream->detach();
|
|
||||||
$this->assertNull($stream->detach());
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testReturnsSize()
|
|
||||||
{
|
|
||||||
$stream = new Stream($this->resource);
|
|
||||||
$this->assertEquals(strlen($this->content), $stream->getSize());
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testReturnsNullForSizeWhenUnableToReadFromFstat()
|
|
||||||
{
|
|
||||||
$stream = new Stream($this->resourceDevNull);
|
|
||||||
$this->assertNull($stream->getSize());
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testTellReturnsHandlePosition()
|
|
||||||
{
|
|
||||||
$stream = new Stream($this->resource);
|
|
||||||
fseek($this->resource, 10);
|
|
||||||
$this->assertEquals(10, $stream->tell());
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testTellThrowsRuntimeExceptionWhenUnableToReadStreamPosition()
|
|
||||||
{
|
|
||||||
StreamHelper::$fail = true;
|
|
||||||
$stream = new Stream($this->resource);
|
|
||||||
$this->expectException(RuntimeException::class);
|
|
||||||
$stream->tell();
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testReturnsOef()
|
|
||||||
{
|
|
||||||
$stream = new Stream($this->resource);
|
|
||||||
$stream->rewind();
|
|
||||||
$stream->getContents();
|
|
||||||
$this->assertTrue($stream->eof());
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testReadsSeekableStatusFromMetadata()
|
|
||||||
{
|
|
||||||
$stream = new Stream($this->resource);
|
|
||||||
$metadata = stream_get_meta_data($this->resource);
|
|
||||||
$seekable = $metadata["seekable"] == 1;
|
|
||||||
$this->assertEquals($seekable, $stream->isSeekable());
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testSeeksToPosition()
|
|
||||||
{
|
|
||||||
$stream = new Stream($this->resource);
|
|
||||||
$stream->seek(10);
|
|
||||||
$this->assertEquals(10, ftell($this->resource));
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testSeekThrowsRuntimeExceptionWhenUnableToSeek()
|
|
||||||
{
|
|
||||||
StreamHelper::$fail = true;
|
|
||||||
$stream = new Stream($this->resource);
|
|
||||||
$this->expectException(RuntimeException::class);
|
|
||||||
$stream->seek(10);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testRewindReturnsToBeginning()
|
|
||||||
{
|
|
||||||
$stream = new Stream($this->resource);
|
|
||||||
$stream->seek(10);
|
|
||||||
$stream->rewind();
|
|
||||||
$this->assertEquals(0, ftell($this->resource));
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testRewindThrowsRuntimeExceptionWhenUnableToRewind()
|
|
||||||
{
|
|
||||||
StreamHelper::$fail = true;
|
|
||||||
$stream = new Stream($this->resource);
|
|
||||||
$this->expectException(RuntimeException::class);
|
|
||||||
$stream->rewind();
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testWritesToHandle()
|
|
||||||
{
|
|
||||||
$message = "\nThis is a stream.";
|
|
||||||
$stream = new Stream($this->resource);
|
|
||||||
$stream->write($message);
|
|
||||||
$this->assertEquals($this->content . $message, (string) $stream);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testThrowsExceptionOnErrorWriting()
|
|
||||||
{
|
|
||||||
$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()
|
|
||||||
{
|
|
||||||
$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()
|
|
||||||
{
|
|
||||||
$stream = new Stream($this->resource);
|
|
||||||
$stream->seek(7);
|
|
||||||
$string = $stream->read(5);
|
|
||||||
$this->assertEquals("world", $string);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testThrowsExceptionOnErrorReadingToEnd()
|
|
||||||
{
|
|
||||||
$this->expectException(RuntimeException::class);
|
|
||||||
$filename = tempnam(sys_get_temp_dir(), "php");
|
|
||||||
$handle = fopen($filename, "w");
|
|
||||||
$stream = new Stream($handle);
|
|
||||||
$stream->getContents();
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testReadsToEnd()
|
|
||||||
{
|
|
||||||
$stream = new Stream($this->resource);
|
|
||||||
$stream->seek(7);
|
|
||||||
$string = $stream->getContents();
|
|
||||||
$this->assertEquals("world!", $string);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testReturnsMetadataArray()
|
|
||||||
{
|
|
||||||
$stream = new Stream($this->resource);
|
|
||||||
$this->assertEquals(stream_get_meta_data($this->resource), $stream->getMetadata());
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testReturnsMetadataItem()
|
|
||||||
{
|
|
||||||
$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($mode, $readable, $writable)
|
|
||||||
{
|
|
||||||
$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($mode, $readable, $writable)
|
|
||||||
{
|
|
||||||
$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()
|
|
||||||
{
|
|
||||||
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,585 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace WellRESTed\Test\Unit\Message;
|
|
||||||
|
|
||||||
use InvalidArgumentException;
|
|
||||||
use WellRESTed\Message\Uri;
|
|
||||||
use WellRESTed\Test\TestCase;
|
|
||||||
|
|
||||||
class UriTest extends TestCase
|
|
||||||
{
|
|
||||||
// ------------------------------------------------------------------------
|
|
||||||
// Scheme
|
|
||||||
|
|
||||||
public function testDefaultSchemeIsEmpty()
|
|
||||||
{
|
|
||||||
$uri = new Uri();
|
|
||||||
$this->assertSame("", $uri->getScheme());
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @dataProvider schemeProvider */
|
|
||||||
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],
|
|
||||||
["", ""]
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testInvalidSchemeThrowsException()
|
|
||||||
{
|
|
||||||
$this->expectException(InvalidArgumentException::class);
|
|
||||||
$uri = new Uri();
|
|
||||||
$uri->withScheme("gopher");
|
|
||||||
}
|
|
||||||
|
|
||||||
// ------------------------------------------------------------------------
|
|
||||||
// Authority
|
|
||||||
|
|
||||||
public function testDefaultAuthorityIsEmpty()
|
|
||||||
{
|
|
||||||
$uri = new Uri();
|
|
||||||
$this->assertSame("", $uri->getAuthority());
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testRespectsMyAuthoritah()
|
|
||||||
{
|
|
||||||
$this->assertTrue(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @dataProvider authorityProvider */
|
|
||||||
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
|
|
||||||
|
|
||||||
public function testDefaultUserInfoIsEmpty()
|
|
||||||
{
|
|
||||||
$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 $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
|
|
||||||
|
|
||||||
public function testDefaultHostIsEmpty()
|
|
||||||
{
|
|
||||||
$uri = new Uri();
|
|
||||||
$this->assertSame("", $uri->getHost());
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @dataProvider hostProvider */
|
|
||||||
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"]
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @dataProvider invalidHostProvider
|
|
||||||
*/
|
|
||||||
public function testInvalidHostThrowsException($host)
|
|
||||||
{
|
|
||||||
$this->expectException(InvalidArgumentException::class);
|
|
||||||
$uri = new Uri();
|
|
||||||
$uri->withHost($host);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function invalidHostProvider()
|
|
||||||
{
|
|
||||||
return [
|
|
||||||
[null],
|
|
||||||
[false],
|
|
||||||
[0]
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
// ------------------------------------------------------------------------
|
|
||||||
// Port
|
|
||||||
|
|
||||||
public function testDefaultPortWithNoSchemeIsNull()
|
|
||||||
{
|
|
||||||
$uri = new Uri();
|
|
||||||
$this->assertNull($uri->getPort());
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testDefaultPortForHttpSchemeIs80()
|
|
||||||
{
|
|
||||||
$uri = new Uri();
|
|
||||||
$this->assertSame(80, $uri->withScheme("http")->getPort());
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testDefaultPortForHttpsSchemeIs443()
|
|
||||||
{
|
|
||||||
$uri = new Uri();
|
|
||||||
$this->assertSame(443, $uri->withScheme("https")->getPort());
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @dataProvider portAndSchemeProvider */
|
|
||||||
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]
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @dataProvider invalidPortProvider
|
|
||||||
*/
|
|
||||||
public function testInvalidPortThrowsException($port)
|
|
||||||
{
|
|
||||||
$this->expectException(InvalidArgumentException::class);
|
|
||||||
$uri = new Uri();
|
|
||||||
$uri->withPort($port);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function invalidPortProvider()
|
|
||||||
{
|
|
||||||
return [
|
|
||||||
[true],
|
|
||||||
[-1],
|
|
||||||
[65536],
|
|
||||||
["dog"]
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
// ------------------------------------------------------------------------
|
|
||||||
// Path
|
|
||||||
|
|
||||||
public function testDefaultPathIsEmpty()
|
|
||||||
{
|
|
||||||
$uri = new Uri();
|
|
||||||
$this->assertSame("", $uri->getPath());
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @dataProvider pathProvider */
|
|
||||||
public function testSetsEncodedPath($expected, $path)
|
|
||||||
{
|
|
||||||
$uri = new Uri();
|
|
||||||
$uri = $uri->withPath($path);
|
|
||||||
$this->assertSame($expected, $uri->getPath());
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @dataProvider pathProvider */
|
|
||||||
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
|
|
||||||
|
|
||||||
public function testDefaultQueryIsEmpty()
|
|
||||||
{
|
|
||||||
$uri = new Uri();
|
|
||||||
$this->assertSame("", $uri->getQuery());
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @dataProvider queryProvider */
|
|
||||||
public function testSetsEncodedQuery($expected, $query)
|
|
||||||
{
|
|
||||||
$uri = new Uri();
|
|
||||||
$uri = $uri->withQuery($query);
|
|
||||||
$this->assertSame($expected, $uri->getQuery());
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @dataProvider queryProvider */
|
|
||||||
public function testDoesNotDoubleEncodeQuery($expected, $query)
|
|
||||||
{
|
|
||||||
$uri = new Uri();
|
|
||||||
$uri = $uri->withQuery($query);
|
|
||||||
$uri = $uri->withQuery($uri->getQuery());
|
|
||||||
$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=áéíóú"]
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @dataProvider invalidPathProvider
|
|
||||||
*/
|
|
||||||
public function testInvalidPathThrowsException($path)
|
|
||||||
{
|
|
||||||
$this->expectException(InvalidArgumentException::class);
|
|
||||||
$uri = new Uri();
|
|
||||||
$uri->withPath($path);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function invalidPathProvider()
|
|
||||||
{
|
|
||||||
return [
|
|
||||||
[null],
|
|
||||||
[false],
|
|
||||||
[0]
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
// ------------------------------------------------------------------------
|
|
||||||
// Fragment
|
|
||||||
|
|
||||||
public function testDefaultFragmentIsEmpty()
|
|
||||||
{
|
|
||||||
$uri = new Uri();
|
|
||||||
$this->assertSame("", $uri->getFragment());
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @dataProvider fragmentProvider */
|
|
||||||
public function testSetsEncodedFragment($expected, $fragment)
|
|
||||||
{
|
|
||||||
$uri = new Uri();
|
|
||||||
$uri = $uri->withFragment($fragment);
|
|
||||||
$this->assertSame($expected, $uri->getFragment());
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @dataProvider fragmentProvider */
|
|
||||||
public function testDoesNotDoubleEncodeFragment($expected, $fragment)
|
|
||||||
{
|
|
||||||
$uri = new Uri();
|
|
||||||
$uri = $uri->withFragment($fragment);
|
|
||||||
$uri = $uri->withFragment($uri->getFragment());
|
|
||||||
$this->assertSame($expected, $uri->getFragment());
|
|
||||||
}
|
|
||||||
|
|
||||||
public function fragmentProvider()
|
|
||||||
{
|
|
||||||
return [
|
|
||||||
["", null],
|
|
||||||
["molly", "molly"],
|
|
||||||
["%C3%A1%C3%A9%C3%AD%C3%B3%C3%BA", "áéíóú"]
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
// ------------------------------------------------------------------------
|
|
||||||
// Concatenation
|
|
||||||
|
|
||||||
/** @dataProvider componentProvider */
|
|
||||||
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" => "*"
|
|
||||||
]
|
|
||||||
],
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @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,451 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace WellRESTed\Test\Unit\Routing;
|
|
||||||
|
|
||||||
use Prophecy\Argument;
|
|
||||||
use WellRESTed\Dispatching\Dispatcher;
|
|
||||||
use WellRESTed\Message\Response;
|
|
||||||
use WellRESTed\Message\ServerRequest;
|
|
||||||
use WellRESTed\Routing\Route\RouteFactory;
|
|
||||||
use WellRESTed\Routing\Route\RouteInterface;
|
|
||||||
use WellRESTed\Routing\Router;
|
|
||||||
use WellRESTed\Test\Doubles\NextMock;
|
|
||||||
use WellRESTed\Test\TestCase;
|
|
||||||
|
|
||||||
class RouterTest extends TestCase
|
|
||||||
{
|
|
||||||
private $factory;
|
|
||||||
private $request;
|
|
||||||
private $response;
|
|
||||||
private $route;
|
|
||||||
private $router;
|
|
||||||
private $next;
|
|
||||||
|
|
||||||
public function setUp(): void
|
|
||||||
{
|
|
||||||
parent::setUp();
|
|
||||||
|
|
||||||
$this->route = $this->prophesize(RouteInterface::class);
|
|
||||||
$this->route->__invoke(Argument::cetera())->willReturn(new Response());
|
|
||||||
$this->route->register(Argument::cetera())->willReturn();
|
|
||||||
$this->route->getType()->willReturn(RouteInterface::TYPE_STATIC);
|
|
||||||
$this->route->getTarget()->willReturn("/");
|
|
||||||
$this->route->getPathVariables()->willReturn([]);
|
|
||||||
|
|
||||||
$this->factory = $this->prophesize(RouteFactory::class);
|
|
||||||
$this->factory->create(Argument::any())
|
|
||||||
->willReturn($this->route->reveal());
|
|
||||||
|
|
||||||
RouterWithFactory::$routeFactory = $this->factory->reveal();
|
|
||||||
|
|
||||||
$this->router = new RouterWithFactory();
|
|
||||||
|
|
||||||
$this->request = new ServerRequest();
|
|
||||||
$this->response = new Response();
|
|
||||||
$this->next = new NextMock();
|
|
||||||
}
|
|
||||||
|
|
||||||
// ------------------------------------------------------------------------
|
|
||||||
// Construction
|
|
||||||
|
|
||||||
public function testCreatesInstance()
|
|
||||||
{
|
|
||||||
$router = new Router();
|
|
||||||
$this->assertNotNull($router);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ------------------------------------------------------------------------
|
|
||||||
// Populating
|
|
||||||
|
|
||||||
public function testCreatesRouteForTarget()
|
|
||||||
{
|
|
||||||
$this->router->register("GET", "/", "middleware");
|
|
||||||
|
|
||||||
$this->factory->create("/")->shouldHaveBeenCalled();
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testDoesNotRecreateRouteForExistingTarget()
|
|
||||||
{
|
|
||||||
$this->router->register("GET", "/", "middleware");
|
|
||||||
$this->router->register("POST", "/", "middleware");
|
|
||||||
|
|
||||||
$this->factory->create("/")->shouldHaveBeenCalledTimes(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testPassesMethodAndMiddlewareToRoute()
|
|
||||||
{
|
|
||||||
$this->router->register("GET", "/", "middleware");
|
|
||||||
|
|
||||||
$this->route->register("GET", "middleware")->shouldHaveBeenCalled();
|
|
||||||
}
|
|
||||||
|
|
||||||
// ------------------------------------------------------------------------
|
|
||||||
// Dispatching
|
|
||||||
|
|
||||||
public function testDispatchesStaticRoute()
|
|
||||||
{
|
|
||||||
$target = "/";
|
|
||||||
$this->request = $this->request->withRequestTarget($target);
|
|
||||||
|
|
||||||
$this->route->getTarget()->willReturn($target);
|
|
||||||
$this->route->getType()->willReturn(RouteInterface::TYPE_STATIC);
|
|
||||||
|
|
||||||
$this->router->register("GET", $target, "middleware");
|
|
||||||
$this->router->__invoke($this->request, $this->response, $this->next);
|
|
||||||
|
|
||||||
$this->route->__invoke(Argument::cetera())
|
|
||||||
->shouldHaveBeenCalled();
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testDispatchesPrefixRoute()
|
|
||||||
{
|
|
||||||
$target = "/animals/cats/*";
|
|
||||||
$this->request = $this->request->withRequestTarget("/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, $this->response, $this->next);
|
|
||||||
|
|
||||||
$this->route->__invoke(Argument::cetera())
|
|
||||||
->shouldHaveBeenCalled();
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testDispatchesPatternRoute()
|
|
||||||
{
|
|
||||||
$target = "/";
|
|
||||||
$this->request = $this->request->withRequestTarget($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, $this->response, $this->next);
|
|
||||||
|
|
||||||
$this->route->__invoke(Argument::cetera())
|
|
||||||
->shouldHaveBeenCalled();
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testDispatchesStaticRouteBeforePrefixRoute()
|
|
||||||
{
|
|
||||||
$staticRoute = $this->prophesize(RouteInterface::class);
|
|
||||||
$staticRoute->register(Argument::cetera())->willReturn();
|
|
||||||
$staticRoute->getTarget()->willReturn("/cats/");
|
|
||||||
$staticRoute->getType()->willReturn(RouteInterface::TYPE_STATIC);
|
|
||||||
$staticRoute->__invoke(Argument::cetera())->willReturn(new Response());
|
|
||||||
|
|
||||||
$prefixRoute = $this->prophesize(RouteInterface::class);
|
|
||||||
$prefixRoute->register(Argument::cetera())->willReturn();
|
|
||||||
$prefixRoute->getTarget()->willReturn("/cats/*");
|
|
||||||
$prefixRoute->getType()->willReturn(RouteInterface::TYPE_PREFIX);
|
|
||||||
$prefixRoute->__invoke(Argument::cetera())->willReturn(new Response());
|
|
||||||
|
|
||||||
$this->request = $this->request->withRequestTarget("/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, $this->response, $this->next);
|
|
||||||
|
|
||||||
$staticRoute->__invoke(Argument::cetera())
|
|
||||||
->shouldHaveBeenCalled();
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testDispatchesLongestMatchingPrefixRoute()
|
|
||||||
{
|
|
||||||
// Note: The longest route is also good for 2 points in Settlers of Catan.
|
|
||||||
|
|
||||||
$shortRoute = $this->prophesize(RouteInterface::class);
|
|
||||||
$shortRoute->register(Argument::cetera())->willReturn();
|
|
||||||
$shortRoute->getTarget()->willReturn("/animals/*");
|
|
||||||
$shortRoute->getType()->willReturn(RouteInterface::TYPE_PREFIX);
|
|
||||||
$shortRoute->__invoke(Argument::cetera())->willReturn(new Response());
|
|
||||||
|
|
||||||
$longRoute = $this->prophesize(RouteInterface::class);
|
|
||||||
$longRoute->register(Argument::cetera())->willReturn();
|
|
||||||
$longRoute->getTarget()->willReturn("/animals/cats/*");
|
|
||||||
$longRoute->getType()->willReturn(RouteInterface::TYPE_PREFIX);
|
|
||||||
$longRoute->__invoke(Argument::cetera())->willReturn(new Response());
|
|
||||||
|
|
||||||
$this->request = $this->request
|
|
||||||
->withRequestTarget("/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, $this->response, $this->next);
|
|
||||||
|
|
||||||
$longRoute->__invoke(Argument::cetera())
|
|
||||||
->shouldHaveBeenCalled();
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testDispatchesPrefixRouteBeforePatternRoute()
|
|
||||||
{
|
|
||||||
$prefixRoute = $this->prophesize(RouteInterface::class);
|
|
||||||
$prefixRoute->register(Argument::cetera())->willReturn();
|
|
||||||
$prefixRoute->getTarget()->willReturn("/cats/*");
|
|
||||||
$prefixRoute->getType()->willReturn(RouteInterface::TYPE_PREFIX);
|
|
||||||
$prefixRoute->__invoke(Argument::cetera())->willReturn(new Response());
|
|
||||||
|
|
||||||
$patternRoute = $this->prophesize(RouteInterface::class);
|
|
||||||
$patternRoute->register(Argument::cetera())->willReturn();
|
|
||||||
$patternRoute->getTarget()->willReturn("/cats/{id}");
|
|
||||||
$patternRoute->getType()->willReturn(RouteInterface::TYPE_PATTERN);
|
|
||||||
$patternRoute->__invoke(Argument::cetera())->willReturn(new Response());
|
|
||||||
|
|
||||||
$this->request = $this->request->withRequestTarget("/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, $this->response, $this->next);
|
|
||||||
|
|
||||||
$prefixRoute->__invoke(Argument::cetera())
|
|
||||||
->shouldHaveBeenCalled();
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testDispatchesFirstMatchingPatternRoute()
|
|
||||||
{
|
|
||||||
$patternRoute1 = $this->prophesize(RouteInterface::class);
|
|
||||||
$patternRoute1->register(Argument::cetera())->willReturn();
|
|
||||||
$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(new Response());
|
|
||||||
|
|
||||||
$patternRoute2 = $this->prophesize(RouteInterface::class);
|
|
||||||
$patternRoute2->register(Argument::cetera())->willReturn();
|
|
||||||
$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(new Response());
|
|
||||||
|
|
||||||
$this->request = $this->request->withRequestTarget("/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, $this->response, $this->next);
|
|
||||||
|
|
||||||
$patternRoute1->__invoke(Argument::cetera())
|
|
||||||
->shouldHaveBeenCalled();
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testStopsTestingPatternsAfterFirstSuccessfulMatch()
|
|
||||||
{
|
|
||||||
$patternRoute1 = $this->prophesize(RouteInterface::class);
|
|
||||||
$patternRoute1->register(Argument::cetera())->willReturn();
|
|
||||||
$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(new Response());
|
|
||||||
|
|
||||||
$patternRoute2 = $this->prophesize(RouteInterface::class);
|
|
||||||
$patternRoute2->register(Argument::cetera())->willReturn();
|
|
||||||
$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(new Response());
|
|
||||||
|
|
||||||
$this->request = $this->request->withRequestTarget("/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, $this->response, $this->next);
|
|
||||||
|
|
||||||
$patternRoute2->matchesRequestTarget(Argument::any())
|
|
||||||
->shouldNotHaveBeenCalled();
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testMatchesPathAgainstRouteWithoutQuery()
|
|
||||||
{
|
|
||||||
$target = "/my/path?cat=molly&dog=bear";
|
|
||||||
|
|
||||||
$this->request = $this->request->withRequestTarget($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, $this->response, $this->next);
|
|
||||||
|
|
||||||
$this->route->matchesRequestTarget("/my/path")->shouldHaveBeenCalled();
|
|
||||||
}
|
|
||||||
|
|
||||||
// ------------------------------------------------------------------------
|
|
||||||
// Path Variables
|
|
||||||
|
|
||||||
/** @dataProvider pathVariableProvider */
|
|
||||||
public function testSetPathVariablesAttributeIndividually($name, $value)
|
|
||||||
{
|
|
||||||
$target = "/";
|
|
||||||
$variables = [
|
|
||||||
"id" => "1024",
|
|
||||||
"name" => "Molly"
|
|
||||||
];
|
|
||||||
|
|
||||||
$this->request = $this->request->withRequestTarget($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->register("GET", $target, "middleware");
|
|
||||||
$this->router->__invoke($this->request, $this->response, $this->next);
|
|
||||||
|
|
||||||
$isRequestWithExpectedAttribute = function ($request) use ($name, $value) {
|
|
||||||
return $request->getAttribute($name) === $value;
|
|
||||||
};
|
|
||||||
|
|
||||||
$this->route->__invoke(
|
|
||||||
Argument::that($isRequestWithExpectedAttribute),
|
|
||||||
Argument::cetera()
|
|
||||||
)->shouldHaveBeenCalled();
|
|
||||||
}
|
|
||||||
|
|
||||||
public function pathVariableProvider()
|
|
||||||
{
|
|
||||||
return [
|
|
||||||
["id", "1024"],
|
|
||||||
["name", "Molly"]
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testSetPathVariablesAttributeAsArray()
|
|
||||||
{
|
|
||||||
$attributeName = "pathVariables";
|
|
||||||
|
|
||||||
$target = "/";
|
|
||||||
$variables = [
|
|
||||||
"id" => "1024",
|
|
||||||
"name" => "Molly"
|
|
||||||
];
|
|
||||||
|
|
||||||
$this->request = $this->request->withRequestTarget($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(new Dispatcher(), $attributeName);
|
|
||||||
$this->router->register("GET", $target, "middleware");
|
|
||||||
$this->router->__invoke($this->request, $this->response, $this->next);
|
|
||||||
|
|
||||||
$isRequestWithExpectedAttribute = function ($request) use ($attributeName, $variables) {
|
|
||||||
return $request->getAttribute($attributeName) === $variables;
|
|
||||||
};
|
|
||||||
|
|
||||||
$this->route->__invoke(
|
|
||||||
Argument::that($isRequestWithExpectedAttribute),
|
|
||||||
Argument::cetera()
|
|
||||||
)->shouldHaveBeenCalled();
|
|
||||||
}
|
|
||||||
|
|
||||||
// ------------------------------------------------------------------------
|
|
||||||
// No Match
|
|
||||||
|
|
||||||
public function testWhenNoRouteMatchesByDefaultResponds404()
|
|
||||||
{
|
|
||||||
$this->request = $this->request->withRequestTarget("/no/match");
|
|
||||||
$response = $this->router->__invoke($this->request, $this->response, $this->next);
|
|
||||||
$this->assertEquals(404, $response->getStatusCode());
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testWhenNoRouteMatchesByDefaultDoesNotPropagatesToNextMiddleware()
|
|
||||||
{
|
|
||||||
$this->request = $this->request->withRequestTarget("/no/match");
|
|
||||||
$this->router->__invoke($this->request, $this->response, $this->next);
|
|
||||||
$this->assertFalse($this->next->called);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testWhenNoRouteMatchesAndContinueModePropagatesToNextMiddleware()
|
|
||||||
{
|
|
||||||
$this->request = $this->request->withRequestTarget("/no/match");
|
|
||||||
$this->router->continueOnNotFound();
|
|
||||||
$this->router->__invoke($this->request, $this->response, $this->next);
|
|
||||||
$this->assertTrue($this->next->called);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ------------------------------------------------------------------------
|
|
||||||
// Middleware for Routes
|
|
||||||
|
|
||||||
public function testCallsRouterMiddlewareBeforeRouteMiddleware()
|
|
||||||
{
|
|
||||||
$middlewareRequest = new ServerRequest();
|
|
||||||
$middlewareResponse = new Response();
|
|
||||||
|
|
||||||
$middleware = function ($rqst, $resp, $next) use (
|
|
||||||
$middlewareRequest,
|
|
||||||
$middlewareResponse
|
|
||||||
) {
|
|
||||||
return $next($middlewareRequest, $middlewareResponse);
|
|
||||||
};
|
|
||||||
|
|
||||||
$this->router->add($middleware);
|
|
||||||
$this->router->register("GET", "/", "Handler");
|
|
||||||
|
|
||||||
$this->router->__invoke($this->request, $this->response, $this->next);
|
|
||||||
|
|
||||||
$this->route->__invoke(
|
|
||||||
$middlewareRequest,
|
|
||||||
$middlewareResponse,
|
|
||||||
Argument::any())->shouldHaveBeenCalled();
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testDoesNotCallRouterMiddlewareWhenNoRouteMatches()
|
|
||||||
{
|
|
||||||
$middlewareCalled = false;
|
|
||||||
$middlewareRequest = new ServerRequest();
|
|
||||||
$middlewareResponse = new Response();
|
|
||||||
|
|
||||||
$middleware = function ($rqst, $resp, $next) use (
|
|
||||||
$middlewareRequest,
|
|
||||||
$middlewareResponse,
|
|
||||||
&$middlewareCalled
|
|
||||||
) {
|
|
||||||
$middlewareCalled = true;
|
|
||||||
return $next($middlewareRequest, $middlewareResponse);
|
|
||||||
};
|
|
||||||
|
|
||||||
$this->request = $this->request->withRequestTarget("/no/match");
|
|
||||||
|
|
||||||
$this->router->add($middleware);
|
|
||||||
$this->router->register("GET", "/", "Handler");
|
|
||||||
|
|
||||||
$this->router->__invoke($this->request, $this->response, $this->next);
|
|
||||||
|
|
||||||
$this->assertFalse($middlewareCalled);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// -----------------------------------------------------------------------------
|
|
||||||
|
|
||||||
class RouterWithFactory extends Router
|
|
||||||
{
|
|
||||||
static $routeFactory;
|
|
||||||
|
|
||||||
protected function getRouteFactory($dispatcher)
|
|
||||||
{
|
|
||||||
return self::$routeFactory;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,9 +1,7 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace WellRESTed\Test\Unit\Dispatching;
|
namespace WellRESTed\Dispatching;
|
||||||
|
|
||||||
use WellRESTed\Dispatching\Dispatcher;
|
|
||||||
use WellRESTed\Dispatching\DispatchStack;
|
|
||||||
use WellRESTed\Message\Response;
|
use WellRESTed\Message\Response;
|
||||||
use WellRESTed\Message\ServerRequest;
|
use WellRESTed\Message\ServerRequest;
|
||||||
use WellRESTed\Test\Doubles\NextMock;
|
use WellRESTed\Test\Doubles\NextMock;
|
||||||
|
|
@ -15,7 +13,7 @@ class DispatchStackTest extends TestCase
|
||||||
private $response;
|
private $response;
|
||||||
private $next;
|
private $next;
|
||||||
|
|
||||||
public function setUp(): void
|
protected function setUp(): void
|
||||||
{
|
{
|
||||||
parent::setUp();
|
parent::setUp();
|
||||||
$this->request = new ServerRequest();
|
$this->request = new ServerRequest();
|
||||||
|
|
@ -23,35 +21,35 @@ class DispatchStackTest extends TestCase
|
||||||
$this->next = new NextMock();
|
$this->next = new NextMock();
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testDispatchesMiddlewareInOrderAdded()
|
public function testDispatchesMiddlewareInOrderAdded(): void
|
||||||
{
|
{
|
||||||
// Each middleware will add its "name" to this array.
|
// Each middleware will add its "name" to this array.
|
||||||
$callOrder = [];
|
$callOrder = [];
|
||||||
$stack = new DispatchStack(new Dispatcher());
|
$stack = new DispatchStack(new Dispatcher());
|
||||||
$stack->add(function ($request, $response, $next) use (&$callOrder) {
|
$stack->add(function ($request, $response, $next) use (&$callOrder) {
|
||||||
$callOrder[] = "first";
|
$callOrder[] = 'first';
|
||||||
return $next($request, $response);
|
return $next($request, $response);
|
||||||
});
|
});
|
||||||
$stack->add(function ($request, $response, $next) use (&$callOrder) {
|
$stack->add(function ($request, $response, $next) use (&$callOrder) {
|
||||||
$callOrder[] = "second";
|
$callOrder[] = 'second';
|
||||||
return $next($request, $response);
|
return $next($request, $response);
|
||||||
});
|
});
|
||||||
$stack->add(function ($request, $response, $next) use (&$callOrder) {
|
$stack->add(function ($request, $response, $next) use (&$callOrder) {
|
||||||
$callOrder[] = "third";
|
$callOrder[] = 'third';
|
||||||
return $next($request, $response);
|
return $next($request, $response);
|
||||||
});
|
});
|
||||||
$stack($this->request, $this->response, $this->next);
|
$stack($this->request, $this->response, $this->next);
|
||||||
$this->assertEquals(["first", "second", "third"], $callOrder);
|
$this->assertEquals(['first', 'second', 'third'], $callOrder);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testCallsNextAfterDispatchingEmptyStack()
|
public function testCallsNextAfterDispatchingEmptyStack(): void
|
||||||
{
|
{
|
||||||
$stack = new DispatchStack(new Dispatcher());
|
$stack = new DispatchStack(new Dispatcher());
|
||||||
$stack($this->request, $this->response, $this->next);
|
$stack($this->request, $this->response, $this->next);
|
||||||
$this->assertTrue($this->next->called);
|
$this->assertTrue($this->next->called);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testCallsNextAfterDispatchingStack()
|
public function testCallsNextAfterDispatchingStack(): void
|
||||||
{
|
{
|
||||||
$middleware = function ($request, $response, $next) use (&$callOrder) {
|
$middleware = function ($request, $response, $next) use (&$callOrder) {
|
||||||
return $next($request, $response);
|
return $next($request, $response);
|
||||||
|
|
@ -66,7 +64,7 @@ class DispatchStackTest extends TestCase
|
||||||
$this->assertTrue($this->next->called);
|
$this->assertTrue($this->next->called);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testDoesNotCallNextWhenStackStopsEarly()
|
public function testDoesNotCallNextWhenStackStopsEarly(): void
|
||||||
{
|
{
|
||||||
$middlewareGo = function ($request, $response, $next) use (&$callOrder) {
|
$middlewareGo = function ($request, $response, $next) use (&$callOrder) {
|
||||||
return $next($request, $response);
|
return $next($request, $response);
|
||||||
|
|
@ -1,12 +1,10 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace WellRESTed\Test\Unit\Dispatching;
|
namespace WellRESTed\Dispatching;
|
||||||
|
|
||||||
use Psr\Http\Message\ResponseInterface;
|
use Psr\Http\Message\ResponseInterface;
|
||||||
use Psr\Http\Message\ServerRequestInterface;
|
use Psr\Http\Message\ServerRequestInterface;
|
||||||
use Psr\Http\Server\RequestHandlerInterface;
|
use Psr\Http\Server\RequestHandlerInterface;
|
||||||
use WellRESTed\Dispatching\Dispatcher;
|
|
||||||
use WellRESTed\Dispatching\DispatchException;
|
|
||||||
use WellRESTed\Message\Response;
|
use WellRESTed\Message\Response;
|
||||||
use WellRESTed\Message\ServerRequest;
|
use WellRESTed\Message\ServerRequest;
|
||||||
use WellRESTed\MiddlewareInterface;
|
use WellRESTed\MiddlewareInterface;
|
||||||
|
|
@ -24,7 +22,7 @@ class DispatcherTest extends TestCase
|
||||||
/** @var ResponseInterface */
|
/** @var ResponseInterface */
|
||||||
private $stubResponse;
|
private $stubResponse;
|
||||||
|
|
||||||
public function setUp(): void
|
protected function setUp(): void
|
||||||
{
|
{
|
||||||
$this->request = new ServerRequest();
|
$this->request = new ServerRequest();
|
||||||
$this->response = new Response();
|
$this->response = new Response();
|
||||||
|
|
@ -35,6 +33,8 @@ class DispatcherTest extends TestCase
|
||||||
/**
|
/**
|
||||||
* Dispatch the provided dispatchable using the class under test and the
|
* Dispatch the provided dispatchable using the class under test and the
|
||||||
* ivars $request, $response, and $next. Return the response.
|
* ivars $request, $response, and $next. Return the response.
|
||||||
|
* @param $dispatchable
|
||||||
|
* @return ResponseInterface
|
||||||
*/
|
*/
|
||||||
private function dispatch($dispatchable): ResponseInterface
|
private function dispatch($dispatchable): ResponseInterface
|
||||||
{
|
{
|
||||||
|
|
@ -50,14 +50,14 @@ class DispatcherTest extends TestCase
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
// PSR-15 Handler
|
// PSR-15 Handler
|
||||||
|
|
||||||
public function testDispatchesPsr15Handler()
|
public function testDispatchesPsr15Handler(): void
|
||||||
{
|
{
|
||||||
$handler = new HandlerDouble($this->stubResponse);
|
$handler = new HandlerDouble($this->stubResponse);
|
||||||
$response = $this->dispatch($handler);
|
$response = $this->dispatch($handler);
|
||||||
$this->assertSame($this->stubResponse, $response);
|
$this->assertSame($this->stubResponse, $response);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testDispatchesPsr15HandlerFromFactory()
|
public function testDispatchesPsr15HandlerFromFactory(): void
|
||||||
{
|
{
|
||||||
$factory = function () {
|
$factory = function () {
|
||||||
return new HandlerDouble($this->stubResponse);
|
return new HandlerDouble($this->stubResponse);
|
||||||
|
|
@ -70,7 +70,8 @@ class DispatcherTest extends TestCase
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
// PSR-15 Middleware
|
// PSR-15 Middleware
|
||||||
|
|
||||||
public function testDispatchesPsr15MiddlewareWithDelegate() {
|
public function testDispatchesPsr15MiddlewareWithDelegate(): void
|
||||||
|
{
|
||||||
$this->next->upstreamResponse = $this->stubResponse;
|
$this->next->upstreamResponse = $this->stubResponse;
|
||||||
$middleware = new MiddlewareDouble();
|
$middleware = new MiddlewareDouble();
|
||||||
|
|
||||||
|
|
@ -78,7 +79,8 @@ class DispatcherTest extends TestCase
|
||||||
$this->assertSame($this->stubResponse, $response);
|
$this->assertSame($this->stubResponse, $response);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testDispatchesPsr15MiddlewareFromFactoryWithDelegate() {
|
public function testDispatchesPsr15MiddlewareFromFactoryWithDelegate(): void
|
||||||
|
{
|
||||||
$this->next->upstreamResponse = $this->stubResponse;
|
$this->next->upstreamResponse = $this->stubResponse;
|
||||||
$factory = function () {
|
$factory = function () {
|
||||||
return new MiddlewareDouble();
|
return new MiddlewareDouble();
|
||||||
|
|
@ -91,7 +93,7 @@ class DispatcherTest extends TestCase
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
// Double-Pass Middleware Callable
|
// Double-Pass Middleware Callable
|
||||||
|
|
||||||
public function testDispatchesDoublePassMiddlewareCallable()
|
public function testDispatchesDoublePassMiddlewareCallable(): void
|
||||||
{
|
{
|
||||||
$doublePass = function ($request, $response, $next) {
|
$doublePass = function ($request, $response, $next) {
|
||||||
return $next($request, $this->stubResponse);
|
return $next($request, $this->stubResponse);
|
||||||
|
|
@ -101,7 +103,7 @@ class DispatcherTest extends TestCase
|
||||||
$this->assertSame($this->stubResponse, $response);
|
$this->assertSame($this->stubResponse, $response);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testDispatchesDoublePassMiddlewareCallableFromFactory()
|
public function testDispatchesDoublePassMiddlewareCallableFromFactory(): void
|
||||||
{
|
{
|
||||||
$factory = function () {
|
$factory = function () {
|
||||||
return function ($request, $response, $next) {
|
return function ($request, $response, $next) {
|
||||||
|
|
@ -116,14 +118,14 @@ class DispatcherTest extends TestCase
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
// Double-Pass Middleware Instance
|
// Double-Pass Middleware Instance
|
||||||
|
|
||||||
public function testDispatchesDoublePassMiddlewareInstance()
|
public function testDispatchesDoublePassMiddlewareInstance(): void
|
||||||
{
|
{
|
||||||
$doublePass = new DoublePassMiddlewareDouble();
|
$doublePass = new DoublePassMiddlewareDouble();
|
||||||
$response = $this->dispatch($doublePass);
|
$response = $this->dispatch($doublePass);
|
||||||
$this->assertEquals(200, $response->getStatusCode());
|
$this->assertEquals(200, $response->getStatusCode());
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testDispatchesDoublePassMiddlewareInstanceFromFactory()
|
public function testDispatchesDoublePassMiddlewareInstanceFromFactory(): void
|
||||||
{
|
{
|
||||||
$factory = function () {
|
$factory = function () {
|
||||||
return new DoublePassMiddlewareDouble();
|
return new DoublePassMiddlewareDouble();
|
||||||
|
|
@ -135,7 +137,7 @@ class DispatcherTest extends TestCase
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
// String
|
// String
|
||||||
|
|
||||||
public function testDispatchesInstanceFromStringName()
|
public function testDispatchesInstanceFromStringName(): void
|
||||||
{
|
{
|
||||||
$response = $this->dispatch(DoublePassMiddlewareDouble::class);
|
$response = $this->dispatch(DoublePassMiddlewareDouble::class);
|
||||||
$this->assertEquals(200, $response->getStatusCode());
|
$this->assertEquals(200, $response->getStatusCode());
|
||||||
|
|
@ -144,14 +146,14 @@ class DispatcherTest extends TestCase
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
// Arrays
|
// Arrays
|
||||||
|
|
||||||
public function testDispatchesArrayAsDispatchStack()
|
public function testDispatchesArrayAsDispatchStack(): void
|
||||||
{
|
{
|
||||||
$doublePass = new DoublePassMiddlewareDouble();
|
$doublePass = new DoublePassMiddlewareDouble();
|
||||||
$response = $this->dispatch([$doublePass]);
|
$response = $this->dispatch([$doublePass]);
|
||||||
$this->assertEquals(200, $response->getStatusCode());
|
$this->assertEquals(200, $response->getStatusCode());
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testThrowsExceptionWhenUnableToDispatch()
|
public function testThrowsExceptionWhenUnableToDispatch(): void
|
||||||
{
|
{
|
||||||
$this->expectException(DispatchException::class);
|
$this->expectException(DispatchException::class);
|
||||||
$this->dispatch(null);
|
$this->dispatch(null);
|
||||||
|
|
@ -188,6 +190,7 @@ class HandlerDouble implements RequestHandlerInterface
|
||||||
{
|
{
|
||||||
/** @var ResponseInterface */
|
/** @var ResponseInterface */
|
||||||
private $response;
|
private $response;
|
||||||
|
|
||||||
public function __construct(ResponseInterface $response)
|
public function __construct(ResponseInterface $response)
|
||||||
{
|
{
|
||||||
$this->response = $response;
|
$this->response = $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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,38 +1,34 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace WellRESTed\Test\Unit\Message;
|
namespace WellRESTed\Message;
|
||||||
|
|
||||||
use InvalidArgumentException;
|
use InvalidArgumentException;
|
||||||
use WellRESTed\Message\Message;
|
|
||||||
use WellRESTed\Message\Response;
|
|
||||||
use WellRESTed\Message\Stream;
|
|
||||||
use WellRESTed\Test\TestCase;
|
use WellRESTed\Test\TestCase;
|
||||||
|
|
||||||
class MessageTest extends TestCase
|
class MessageTest extends TestCase
|
||||||
{
|
{
|
||||||
/** @var Message */
|
public function testSetsHeadersWithStringValueOnConstruction(): void
|
||||||
private $message;
|
|
||||||
|
|
||||||
public function setUp(): void
|
|
||||||
{
|
{
|
||||||
$this->message = new Response();
|
$headers = ['X-foo' => 'bar'];
|
||||||
|
$message = new Response(200, $headers);
|
||||||
|
$this->assertEquals(['bar'], $message->getHeader('X-foo'));
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testSetsHeadersOnConstruction()
|
public function testSetsHeadersWithArrayValueOnConstruction(): void
|
||||||
{
|
{
|
||||||
$headers = ['X-foo' => ['bar', 'baz']];
|
$headers = ['X-foo' => ['bar', 'baz']];
|
||||||
$message = new Response(200, $headers);
|
$message = new Response(200, $headers);
|
||||||
$this->assertEquals(['bar', 'baz'], $message->getHeader('X-foo'));
|
$this->assertEquals(['bar', 'baz'], $message->getHeader('X-foo'));
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testSetsBodyOnConstruction()
|
public function testSetsBodyOnConstruction(): void
|
||||||
{
|
{
|
||||||
$body = new Stream('Hello, world');
|
$body = new Stream('Hello, world');
|
||||||
$message = new Response(200, [], $body);
|
$message = new Response(200, [], $body);
|
||||||
$this->assertSame($body, $message->getBody());
|
$this->assertSame($body, $message->getBody());
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testCloneMakesDeepCopyOfHeaders()
|
public function testCloneMakesDeepCopyOfHeaders(): void
|
||||||
{
|
{
|
||||||
$message1 = (new Response())
|
$message1 = (new Response())
|
||||||
->withHeader('Content-type', 'text/plain');
|
->withHeader('Content-type', 'text/plain');
|
||||||
|
|
@ -41,37 +37,42 @@ class MessageTest extends TestCase
|
||||||
|
|
||||||
$this->assertNotEquals(
|
$this->assertNotEquals(
|
||||||
$message1->getHeader('Content-type'),
|
$message1->getHeader('Content-type'),
|
||||||
$message2->getHeader('Content-type'));
|
$message2->getHeader('Content-type')
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
// Protocol Version
|
// Protocol Version
|
||||||
|
|
||||||
public function testGetProtocolVersionReturnsProtocolVersion1Point1ByDefault()
|
public function testGetProtocolVersionReturnsProtocolVersion1Point1ByDefault(): void
|
||||||
{
|
{
|
||||||
$message = new Response();
|
$message = new Response();
|
||||||
$this->assertEquals('1.1', $message->getProtocolVersion());
|
$this->assertEquals('1.1', $message->getProtocolVersion());
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testGetProtocolVersionReturnsProtocolVersion()
|
public function testGetProtocolVersionReturnsProtocolVersion(): void
|
||||||
{
|
{
|
||||||
$message = (new Response())
|
$message = (new Response())
|
||||||
->withProtocolVersion('1.0');
|
->withProtocolVersion('1.0');
|
||||||
$this->assertEquals('1.0', $message->getProtocolVersion());
|
$this->assertEquals('1.0', $message->getProtocolVersion());
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testGetProtocolVersionReplacesProtocolVersion()
|
public function testGetProtocolVersionReplacesProtocolVersion(): void
|
||||||
{
|
{
|
||||||
$message = (new Response())
|
$message = (new Response())
|
||||||
->withProtocolVersion('1.0');
|
->withProtocolVersion('1.0');
|
||||||
$this->assertEquals('1.0', $message->getProtocolVersion());
|
$this->assertEquals('1.0', $message->getProtocolVersion());
|
||||||
}
|
}
|
||||||
|
|
||||||
// ------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
// Headers
|
// Headers
|
||||||
|
|
||||||
/** @dataProvider validHeaderValueProvider */
|
/**
|
||||||
public function testWithHeaderReplacesHeader($expected, $value)
|
* @dataProvider validHeaderValueProvider
|
||||||
|
* @param array $expected
|
||||||
|
* @param mixed $value
|
||||||
|
*/
|
||||||
|
public function testWithHeaderReplacesHeader(array $expected, $value): void
|
||||||
{
|
{
|
||||||
$message = (new Response())
|
$message = (new Response())
|
||||||
->withHeader('X-foo', 'Original value')
|
->withHeader('X-foo', 'Original value')
|
||||||
|
|
@ -79,7 +80,7 @@ class MessageTest extends TestCase
|
||||||
$this->assertEquals($expected, $message->getHeader('X-foo'));
|
$this->assertEquals($expected, $message->getHeader('X-foo'));
|
||||||
}
|
}
|
||||||
|
|
||||||
public function validHeaderValueProvider()
|
public function validHeaderValueProvider(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
[['0'], 0],
|
[['0'], 0],
|
||||||
|
|
@ -89,15 +90,17 @@ class MessageTest extends TestCase
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @dataProvider invalidHeaderProvider
|
* @dataProvider invalidHeaderProvider
|
||||||
|
* @param mixed $name
|
||||||
|
* @param mixed $value
|
||||||
*/
|
*/
|
||||||
public function testWithHeaderThrowsExceptionWithInvalidArgument($name, $value)
|
public function testWithHeaderThrowsExceptionWithInvalidArgument($name, $value): void
|
||||||
{
|
{
|
||||||
$this->expectException(InvalidArgumentException::class);
|
$this->expectException(InvalidArgumentException::class);
|
||||||
$message = (new Response())
|
(new Response())
|
||||||
->withHeader($name, $value);
|
->withHeader($name, $value);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function invalidHeaderProvider()
|
public function invalidHeaderProvider(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
[0, 1024],
|
[0, 1024],
|
||||||
|
|
@ -106,14 +109,14 @@ class MessageTest extends TestCase
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testWithAddedHeaderSetsHeader()
|
public function testWithAddedHeaderSetsHeader(): void
|
||||||
{
|
{
|
||||||
$message = (new Response())
|
$message = (new Response())
|
||||||
->withAddedHeader('Content-type', 'application/json');
|
->withAddedHeader('Content-type', 'application/json');
|
||||||
$this->assertEquals(['application/json'], $message->getHeader('Content-type'));
|
$this->assertEquals(['application/json'], $message->getHeader('Content-type'));
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testWithAddedHeaderAppendsValue()
|
public function testWithAddedHeaderAppendsValue(): void
|
||||||
{
|
{
|
||||||
$message = (new Response())
|
$message = (new Response())
|
||||||
->withAddedHeader('Set-Cookie', ['cat=Molly'])
|
->withAddedHeader('Set-Cookie', ['cat=Molly'])
|
||||||
|
|
@ -121,10 +124,11 @@ class MessageTest extends TestCase
|
||||||
$cookies = $message->getHeader('Set-Cookie');
|
$cookies = $message->getHeader('Set-Cookie');
|
||||||
$this->assertTrue(
|
$this->assertTrue(
|
||||||
in_array('cat=Molly', $cookies) &&
|
in_array('cat=Molly', $cookies) &&
|
||||||
in_array('dog=Bear', $cookies));
|
in_array('dog=Bear', $cookies)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testWithoutHeaderRemovesHeader()
|
public function testWithoutHeaderRemovesHeader(): void
|
||||||
{
|
{
|
||||||
$message = (new Response())
|
$message = (new Response())
|
||||||
->withHeader('Content-type', 'application/json')
|
->withHeader('Content-type', 'application/json')
|
||||||
|
|
@ -132,20 +136,20 @@ class MessageTest extends TestCase
|
||||||
$this->assertFalse($message->hasHeader('Content-type'));
|
$this->assertFalse($message->hasHeader('Content-type'));
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testGetHeaderReturnsEmptyArrayForUnsetHeader()
|
public function testGetHeaderReturnsEmptyArrayForUnsetHeader(): void
|
||||||
{
|
{
|
||||||
$message = new Response();
|
$message = new Response();
|
||||||
$this->assertEquals([], $message->getHeader('X-name'));
|
$this->assertEquals([], $message->getHeader('X-name'));
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testGetHeaderReturnsSingleHeader()
|
public function testGetHeaderReturnsSingleHeader(): void
|
||||||
{
|
{
|
||||||
$message = (new Response())
|
$message = (new Response())
|
||||||
->withAddedHeader('Content-type', 'application/json');
|
->withAddedHeader('Content-type', 'application/json');
|
||||||
$this->assertEquals(['application/json'], $message->getHeader('Content-type'));
|
$this->assertEquals(['application/json'], $message->getHeader('Content-type'));
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testGetHeaderReturnsMultipleValuesForHeader()
|
public function testGetHeaderReturnsMultipleValuesForHeader(): void
|
||||||
{
|
{
|
||||||
$message = (new Response())
|
$message = (new Response())
|
||||||
->withAddedHeader('X-name', 'cat=Molly')
|
->withAddedHeader('X-name', 'cat=Molly')
|
||||||
|
|
@ -153,13 +157,13 @@ class MessageTest extends TestCase
|
||||||
$this->assertEquals(['cat=Molly', 'dog=Bear'], $message->getHeader('X-name'));
|
$this->assertEquals(['cat=Molly', 'dog=Bear'], $message->getHeader('X-name'));
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testGetHeaderLineReturnsEmptyStringForUnsetHeader()
|
public function testGetHeaderLineReturnsEmptyStringForUnsetHeader(): void
|
||||||
{
|
{
|
||||||
$message = new Response();
|
$message = new Response();
|
||||||
$this->assertSame('', $message->getHeaderLine('X-not-set'));
|
$this->assertSame('', $message->getHeaderLine('X-not-set'));
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testGetHeaderLineReturnsMultipleHeadersJoinedByCommas()
|
public function testGetHeaderLineReturnsMultipleHeadersJoinedByCommas(): void
|
||||||
{
|
{
|
||||||
$message = (new Response())
|
$message = (new Response())
|
||||||
->withAddedHeader('X-name', 'cat=Molly')
|
->withAddedHeader('X-name', 'cat=Molly')
|
||||||
|
|
@ -167,20 +171,20 @@ class MessageTest extends TestCase
|
||||||
$this->assertEquals('cat=Molly, dog=Bear', $message->getHeaderLine('X-name'));
|
$this->assertEquals('cat=Molly, dog=Bear', $message->getHeaderLine('X-name'));
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testHasHeaderReturnsTrueWhenHeaderIsSet()
|
public function testHasHeaderReturnsTrueWhenHeaderIsSet(): void
|
||||||
{
|
{
|
||||||
$message = (new Response())
|
$message = (new Response())
|
||||||
->withHeader('Content-type', 'application/json');
|
->withHeader('Content-type', 'application/json');
|
||||||
$this->assertTrue($message->hasHeader('Content-type'));
|
$this->assertTrue($message->hasHeader('Content-type'));
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testHasHeaderReturnsFalseWhenHeaderIsNotSet()
|
public function testHasHeaderReturnsFalseWhenHeaderIsNotSet(): void
|
||||||
{
|
{
|
||||||
$message = new Response();
|
$message = new Response();
|
||||||
$this->assertFalse($message->hasHeader('Content-type'));
|
$this->assertFalse($message->hasHeader('Content-type'));
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testGetHeadersReturnOriginalHeaderNamesAsKeys()
|
public function testGetHeadersReturnOriginalHeaderNamesAsKeys(): void
|
||||||
{
|
{
|
||||||
$message = (new Response())
|
$message = (new Response())
|
||||||
->withHeader('Set-Cookie', 'cat=Molly')
|
->withHeader('Set-Cookie', 'cat=Molly')
|
||||||
|
|
@ -199,7 +203,7 @@ class MessageTest extends TestCase
|
||||||
$this->assertEquals(0, $countUnmatched);
|
$this->assertEquals(0, $countUnmatched);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testGetHeadersReturnOriginalHeaderNamesAndValues()
|
public function testGetHeadersReturnOriginalHeaderNamesAndValues(): void
|
||||||
{
|
{
|
||||||
$message = (new Response())
|
$message = (new Response())
|
||||||
->withHeader('Set-Cookie', 'cat=Molly')
|
->withHeader('Set-Cookie', 'cat=Molly')
|
||||||
|
|
@ -226,16 +230,16 @@ class MessageTest extends TestCase
|
||||||
$this->assertEquals($expected, $headers);
|
$this->assertEquals($expected, $headers);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
// Body
|
// Body
|
||||||
|
|
||||||
public function testGetBodyReturnsEmptyStreamByDefault()
|
public function testGetBodyReturnsEmptyStreamByDefault(): void
|
||||||
{
|
{
|
||||||
$message = new Response();
|
$message = new Response();
|
||||||
$this->assertEquals('', (string) $message->getBody());
|
$this->assertEquals('', (string) $message->getBody());
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testGetBodyReturnsAttachedStream()
|
public function testGetBodyReturnsAttachedStream(): void
|
||||||
{
|
{
|
||||||
$stream = new Stream('Hello, world!');
|
$stream = new Stream('Hello, world!');
|
||||||
|
|
||||||
|
|
@ -1,110 +1,109 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace WellRESTed\Test\Unit\Message;
|
namespace WellRESTed\Message;
|
||||||
|
|
||||||
use RuntimeException;
|
use RuntimeException;
|
||||||
use WellRESTed\Message\NullStream;
|
|
||||||
use WellRESTed\Test\TestCase;
|
use WellRESTed\Test\TestCase;
|
||||||
|
|
||||||
class NullStreamTest extends TestCase
|
class NullStreamTest extends TestCase
|
||||||
{
|
{
|
||||||
public function testCastsToString()
|
public function testCastsToString(): void
|
||||||
{
|
{
|
||||||
$stream = new NullStream();
|
$stream = new NullStream();
|
||||||
$this->assertEquals("", (string) $stream);
|
$this->assertEquals('', (string) $stream);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testCloseDoesNothing()
|
public function testCloseDoesNothing(): void
|
||||||
{
|
{
|
||||||
$stream = new NullStream();
|
$stream = new NullStream();
|
||||||
$stream->close();
|
$stream->close();
|
||||||
$this->assertTrue(true); // Asserting no exception occurred.
|
$this->assertTrue(true); // Asserting no exception occurred.
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testDetachReturnsNull()
|
public function testDetachReturnsNull(): void
|
||||||
{
|
{
|
||||||
$stream = new NullStream();
|
$stream = new NullStream();
|
||||||
$this->assertNull($stream->detach());
|
$this->assertNull($stream->detach());
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testSizeReturnsZero()
|
public function testSizeReturnsZero(): void
|
||||||
{
|
{
|
||||||
$stream = new NullStream();
|
$stream = new NullStream();
|
||||||
$this->assertEquals(0, $stream->getSize());
|
$this->assertEquals(0, $stream->getSize());
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testTellReturnsZero()
|
public function testTellReturnsZero(): void
|
||||||
{
|
{
|
||||||
$stream = new NullStream();
|
$stream = new NullStream();
|
||||||
$this->assertEquals(0, $stream->tell());
|
$this->assertEquals(0, $stream->tell());
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testEofReturnsTrue()
|
public function testEofReturnsTrue(): void
|
||||||
{
|
{
|
||||||
$stream = new NullStream();
|
$stream = new NullStream();
|
||||||
$this->assertTrue($stream->eof());
|
$this->assertTrue($stream->eof());
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testIsSeekableReturnsFalse()
|
public function testIsSeekableReturnsFalse(): void
|
||||||
{
|
{
|
||||||
$stream = new NullStream();
|
$stream = new NullStream();
|
||||||
$this->assertFalse($stream->isSeekable());
|
$this->assertFalse($stream->isSeekable());
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testSeekReturnsFalse()
|
public function testSeekReturnsFalse(): void
|
||||||
{
|
{
|
||||||
$this->expectException(RuntimeException::class);
|
$this->expectException(RuntimeException::class);
|
||||||
$stream = new NullStream();
|
$stream = new NullStream();
|
||||||
$stream->seek(10);
|
$stream->seek(10);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testRewindThrowsException()
|
public function testRewindThrowsException(): void
|
||||||
{
|
{
|
||||||
$this->expectException(RuntimeException::class);
|
$this->expectException(RuntimeException::class);
|
||||||
$stream = new NullStream();
|
$stream = new NullStream();
|
||||||
$stream->rewind();
|
$stream->rewind();
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testIsWritableReturnsFalse()
|
public function testIsWritableReturnsFalse(): void
|
||||||
{
|
{
|
||||||
$stream = new NullStream();
|
$stream = new NullStream();
|
||||||
$this->assertFalse($stream->isWritable());
|
$this->assertFalse($stream->isWritable());
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testWriteThrowsException()
|
public function testWriteThrowsException(): void
|
||||||
{
|
{
|
||||||
$this->expectException(RuntimeException::class);
|
$this->expectException(RuntimeException::class);
|
||||||
$stream = new NullStream();
|
$stream = new NullStream();
|
||||||
$stream->write("");
|
$stream->write('');
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testIsReadableReturnsTrue()
|
public function testIsReadableReturnsTrue(): void
|
||||||
{
|
{
|
||||||
$stream = new NullStream();
|
$stream = new NullStream();
|
||||||
$this->assertTrue($stream->isReadable());
|
$this->assertTrue($stream->isReadable());
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testReadReturnsEmptyString()
|
public function testReadReturnsEmptyString(): void
|
||||||
{
|
{
|
||||||
$stream = new NullStream();
|
$stream = new NullStream();
|
||||||
$this->assertEquals("", $stream->read(100));
|
$this->assertEquals('', $stream->read(100));
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testGetContentsReturnsEmptyString()
|
public function testGetContentsReturnsEmptyString(): void
|
||||||
{
|
{
|
||||||
$stream = new NullStream();
|
$stream = new NullStream();
|
||||||
$this->assertEquals("", $stream->getContents());
|
$this->assertEquals('', $stream->getContents());
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testGetMetadataReturnsNull()
|
public function testGetMetadataReturnsNull(): void
|
||||||
{
|
{
|
||||||
$stream = new NullStream();
|
$stream = new NullStream();
|
||||||
$this->assertNull($stream->getMetadata());
|
$this->assertNull($stream->getMetadata());
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testGetMetadataReturnsNullWithKey()
|
public function testGetMetadataReturnsNullWithKey(): void
|
||||||
{
|
{
|
||||||
$stream = new NullStream();
|
$stream = new NullStream();
|
||||||
$this->assertNull($stream->getMetadata("size"));
|
$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);
|
||||||
|
}
|
||||||
|
|
@ -1,30 +1,25 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace WellRESTed\Test\Unit\Message;
|
namespace WellRESTed\Message;
|
||||||
|
|
||||||
use Psr\Http\Message\StreamInterface;
|
use Psr\Http\Message\StreamInterface;
|
||||||
use RuntimeException;
|
use RuntimeException;
|
||||||
use WellRESTed\Message\UploadedFile;
|
|
||||||
use WellRESTed\Message\UploadedFileState;
|
|
||||||
use WellRESTed\Test\TestCase;
|
use WellRESTed\Test\TestCase;
|
||||||
|
|
||||||
// Hides several php core functions for testing.
|
|
||||||
require_once __DIR__ . "/../../../src/UploadedFileState.php";
|
|
||||||
|
|
||||||
class UploadedFileTest extends TestCase
|
class UploadedFileTest extends TestCase
|
||||||
{
|
{
|
||||||
private $tmpName;
|
private $tmpName;
|
||||||
private $movePath;
|
private $movePath;
|
||||||
|
|
||||||
public function setUp(): void
|
protected function setUp(): void
|
||||||
{
|
{
|
||||||
parent::setUp();
|
parent::setUp();
|
||||||
UploadedFileState::$php_sapi_name = "cli";
|
UploadedFileState::$php_sapi_name = 'cli';
|
||||||
$this->tmpName = tempnam(sys_get_temp_dir(), "tst");
|
$this->tmpName = tempnam(sys_get_temp_dir(), 'tst');
|
||||||
$this->movePath = tempnam(sys_get_temp_dir(), "tst");
|
$this->movePath = tempnam(sys_get_temp_dir(), 'tst');
|
||||||
}
|
}
|
||||||
|
|
||||||
public function tearDown(): void
|
protected function tearDown(): void
|
||||||
{
|
{
|
||||||
parent::tearDown();
|
parent::tearDown();
|
||||||
if (file_exists($this->tmpName)) {
|
if (file_exists($this->tmpName)) {
|
||||||
|
|
@ -35,123 +30,150 @@ class UploadedFileTest extends TestCase
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
// getStream
|
// getStream
|
||||||
|
|
||||||
public function testGetStreamReturnsStreamInterface()
|
public function testGetStreamReturnsStreamInterface(): void
|
||||||
{
|
{
|
||||||
$file = new UploadedFile("", "", 0, "", 0);
|
$file = new UploadedFile('', '', 0, $this->tmpName, 0);
|
||||||
$this->assertInstanceOf(StreamInterface::class, $file->getStream());
|
$this->assertInstanceOf(StreamInterface::class, $file->getStream());
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testGetStreamReturnsStreamWrappingUploadedFile()
|
public function testGetStreamReturnsStreamWrappingUploadedFile(): void
|
||||||
{
|
{
|
||||||
$content = "Hello, World!";
|
$content = 'Hello, World!';
|
||||||
file_put_contents($this->tmpName, $content);
|
file_put_contents($this->tmpName, $content);
|
||||||
$file = new UploadedFile("", "", 0, $this->tmpName, "");
|
$file = new UploadedFile('', '', 0, $this->tmpName, '');
|
||||||
$stream = $file->getStream();
|
$stream = $file->getStream();
|
||||||
$this->assertEquals($content, (string) $stream);
|
$this->assertEquals($content, (string) $stream);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testGetStreamReturnsEmptyStreamForNoFile()
|
public function testGetStreamThrowsRuntimeExceptionForNoFile(): void
|
||||||
{
|
{
|
||||||
$file = new UploadedFile("", "", 0, "", 0);
|
$file = new UploadedFile('', '', 0, '', 0);
|
||||||
$this->assertTrue($file->getStream()->eof());
|
$this->expectException(RuntimeException::class);
|
||||||
|
$file->getStream();
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testGetStreamThrowsExceptionAfterMoveTo()
|
public function testGetStreamThrowsExceptionAfterMoveTo(): void
|
||||||
{
|
{
|
||||||
$this->expectException(RuntimeException::class);
|
$this->expectException(RuntimeException::class);
|
||||||
$content = "Hello, World!";
|
$content = 'Hello, World!';
|
||||||
file_put_contents($this->tmpName, $content);
|
file_put_contents($this->tmpName, $content);
|
||||||
$file = new UploadedFile("", "", 0, $this->tmpName, "");
|
$file = new UploadedFile('', '', 0, $this->tmpName, '');
|
||||||
$file->moveTo($this->movePath);
|
$file->moveTo($this->movePath);
|
||||||
$file->getStream();
|
$file->getStream();
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testGetStreamThrowsExceptionForNonUploadedFile()
|
public function testGetStreamThrowsExceptionForNonUploadedFile(): void
|
||||||
{
|
{
|
||||||
$this->expectException(RuntimeException::class);
|
$this->expectException(RuntimeException::class);
|
||||||
UploadedFileState::$php_sapi_name = "apache";
|
UploadedFileState::$php_sapi_name = 'apache';
|
||||||
UploadedFileState::$is_uploaded_file = false;
|
UploadedFileState::$is_uploaded_file = false;
|
||||||
$file = new UploadedFile("", "", 0, "", 0);
|
$file = new UploadedFile('', '', 0, $this->tmpName, 0);
|
||||||
$file->getStream();
|
$file->getStream();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
// moveTo
|
// moveTo
|
||||||
|
|
||||||
public function testMoveToSapiRelocatesUploadedFileToDestinationIfExists()
|
public function testMoveToSapiRelocatesUploadedFileToDestinationIfExists(): void
|
||||||
{
|
{
|
||||||
UploadedFileState::$php_sapi_name = "fpm-fcgi";
|
UploadedFileState::$php_sapi_name = 'fpm-fcgi';
|
||||||
|
|
||||||
$content = "Hello, World!";
|
$content = 'Hello, World!';
|
||||||
file_put_contents($this->tmpName, $content);
|
file_put_contents($this->tmpName, $content);
|
||||||
$originalMd5 = md5_file($this->tmpName);
|
$originalMd5 = md5_file($this->tmpName);
|
||||||
|
|
||||||
$file = new UploadedFile("", "", 0, $this->tmpName, "");
|
$file = new UploadedFile('', '', 0, $this->tmpName, '');
|
||||||
$file->moveTo($this->movePath);
|
$file->moveTo($this->movePath);
|
||||||
|
|
||||||
$this->assertEquals($originalMd5, md5_file($this->movePath));
|
$this->assertEquals($originalMd5, md5_file($this->movePath));
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testMoveToNonSapiRelocatesUploadedFileToDestinationIfExists()
|
public function testMoveToNonSapiRelocatesUploadedFileToDestinationIfExists(): void
|
||||||
{
|
{
|
||||||
$content = "Hello, World!";
|
$content = 'Hello, World!';
|
||||||
file_put_contents($this->tmpName, $content);
|
file_put_contents($this->tmpName, $content);
|
||||||
$originalMd5 = md5_file($this->tmpName);
|
$originalMd5 = md5_file($this->tmpName);
|
||||||
|
|
||||||
$file = new UploadedFile("", "", 0, $this->tmpName, "");
|
$file = new UploadedFile('', '', 0, $this->tmpName, '');
|
||||||
$file->moveTo($this->movePath);
|
$file->moveTo($this->movePath);
|
||||||
|
|
||||||
$this->assertEquals($originalMd5, md5_file($this->movePath));
|
$this->assertEquals($originalMd5, md5_file($this->movePath));
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testMoveToThrowsExceptionOnSubsequentCall()
|
public function testMoveToThrowsExceptionOnSubsequentCall(): void
|
||||||
{
|
{
|
||||||
$this->expectException(RuntimeException::class);
|
$this->expectException(RuntimeException::class);
|
||||||
|
|
||||||
$content = "Hello, World!";
|
$content = 'Hello, World!';
|
||||||
file_put_contents($this->tmpName, $content);
|
file_put_contents($this->tmpName, $content);
|
||||||
|
|
||||||
$file = new UploadedFile("", "", 0, $this->tmpName, "");
|
$file = new UploadedFile('', '', 0, $this->tmpName, '');
|
||||||
$file->moveTo($this->movePath);
|
$file->moveTo($this->movePath);
|
||||||
$file->moveTo($this->movePath);
|
$file->moveTo($this->movePath);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
// getSize
|
// getSize
|
||||||
|
|
||||||
public function testGetSizeReturnsSize()
|
public function testGetSizeReturnsSize(): void
|
||||||
{
|
{
|
||||||
$file = new UploadedFile("", "", 1024, "", 0);
|
$file = new UploadedFile('', '', 1024, '', 0);
|
||||||
$this->assertEquals(1024, $file->getSize());
|
$this->assertEquals(1024, $file->getSize());
|
||||||
}
|
}
|
||||||
|
|
||||||
// ------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
// getError
|
// getError
|
||||||
|
|
||||||
public function testGetErrorReturnsError()
|
public function testGetErrorReturnsError(): void
|
||||||
{
|
{
|
||||||
$file = new UploadedFile("", "", 1024, "", UPLOAD_ERR_INI_SIZE);
|
$file = new UploadedFile('', '', 1024, '', UPLOAD_ERR_INI_SIZE);
|
||||||
$this->assertEquals(UPLOAD_ERR_INI_SIZE, $file->getError());
|
$this->assertEquals(UPLOAD_ERR_INI_SIZE, $file->getError());
|
||||||
}
|
}
|
||||||
|
|
||||||
// ------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
// clientFilename
|
// clientFilename
|
||||||
|
|
||||||
public function testGetClientFilenameReturnsClientFilename()
|
public function testGetClientFilenameReturnsClientFilename(): void
|
||||||
{
|
{
|
||||||
$file = new UploadedFile("clientFilename", "", 0, "", 0);
|
$file = new UploadedFile('clientFilename', '', 0, '', 0);
|
||||||
$this->assertEquals("clientFilename", $file->getClientFilename());
|
$this->assertEquals('clientFilename', $file->getClientFilename());
|
||||||
}
|
}
|
||||||
|
|
||||||
// ------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
// clientMediaType
|
// clientMediaType
|
||||||
|
|
||||||
public function testGetClientMediaTypeReturnsClientMediaType()
|
public function testGetClientMediaTypeReturnsClientMediaType(): void
|
||||||
{
|
{
|
||||||
$file = new UploadedFile("", "clientMediaType", 0, "", 0);
|
$file = new UploadedFile('', 'clientMediaType', 0, '', 0);
|
||||||
$this->assertEquals("clientMediaType", $file->getClientMediaType());
|
$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'
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'*',
|
||||||
|
'*'
|
||||||
|
]
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,11 +1,10 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace WellRESTed\Test\Unit\Routing\Route;
|
namespace WellRESTed\Routing\Route;
|
||||||
|
|
||||||
use WellRESTed\Dispatching\Dispatcher;
|
use WellRESTed\Dispatching\Dispatcher;
|
||||||
use WellRESTed\Message\Response;
|
use WellRESTed\Message\Response;
|
||||||
use WellRESTed\Message\ServerRequest;
|
use WellRESTed\Message\ServerRequest;
|
||||||
use WellRESTed\Routing\Route\MethodMap;
|
|
||||||
use WellRESTed\Test\Doubles\MiddlewareMock;
|
use WellRESTed\Test\Doubles\MiddlewareMock;
|
||||||
use WellRESTed\Test\Doubles\NextMock;
|
use WellRESTed\Test\Doubles\NextMock;
|
||||||
use WellRESTed\Test\TestCase;
|
use WellRESTed\Test\TestCase;
|
||||||
|
|
@ -18,7 +17,7 @@ class MethodMapTest extends TestCase
|
||||||
private $next;
|
private $next;
|
||||||
private $middleware;
|
private $middleware;
|
||||||
|
|
||||||
public function setUp(): void
|
protected function setUp(): void
|
||||||
{
|
{
|
||||||
$this->request = new ServerRequest();
|
$this->request = new ServerRequest();
|
||||||
$this->response = new Response();
|
$this->response = new Response();
|
||||||
|
|
@ -27,112 +26,117 @@ class MethodMapTest extends TestCase
|
||||||
$this->dispatcher = new Dispatcher();
|
$this->dispatcher = new Dispatcher();
|
||||||
}
|
}
|
||||||
|
|
||||||
private function getMethodMap() {
|
private function getMethodMap(): MethodMap
|
||||||
|
{
|
||||||
return new MethodMap($this->dispatcher);
|
return new MethodMap($this->dispatcher);
|
||||||
}
|
}
|
||||||
|
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
|
|
||||||
public function testDispatchesMiddlewareWithMatchingMethod()
|
public function testDispatchesMiddlewareWithMatchingMethod(): void
|
||||||
{
|
{
|
||||||
$this->request = $this->request->withMethod("GET");
|
$this->request = $this->request->withMethod('GET');
|
||||||
|
|
||||||
$map = $this->getMethodMap();
|
$map = $this->getMethodMap();
|
||||||
$map->register("GET", $this->middleware);
|
$map->register('GET', $this->middleware);
|
||||||
$map($this->request, $this->response, $this->next);
|
$map($this->request, $this->response, $this->next);
|
||||||
|
|
||||||
$this->assertTrue($this->middleware->called);
|
$this->assertTrue($this->middleware->called);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testTreatsMethodNamesCaseSensitively()
|
public function testTreatsMethodNamesCaseSensitively(): void
|
||||||
{
|
{
|
||||||
$this->request = $this->request->withMethod("get");
|
$this->request = $this->request->withMethod('get');
|
||||||
|
|
||||||
$middlewareUpper = new MiddlewareMock();
|
$middlewareUpper = new MiddlewareMock();
|
||||||
$middlewareLower = new MiddlewareMock();
|
$middlewareLower = new MiddlewareMock();
|
||||||
|
|
||||||
$map = $this->getMethodMap();
|
$map = $this->getMethodMap();
|
||||||
$map->register("GET", $middlewareUpper);
|
$map->register('GET', $middlewareUpper);
|
||||||
$map->register("get", $middlewareLower);
|
$map->register('get', $middlewareLower);
|
||||||
$map($this->request, $this->response, $this->next);
|
$map($this->request, $this->response, $this->next);
|
||||||
|
|
||||||
$this->assertTrue($middlewareLower->called);
|
$this->assertTrue($middlewareLower->called);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testDispatchesWildcardMiddlewareWithNonMatchingMethod()
|
public function testDispatchesWildcardMiddlewareWithNonMatchingMethod(): void
|
||||||
{
|
{
|
||||||
$this->request = $this->request->withMethod("GET");
|
$this->request = $this->request->withMethod('GET');
|
||||||
|
|
||||||
$map = $this->getMethodMap();
|
$map = $this->getMethodMap();
|
||||||
$map->register("*", $this->middleware);
|
$map->register('*', $this->middleware);
|
||||||
$map($this->request, $this->response, $this->next);
|
$map($this->request, $this->response, $this->next);
|
||||||
|
|
||||||
$this->assertTrue($this->middleware->called);
|
$this->assertTrue($this->middleware->called);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testDispatchesGetMiddlewareForHeadByDefault()
|
public function testDispatchesGetMiddlewareForHeadByDefault(): void
|
||||||
{
|
{
|
||||||
$this->request = $this->request->withMethod("HEAD");
|
$this->request = $this->request->withMethod('HEAD');
|
||||||
|
|
||||||
$map = $this->getMethodMap();
|
$map = $this->getMethodMap();
|
||||||
$map->register("GET", $this->middleware);
|
$map->register('GET', $this->middleware);
|
||||||
$map($this->request, $this->response, $this->next);
|
$map($this->request, $this->response, $this->next);
|
||||||
|
|
||||||
$this->assertTrue($this->middleware->called);
|
$this->assertTrue($this->middleware->called);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testRegistersMiddlewareForMultipleMethods()
|
public function testRegistersMiddlewareForMultipleMethods(): void
|
||||||
{
|
{
|
||||||
$map = $this->getMethodMap();
|
$map = $this->getMethodMap();
|
||||||
$map->register("GET,POST", $this->middleware);
|
$map->register('GET,POST', $this->middleware);
|
||||||
|
|
||||||
$this->request = $this->request->withMethod("GET");
|
$this->request = $this->request->withMethod('GET');
|
||||||
$map($this->request, $this->response, $this->next);
|
$map($this->request, $this->response, $this->next);
|
||||||
|
|
||||||
$this->request = $this->request->withMethod("POST");
|
$this->request = $this->request->withMethod('POST');
|
||||||
$map($this->request, $this->response, $this->next);
|
$map($this->request, $this->response, $this->next);
|
||||||
|
|
||||||
$this->assertEquals(2, $this->middleware->callCount);
|
$this->assertEquals(2, $this->middleware->callCount);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testSettingNullDeregistersMiddleware()
|
public function testSettingNullUnregistersMiddleware(): void
|
||||||
{
|
{
|
||||||
$this->request = $this->request->withMethod("POST");
|
$this->request = $this->request->withMethod('POST');
|
||||||
|
|
||||||
$map = $this->getMethodMap();
|
$map = $this->getMethodMap();
|
||||||
$map->register("POST", $this->middleware);
|
$map->register('POST', $this->middleware);
|
||||||
$map->register("POST", null);
|
$map->register('POST', null);
|
||||||
$response = $map($this->request, $this->response, $this->next);
|
$response = $map($this->request, $this->response, $this->next);
|
||||||
|
|
||||||
$this->assertEquals(405, $response->getStatusCode());
|
$this->assertEquals(405, $response->getStatusCode());
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testSetsStatusTo200ForOptions()
|
public function testSetsStatusTo200ForOptions(): void
|
||||||
{
|
{
|
||||||
$this->request = $this->request->withMethod("OPTIONS");
|
$this->request = $this->request->withMethod('OPTIONS');
|
||||||
|
|
||||||
$map = $this->getMethodMap();
|
$map = $this->getMethodMap();
|
||||||
$map->register("GET", $this->middleware);
|
$map->register('GET', $this->middleware);
|
||||||
$response = $map($this->request, $this->response, $this->next);
|
$response = $map($this->request, $this->response, $this->next);
|
||||||
|
|
||||||
$this->assertEquals(200, $response->getStatusCode());
|
$this->assertEquals(200, $response->getStatusCode());
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testStopsPropagatingAfterOptions()
|
public function testStopsPropagatingAfterOptions(): void
|
||||||
{
|
{
|
||||||
$this->request = $this->request->withMethod("OPTIONS");
|
$this->request = $this->request->withMethod('OPTIONS');
|
||||||
|
|
||||||
$map = $this->getMethodMap();
|
$map = $this->getMethodMap();
|
||||||
$map->register("GET", $this->middleware);
|
$map->register('GET', $this->middleware);
|
||||||
$map($this->request, $this->response, $this->next);
|
$map($this->request, $this->response, $this->next);
|
||||||
|
|
||||||
$this->assertFalse($this->next->called);
|
$this->assertFalse($this->next->called);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @dataProvider allowedMethodProvider */
|
/**
|
||||||
public function testSetsAllowHeaderForOptions($methodsDeclared, $methodsAllowed)
|
* @dataProvider allowedMethodProvider
|
||||||
|
* @param string[] $methodsDeclared
|
||||||
|
* @param string[] $methodsAllowed
|
||||||
|
*/
|
||||||
|
public function testSetsAllowHeaderForOptions(array $methodsDeclared, array $methodsAllowed): void
|
||||||
{
|
{
|
||||||
$this->request = $this->request->withMethod("OPTIONS");
|
$this->request = $this->request->withMethod('OPTIONS');
|
||||||
|
|
||||||
$map = $this->getMethodMap();
|
$map = $this->getMethodMap();
|
||||||
foreach ($methodsDeclared as $method) {
|
foreach ($methodsDeclared as $method) {
|
||||||
|
|
@ -140,39 +144,38 @@ class MethodMapTest extends TestCase
|
||||||
}
|
}
|
||||||
$response = $map($this->request, $this->response, $this->next);
|
$response = $map($this->request, $this->response, $this->next);
|
||||||
|
|
||||||
$this->assertContainsEach($methodsAllowed, $response->getHeaderLine("Allow"));
|
$this->assertContainsEach($methodsAllowed, $response->getHeaderLine('Allow'));
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @dataProvider allowedMethodProvider */
|
public function testSetsStatusTo405ForBadMethod(): void
|
||||||
public function testSetsStatusTo405ForBadMethod()
|
|
||||||
{
|
{
|
||||||
$this->request = $this->request->withMethod("POST");
|
$this->request = $this->request->withMethod('POST');
|
||||||
|
|
||||||
$map = $this->getMethodMap();
|
$map = $this->getMethodMap();
|
||||||
$map->register("GET", $this->middleware);
|
$map->register('GET', $this->middleware);
|
||||||
$response = $map($this->request, $this->response, $this->next);
|
$response = $map($this->request, $this->response, $this->next);
|
||||||
|
|
||||||
$this->assertEquals(405, $response->getStatusCode());
|
$this->assertEquals(405, $response->getStatusCode());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
public function testStopsPropagatingAfterBadMethod(): void
|
||||||
* @coversNothing
|
|
||||||
* @dataProvider allowedMethodProvider
|
|
||||||
*/
|
|
||||||
public function testStopsPropagatingAfterBadMethod()
|
|
||||||
{
|
{
|
||||||
$this->request = $this->request->withMethod("POST");
|
$this->request = $this->request->withMethod('POST');
|
||||||
|
|
||||||
$map = $this->getMethodMap();
|
$map = $this->getMethodMap();
|
||||||
$map->register("GET", $this->middleware);
|
$map->register('GET', $this->middleware);
|
||||||
$map($this->request, $this->response, $this->next);
|
$map($this->request, $this->response, $this->next);
|
||||||
$this->assertFalse($this->next->called);
|
$this->assertFalse($this->next->called);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @dataProvider allowedMethodProvider */
|
/**
|
||||||
public function testSetsAllowHeaderForBadMethod($methodsDeclared, $methodsAllowed)
|
* @dataProvider allowedMethodProvider
|
||||||
|
* @param string[] $methodsDeclared
|
||||||
|
* @param string[] $methodsAllowed
|
||||||
|
*/
|
||||||
|
public function testSetsAllowHeaderForBadMethod(array $methodsDeclared, array $methodsAllowed): void
|
||||||
{
|
{
|
||||||
$this->request = $this->request->withMethod("BAD");
|
$this->request = $this->request->withMethod('BAD');
|
||||||
|
|
||||||
$map = $this->getMethodMap();
|
$map = $this->getMethodMap();
|
||||||
foreach ($methodsDeclared as $method) {
|
foreach ($methodsDeclared as $method) {
|
||||||
|
|
@ -180,21 +183,22 @@ class MethodMapTest extends TestCase
|
||||||
}
|
}
|
||||||
$response = $map($this->request, $this->response, $this->next);
|
$response = $map($this->request, $this->response, $this->next);
|
||||||
|
|
||||||
$this->assertContainsEach($methodsAllowed, $response->getHeaderLine("Allow"));
|
$this->assertContainsEach($methodsAllowed, $response->getHeaderLine('Allow'));
|
||||||
}
|
}
|
||||||
|
|
||||||
public function allowedMethodProvider()
|
public function allowedMethodProvider(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
[["GET"], ["GET", "HEAD", "OPTIONS"]],
|
[['GET'], ['GET', 'HEAD', 'OPTIONS']],
|
||||||
[["GET", "POST"], ["GET", "POST", "HEAD", "OPTIONS"]],
|
[['GET', 'POST'], ['GET', 'POST', 'HEAD', 'OPTIONS']],
|
||||||
[["POST"], ["POST", "OPTIONS"]],
|
[['POST'], ['POST', 'OPTIONS']],
|
||||||
[["POST"], ["POST", "OPTIONS"]],
|
[['POST'], ['POST', 'OPTIONS']],
|
||||||
[["GET", "PUT,DELETE"], ["GET", "PUT", "DELETE", "HEAD", "OPTIONS"]],
|
[['GET', 'PUT,DELETE'], ['GET', 'PUT', 'DELETE', 'HEAD', 'OPTIONS']],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
private function assertContainsEach($expectedList, $actual) {
|
private function assertContainsEach($expectedList, $actual): void
|
||||||
|
{
|
||||||
foreach ($expectedList as $expected) {
|
foreach ($expectedList as $expected) {
|
||||||
if (strpos($actual, $expected) === false) {
|
if (strpos($actual, $expected) === false) {
|
||||||
$this->assertTrue(false, "'$actual' does not contain expected '$expected'");
|
$this->assertTrue(false, "'$actual' does not contain expected '$expected'");
|
||||||
|
|
@ -1,50 +1,50 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace WellRESTed\Test\Unit\Routing\Route;
|
namespace WellRESTed\Routing\Route;
|
||||||
|
|
||||||
use WellRESTed\Routing\Route\MethodMap;
|
use Prophecy\PhpUnit\ProphecyTrait;
|
||||||
use WellRESTed\Routing\Route\PrefixRoute;
|
|
||||||
use WellRESTed\Routing\Route\RouteInterface;
|
|
||||||
use WellRESTed\Test\TestCase;
|
use WellRESTed\Test\TestCase;
|
||||||
|
|
||||||
class PrefixRouteTest extends TestCase
|
class PrefixRouteTest extends TestCase
|
||||||
{
|
{
|
||||||
public function testTrimsAsteriskFromEndOfTarget()
|
use ProphecyTrait;
|
||||||
|
|
||||||
|
public function testTrimsAsteriskFromEndOfTarget(): void
|
||||||
{
|
{
|
||||||
$methodMap = $this->prophesize(MethodMap::class);
|
$methodMap = $this->prophesize(MethodMap::class);
|
||||||
$route = new PrefixRoute('/cats/*', $methodMap->reveal());
|
$route = new PrefixRoute('/cats/*', $methodMap->reveal());
|
||||||
$this->assertEquals('/cats/', $route->getTarget());
|
$this->assertEquals('/cats/', $route->getTarget());
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testReturnsPrefixType()
|
public function testReturnsPrefixType(): void
|
||||||
{
|
{
|
||||||
$methodMap = $this->prophesize(MethodMap::class);
|
$methodMap = $this->prophesize(MethodMap::class);
|
||||||
$route = new PrefixRoute('/*', $methodMap->reveal());
|
$route = new PrefixRoute('/*', $methodMap->reveal());
|
||||||
$this->assertSame(RouteInterface::TYPE_PREFIX, $route->getType());
|
$this->assertSame(Route::TYPE_PREFIX, $route->getType());
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testReturnsEmptyArrayForPathVariables()
|
public function testReturnsEmptyArrayForPathVariables(): void
|
||||||
{
|
{
|
||||||
$methodMap = $this->prophesize(MethodMap::class);
|
$methodMap = $this->prophesize(MethodMap::class);
|
||||||
$route = new PrefixRoute('/*', $methodMap->reveal());
|
$route = new PrefixRoute('/*', $methodMap->reveal());
|
||||||
$this->assertSame([], $route->getPathVariables());
|
$this->assertSame([], $route->getPathVariables());
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testMatchesExactRequestTarget()
|
public function testMatchesExactRequestTarget(): void
|
||||||
{
|
{
|
||||||
$methodMap = $this->prophesize(MethodMap::class);
|
$methodMap = $this->prophesize(MethodMap::class);
|
||||||
$route = new PrefixRoute('/*', $methodMap->reveal());
|
$route = new PrefixRoute('/*', $methodMap->reveal());
|
||||||
$this->assertTrue($route->matchesRequestTarget('/'));
|
$this->assertTrue($route->matchesRequestTarget('/'));
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testMatchesRequestTargetWithSamePrefix()
|
public function testMatchesRequestTargetWithSamePrefix(): void
|
||||||
{
|
{
|
||||||
$methodMap = $this->prophesize(MethodMap::class);
|
$methodMap = $this->prophesize(MethodMap::class);
|
||||||
$route = new PrefixRoute('/*', $methodMap->reveal());
|
$route = new PrefixRoute('/*', $methodMap->reveal());
|
||||||
$this->assertTrue($route->matchesRequestTarget('/cats/'));
|
$this->assertTrue($route->matchesRequestTarget('/cats/'));
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testDoesNotMatchNonmatchingRequestTarget()
|
public function testDoesNotMatchNonMatchingRequestTarget(): void
|
||||||
{
|
{
|
||||||
$methodMap = $this->prophesize(MethodMap::class);
|
$methodMap = $this->prophesize(MethodMap::class);
|
||||||
$route = new PrefixRoute('/animals/cats/', $methodMap->reveal());
|
$route = new PrefixRoute('/animals/cats/', $methodMap->reveal());
|
||||||
|
|
@ -1,53 +1,64 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace WellRESTed\Test\Unit\Routing\Route;
|
namespace WellRESTed\Routing\Route;
|
||||||
|
|
||||||
use PHPUnit\Framework\Error\Notice;
|
use Prophecy\PhpUnit\ProphecyTrait;
|
||||||
use PHPUnit\Framework\Error\Warning;
|
|
||||||
use RuntimeException;
|
use RuntimeException;
|
||||||
use WellRESTed\Routing\Route\MethodMap;
|
|
||||||
use WellRESTed\Routing\Route\RegexRoute;
|
|
||||||
use WellRESTed\Routing\Route\RouteInterface;
|
|
||||||
use WellRESTed\Test\TestCase;
|
use WellRESTed\Test\TestCase;
|
||||||
|
|
||||||
class RegexRouteTest extends TestCase
|
class RegexRouteTest extends TestCase
|
||||||
{
|
{
|
||||||
|
use ProphecyTrait;
|
||||||
|
|
||||||
private $methodMap;
|
private $methodMap;
|
||||||
|
|
||||||
public function setUp(): void
|
protected function setUp(): void
|
||||||
{
|
{
|
||||||
$this->methodMap = $this->prophesize(MethodMap::class);
|
$this->methodMap = $this->prophesize(MethodMap::class);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testReturnsPatternType()
|
public function testReturnsPatternType(): void
|
||||||
{
|
{
|
||||||
$route = new RegexRoute('/', $this->methodMap->reveal());
|
$route = new RegexRoute('/', $this->methodMap->reveal());
|
||||||
$this->assertSame(RouteInterface::TYPE_PATTERN, $route->getType());
|
$this->assertSame(Route::TYPE_PATTERN, $route->getType());
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @dataProvider matchingRouteProvider */
|
/**
|
||||||
public function testMatchesTarget($pattern, $path)
|
* @dataProvider matchingRouteProvider
|
||||||
|
* @param string $pattern
|
||||||
|
* @param string $path
|
||||||
|
*/
|
||||||
|
public function testMatchesTarget(string $pattern, string $path): void
|
||||||
{
|
{
|
||||||
$route = new RegexRoute($pattern, $this->methodMap->reveal());
|
$route = new RegexRoute($pattern, $this->methodMap->reveal());
|
||||||
$this->assertTrue($route->matchesRequestTarget($path));
|
$this->assertTrue($route->matchesRequestTarget($path));
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @dataProvider matchingRouteProvider */
|
/**
|
||||||
public function testMatchesTargetByRegex($pattern, $target)
|
* @dataProvider matchingRouteProvider
|
||||||
|
* @param string $pattern
|
||||||
|
* @param string $path
|
||||||
|
*/
|
||||||
|
public function testMatchesTargetByRegex(string $pattern, string $path): void
|
||||||
{
|
{
|
||||||
$route = new RegexRoute($pattern, $this->methodMap->reveal());
|
$route = new RegexRoute($pattern, $this->methodMap->reveal());
|
||||||
$this->assertTrue($route->matchesRequestTarget($target));
|
$this->assertTrue($route->matchesRequestTarget($path));
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @dataProvider matchingRouteProvider */
|
/**
|
||||||
public function testExtractsPathVariablesByRegex($pattern, $target, $expectedCaptures)
|
* @dataProvider matchingRouteProvider
|
||||||
|
* @param string $pattern
|
||||||
|
* @param string $path
|
||||||
|
* @param array $expectedCaptures
|
||||||
|
*/
|
||||||
|
public function testExtractsPathVariablesByRegex(string $pattern, string $path, array $expectedCaptures): void
|
||||||
{
|
{
|
||||||
$route = new RegexRoute($pattern, $this->methodMap->reveal());
|
$route = new RegexRoute($pattern, $this->methodMap->reveal());
|
||||||
$route->matchesRequestTarget($target);
|
$route->matchesRequestTarget($path);
|
||||||
$this->assertEquals($expectedCaptures, $route->getPathVariables());
|
$this->assertEquals($expectedCaptures, $route->getPathVariables());
|
||||||
}
|
}
|
||||||
|
|
||||||
public function matchingRouteProvider()
|
public function matchingRouteProvider(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
['~/cat/[0-9]+~', '/cat/2', [0 => '/cat/2']],
|
['~/cat/[0-9]+~', '/cat/2', [0 => '/cat/2']],
|
||||||
|
|
@ -64,14 +75,18 @@ class RegexRouteTest extends TestCase
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @dataProvider mismatchingRouteProvider */
|
/**
|
||||||
public function testDoesNotMatchNonmatchingTarget($pattern, $path)
|
* @dataProvider mismatchingRouteProvider
|
||||||
|
* @param string $pattern
|
||||||
|
* @param string $path
|
||||||
|
*/
|
||||||
|
public function testDoesNotMatchNonMatchingTarget(string $pattern, string $path): void
|
||||||
{
|
{
|
||||||
$route = new RegexRoute($pattern, $this->methodMap->reveal());
|
$route = new RegexRoute($pattern, $this->methodMap->reveal());
|
||||||
$this->assertFalse($route->matchesRequestTarget($path));
|
$this->assertFalse($route->matchesRequestTarget($path));
|
||||||
}
|
}
|
||||||
|
|
||||||
public function mismatchingRouteProvider()
|
public function mismatchingRouteProvider(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
['~/cat/[0-9]+~', '/cat/molly'],
|
['~/cat/[0-9]+~', '/cat/molly'],
|
||||||
|
|
@ -82,8 +97,9 @@ class RegexRouteTest extends TestCase
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @dataProvider invalidRouteProvider
|
* @dataProvider invalidRouteProvider
|
||||||
|
* @param string $pattern
|
||||||
*/
|
*/
|
||||||
public function testThrowsExceptionOnInvalidPattern($pattern)
|
public function testThrowsExceptionOnInvalidPattern(string $pattern): void
|
||||||
{
|
{
|
||||||
$this->expectException(RuntimeException::class);
|
$this->expectException(RuntimeException::class);
|
||||||
$route = new RegexRoute($pattern, $this->methodMap->reveal());
|
$route = new RegexRoute($pattern, $this->methodMap->reveal());
|
||||||
|
|
@ -1,46 +1,47 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace WellRESTed\Test\Unit\Routing\Route;
|
namespace WellRESTed\Routing\Route;
|
||||||
|
|
||||||
|
use Prophecy\PhpUnit\ProphecyTrait;
|
||||||
use WellRESTed\Dispatching\DispatcherInterface;
|
use WellRESTed\Dispatching\DispatcherInterface;
|
||||||
use WellRESTed\Routing\Route\RouteFactory;
|
|
||||||
use WellRESTed\Routing\Route\RouteInterface;
|
|
||||||
use WellRESTed\Test\TestCase;
|
use WellRESTed\Test\TestCase;
|
||||||
|
|
||||||
class RouteFactoryTest extends TestCase
|
class RouteFactoryTest extends TestCase
|
||||||
{
|
{
|
||||||
|
use ProphecyTrait;
|
||||||
|
|
||||||
private $dispatcher;
|
private $dispatcher;
|
||||||
|
|
||||||
public function setUp(): void
|
protected function setUp(): void
|
||||||
{
|
{
|
||||||
$this->dispatcher = $this->prophesize(DispatcherInterface::class);
|
$this->dispatcher = $this->prophesize(DispatcherInterface::class);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testCreatesStaticRoute()
|
public function testCreatesStaticRoute(): void
|
||||||
{
|
{
|
||||||
$factory = new RouteFactory($this->dispatcher->reveal());
|
$factory = new RouteFactory($this->dispatcher->reveal());
|
||||||
$route = $factory->create('/cats/');
|
$route = $factory->create('/cats/');
|
||||||
$this->assertSame(RouteInterface::TYPE_STATIC, $route->getType());
|
$this->assertSame(Route::TYPE_STATIC, $route->getType());
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testCreatesPrefixRoute()
|
public function testCreatesPrefixRoute(): void
|
||||||
{
|
{
|
||||||
$factory = new RouteFactory($this->dispatcher->reveal());
|
$factory = new RouteFactory($this->dispatcher->reveal());
|
||||||
$route = $factory->create('/cats/*');
|
$route = $factory->create('/cats/*');
|
||||||
$this->assertSame(RouteInterface::TYPE_PREFIX, $route->getType());
|
$this->assertSame(Route::TYPE_PREFIX, $route->getType());
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testCreatesRegexRoute()
|
public function testCreatesRegexRoute(): void
|
||||||
{
|
{
|
||||||
$factory = new RouteFactory($this->dispatcher->reveal());
|
$factory = new RouteFactory($this->dispatcher->reveal());
|
||||||
$route = $factory->create('~/cat/[0-9]+~');
|
$route = $factory->create('~/cat/[0-9]+~');
|
||||||
$this->assertSame(RouteInterface::TYPE_PATTERN, $route->getType());
|
$this->assertSame(Route::TYPE_PATTERN, $route->getType());
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testCreatesTemplateRoute()
|
public function testCreatesTemplateRoute(): void
|
||||||
{
|
{
|
||||||
$factory = new RouteFactory($this->dispatcher->reveal());
|
$factory = new RouteFactory($this->dispatcher->reveal());
|
||||||
$route = $factory->create('/cat/{id}');
|
$route = $factory->create('/cat/{id}');
|
||||||
$this->assertSame(RouteInterface::TYPE_PATTERN, $route->getType());
|
$this->assertSame(Route::TYPE_PATTERN, $route->getType());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,40 +1,42 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace WellRESTed\Test\Unit\Routing\Route;
|
namespace WellRESTed\Routing\Route;
|
||||||
|
|
||||||
use Prophecy\Argument;
|
use Prophecy\Argument;
|
||||||
|
use Prophecy\PhpUnit\ProphecyTrait;
|
||||||
use Psr\Http\Server\RequestHandlerInterface;
|
use Psr\Http\Server\RequestHandlerInterface;
|
||||||
use WellRESTed\Message\Response;
|
use WellRESTed\Message\Response;
|
||||||
use WellRESTed\Message\ServerRequest;
|
use WellRESTed\Message\ServerRequest;
|
||||||
use WellRESTed\Routing\Route\MethodMap;
|
|
||||||
use WellRESTed\Routing\Route\StaticRoute;
|
|
||||||
use WellRESTed\Test\TestCase;
|
use WellRESTed\Test\TestCase;
|
||||||
|
|
||||||
class RouteTest extends TestCase
|
class RouteTest extends TestCase
|
||||||
{
|
{
|
||||||
const TARGET = '/target';
|
use ProphecyTrait;
|
||||||
|
|
||||||
|
private const TARGET = '/target';
|
||||||
|
|
||||||
private $methodMap;
|
private $methodMap;
|
||||||
private $route;
|
private $route;
|
||||||
|
|
||||||
public function setUp(): void
|
protected function setUp(): void
|
||||||
{
|
{
|
||||||
$this->methodMap = $this->prophesize(MethodMap::class);
|
$this->methodMap = $this->prophesize(MethodMap::class);
|
||||||
$this->methodMap->register(Argument::cetera())
|
$this->methodMap->register(Argument::cetera());
|
||||||
->willReturn();
|
|
||||||
$this->methodMap->__invoke(Argument::cetera())
|
$this->methodMap->__invoke(Argument::cetera())
|
||||||
->willReturn(new Response());
|
->willReturn(new Response());
|
||||||
|
|
||||||
$this->route = new StaticRoute(
|
$this->route = new StaticRoute(
|
||||||
self::TARGET, $this->methodMap->reveal());
|
self::TARGET,
|
||||||
|
$this->methodMap->reveal()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testReturnsTarget()
|
public function testReturnsTarget(): void
|
||||||
{
|
{
|
||||||
$this->assertSame(self::TARGET, $this->route->getTarget());
|
$this->assertSame(self::TARGET, $this->route->getTarget());
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testRegistersDispatchableWithMethodMap()
|
public function testRegistersDispatchableWithMethodMap(): void
|
||||||
{
|
{
|
||||||
$handler = $this->prophesize(RequestHandlerInterface::class)->reveal();
|
$handler = $this->prophesize(RequestHandlerInterface::class)->reveal();
|
||||||
|
|
||||||
|
|
@ -43,7 +45,7 @@ class RouteTest extends TestCase
|
||||||
$this->methodMap->register('GET', $handler)->shouldHaveBeenCalled();
|
$this->methodMap->register('GET', $handler)->shouldHaveBeenCalled();
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testDispatchesMethodMap()
|
public function testDispatchesMethodMap(): void
|
||||||
{
|
{
|
||||||
$request = new ServerRequest();
|
$request = new ServerRequest();
|
||||||
$response = new Response();
|
$response = new Response();
|
||||||
|
|
@ -51,7 +53,7 @@ class RouteTest extends TestCase
|
||||||
return $resp;
|
return $resp;
|
||||||
};
|
};
|
||||||
|
|
||||||
$this->route->__invoke($request, $response, $next);
|
call_user_func($this->route, $request, $response, $next);
|
||||||
|
|
||||||
$this->methodMap->__invoke(Argument::cetera())->shouldHaveBeenCalled();
|
$this->methodMap->__invoke(Argument::cetera())->shouldHaveBeenCalled();
|
||||||
}
|
}
|
||||||
|
|
@ -1,36 +1,36 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace WellRESTed\Test\Unit\Routing\Route;
|
namespace WellRESTed\Routing\Route;
|
||||||
|
|
||||||
use WellRESTed\Routing\Route\MethodMap;
|
use Prophecy\PhpUnit\ProphecyTrait;
|
||||||
use WellRESTed\Routing\Route\RouteInterface;
|
|
||||||
use WellRESTed\Routing\Route\StaticRoute;
|
|
||||||
use WellRESTed\Test\TestCase;
|
use WellRESTed\Test\TestCase;
|
||||||
|
|
||||||
class StaticRouteTest extends TestCase
|
class StaticRouteTest extends TestCase
|
||||||
{
|
{
|
||||||
public function testReturnsStaticType()
|
use ProphecyTrait;
|
||||||
|
|
||||||
|
public function testReturnsStaticType(): void
|
||||||
{
|
{
|
||||||
$methodMap = $this->prophesize(MethodMap::class);
|
$methodMap = $this->prophesize(MethodMap::class);
|
||||||
$route = new StaticRoute('/', $methodMap->reveal());
|
$route = new StaticRoute('/', $methodMap->reveal());
|
||||||
$this->assertSame(RouteInterface::TYPE_STATIC, $route->getType());
|
$this->assertSame(Route::TYPE_STATIC, $route->getType());
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testMatchesExactRequestTarget()
|
public function testMatchesExactRequestTarget(): void
|
||||||
{
|
{
|
||||||
$methodMap = $this->prophesize(MethodMap::class);
|
$methodMap = $this->prophesize(MethodMap::class);
|
||||||
$route = new StaticRoute('/', $methodMap->reveal());
|
$route = new StaticRoute('/', $methodMap->reveal());
|
||||||
$this->assertTrue($route->matchesRequestTarget('/'));
|
$this->assertTrue($route->matchesRequestTarget('/'));
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testReturnsEmptyArrayForPathVariables()
|
public function testReturnsEmptyArrayForPathVariables(): void
|
||||||
{
|
{
|
||||||
$methodMap = $this->prophesize(MethodMap::class);
|
$methodMap = $this->prophesize(MethodMap::class);
|
||||||
$route = new StaticRoute('/', $methodMap->reveal());
|
$route = new StaticRoute('/', $methodMap->reveal());
|
||||||
$this->assertSame([], $route->getPathVariables());
|
$this->assertSame([], $route->getPathVariables());
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testDoesNotMatchNonmatchingRequestTarget()
|
public function testDoesNotMatchNonMatchingRequestTarget(): void
|
||||||
{
|
{
|
||||||
$methodMap = $this->prophesize(MethodMap::class);
|
$methodMap = $this->prophesize(MethodMap::class);
|
||||||
$route = new StaticRoute('/', $methodMap->reveal());
|
$route = new StaticRoute('/', $methodMap->reveal());
|
||||||
|
|
@ -1,22 +1,22 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace WellRESTed\Test\Unit\Routing\Route;
|
namespace WellRESTed\Routing\Route;
|
||||||
|
|
||||||
use WellRESTed\Routing\Route\MethodMap;
|
use Prophecy\PhpUnit\ProphecyTrait;
|
||||||
use WellRESTed\Routing\Route\RouteInterface;
|
|
||||||
use WellRESTed\Routing\Route\TemplateRoute;
|
|
||||||
use WellRESTed\Test\TestCase;
|
use WellRESTed\Test\TestCase;
|
||||||
|
|
||||||
class TemplateRouteTest extends TestCase
|
class TemplateRouteTest extends TestCase
|
||||||
{
|
{
|
||||||
|
use ProphecyTrait;
|
||||||
|
|
||||||
private $methodMap;
|
private $methodMap;
|
||||||
|
|
||||||
public function setUp(): void
|
protected function setUp(): void
|
||||||
{
|
{
|
||||||
$this->methodMap = $this->prophesize(MethodMap::class);
|
$this->methodMap = $this->prophesize(MethodMap::class);
|
||||||
}
|
}
|
||||||
|
|
||||||
private function getExpectedValues($keys)
|
private function getExpectedValues(array $keys): array
|
||||||
{
|
{
|
||||||
$expectedValues = [
|
$expectedValues = [
|
||||||
'var' => 'value',
|
'var' => 'value',
|
||||||
|
|
@ -33,28 +33,32 @@ class TemplateRouteTest extends TestCase
|
||||||
return array_intersect_key($expectedValues, array_flip($keys));
|
return array_intersect_key($expectedValues, array_flip($keys));
|
||||||
}
|
}
|
||||||
|
|
||||||
private function assertArrayHasSameContents($expected, $actual)
|
private function assertArrayHasSameContents($expected, $actual): void
|
||||||
{
|
{
|
||||||
ksort($expected);
|
ksort($expected);
|
||||||
ksort($actual);
|
ksort($actual);
|
||||||
$this->assertEquals($expected, $actual);
|
$this->assertEquals($expected, $actual);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
|
|
||||||
public function testReturnsPatternType()
|
public function testReturnsPatternType(): void
|
||||||
{
|
{
|
||||||
$route = new TemplateRoute('/', $this->methodMap->reveal());
|
$route = new TemplateRoute('/', $this->methodMap->reveal());
|
||||||
$this->assertSame(RouteInterface::TYPE_PATTERN, $route->getType());
|
$this->assertSame(Route::TYPE_PATTERN, $route->getType());
|
||||||
}
|
}
|
||||||
|
|
||||||
// ------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
// Matching
|
// Matching
|
||||||
|
|
||||||
/** @dataProvider nonMatchingTargetProvider */
|
/**
|
||||||
public function testFailsToMatchNonMatchingTarget($template, $target)
|
* @dataProvider nonMatchingTargetProvider
|
||||||
|
* @param string $template
|
||||||
|
* @param string $target
|
||||||
|
*/
|
||||||
|
public function testFailsToMatchNonMatchingTarget(string $template, string $target): void
|
||||||
{
|
{
|
||||||
$route = new TemplateRoute($template, $this->methodMap);
|
$route = new TemplateRoute($template, $this->methodMap->reveal());
|
||||||
$this->assertFalse($route->matchesRequestTarget($target));
|
$this->assertFalse($route->matchesRequestTarget($target));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -68,25 +72,34 @@ class TemplateRouteTest extends TestCase
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
// ------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
// Matching :: Simple Strings
|
// Matching :: Simple Strings
|
||||||
|
|
||||||
/** @dataProvider simpleStringProvider */
|
/**
|
||||||
public function testMatchesSimpleStrings($template, $target)
|
* @dataProvider simpleStringProvider
|
||||||
|
* @param string $template
|
||||||
|
* @param string $target
|
||||||
|
*/
|
||||||
|
public function testMatchesSimpleStrings(string $template, string $target): void
|
||||||
{
|
{
|
||||||
$route = new TemplateRoute($template, $this->methodMap);
|
$route = new TemplateRoute($template, $this->methodMap->reveal());
|
||||||
$this->assertTrue($route->matchesRequestTarget($target));
|
$this->assertTrue($route->matchesRequestTarget($target));
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @dataProvider simpleStringProvider */
|
/**
|
||||||
public function testCapturesFromSimpleStrings($template, $target, $variables)
|
* @dataProvider simpleStringProvider
|
||||||
|
* @param string $template
|
||||||
|
* @param string $target
|
||||||
|
* @param string[] $variables
|
||||||
|
*/
|
||||||
|
public function testCapturesFromSimpleStrings(string $template, string $target, array $variables): void
|
||||||
{
|
{
|
||||||
$route = new TemplateRoute($template, $this->methodMap);
|
$route = new TemplateRoute($template, $this->methodMap->reveal());
|
||||||
$route->matchesRequestTarget($target);
|
$route->matchesRequestTarget($target);
|
||||||
$this->assertArrayHasSameContents($this->getExpectedValues($variables), $route->getPathVariables());
|
$this->assertArrayHasSameContents($this->getExpectedValues($variables), $route->getPathVariables());
|
||||||
}
|
}
|
||||||
|
|
||||||
public function simpleStringProvider()
|
public function simpleStringProvider(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
['/foo', '/foo', []],
|
['/foo', '/foo', []],
|
||||||
|
|
@ -97,25 +110,34 @@ class TemplateRouteTest extends TestCase
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
// ------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
// Matching :: Reserved
|
// Matching :: Reserved
|
||||||
|
|
||||||
/** @dataProvider reservedStringProvider */
|
/**
|
||||||
public function testMatchesReservedStrings($template, $target)
|
* @dataProvider reservedStringProvider
|
||||||
|
* @param string $template
|
||||||
|
* @param string $target
|
||||||
|
*/
|
||||||
|
public function testMatchesReservedStrings(string $template, string $target): void
|
||||||
{
|
{
|
||||||
$route = new TemplateRoute($template, $this->methodMap);
|
$route = new TemplateRoute($template, $this->methodMap->reveal());
|
||||||
$this->assertTrue($route->matchesRequestTarget($target));
|
$this->assertTrue($route->matchesRequestTarget($target));
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @dataProvider reservedStringProvider */
|
/**
|
||||||
public function testCapturesFromReservedStrings($template, $target, $variables)
|
* @dataProvider reservedStringProvider
|
||||||
|
* @param string $template
|
||||||
|
* @param string $target
|
||||||
|
* @param array $variables
|
||||||
|
*/
|
||||||
|
public function testCapturesFromReservedStrings(string $template, string $target, array $variables): void
|
||||||
{
|
{
|
||||||
$route = new TemplateRoute($template, $this->methodMap);
|
$route = new TemplateRoute($template, $this->methodMap->reveal());
|
||||||
$route->matchesRequestTarget($target);
|
$route->matchesRequestTarget($target);
|
||||||
$this->assertSame($this->getExpectedValues($variables), $route->getPathVariables());
|
$this->assertSame($this->getExpectedValues($variables), $route->getPathVariables());
|
||||||
}
|
}
|
||||||
|
|
||||||
public function reservedStringProvider()
|
public function reservedStringProvider(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
['/{+var}', '/value', ['var']],
|
['/{+var}', '/value', ['var']],
|
||||||
|
|
@ -124,25 +146,34 @@ class TemplateRouteTest extends TestCase
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
// ------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
// Matching :: Label Expansion
|
// Matching :: Label Expansion
|
||||||
|
|
||||||
/** @dataProvider labelWithDotPrefixProvider */
|
/**
|
||||||
public function testMatchesLabelWithDotPrefix($template, $target)
|
* @dataProvider labelWithDotPrefixProvider
|
||||||
|
* @param string $template
|
||||||
|
* @param string $target
|
||||||
|
*/
|
||||||
|
public function testMatchesLabelWithDotPrefix(string $template, string $target): void
|
||||||
{
|
{
|
||||||
$route = new TemplateRoute($template, $this->methodMap);
|
$route = new TemplateRoute($template, $this->methodMap->reveal());
|
||||||
$this->assertTrue($route->matchesRequestTarget($target));
|
$this->assertTrue($route->matchesRequestTarget($target));
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @dataProvider labelWithDotPrefixProvider */
|
/**
|
||||||
public function testCapturesFromLabelWithDotPrefix($template, $target, $variables)
|
* @dataProvider labelWithDotPrefixProvider
|
||||||
|
* @param string $template
|
||||||
|
* @param string $target
|
||||||
|
* @param array $variables
|
||||||
|
*/
|
||||||
|
public function testCapturesFromLabelWithDotPrefix(string $template, string $target, array $variables): void
|
||||||
{
|
{
|
||||||
$route = new TemplateRoute($template, $this->methodMap);
|
$route = new TemplateRoute($template, $this->methodMap->reveal());
|
||||||
$route->matchesRequestTarget($target);
|
$route->matchesRequestTarget($target);
|
||||||
$this->assertArrayHasSameContents($this->getExpectedValues($variables), $route->getPathVariables());
|
$this->assertArrayHasSameContents($this->getExpectedValues($variables), $route->getPathVariables());
|
||||||
}
|
}
|
||||||
|
|
||||||
public function labelWithDotPrefixProvider()
|
public function labelWithDotPrefixProvider(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
['/{.who}', '/.fred', ['who']],
|
['/{.who}', '/.fred', ['who']],
|
||||||
|
|
@ -151,25 +182,34 @@ class TemplateRouteTest extends TestCase
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
// ------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
// Matching :: Path Segments
|
// Matching :: Path Segments
|
||||||
|
|
||||||
/** @dataProvider pathSegmentProvider */
|
/**
|
||||||
public function testMatchesPathSegments($template, $target)
|
* @dataProvider pathSegmentProvider
|
||||||
|
* @param string $template
|
||||||
|
* @param string $target
|
||||||
|
*/
|
||||||
|
public function testMatchesPathSegments(string $template, string $target): void
|
||||||
{
|
{
|
||||||
$route = new TemplateRoute($template, $this->methodMap);
|
$route = new TemplateRoute($template, $this->methodMap->reveal());
|
||||||
$this->assertTrue($route->matchesRequestTarget($target));
|
$this->assertTrue($route->matchesRequestTarget($target));
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @dataProvider pathSegmentProvider */
|
/**
|
||||||
public function testCapturesFromPathSegments($template, $target, $variables)
|
* @dataProvider pathSegmentProvider
|
||||||
|
* @param string $template
|
||||||
|
* @param string $target
|
||||||
|
* @param array $variables
|
||||||
|
*/
|
||||||
|
public function testCapturesFromPathSegments(string $template, string $target, array $variables): void
|
||||||
{
|
{
|
||||||
$route = new TemplateRoute($template, $this->methodMap);
|
$route = new TemplateRoute($template, $this->methodMap->reveal());
|
||||||
$route->matchesRequestTarget($target);
|
$route->matchesRequestTarget($target);
|
||||||
$this->assertArrayHasSameContents($this->getExpectedValues($variables), $route->getPathVariables());
|
$this->assertArrayHasSameContents($this->getExpectedValues($variables), $route->getPathVariables());
|
||||||
}
|
}
|
||||||
|
|
||||||
public function pathSegmentProvider()
|
public function pathSegmentProvider(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
['{/who}', '/fred', ['who']],
|
['{/who}', '/fred', ['who']],
|
||||||
|
|
@ -178,25 +218,34 @@ class TemplateRouteTest extends TestCase
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
// ------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
// Matching :: Explosion
|
// Matching :: Explosion
|
||||||
|
|
||||||
/** @dataProvider pathExplosionProvider */
|
/**
|
||||||
public function testMatchesExplosion($template, $target)
|
* @dataProvider pathExplosionProvider
|
||||||
|
* @param string $template
|
||||||
|
* @param string $target
|
||||||
|
*/
|
||||||
|
public function testMatchesExplosion(string $template, string $target): void
|
||||||
{
|
{
|
||||||
$route = new TemplateRoute($template, $this->methodMap);
|
$route = new TemplateRoute($template, $this->methodMap->reveal());
|
||||||
$this->assertTrue($route->matchesRequestTarget($target));
|
$this->assertTrue($route->matchesRequestTarget($target));
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @dataProvider pathExplosionProvider */
|
/**
|
||||||
public function testCapturesFromExplosion($template, $target, $variables)
|
* @dataProvider pathExplosionProvider
|
||||||
|
* @param string $template
|
||||||
|
* @param string $target
|
||||||
|
* @param array $variables
|
||||||
|
*/
|
||||||
|
public function testCapturesFromExplosion(string $template, string $target, array $variables): void
|
||||||
{
|
{
|
||||||
$route = new TemplateRoute($template, $this->methodMap);
|
$route = new TemplateRoute($template, $this->methodMap->reveal());
|
||||||
$route->matchesRequestTarget($target);
|
$route->matchesRequestTarget($target);
|
||||||
$this->assertArrayHasSameContents($this->getExpectedValues($variables), $route->getPathVariables());
|
$this->assertArrayHasSameContents($this->getExpectedValues($variables), $route->getPathVariables());
|
||||||
}
|
}
|
||||||
|
|
||||||
public function pathExplosionProvider()
|
public function pathExplosionProvider(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
['/{count*}', '/one,two,three', ['count']],
|
['/{count*}', '/one,two,three', ['count']],
|
||||||
|
|
@ -0,0 +1,491 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace WellRESTed\Routing;
|
||||||
|
|
||||||
|
use Prophecy\Argument;
|
||||||
|
use Prophecy\PhpUnit\ProphecyTrait;
|
||||||
|
use Psr\Http\Message\ResponseInterface;
|
||||||
|
use WellRESTed\Dispatching\Dispatcher;
|
||||||
|
use WellRESTed\Message\Response;
|
||||||
|
use WellRESTed\Message\ServerRequest;
|
||||||
|
use WellRESTed\Routing\Route\Route;
|
||||||
|
use WellRESTed\Routing\Route\RouteFactory;
|
||||||
|
use WellRESTed\Test\Doubles\NextMock;
|
||||||
|
use WellRESTed\Test\TestCase;
|
||||||
|
|
||||||
|
class RouterTest extends TestCase
|
||||||
|
{
|
||||||
|
use ProphecyTrait;
|
||||||
|
|
||||||
|
private $factory;
|
||||||
|
private $request;
|
||||||
|
private $response;
|
||||||
|
private $route;
|
||||||
|
private $router;
|
||||||
|
private $next;
|
||||||
|
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
parent::setUp();
|
||||||
|
|
||||||
|
$this->route = $this->prophesize(Route::class);
|
||||||
|
$this->route->__invoke(Argument::cetera())->willReturn(new Response());
|
||||||
|
$this->route->register(Argument::cetera());
|
||||||
|
$this->route->getType()->willReturn(Route::TYPE_STATIC);
|
||||||
|
$this->route->getTarget()->willReturn('/');
|
||||||
|
$this->route->getPathVariables()->willReturn([]);
|
||||||
|
|
||||||
|
$this->factory = $this->prophesize(RouteFactory::class);
|
||||||
|
$this->factory->create(Argument::any())
|
||||||
|
->willReturn($this->route->reveal());
|
||||||
|
|
||||||
|
$this->router = new Router(null, null, $this->factory->reveal());
|
||||||
|
|
||||||
|
$this->request = new ServerRequest();
|
||||||
|
$this->response = new Response();
|
||||||
|
$this->next = new NextMock();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Run a request through the class under test and return the response.
|
||||||
|
*
|
||||||
|
* @return ResponseInterface
|
||||||
|
*/
|
||||||
|
private function dispatch(): ResponseInterface
|
||||||
|
{
|
||||||
|
return call_user_func(
|
||||||
|
$this->router,
|
||||||
|
$this->request,
|
||||||
|
$this->response,
|
||||||
|
$this->next
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
// Populating
|
||||||
|
|
||||||
|
public function testCreatesRouteForTarget(): void
|
||||||
|
{
|
||||||
|
$this->router->register('GET', '/', 'middleware');
|
||||||
|
|
||||||
|
$this->factory->create('/')->shouldHaveBeenCalled();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testDoesNotRecreateRouteForExistingTarget(): void
|
||||||
|
{
|
||||||
|
$this->router->register('GET', '/', 'middleware');
|
||||||
|
$this->router->register('POST', '/', 'middleware');
|
||||||
|
|
||||||
|
$this->factory->create('/')->shouldHaveBeenCalledTimes(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testPassesMethodAndMiddlewareToRoute(): void
|
||||||
|
{
|
||||||
|
$this->router->register('GET', '/', 'middleware');
|
||||||
|
|
||||||
|
$this->route->register('GET', 'middleware')->shouldHaveBeenCalled();
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
// Dispatching
|
||||||
|
|
||||||
|
public function testDispatchesStaticRoute(): void
|
||||||
|
{
|
||||||
|
$target = '/';
|
||||||
|
$this->request = $this->request->withRequestTarget($target);
|
||||||
|
|
||||||
|
$this->route->getTarget()->willReturn($target);
|
||||||
|
$this->route->getType()->willReturn(Route::TYPE_STATIC);
|
||||||
|
|
||||||
|
$this->router->register('GET', $target, 'middleware');
|
||||||
|
|
||||||
|
$this->dispatch();
|
||||||
|
|
||||||
|
$this->route->__invoke(Argument::cetera())
|
||||||
|
->shouldHaveBeenCalled();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testDispatchesPrefixRoute(): void
|
||||||
|
{
|
||||||
|
$target = '/animals/cats/*';
|
||||||
|
$this->request = $this->request->withRequestTarget('/animals/cats/molly');
|
||||||
|
|
||||||
|
$this->route->getTarget()->willReturn($target);
|
||||||
|
$this->route->getType()->willReturn(Route::TYPE_PREFIX);
|
||||||
|
|
||||||
|
$this->router->register('GET', $target, 'middleware');
|
||||||
|
|
||||||
|
$this->dispatch();
|
||||||
|
|
||||||
|
$this->route->__invoke(Argument::cetera())
|
||||||
|
->shouldHaveBeenCalled();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testDispatchesPatternRoute(): void
|
||||||
|
{
|
||||||
|
$target = '/';
|
||||||
|
$this->request = $this->request->withRequestTarget($target);
|
||||||
|
|
||||||
|
$this->route->getTarget()->willReturn($target);
|
||||||
|
$this->route->getType()->willReturn(Route::TYPE_PATTERN);
|
||||||
|
$this->route->matchesRequestTarget(Argument::cetera())->willReturn(true);
|
||||||
|
|
||||||
|
$this->router->register('GET', $target, 'middleware');
|
||||||
|
|
||||||
|
$this->dispatch();
|
||||||
|
|
||||||
|
$this->route->__invoke(Argument::cetera())
|
||||||
|
->shouldHaveBeenCalled();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testDispatchesStaticRouteBeforePrefixRoute(): void
|
||||||
|
{
|
||||||
|
$staticRoute = $this->prophesize(Route::class);
|
||||||
|
$staticRoute->register(Argument::cetera());
|
||||||
|
$staticRoute->getTarget()->willReturn('/cats/');
|
||||||
|
$staticRoute->getType()->willReturn(Route::TYPE_STATIC);
|
||||||
|
$staticRoute->__invoke(Argument::cetera())->willReturn(new Response());
|
||||||
|
|
||||||
|
$prefixRoute = $this->prophesize(Route::class);
|
||||||
|
$prefixRoute->register(Argument::cetera());
|
||||||
|
$prefixRoute->getTarget()->willReturn('/cats/*');
|
||||||
|
$prefixRoute->getType()->willReturn(Route::TYPE_PREFIX);
|
||||||
|
$prefixRoute->__invoke(Argument::cetera())->willReturn(new Response());
|
||||||
|
|
||||||
|
$this->request = $this->request->withRequestTarget('/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->dispatch();
|
||||||
|
|
||||||
|
$staticRoute->__invoke(Argument::cetera())
|
||||||
|
->shouldHaveBeenCalled();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testDispatchesLongestMatchingPrefixRoute(): void
|
||||||
|
{
|
||||||
|
// Note: The longest route is also good for 2 points in Settlers of Catan.
|
||||||
|
|
||||||
|
$shortRoute = $this->prophesize(Route::class);
|
||||||
|
$shortRoute->register(Argument::cetera());
|
||||||
|
$shortRoute->getTarget()->willReturn('/animals/*');
|
||||||
|
$shortRoute->getType()->willReturn(Route::TYPE_PREFIX);
|
||||||
|
$shortRoute->__invoke(Argument::cetera())->willReturn(new Response());
|
||||||
|
|
||||||
|
$longRoute = $this->prophesize(Route::class);
|
||||||
|
$longRoute->register(Argument::cetera());
|
||||||
|
$longRoute->getTarget()->willReturn('/animals/cats/*');
|
||||||
|
$longRoute->getType()->willReturn(Route::TYPE_PREFIX);
|
||||||
|
$longRoute->__invoke(Argument::cetera())->willReturn(new Response());
|
||||||
|
|
||||||
|
$this->request = $this->request
|
||||||
|
->withRequestTarget('/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->dispatch();
|
||||||
|
|
||||||
|
$longRoute->__invoke(Argument::cetera())
|
||||||
|
->shouldHaveBeenCalled();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testDispatchesPrefixRouteBeforePatternRoute(): void
|
||||||
|
{
|
||||||
|
$prefixRoute = $this->prophesize(Route::class);
|
||||||
|
$prefixRoute->register(Argument::cetera());
|
||||||
|
$prefixRoute->getTarget()->willReturn('/cats/*');
|
||||||
|
$prefixRoute->getType()->willReturn(Route::TYPE_PREFIX);
|
||||||
|
$prefixRoute->__invoke(Argument::cetera())->willReturn(new Response());
|
||||||
|
|
||||||
|
$patternRoute = $this->prophesize(Route::class);
|
||||||
|
$patternRoute->register(Argument::cetera());
|
||||||
|
$patternRoute->getTarget()->willReturn('/cats/{id}');
|
||||||
|
$patternRoute->getType()->willReturn(Route::TYPE_PATTERN);
|
||||||
|
$patternRoute->__invoke(Argument::cetera())->willReturn(new Response());
|
||||||
|
|
||||||
|
$this->request = $this->request->withRequestTarget('/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->dispatch();
|
||||||
|
|
||||||
|
$prefixRoute->__invoke(Argument::cetera())
|
||||||
|
->shouldHaveBeenCalled();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testDispatchesFirstMatchingPatternRoute(): void
|
||||||
|
{
|
||||||
|
$patternRoute1 = $this->prophesize(Route::class);
|
||||||
|
$patternRoute1->register(Argument::cetera());
|
||||||
|
$patternRoute1->getTarget()->willReturn('/cats/{id}');
|
||||||
|
$patternRoute1->getType()->willReturn(Route::TYPE_PATTERN);
|
||||||
|
$patternRoute1->getPathVariables()->willReturn([]);
|
||||||
|
$patternRoute1->matchesRequestTarget(Argument::any())->willReturn(true);
|
||||||
|
$patternRoute1->__invoke(Argument::cetera())->willReturn(new Response());
|
||||||
|
|
||||||
|
$patternRoute2 = $this->prophesize(Route::class);
|
||||||
|
$patternRoute2->register(Argument::cetera());
|
||||||
|
$patternRoute2->getTarget()->willReturn('/cats/{name}');
|
||||||
|
$patternRoute2->getType()->willReturn(Route::TYPE_PATTERN);
|
||||||
|
$patternRoute2->getPathVariables()->willReturn([]);
|
||||||
|
$patternRoute2->matchesRequestTarget(Argument::any())->willReturn(true);
|
||||||
|
$patternRoute2->__invoke(Argument::cetera())->willReturn(new Response());
|
||||||
|
|
||||||
|
$this->request = $this->request->withRequestTarget('/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->dispatch();
|
||||||
|
|
||||||
|
$patternRoute1->__invoke(Argument::cetera())
|
||||||
|
->shouldHaveBeenCalled();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testStopsTestingPatternsAfterFirstSuccessfulMatch(): void
|
||||||
|
{
|
||||||
|
$patternRoute1 = $this->prophesize(Route::class);
|
||||||
|
$patternRoute1->register(Argument::cetera());
|
||||||
|
$patternRoute1->getTarget()->willReturn('/cats/{id}');
|
||||||
|
$patternRoute1->getType()->willReturn(Route::TYPE_PATTERN);
|
||||||
|
$patternRoute1->getPathVariables()->willReturn([]);
|
||||||
|
$patternRoute1->matchesRequestTarget(Argument::any())->willReturn(true);
|
||||||
|
$patternRoute1->__invoke(Argument::cetera())->willReturn(new Response());
|
||||||
|
|
||||||
|
$patternRoute2 = $this->prophesize(Route::class);
|
||||||
|
$patternRoute2->register(Argument::cetera());
|
||||||
|
$patternRoute2->getTarget()->willReturn('/cats/{name}');
|
||||||
|
$patternRoute2->getType()->willReturn(Route::TYPE_PATTERN);
|
||||||
|
$patternRoute2->getPathVariables()->willReturn([]);
|
||||||
|
$patternRoute2->matchesRequestTarget(Argument::any())->willReturn(true);
|
||||||
|
$patternRoute2->__invoke(Argument::cetera())->willReturn(new Response());
|
||||||
|
|
||||||
|
$this->request = $this->request->withRequestTarget('/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->dispatch();
|
||||||
|
|
||||||
|
$patternRoute2->matchesRequestTarget(Argument::any())
|
||||||
|
->shouldNotHaveBeenCalled();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testMatchesPathAgainstRouteWithoutQuery(): void
|
||||||
|
{
|
||||||
|
$path = '/my/path';
|
||||||
|
$target = $path . '?cat=molly&dog=bear';
|
||||||
|
|
||||||
|
$this->request = $this->request->withRequestTarget($target);
|
||||||
|
|
||||||
|
$this->route->getTarget()->willReturn($target);
|
||||||
|
$this->route->getType()->willReturn(Route::TYPE_PATTERN);
|
||||||
|
$this->route->matchesRequestTarget(Argument::cetera())->willReturn(true);
|
||||||
|
|
||||||
|
$this->router->register('GET', $path, 'middleware');
|
||||||
|
|
||||||
|
$this->dispatch();
|
||||||
|
|
||||||
|
$this->route->matchesRequestTarget($path)->shouldHaveBeenCalled();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testMatchesPathWithDuplicateLeadingSlashes(): void
|
||||||
|
{
|
||||||
|
$path = '//my/path';
|
||||||
|
|
||||||
|
$this->request = $this->request->withRequestTarget($path);
|
||||||
|
|
||||||
|
$this->route->getTarget()->willReturn($path);
|
||||||
|
$this->route->getType()->willReturn(Route::TYPE_PATTERN);
|
||||||
|
$this->route->matchesRequestTarget(Argument::cetera())->willReturn(true);
|
||||||
|
|
||||||
|
$this->router->register('GET', $path, 'middleware');
|
||||||
|
|
||||||
|
$this->dispatch();
|
||||||
|
|
||||||
|
$this->route->matchesRequestTarget($path)->shouldHaveBeenCalled();
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
// Path Variables
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dataProvider pathVariableProvider
|
||||||
|
* @param string $name
|
||||||
|
* @param string $value
|
||||||
|
*/
|
||||||
|
public function testSetPathVariablesAttributeIndividually(string $name, string $value): void
|
||||||
|
{
|
||||||
|
$target = '/';
|
||||||
|
$variables = [
|
||||||
|
'id' => '1024',
|
||||||
|
'name' => 'Molly'
|
||||||
|
];
|
||||||
|
|
||||||
|
$this->request = $this->request->withRequestTarget($target);
|
||||||
|
|
||||||
|
$this->route->getTarget()->willReturn($target);
|
||||||
|
$this->route->getType()->willReturn(Route::TYPE_PATTERN);
|
||||||
|
$this->route->matchesRequestTarget(Argument::cetera())->willReturn(true);
|
||||||
|
$this->route->getPathVariables()->willReturn($variables);
|
||||||
|
|
||||||
|
$this->router->register('GET', $target, 'middleware');
|
||||||
|
|
||||||
|
$this->dispatch();
|
||||||
|
|
||||||
|
$isRequestWithExpectedAttribute = function ($request) use ($name, $value) {
|
||||||
|
return $request->getAttribute($name) === $value;
|
||||||
|
};
|
||||||
|
|
||||||
|
$this->route->__invoke(
|
||||||
|
Argument::that($isRequestWithExpectedAttribute),
|
||||||
|
Argument::cetera()
|
||||||
|
)->shouldHaveBeenCalled();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function pathVariableProvider(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
['id', '1024'],
|
||||||
|
['name', 'Molly']
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testSetPathVariablesAttributeAsArray(): void
|
||||||
|
{
|
||||||
|
$attributeName = 'pathVariables';
|
||||||
|
|
||||||
|
$target = '/';
|
||||||
|
$variables = [
|
||||||
|
'id' => '1024',
|
||||||
|
'name' => 'Molly'
|
||||||
|
];
|
||||||
|
|
||||||
|
$this->request = $this->request->withRequestTarget($target);
|
||||||
|
|
||||||
|
$this->route->getTarget()->willReturn($target);
|
||||||
|
$this->route->getType()->willReturn(Route::TYPE_PATTERN);
|
||||||
|
$this->route->matchesRequestTarget(Argument::cetera())->willReturn(true);
|
||||||
|
$this->route->getPathVariables()->willReturn($variables);
|
||||||
|
|
||||||
|
$this->router = new Router(
|
||||||
|
$attributeName,
|
||||||
|
new Dispatcher(),
|
||||||
|
$this->factory->reveal()
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->router->register('GET', $target, 'middleware');
|
||||||
|
|
||||||
|
$this->dispatch();
|
||||||
|
|
||||||
|
$isRequestWithExpectedAttribute = function ($request) use ($attributeName, $variables) {
|
||||||
|
return $request->getAttribute($attributeName) === $variables;
|
||||||
|
};
|
||||||
|
|
||||||
|
$this->route->__invoke(
|
||||||
|
Argument::that($isRequestWithExpectedAttribute),
|
||||||
|
Argument::cetera()
|
||||||
|
)->shouldHaveBeenCalled();
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
// No Match
|
||||||
|
|
||||||
|
public function testWhenNoRouteMatchesByDefaultResponds404(): void
|
||||||
|
{
|
||||||
|
$this->request = $this->request->withRequestTarget('/no/match');
|
||||||
|
|
||||||
|
$response = $this->dispatch();
|
||||||
|
|
||||||
|
$this->assertEquals(404, $response->getStatusCode());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testWhenNoRouteMatchesByDefaultDoesNotPropagatesToNextMiddleware(): void
|
||||||
|
{
|
||||||
|
$this->request = $this->request->withRequestTarget('/no/match');
|
||||||
|
|
||||||
|
$this->dispatch();
|
||||||
|
|
||||||
|
$this->assertFalse($this->next->called);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testWhenNoRouteMatchesAndContinueModePropagatesToNextMiddleware(): void
|
||||||
|
{
|
||||||
|
$this->request = $this->request->withRequestTarget('/no/match');
|
||||||
|
$this->router->continueOnNotFound();
|
||||||
|
|
||||||
|
$this->dispatch();
|
||||||
|
|
||||||
|
$this->assertTrue($this->next->called);
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
// Middleware for Routes
|
||||||
|
|
||||||
|
public function testCallsRouterMiddlewareBeforeRouteMiddleware(): void
|
||||||
|
{
|
||||||
|
$middlewareRequest = new ServerRequest();
|
||||||
|
$middlewareResponse = new Response();
|
||||||
|
|
||||||
|
$middleware = function ($rqst, $resp, $next) use (
|
||||||
|
$middlewareRequest,
|
||||||
|
$middlewareResponse
|
||||||
|
) {
|
||||||
|
return $next($middlewareRequest, $middlewareResponse);
|
||||||
|
};
|
||||||
|
|
||||||
|
$this->router->add($middleware);
|
||||||
|
$this->router->register('GET', '/', 'Handler');
|
||||||
|
|
||||||
|
$this->dispatch();
|
||||||
|
|
||||||
|
$this->route->__invoke(
|
||||||
|
$middlewareRequest,
|
||||||
|
$middlewareResponse,
|
||||||
|
Argument::any()
|
||||||
|
)->shouldHaveBeenCalled();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testDoesNotCallRouterMiddlewareWhenNoRouteMatches(): void
|
||||||
|
{
|
||||||
|
$middlewareCalled = false;
|
||||||
|
$middlewareRequest = new ServerRequest();
|
||||||
|
$middlewareResponse = new Response();
|
||||||
|
|
||||||
|
$middleware = function ($rqst, $resp, $next) use (
|
||||||
|
$middlewareRequest,
|
||||||
|
$middlewareResponse,
|
||||||
|
&$middlewareCalled
|
||||||
|
) {
|
||||||
|
$middlewareCalled = true;
|
||||||
|
return $next($middlewareRequest, $middlewareResponse);
|
||||||
|
};
|
||||||
|
|
||||||
|
$this->request = $this->request->withRequestTarget('/no/match');
|
||||||
|
|
||||||
|
$this->router->add($middleware);
|
||||||
|
$this->router->register('GET', '/', 'Handler');
|
||||||
|
|
||||||
|
$this->dispatch();
|
||||||
|
|
||||||
|
$this->assertFalse($middlewareCalled);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace WellRESTed\Test\Integration;
|
namespace WellRESTed\Routing;
|
||||||
|
|
||||||
use Psr\Http\Message\ResponseInterface;
|
use Psr\Http\Message\ResponseInterface;
|
||||||
use Psr\Http\Message\ServerRequestInterface;
|
use Psr\Http\Message\ServerRequestInterface;
|
||||||
|
|
@ -13,7 +13,10 @@ use WellRESTed\Server;
|
||||||
use WellRESTed\Test\TestCase;
|
use WellRESTed\Test\TestCase;
|
||||||
use WellRESTed\Transmission\TransmitterInterface;
|
use WellRESTed\Transmission\TransmitterInterface;
|
||||||
|
|
||||||
/** @coversNothing */
|
/**
|
||||||
|
* Integration test for Routing components
|
||||||
|
* @coversNothing
|
||||||
|
*/
|
||||||
class RoutingTest extends TestCase
|
class RoutingTest extends TestCase
|
||||||
{
|
{
|
||||||
/** @var Server */
|
/** @var Server */
|
||||||
|
|
@ -25,7 +28,7 @@ class RoutingTest extends TestCase
|
||||||
/** @var ResponseInterface */
|
/** @var ResponseInterface */
|
||||||
private $response;
|
private $response;
|
||||||
|
|
||||||
public function setUp(): void
|
protected function setUp(): void
|
||||||
{
|
{
|
||||||
parent::setUp();
|
parent::setUp();
|
||||||
|
|
||||||
|
|
@ -108,7 +111,9 @@ class RoutingTest extends TestCase
|
||||||
->register('GET', '/oscar', new StringHandler('Oscar'));
|
->register('GET', '/oscar', new StringHandler('Oscar'));
|
||||||
|
|
||||||
$this->server->add(new HeaderAdderMiddleware(
|
$this->server->add(new HeaderAdderMiddleware(
|
||||||
'Content-type', 'application/cat'));
|
'Content-type',
|
||||||
|
'application/cat'
|
||||||
|
));
|
||||||
$this->server->add($router);
|
$this->server->add($router);
|
||||||
|
|
||||||
$this->request = $this->request
|
$this->request = $this->request
|
||||||
|
|
@ -118,15 +123,19 @@ class RoutingTest extends TestCase
|
||||||
$response = $this->respond();
|
$response = $this->respond();
|
||||||
|
|
||||||
$this->assertEquals('Molly', (string) $response->getBody());
|
$this->assertEquals('Molly', (string) $response->getBody());
|
||||||
$this->assertEquals('application/cat',
|
$this->assertEquals(
|
||||||
$response->getHeaderLine('Content-type'));
|
'application/cat',
|
||||||
|
$response->getHeaderLine('Content-type')
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testDispatchesMiddlewareSpecificToRouter()
|
public function testDispatchesMiddlewareSpecificToRouter()
|
||||||
{
|
{
|
||||||
$catRouter = $this->server->createRouter()
|
$catRouter = $this->server->createRouter()
|
||||||
->add(new HeaderAdderMiddleware(
|
->add(new HeaderAdderMiddleware(
|
||||||
'Content-type', 'application/cat'))
|
'Content-type',
|
||||||
|
'application/cat'
|
||||||
|
))
|
||||||
->register('GET', '/molly', new StringHandler('Molly'))
|
->register('GET', '/molly', new StringHandler('Molly'))
|
||||||
->register('GET', '/oscar', new StringHandler('Oscar'))
|
->register('GET', '/oscar', new StringHandler('Oscar'))
|
||||||
->continueOnNotFound();
|
->continueOnNotFound();
|
||||||
|
|
@ -134,7 +143,9 @@ class RoutingTest extends TestCase
|
||||||
|
|
||||||
$dogRouter = $this->server->createRouter()
|
$dogRouter = $this->server->createRouter()
|
||||||
->add(new HeaderAdderMiddleware(
|
->add(new HeaderAdderMiddleware(
|
||||||
'Content-type', 'application/dog'))
|
'Content-type',
|
||||||
|
'application/dog'
|
||||||
|
))
|
||||||
->register('GET', '/bear', new StringHandler('Bear'));
|
->register('GET', '/bear', new StringHandler('Bear'));
|
||||||
$this->server->add($dogRouter);
|
$this->server->add($dogRouter);
|
||||||
|
|
||||||
|
|
@ -145,15 +156,19 @@ class RoutingTest extends TestCase
|
||||||
$response = $this->respond();
|
$response = $this->respond();
|
||||||
|
|
||||||
$this->assertEquals('Bear', (string) $response->getBody());
|
$this->assertEquals('Bear', (string) $response->getBody());
|
||||||
$this->assertEquals('application/dog',
|
$this->assertEquals(
|
||||||
$response->getHeaderLine('Content-type'));
|
'application/dog',
|
||||||
|
$response->getHeaderLine('Content-type')
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testResponds404WhenNoRouteMatched()
|
public function testResponds404WhenNoRouteMatched()
|
||||||
{
|
{
|
||||||
$catRouter = $this->server->createRouter()
|
$catRouter = $this->server->createRouter()
|
||||||
->add(new HeaderAdderMiddleware(
|
->add(new HeaderAdderMiddleware(
|
||||||
'Content-type', 'application/cat'))
|
'Content-type',
|
||||||
|
'application/cat'
|
||||||
|
))
|
||||||
->register('GET', '/molly', new StringHandler('Molly'))
|
->register('GET', '/molly', new StringHandler('Molly'))
|
||||||
->register('GET', '/oscar', new StringHandler('Oscar'))
|
->register('GET', '/oscar', new StringHandler('Oscar'))
|
||||||
->continueOnNotFound();
|
->continueOnNotFound();
|
||||||
|
|
@ -161,7 +176,9 @@ class RoutingTest extends TestCase
|
||||||
|
|
||||||
$dogRouter = $this->server->createRouter()
|
$dogRouter = $this->server->createRouter()
|
||||||
->add(new HeaderAdderMiddleware(
|
->add(new HeaderAdderMiddleware(
|
||||||
'Content-type', 'application/dog'))
|
'Content-type',
|
||||||
|
'application/dog'
|
||||||
|
))
|
||||||
->register('GET', '/bear', new StringHandler('Bear'));
|
->register('GET', '/bear', new StringHandler('Bear'));
|
||||||
$this->server->add($dogRouter);
|
$this->server->add($dogRouter);
|
||||||
|
|
||||||
|
|
@ -185,7 +202,7 @@ class TransmitterMock implements TransmitterInterface
|
||||||
public function transmit(
|
public function transmit(
|
||||||
ServerRequestInterface $request,
|
ServerRequestInterface $request,
|
||||||
ResponseInterface $response
|
ResponseInterface $response
|
||||||
) {
|
): void {
|
||||||
$this->response = $response;
|
$this->response = $response;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,30 +1,29 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace WellRESTed\Test\Unit;
|
namespace WellRESTed;
|
||||||
|
|
||||||
use Prophecy\Argument;
|
use Prophecy\Argument;
|
||||||
|
use Prophecy\PhpUnit\ProphecyTrait;
|
||||||
use WellRESTed\Dispatching\DispatcherInterface;
|
use WellRESTed\Dispatching\DispatcherInterface;
|
||||||
use WellRESTed\Message\Response;
|
use WellRESTed\Message\Response;
|
||||||
use WellRESTed\Message\ServerRequest;
|
use WellRESTed\Message\ServerRequest;
|
||||||
use WellRESTed\Message\Stream;
|
|
||||||
use WellRESTed\Server;
|
|
||||||
use WellRESTed\Test\TestCase;
|
use WellRESTed\Test\TestCase;
|
||||||
use WellRESTed\Transmission\TransmitterInterface;
|
use WellRESTed\Transmission\TransmitterInterface;
|
||||||
|
|
||||||
require_once __DIR__ . '/../../src/HeaderStack.php';
|
|
||||||
|
|
||||||
class ServerTest extends TestCase
|
class ServerTest extends TestCase
|
||||||
{
|
{
|
||||||
|
use ProphecyTrait;
|
||||||
|
|
||||||
private $transmitter;
|
private $transmitter;
|
||||||
/** @var Server */
|
/** @var Server */
|
||||||
private $server;
|
private $server;
|
||||||
|
|
||||||
public function setUp(): void
|
protected function setUp(): void
|
||||||
{
|
{
|
||||||
parent::setUp();
|
parent::setUp();
|
||||||
|
|
||||||
$this->transmitter = $this->prophesize(TransmitterInterface::class);
|
$this->transmitter = $this->prophesize(TransmitterInterface::class);
|
||||||
$this->transmitter->transmit(Argument::cetera())->willReturn();
|
$this->transmitter->transmit(Argument::cetera());
|
||||||
|
|
||||||
$this->server = new Server();
|
$this->server = new Server();
|
||||||
$this->server->setTransmitter($this->transmitter->reveal());
|
$this->server->setTransmitter($this->transmitter->reveal());
|
||||||
|
|
@ -32,7 +31,7 @@ class ServerTest extends TestCase
|
||||||
|
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
|
|
||||||
public function testDispatchesMiddlewareStack()
|
public function testDispatchesMiddlewareStack(): void
|
||||||
{
|
{
|
||||||
// This test will add a string to this array from each middleware.
|
// This test will add a string to this array from each middleware.
|
||||||
|
|
||||||
|
|
@ -64,7 +63,7 @@ class ServerTest extends TestCase
|
||||||
$this->assertEquals(['first', 'second', 'third'], $steps);
|
$this->assertEquals(['first', 'second', 'third'], $steps);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testDispatchedRequest()
|
public function testDispatchedRequest(): void
|
||||||
{
|
{
|
||||||
$request = new ServerRequest();
|
$request = new ServerRequest();
|
||||||
$capturedRequest = null;
|
$capturedRequest = null;
|
||||||
|
|
@ -79,7 +78,7 @@ class ServerTest extends TestCase
|
||||||
$this->assertSame($request, $capturedRequest);
|
$this->assertSame($request, $capturedRequest);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testDispatchedResponse()
|
public function testDispatchedResponse(): void
|
||||||
{
|
{
|
||||||
$response = new Response();
|
$response = new Response();
|
||||||
$capturedResponse = null;
|
$capturedResponse = null;
|
||||||
|
|
@ -97,7 +96,7 @@ class ServerTest extends TestCase
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
// Respond
|
// Respond
|
||||||
|
|
||||||
public function testRespondSendsResponseToTransmitter()
|
public function testRespondSendsResponseToTransmitter(): void
|
||||||
{
|
{
|
||||||
$expectedResponse = new Response(200);
|
$expectedResponse = new Response(200);
|
||||||
|
|
||||||
|
|
@ -130,7 +129,7 @@ class ServerTest extends TestCase
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
// Router
|
// Router
|
||||||
|
|
||||||
public function testCreatesRouterWithDispatcher()
|
public function testCreatesRouterWithDispatcher(): void
|
||||||
{
|
{
|
||||||
$dispatcher = $this->prophesize(DispatcherInterface::class);
|
$dispatcher = $this->prophesize(DispatcherInterface::class);
|
||||||
$dispatcher->dispatch(Argument::cetera())->will(
|
$dispatcher->dispatch(Argument::cetera())->will(
|
||||||
|
|
@ -144,15 +143,15 @@ class ServerTest extends TestCase
|
||||||
$this->server->setPathVariablesAttributeName('pathVariables');
|
$this->server->setPathVariablesAttributeName('pathVariables');
|
||||||
|
|
||||||
$request = (new ServerRequest())
|
$request = (new ServerRequest())
|
||||||
->withMethod("GET")
|
->withMethod('GET')
|
||||||
->withRequestTarget("/");
|
->withRequestTarget('/');
|
||||||
$response = new Response();
|
$response = new Response();
|
||||||
$next = function ($rqst, $resp) {
|
$next = function ($rqst, $resp) {
|
||||||
return $resp;
|
return $resp;
|
||||||
};
|
};
|
||||||
|
|
||||||
$router = $this->server->createRouter();
|
$router = $this->server->createRouter();
|
||||||
$router->register("GET", "/", "middleware");
|
$router->register('GET', '/', 'middleware');
|
||||||
$router($request, $response, $next);
|
$router($request, $response, $next);
|
||||||
|
|
||||||
$dispatcher->dispatch(Argument::cetera())
|
$dispatcher->dispatch(Argument::cetera())
|
||||||
|
|
@ -162,7 +161,7 @@ class ServerTest extends TestCase
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
// Attributes
|
// Attributes
|
||||||
|
|
||||||
public function testAddsAttributesToRequest()
|
public function testAddsAttributesToRequest(): void
|
||||||
{
|
{
|
||||||
$this->server->setAttributes([
|
$this->server->setAttributes([
|
||||||
'name' => 'value'
|
'name' => 'value'
|
||||||
|
|
@ -182,7 +181,7 @@ class ServerTest extends TestCase
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
// End of Stack
|
// End of Stack
|
||||||
|
|
||||||
public function testReturnsLastDoublePassResponseAtEndOfStack()
|
public function testReturnsLastDoublePassResponseAtEndOfStack(): void
|
||||||
{
|
{
|
||||||
$defaultResponse = new Response(404);
|
$defaultResponse = new Response(404);
|
||||||
|
|
||||||
|
|
@ -201,26 +200,4 @@ class ServerTest extends TestCase
|
||||||
$defaultResponse
|
$defaultResponse
|
||||||
)->shouldHaveBeenCalled();
|
)->shouldHaveBeenCalled();
|
||||||
}
|
}
|
||||||
|
|
||||||
// -------------------------------------------------------------------------
|
|
||||||
|
|
||||||
public function testCreatesStockTransmitterByDefault()
|
|
||||||
{
|
|
||||||
$content = "Hello, world!";
|
|
||||||
|
|
||||||
$response = (new Response())
|
|
||||||
->withBody(new Stream($content));
|
|
||||||
|
|
||||||
$server = new Server();
|
|
||||||
$server->add(function () use ($response) {
|
|
||||||
return $response;
|
|
||||||
});
|
|
||||||
|
|
||||||
ob_start();
|
|
||||||
$server->respond();
|
|
||||||
$captured = ob_get_contents();
|
|
||||||
ob_end_clean();
|
|
||||||
|
|
||||||
$this->assertEquals($content, $captured);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
@ -1,29 +1,29 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace WellRESTed\Test\Unit\Transmission;
|
namespace WellRESTed\Transmission;
|
||||||
|
|
||||||
use Prophecy\Argument;
|
use Prophecy\Argument;
|
||||||
|
use Prophecy\PhpUnit\ProphecyTrait;
|
||||||
use Psr\Http\Message\StreamInterface;
|
use Psr\Http\Message\StreamInterface;
|
||||||
|
use RuntimeException;
|
||||||
use WellRESTed\Message\Response;
|
use WellRESTed\Message\Response;
|
||||||
use WellRESTed\Message\ServerRequest;
|
use WellRESTed\Message\ServerRequest;
|
||||||
use WellRESTed\Test\TestCase;
|
use WellRESTed\Test\TestCase;
|
||||||
use WellRESTed\Transmission\HeaderStack;
|
|
||||||
use WellRESTed\Transmission\Transmitter;
|
|
||||||
|
|
||||||
require_once __DIR__ . "/../../../src/HeaderStack.php";
|
|
||||||
|
|
||||||
class TransmitterTest extends TestCase
|
class TransmitterTest extends TestCase
|
||||||
{
|
{
|
||||||
|
use ProphecyTrait;
|
||||||
|
|
||||||
private $request;
|
private $request;
|
||||||
private $response;
|
private $response;
|
||||||
private $body;
|
private $body;
|
||||||
|
|
||||||
public function setUp(): void
|
protected function setUp(): void
|
||||||
{
|
{
|
||||||
HeaderStack::reset();
|
HeaderStack::reset();
|
||||||
|
|
||||||
$this->request = (new ServerRequest())
|
$this->request = (new ServerRequest())
|
||||||
->withMethod("HEAD");
|
->withMethod('HEAD');
|
||||||
|
|
||||||
$this->body = $this->prophesize(StreamInterface::class);
|
$this->body = $this->prophesize(StreamInterface::class);
|
||||||
$this->body->isReadable()->willReturn(false);
|
$this->body->isReadable()->willReturn(false);
|
||||||
|
|
@ -36,46 +36,49 @@ class TransmitterTest extends TestCase
|
||||||
->withBody($stream);
|
->withBody($stream);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testSendStatusCodeWithReasonPhrase()
|
public function testSendStatusCodeWithReasonPhrase(): void
|
||||||
{
|
{
|
||||||
$transmitter = new Transmitter();
|
$transmitter = new Transmitter();
|
||||||
$transmitter->transmit($this->request, $this->response);
|
$transmitter->transmit($this->request, $this->response);
|
||||||
$this->assertContains("HTTP/1.1 200 OK", HeaderStack::getHeaders());
|
$this->assertContains('HTTP/1.1 200 OK', HeaderStack::getHeaders());
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testSendStatusCodeWithoutReasonPhrase()
|
public function testSendStatusCodeWithoutReasonPhrase(): void
|
||||||
{
|
{
|
||||||
$this->response = $this->response->withStatus(999);
|
$this->response = $this->response->withStatus(999);
|
||||||
|
|
||||||
$transmitter = new Transmitter();
|
$transmitter = new Transmitter();
|
||||||
$transmitter->transmit($this->request, $this->response);
|
$transmitter->transmit($this->request, $this->response);
|
||||||
$this->assertContains("HTTP/1.1 999", HeaderStack::getHeaders());
|
$this->assertContains('HTTP/1.1 999', HeaderStack::getHeaders());
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @dataProvider headerProvider */
|
/**
|
||||||
public function testSendsHeaders($header)
|
* @dataProvider headerProvider
|
||||||
|
* @param string $header
|
||||||
|
*/
|
||||||
|
public function testSendsHeaders(string $header): void
|
||||||
{
|
{
|
||||||
$this->response = $this->response
|
$this->response = $this->response
|
||||||
->withHeader("Content-length", ["2048"])
|
->withHeader('Content-length', ['2048'])
|
||||||
->withHeader("X-foo", ["bar", "baz"]);
|
->withHeader('X-foo', ['bar', 'baz']);
|
||||||
|
|
||||||
$transmitter = new Transmitter();
|
$transmitter = new Transmitter();
|
||||||
$transmitter->transmit($this->request, $this->response);
|
$transmitter->transmit($this->request, $this->response);
|
||||||
$this->assertContains($header, HeaderStack::getHeaders());
|
$this->assertContains($header, HeaderStack::getHeaders());
|
||||||
}
|
}
|
||||||
|
|
||||||
public function headerProvider()
|
public function headerProvider(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
["Content-length: 2048"],
|
['Content-length: 2048'],
|
||||||
["X-foo: bar"],
|
['X-foo: bar'],
|
||||||
["X-foo: baz"]
|
['X-foo: baz']
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testOutputsBody()
|
public function testOutputsBody(): void
|
||||||
{
|
{
|
||||||
$content = "Hello, world!";
|
$content = 'Hello, world!';
|
||||||
|
|
||||||
$this->body->isReadable()->willReturn(true);
|
$this->body->isReadable()->willReturn(true);
|
||||||
$this->body->__toString()->willReturn($content);
|
$this->body->__toString()->willReturn($content);
|
||||||
|
|
@ -91,9 +94,9 @@ class TransmitterTest extends TestCase
|
||||||
$this->assertEquals($content, $captured);
|
$this->assertEquals($content, $captured);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testOutputsBodyInChunks()
|
public function testOutputsBodyInChunks(): void
|
||||||
{
|
{
|
||||||
$content = "Hello, world!";
|
$content = 'Hello, world!';
|
||||||
$chunkSize = 3;
|
$chunkSize = 3;
|
||||||
$position = 0;
|
$position = 0;
|
||||||
|
|
||||||
|
|
@ -124,15 +127,15 @@ class TransmitterTest extends TestCase
|
||||||
$this->assertEquals($content, $captured);
|
$this->assertEquals($content, $captured);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testOutputsUnseekableStreamInChunks()
|
public function testOutputsUnseekableStreamInChunks(): void
|
||||||
{
|
{
|
||||||
$content = "Hello, world!";
|
$content = 'Hello, world!';
|
||||||
$chunkSize = 3;
|
$chunkSize = 3;
|
||||||
$position = 0;
|
$position = 0;
|
||||||
|
|
||||||
$this->body->isSeekable()->willReturn(false);
|
$this->body->isSeekable()->willReturn(false);
|
||||||
$this->body->isReadable()->willReturn(true);
|
$this->body->isReadable()->willReturn(true);
|
||||||
$this->body->rewind()->willThrow(new \RuntimeException());
|
$this->body->rewind()->willThrow(new RuntimeException());
|
||||||
$this->body->eof()->willReturn(false);
|
$this->body->eof()->willReturn(false);
|
||||||
$this->body->read(Argument::any())->will(
|
$this->body->read(Argument::any())->will(
|
||||||
function ($args) use ($content, &$position) {
|
function ($args) use ($content, &$position) {
|
||||||
|
|
@ -157,14 +160,14 @@ class TransmitterTest extends TestCase
|
||||||
$this->assertEquals($content, $captured);
|
$this->assertEquals($content, $captured);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
// Preparation
|
// Preparation
|
||||||
|
|
||||||
public function testAddContentLengthHeader()
|
public function testAddContentLengthHeader(): void
|
||||||
{
|
{
|
||||||
$bodySize = 1024;
|
$bodySize = 1024;
|
||||||
$this->body->isReadable()->willReturn(true);
|
$this->body->isReadable()->willReturn(true);
|
||||||
$this->body->__toString()->willReturn("");
|
$this->body->__toString()->willReturn('');
|
||||||
$this->body->getSize()->willReturn($bodySize);
|
$this->body->getSize()->willReturn($bodySize);
|
||||||
|
|
||||||
$transmitter = new Transmitter();
|
$transmitter = new Transmitter();
|
||||||
|
|
@ -174,15 +177,15 @@ class TransmitterTest extends TestCase
|
||||||
$this->assertContains("Content-length: $bodySize", HeaderStack::getHeaders());
|
$this->assertContains("Content-length: $bodySize", HeaderStack::getHeaders());
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testDoesNotReplaceContentLengthHeaderWhenContentLengthIsAlreadySet()
|
public function testDoesNotReplaceContentLengthHeaderWhenContentLengthIsAlreadySet(): void
|
||||||
{
|
{
|
||||||
$streamSize = 1024;
|
$streamSize = 1024;
|
||||||
$headerSize = 2048;
|
$headerSize = 2048;
|
||||||
|
|
||||||
$this->response = $this->response->withHeader("Content-length", $headerSize);
|
$this->response = $this->response->withHeader('Content-length', $headerSize);
|
||||||
|
|
||||||
$this->body->isReadable()->willReturn(true);
|
$this->body->isReadable()->willReturn(true);
|
||||||
$this->body->__toString()->willReturn("");
|
$this->body->__toString()->willReturn('');
|
||||||
$this->body->getSize()->willReturn($streamSize);
|
$this->body->getSize()->willReturn($streamSize);
|
||||||
|
|
||||||
$transmitter = new Transmitter();
|
$transmitter = new Transmitter();
|
||||||
|
|
@ -192,37 +195,37 @@ class TransmitterTest extends TestCase
|
||||||
$this->assertContains("Content-length: $headerSize", HeaderStack::getHeaders());
|
$this->assertContains("Content-length: $headerSize", HeaderStack::getHeaders());
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testDoesNotAddContentLengthHeaderWhenTransferEncodingIsChunked()
|
public function testDoesNotAddContentLengthHeaderWhenTransferEncodingIsChunked(): void
|
||||||
{
|
{
|
||||||
$bodySize = 1024;
|
$bodySize = 1024;
|
||||||
|
|
||||||
$this->response = $this->response->withHeader("Transfer-encoding", "CHUNKED");
|
$this->response = $this->response->withHeader('Transfer-encoding', 'CHUNKED');
|
||||||
|
|
||||||
$this->body->isReadable()->willReturn(true);
|
$this->body->isReadable()->willReturn(true);
|
||||||
$this->body->__toString()->willReturn("");
|
$this->body->__toString()->willReturn('');
|
||||||
$this->body->getSize()->willReturn($bodySize);
|
$this->body->getSize()->willReturn($bodySize);
|
||||||
|
|
||||||
$transmitter = new Transmitter();
|
$transmitter = new Transmitter();
|
||||||
$transmitter->setChunkSize(0);
|
$transmitter->setChunkSize(0);
|
||||||
$transmitter->transmit($this->request, $this->response);
|
$transmitter->transmit($this->request, $this->response);
|
||||||
|
|
||||||
$this->assertArrayDoesNotContainValueWithPrefix(HeaderStack::getHeaders(), "Content-length:");
|
$this->assertArrayDoesNotContainValueWithPrefix(HeaderStack::getHeaders(), 'Content-length:');
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testDoesNotAddContentLengthHeaderWhenBodySizeIsNull()
|
public function testDoesNotAddContentLengthHeaderWhenBodySizeIsNull(): void
|
||||||
{
|
{
|
||||||
$this->body->isReadable()->willReturn(true);
|
$this->body->isReadable()->willReturn(true);
|
||||||
$this->body->__toString()->willReturn("");
|
$this->body->__toString()->willReturn('');
|
||||||
$this->body->getSize()->willReturn(null);
|
$this->body->getSize()->willReturn(null);
|
||||||
|
|
||||||
$transmitter = new Transmitter();
|
$transmitter = new Transmitter();
|
||||||
$transmitter->setChunkSize(0);
|
$transmitter->setChunkSize(0);
|
||||||
$transmitter->transmit($this->request, $this->response);
|
$transmitter->transmit($this->request, $this->response);
|
||||||
|
|
||||||
$this->assertArrayDoesNotContainValueWithPrefix(HeaderStack::getHeaders(), "Content-length:");
|
$this->assertArrayDoesNotContainValueWithPrefix(HeaderStack::getHeaders(), 'Content-length:');
|
||||||
}
|
}
|
||||||
|
|
||||||
private function assertArrayDoesNotContainValueWithPrefix($arr, $prefix)
|
private function assertArrayDoesNotContainValueWithPrefix(array $arr, string $prefix): void
|
||||||
{
|
{
|
||||||
$normalPrefix = strtolower($prefix);
|
$normalPrefix = strtolower($prefix);
|
||||||
foreach ($arr as $item) {
|
foreach ($arr as $item) {
|
||||||
|
|
@ -234,3 +237,33 @@ class TransmitterTest extends TestCase
|
||||||
$this->assertTrue(true);
|
$this->assertTrue(true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
// Declare header function in this namespace so the class under test will use
|
||||||
|
// this instead of the internal global functions during testing.
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue