This commit is contained in:
2024-11-27 21:34:07 +02:00
parent 638bcba894
commit b6d1215999
190 changed files with 31518 additions and 0 deletions

View File

@@ -0,0 +1,2 @@
github: clue
custom: https://clue.engineering/support

373
vendor/clue/framework-x/CHANGELOG.md vendored Normal file
View File

@@ -0,0 +1,373 @@
# Changelog
## 0.16.0 (2024-03-05)
We are thrilled to announce the official release of `v0.16.0` to the public! 🎉🚀
Additionally, we are making all previous tagged versions available to simplify the upgrade process.
In addition to the release of `v0.16.0`, this update includes all prior tagged releases.
This release includes exciting new features such as improved performance, additional options
for access logging, updates to our documentation and nginx + Apache configurations,
as well as many more internal improvements to our test suite and integration tests.
* Feature: Improve performance by skipping `AccessLogHandler` if it writes to `/dev/null`.
(#248 by @clue)
* Feature: Add optional `$path` argument for `AccessLogHandler`.
(#247 by @clue)
* Minor documentation improvements and update nginx + Apache configuration.
(#245 and #251 by @clue)
* Improve test suite with improved directory structure for integration tests.
(#250 by @clue)
## 0.15.0 (2023-12-07)
* Feature: Full PHP 8.3 compatibility.
(#244 by @clue)
* Feature: Add `App::__invoke()` method to enable custom integrations.
(#236 by @clue)
* Feature: Improve performance by only using `FiberHandler` for `ReactiveHandler`.
(#237 by @clue)
* Minor documentation improvements.
(#242 by @yadaiio)
## 0.14.0 (2023-07-31)
* Feature: Improve Promise v3 support and use Promise v3 template types.
(#233 and #235 by @clue)
* Feature: Improve handling `OPTIONS *` requests.
(#226 by @clue)
* Refactor logging into new `LogStreamHandler` and reactive server logic into new `ReactiveHandler`.
(#222 and #224 by @clue)
* Improve test suite and ensure 100% code coverage.
(#217, #221, #225 and #228 by @clue)
## 0.13.0 (2023-02-22)
* Feature: Forward compatibility with upcoming Promise v3.
(#188 by @clue)
* Feature: Full PHP 8.2 compatibility.
(#194 and #207 by @clue)
* Feature: Load environment variables from `$_ENV`, `$_SERVER` and `getenv()`.
(#205 by @clue)
* Feature: Update to support `Content-Length` response header on `HEAD` requests.
(#186 by @clue)
* Feature / Fix: Consistent handling for HTTP responses with multiple header values (PHP SAPI).
(#214 by @pfk84)
* Fix: Respect explicit response status code when Location response header is given (PHP SAPI).
(#191 by @jkrzefski)
* Minor documentation improvements.
(#189 by @clue)
* Add PHPStan to test environment on level `max` and improve type definitions.
(#200, #201 and #204 by @clue)
* Improve test suite and report failed assertions.
(#199 by @clue and #208 by @SimonFrings)
## 0.12.0 (2022-08-03)
* Feature: Support loading environment variables from DI container configuration.
(#184 by @clue)
* Feature: Support typed container variables for container factory functions.
(#178, #179 and #180 by @clue)
* Feature: Support nullable and `null` arguments and default values for DI container configuration.
(#181 and #183 by @clue)
* Feature: Support untyped and `mixed` arguments for container factory.
(#182 by @clue)
## 0.11.0 (2022-07-26)
* Feature: Make `AccessLogHandler` and `ErrorHandler` part of public API.
(#173 and #174 by @clue)
```php
<?php
require __DIR__ . '/../vendor/autoload.php';
$app = new FrameworkX\App(
new FrameworkX\AccessLogHandler(),
new FrameworkX\ErrorHandler()
);
// Register routes here, see routing…
$app->run();
```
* Feature: Support loading `AccessLogHandler` and `ErrorHandler` from `Container`.
(#175 by @clue)
* Feature: Read `$remote_addr` attribute for `AccessLogHandler` (trusted proxies).
(#177 by @clue)
* Internal refactoring to move all handlers to `Io` namespace.
(#176 by @clue)
* Update test suite to remove deprecated `utf8_decode()` (PHP 8.2 preparation).
(#171 by SimonFrings)
## 0.10.0 (2022-07-14)
* Feature: Built-in support for fibers on PHP 8.1+ with stable reactphp/async.
(#168 by @clue)
```php
$app->get('/book/{isbn}', function (Psr\Http\Message\ServerRequestInterface $request) use ($db) {
$isbn = $request->getAttribute('isbn');
$result = await($db->query(
'SELECT title FROM book WHERE isbn = ?',
[$isbn]
));
assert($result instanceof React\MySQL\QueryResult);
$data = $result->resultRows[0]['title'];
return React\Http\Message\Response::plaintext(
$data
);
});
```
* Feature: Support PSR-11 container interface by using DI container as adapter.
(#163 by @clue)
* Minor documentation improvements.
(#158 by @clue and #160 by @SimonFrings)
## 0.9.0 (2022-05-13)
* Feature: Add signal handling support for `SIGINT` and `SIGTERM`.
(#150 by @clue)
* Feature: Improve error output for exception messages with special characters.
(#131 by @clue)
* Add new documentation chapters for Docker containers and HTTP redirecting.
(#138 by SimonFrings and #136, #151 and #156 by @clue)
* Minor documentation improvements.
(#143 by @zf2timo, #153 by @mattschlosser and #129 and #154 by @clue)
* Improve test suite and add tests for `Dockerfile` instructions.
(#148 and #149 by @clue)
## 0.8.0 (2022-03-07)
* Feature: Automatically start new fiber for each request on PHP 8.1+.
(#117 by @clue)
* Feature: Add fiber compatibility mode for PHP < 8.1.
(#128 by @clue)
* Improve documentation and update installation instructions for react/async.
(#116 and #126 by @clue and #124, #125 and #127 by @SimonFrings)
* Improve fiber tests to avoid now unneeded `await()` calls.
(#118 by @clue)
## 0.7.0 (2022-02-05)
* Feature: Update to use HTTP status code constants and JSON/HTML response helpers.
(#114 by @clue)
```php
$app->get('/users/{name}', function (Psr\Http\Message\ServerRequestInterface $request) {
return React\Http\Message\Response::plaintext(
"Hello " . $request->getAttribute('name') . "!\n"
);
});
```
* Feature / Fix: Update to improve protocol handling for HTTP responses with no body.
(#113 by @clue)
* Minor documentation improvements.
(#112 by @SimonFrings and #115 by @netcarver)
## 0.6.0 (2021-12-20)
* Feature: Support automatic dependency injection by using class names (DI container).
(#89, #92 and #94 by @clue)
```php
<?php
require __DIR__ . '/../vendor/autoload.php';
$app = new FrameworkX\App(Acme\Todo\JsonMiddleware::class);
$app->get('/', Acme\Todo\HelloController::class);
$app->get('/users/{name}', Acme\Todo\UserController::class);
$app->run();
```
* Feature: Add support for explicit DI container configuration.
(#95, #96 and #97 by @clue)
```php
<?php
require __DIR__ . '/../vendor/autoload.php';
$container = new FrameworkX\Container([
Acme\Todo\HelloController::class => fn() => new Acme\Todo\HelloController();
Acme\Todo\UserController::class => function (React\Http\Browser $browser) {
// example UserController class requires two arguments:
// - first argument will be autowired based on class reference
// - second argument expects some manual value
return new Acme\Todo\UserController($browser, 42);
}
]);
// …
```
* Feature: Refactor to use `$_SERVER` instead of `getenv()`.
(#91 by @bpolaszek)
* Minor documentation improvements.
(#100 by @clue)
* Update test suite to use stable PHP 8.1 Docker image.
(#90 by @clue)
## 0.5.0 (2021-11-30)
* Feature / BC break: Simplify `App` by always using default loop, drop optional loop instance.
(#88 by @clue)
```php
// old
$loop = React\EventLoop\Loop::get();
$app = new FrameworkX\App($loop);
// new (already supported before)
$app = new FrameworkX\App();
```
* Add documentation for manual restart of systemd service and chapter for Caddy deployment.
(#87 by @SimonFrings and #82 by @francislavoie)
* Improve documentation, remove leftover `$loop` references and fix typos.
(#72 by @shuvroroy, #80 by @Ivanshamir, #81 by @clue and #83 by @rattuscz)
## 0.4.0 (2021-11-23)
We are excited to announce the official release of Framework X to the public! 🎉🚀
This release includes exciting new features such as full compatibility with PHP 8.1,
improvements to response handling, and enhanced documentation covering nginx,
Apache, and async database usage.
* Feature: Announce Framework X public beta.
(#64 by @clue)
* Feature: Full PHP 8.1 compatibility.
(#58 by @clue)
* Feature: Improve `AccessLogHandler` and fix response size for streaming response body.
(#47, #48, #49 and #50 by @clue)
* Feature / Fix: Skip sending body and `Content-Length` for responses with no body.
(#51 by @clue)
* Feature / Fix: Consistently reject proxy requests and handle `OPTIONS *` requests.
(#46 by @clue)
* Add new documentation chapters for nginx, Apache and async database.
(#57, #59 and #60 by @clue)
* Improve documentation, examples and describe HTTP caching and output buffering.
(#52, #53, #55, #56, #61, #62 and #63 by @clue)
## 0.3.0 (2021-09-23)
* Feature: Add support for global middleware.
(#23 by @clue)
* Feature: Improve error output and refactor internal error handler.
(#37, #39 and #41 by @clue)
* Feature: Support changing listening address via new `X_LISTEN` environment variable.
(#38 by @clue)
* Feature: Update to new ReactPHP HTTP and Socket API.
(#26 and #29 by @HLeithner and #34 by @clue)
* Feature: Refactor to use new `AccessLogHandler`, `RouteHandler`, `RedirectHandler` and `SapiHandler`.
(#42, #43, #44 and #45 by @clue)
* Fix: Fix path filter regex.
(#27 by @HLeithner)
* Add documentation for async middleware and systemd service unit configuration.
(#24 by @Degra1991 and #32, #35, #36 and #40 by @clue)
* Improve test suite and run tests on Windows with PHPUnit.
(#31 by @SimonFrings and #28 and #33 by @clue)
## 0.2.0 (2021-06-18)
* Feature: Simplify `App` usage by making `LoopInterface` argument optional.
(#22 by @clue)
```php
// old (still supported)
$loop = React\EventLoop\Factory::create();
$app = new FrameworkX\App($loop);
// new (using default loop)
$app = new FrameworkX\App();
```
* Feature: Add middleware support.
(#18 by @clue)
* Feature: Refactor and simplify route dispatcher.
(#21 by @clue)
* Feature: Add Generator-based coroutine implementation.
(#17 by @clue)
* Minor documentation improvements.
(#15, #16 and #19 by @clue)
## 0.1.0 (2021-04-30)
We're excited to announce the release of the first version of Framework X in
private beta! This version marks the starting point of our project and is the
first of many milestones for making async PHP easier than ever before.
* Release Framework X, major documentation overhaul and improve examples.
(#14, #13 and #2 by @clue)
* Feature: Support running behind nginx and Apache (PHP-FPM and mod_php).
(#3, #11 and #12 by @clue)
* Feature / Fix: Consistently parse request URI and improve URL handling.
(#4, #5, #6 and #7 by @clue)
* Feature: Rewrite `FilesystemHandler`, improve file access and directory listing.
(#8 and #9 by @clue)
* Feature: Add `any()` router method to match any request method.
(#10 by @clue)

21
vendor/clue/framework-x/LICENSE vendored Normal file
View File

@@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2019 Christian Lück
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is furnished
to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

147
vendor/clue/framework-x/README.md vendored Normal file
View File

@@ -0,0 +1,147 @@
# Framework X
[![CI status](https://github.com/clue-access/framework-x/workflows/CI/badge.svg)](https://github.com/clue-access/framework-x/actions)
[![code coverage](https://img.shields.io/badge/code%20coverage-100%25-success)](#tests)
Framework X the simple and fast micro framework for building reactive web applications that run anywhere.
* [Support us](#support-us)
* [Quickstart](#quickstart)
* [Documentation](#documentation)
* [Contribute](#contribute)
* [Tests](#tests)
* [License](#license)
## Support us
We invest a lot of time developing, maintaining and updating our awesome
open-source projects. You can help us sustain this high-quality of our work by
[becoming a sponsor on GitHub](https://github.com/sponsors/clue). Sponsors get
numerous benefits in return, see our [sponsoring page](https://github.com/sponsors/clue)
for details.
Let's take these projects to the next level together! 🚀
## Quickstart
Start by creating an empty project directory.
Next, we can start by taking a look at a simple example application.
You can use this example to get started by creating a new `public/` directory with
an `index.php` file inside:
```php
<?php
require __DIR__ . '/../vendor/autoload.php';
$app = new FrameworkX\App();
$app->get('/', function () {
return React\Http\Message\Response::plaintext(
"Hello wörld!\n"
);
});
$app->get('/users/{name}', function (Psr\Http\Message\ServerRequestInterface $request) {
return React\Http\Message\Response::plaintext(
"Hello " . $request->getAttribute('name') . "!\n"
);
});
$app->run();
```
Next, we need to install X and its dependencies to actually run this project.
In your project directory, simply run the following command:
```bash
$ composer require clue/framework-x:^0.16
```
> See also the [CHANGELOG](CHANGELOG.md) for details about version upgrades.
That's it already! The next step is now to serve this web application.
One of the nice properties of this project is that is works both behind
traditional web server setups as well as in a stand-alone environment.
For example, you can run the above example using the built-in web server like
this:
```bash
$ php public/index.php
```
You can now use your favorite web browser or command line tool to check your web
application responds as expected:
```bash
$ curl http://localhost:8080/
Hello wörld!
```
## Documentation
Hooked?
See [website](https://framework-x.org/) for full documentation.
Found a typo or want to contribute?
The website documentation is built from the source documentation files in
the [docs/](docs/) folder.
## Contribute
You want to contribute to the Framework X source code or documentation? You've
come to the right place!
To contribute to the source code just locate the [src/](src/) folder and you'll find all
content in there. Additionally, our [tests/](tests/) folder contains all our unit
tests and acceptance tests to assure our code works as expected. For more
information on how to run the test suite check out our [testing chapter](#tests).
If you want to contribute to the [documentation](#documentation) of Framework X
found on the website, take a look inside the [docs/](docs/) folder. You'll find further
instructions inside the `README.md` in there.
Found a typo on our [website](https://framework-x.org/)? Simply go to our
[website repository](https://github.com/clue/framework-x-website)
and follow the instructions found in the `README`.
## Tests
To run the test suite, you first need to clone this repo and then install all
dependencies [through Composer](https://getcomposer.org/):
```bash
$ composer install
```
To run the test suite, go to the project root and run:
```bash
$ vendor/bin/phpunit
```
The test suite is set up to always ensure 100% code coverage across all
supported environments. If you have the Xdebug extension installed, you can also
generate a code coverage report locally like this:
```bash
$ XDEBUG_MODE=coverage vendor/bin/phpunit --coverage-text
```
Additionally, you can run our sophisticated integration tests to verify the
framework examples work as expected behind your web server. Use your web server
of choice (see deployment documentation) and execute the tests with the URL to
your installation like this:
```bash
$ php tests/integration/public/index.php
$ tests/integration.bash http://localhost:8080
```
## License
This project is released under the permissive [MIT license](LICENSE).
> Did you know that I offer custom development services and issuing invoices for
sponsorships of releases and for contributions? Contact me (@clue) for details.

40
vendor/clue/framework-x/composer.json vendored Normal file
View File

@@ -0,0 +1,40 @@
{
"name": "clue/framework-x",
"description": "Framework X the simple and fast micro framework for building reactive web applications that run anywhere.",
"keywords": ["microframework", "micro", "framework", "web", "http", "event-driven", "async", "ReactPHP"],
"homepage": "https://framework-x.org/",
"license": "MIT",
"authors": [
{
"name": "Christian Lück",
"email": "christian@clue.engineering"
}
],
"require": {
"php": ">=7.1",
"nikic/fast-route": "^1.3",
"react/async": "^4 || ^3",
"react/http": "^1.9",
"react/promise": "^3 || ^2.10",
"react/socket": "^1.13"
},
"require-dev": {
"phpstan/phpstan": "1.10.47 || 1.4.10",
"phpunit/phpunit": "^9.6 || ^7.5",
"psr/container": "^2 || ^1",
"react/promise-timer": "^1.10"
},
"autoload": {
"psr-4": {
"FrameworkX\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"FrameworkX\\Tests\\": "tests/"
},
"files": [
"tests/FiberStub.php"
]
}
}

View File

@@ -0,0 +1,140 @@
<?php
namespace FrameworkX;
use FrameworkX\Io\LogStreamHandler;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use React\Http\Message\Response;
use React\Promise\PromiseInterface;
use React\Stream\ReadableStreamInterface;
/**
* @final
*/
class AccessLogHandler
{
/** @var ?LogStreamHandler */
private $logger;
/** @var bool */
private $hasHighResolution;
/**
* @param ?string $path (optional) absolute log file path or will log to console output by default
* @throws \InvalidArgumentException if given `$path` is not an absolute file path
* @throws \RuntimeException if given `$path` can not be opened in append mode
*/
public function __construct(?string $path = null)
{
if ($path === null) {
$path = \PHP_SAPI === 'cli' ? 'php://output' : 'php://stderr';
}
$logger = new LogStreamHandler($path);
if (!$logger->isDevNull()) {
// only assign logger if we're not logging to /dev/null (which would discard any logs)
$this->logger = $logger;
}
$this->hasHighResolution = \function_exists('hrtime'); // PHP 7.3+
}
/**
* [Internal] Returns whether we're writing to /dev/null (which will discard any logs)
*
* @internal
* @return bool
*/
public function isDevNull(): bool
{
return $this->logger === null;
}
/**
* @return ResponseInterface|PromiseInterface<ResponseInterface>|\Generator
*/
public function __invoke(ServerRequestInterface $request, callable $next)
{
if ($this->logger === null) {
// Skip if we're logging to /dev/null (which will discard any logs).
// As an additional optimization, the `App` will automatically
// detect we no longer need to invoke this instance at all.
return $next($request); // @codeCoverageIgnore
}
$now = $this->now();
$response = $next($request);
if ($response instanceof PromiseInterface) {
/** @var PromiseInterface<ResponseInterface> $response */
return $response->then(function (ResponseInterface $response) use ($request, $now) {
$this->logWhenClosed($request, $response, $now);
return $response;
});
} elseif ($response instanceof \Generator) {
return (function (\Generator $generator) use ($request, $now) {
$response = yield from $generator;
$this->logWhenClosed($request, $response, $now);
return $response;
})($response);
} else {
$this->logWhenClosed($request, $response, $now);
return $response;
}
}
/**
* checks if response body is closed (not streaming) before writing log message for response
*/
private function logWhenClosed(ServerRequestInterface $request, ResponseInterface $response, float $start): void
{
$body = $response->getBody();
if ($body instanceof ReadableStreamInterface && $body->isReadable()) {
$size = 0;
$body->on('data', function (string $chunk) use (&$size) {
$size += strlen($chunk);
});
$body->on('close', function () use (&$size, $request, $response, $start) {
$this->log($request, $response, $size, $this->now() - $start);
});
} else {
$this->log($request, $response, $body->getSize() ?? strlen((string) $body), $this->now() - $start);
}
}
/**
* writes log message for response after response body is closed (not streaming anymore)
*/
private function log(ServerRequestInterface $request, ResponseInterface $response, int $responseSize, float $time): void
{
$method = $request->getMethod();
$status = $response->getStatusCode();
// HEAD requests and `204 No Content` and `304 Not Modified` always use an empty response body
if ($method === 'HEAD' || $status === Response::STATUS_NO_CONTENT || $status === Response::STATUS_NOT_MODIFIED) {
$responseSize = 0;
}
\assert($this->logger instanceof LogStreamHandler);
$this->logger->log(
($request->getAttribute('remote_addr') ?? $request->getServerParams()['REMOTE_ADDR'] ?? '-') . ' ' .
'"' . $this->escape($method) . ' ' . $this->escape($request->getRequestTarget()) . ' HTTP/' . $request->getProtocolVersion() . '" ' .
$status . ' ' . $responseSize . ' ' . sprintf('%.3F', $time < 0 ? 0 : $time)
);
}
private function escape(string $s): string
{
return (string) preg_replace_callback('/[\x00-\x1F\x7F-\xFF"\\\\]+/', function (array $m) {
return str_replace('%', '\x', rawurlencode($m[0]));
}, $s);
}
private function now(): float
{
return $this->hasHighResolution ? hrtime(true) * 1e-9 : microtime(true);
}
}

354
vendor/clue/framework-x/src/App.php vendored Normal file
View File

@@ -0,0 +1,354 @@
<?php
namespace FrameworkX;
use FrameworkX\Io\MiddlewareHandler;
use FrameworkX\Io\ReactiveHandler;
use FrameworkX\Io\RedirectHandler;
use FrameworkX\Io\RouteHandler;
use FrameworkX\Io\SapiHandler;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use React\Http\Message\Response;
use React\Promise\Deferred;
use React\Promise\PromiseInterface;
use function React\Async\await;
class App
{
/** @var MiddlewareHandler */
private $handler;
/** @var RouteHandler */
private $router;
/** @var ReactiveHandler|SapiHandler */
private $sapi;
/**
* Instantiate new X application
*
* ```php
* // instantiate
* $app = new App();
*
* // instantiate with global middleware
* $app = new App($middleware);
* $app = new App($middleware1, $middleware2);
* ```
*
* @param callable|class-string ...$middleware
*/
public function __construct(...$middleware)
{
// new MiddlewareHandler([$fiberHandler, $accessLogHandler, $errorHandler, ...$middleware, $routeHandler])
$handlers = [];
$container = $needsErrorHandler = new Container();
// only log for built-in webserver and PHP development webserver by default, others have their own access log
$needsAccessLog = (\PHP_SAPI === 'cli' || \PHP_SAPI === 'cli-server') ? $container : null;
if ($middleware) {
$needsErrorHandlerNext = false;
foreach ($middleware as $handler) {
// load AccessLogHandler and ErrorHandler instance from last Container
if ($handler === AccessLogHandler::class) {
$handler = $container->getAccessLogHandler();
} elseif ($handler === ErrorHandler::class) {
$handler = $container->getErrorHandler();
}
// ensure AccessLogHandler is always followed by ErrorHandler
if ($needsErrorHandlerNext && !$handler instanceof ErrorHandler) {
break;
}
$needsErrorHandlerNext = false;
if ($handler instanceof Container) {
// remember last Container to load any following class names
$container = $handler;
// add default ErrorHandler from last Container before adding any other handlers, may be followed by other Container instances (unlikely)
if (!$handlers) {
$needsErrorHandler = $needsAccessLog = $container;
}
} elseif (!\is_callable($handler)) {
$handlers[] = $container->callable($handler);
} else {
// don't need a default ErrorHandler if we're adding one as first handler or AccessLogHandler as first followed by one
if ($needsErrorHandler && ($handler instanceof ErrorHandler || $handler instanceof AccessLogHandler) && !$handlers) {
$needsErrorHandler = null;
}
// only add to list of handlers if this is not a NOOP
if (!$handler instanceof AccessLogHandler || !$handler->isDevNull()) {
$handlers[] = $handler;
}
if ($handler instanceof AccessLogHandler) {
$needsAccessLog = null;
$needsErrorHandlerNext = true;
}
}
}
if ($needsErrorHandlerNext) {
throw new \TypeError('AccessLogHandler must be followed by ErrorHandler');
}
}
// add default ErrorHandler as first handler unless it is already added explicitly
if ($needsErrorHandler instanceof Container) {
\array_unshift($handlers, $needsErrorHandler->getErrorHandler());
}
// only log for built-in webserver and PHP development webserver by default, others have their own access log
if ($needsAccessLog instanceof Container) {
$handler = $needsAccessLog->getAccessLogHandler();
if (!$handler->isDevNull()) {
\array_unshift($handlers, $handler);
}
}
$this->router = new RouteHandler($container);
$handlers[] = $this->router;
$this->handler = new MiddlewareHandler($handlers);
$this->sapi = \PHP_SAPI === 'cli' ? new ReactiveHandler($container->getEnv('X_LISTEN')) : new SapiHandler();
}
/**
* @param string $route
* @param callable|class-string $handler
* @param callable|class-string ...$handlers
*/
public function get(string $route, $handler, ...$handlers): void
{
$this->map(['GET'], $route, $handler, ...$handlers);
}
/**
* @param string $route
* @param callable|class-string $handler
* @param callable|class-string ...$handlers
*/
public function head(string $route, $handler, ...$handlers): void
{
$this->map(['HEAD'], $route, $handler, ...$handlers);
}
/**
* @param string $route
* @param callable|class-string $handler
* @param callable|class-string ...$handlers
*/
public function post(string $route, $handler, ...$handlers): void
{
$this->map(['POST'], $route, $handler, ...$handlers);
}
/**
* @param string $route
* @param callable|class-string $handler
* @param callable|class-string ...$handlers
*/
public function put(string $route, $handler, ...$handlers): void
{
$this->map(['PUT'], $route, $handler, ...$handlers);
}
/**
* @param string $route
* @param callable|class-string $handler
* @param callable|class-string ...$handlers
*/
public function patch(string $route, $handler, ...$handlers): void
{
$this->map(['PATCH'], $route, $handler, ...$handlers);
}
/**
* @param string $route
* @param callable|class-string $handler
* @param callable|class-string ...$handlers
*/
public function delete(string $route, $handler, ...$handlers): void
{
$this->map(['DELETE'], $route, $handler, ...$handlers);
}
/**
* @param string $route
* @param callable|class-string $handler
* @param callable|class-string ...$handlers
*/
public function options(string $route, $handler, ...$handlers): void
{
// backward compatibility: `OPTIONS * HTTP/1.1` can be matched with empty path (legacy)
if ($route === '') {
$route = '*';
}
$this->map(['OPTIONS'], $route, $handler, ...$handlers);
}
/**
* @param string $route
* @param callable|class-string $handler
* @param callable|class-string ...$handlers
*/
public function any(string $route, $handler, ...$handlers): void
{
$this->map(['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'], $route, $handler, ...$handlers);
}
/**
*
* @param string[] $methods
* @param string $route
* @param callable|class-string $handler
* @param callable|class-string ...$handlers
*/
public function map(array $methods, string $route, $handler, ...$handlers): void
{
$this->router->map($methods, $route, $handler, ...$handlers);
}
/**
* @param string $route
* @param string $target
* @param int $code
*/
public function redirect(string $route, string $target, int $code = Response::STATUS_FOUND): void
{
$this->any($route, new RedirectHandler($target, $code));
}
/**
* Runs the app to handle HTTP requests according to any registered routes and middleware.
*
* This is where the magic happens: When executed on the command line (CLI),
* this will run the powerful reactive request handler built on top of
* ReactPHP. This works by running the efficient built-in HTTP web server to
* handle incoming HTTP requests through ReactPHP's HTTP and socket server.
* This async execution mode is usually recommended as it can efficiently
* process a large number of concurrent connections and process multiple
* incoming requests simultaneously. The long-running server process will
* continue to run until it is interrupted by a signal.
*
* When executed behind traditional PHP SAPIs (PHP-FPM, FastCGI, Apache, etc.),
* this will handle a single request and run until a single response is sent.
* This is particularly useful because it allows you to run the exact same
* app in any environment.
*
* @see ReactiveHandler::run()
* @see SapiHandler::run()
*/
public function run(): void
{
$this->sapi->run(\Closure::fromCallable([$this, 'handleRequest']));
}
/**
* Invokes the app to handle a single HTTP request according to any registered routes and middleware.
*
* This method allows you to pass in a single HTTP request object that will
* be processed according to any registered routes and middleware and will
* return an HTTP response object as a result.
*
* ```php
* $app = new FrameworkX\App();
* $app->get('/', fn() => React\Http\Message\Response::plaintext("Hello!\n"));
*
* $request = new React\Http\Message\ServerRequest('GET', 'https://example.com/');
* $response = $app($request);
*
* assert($response instanceof Psr\Http\Message\ResponseInterface);
* assert($response->getStatusCode() === 200);
* assert($response->getBody()->getContents() === "Hello\n");
* ```
*
* This is particularly useful for higher-level integration test suites and
* for custom integrations with other runtime environments like serverless
* functions or other frameworks. Otherwise, most applications would likely
* want to use the `run()` method to run the application and automatically
* accept incoming HTTP requests according to the PHP SAPI in use.
*
* @param ServerRequestInterface $request The HTTP request object to process.
* @return ResponseInterface This method returns an HTTP response object
* according to any registered routes and middleware. If any handler is
* async, it will await its execution before returning, running the
* event loop as needed. If the request can not be routed or any handler
* fails, it will return a matching HTTP error response object.
* @throws void This method never throws. If the request can not be routed
* or any handler fails, it will be turned into a valid error response
* before returning.
* @see self::run()
*/
public function __invoke(ServerRequestInterface $request): ResponseInterface
{
$response = $this->handleRequest($request);
if ($response instanceof PromiseInterface) {
/** @throws void */
$response = await($response);
assert($response instanceof ResponseInterface);
}
return $response;
}
/**
* @param ServerRequestInterface $request
* @return ResponseInterface|PromiseInterface<ResponseInterface>
* Returns a response or a Promise which eventually fulfills with a
* response. This method never throws or resolves a rejected promise.
* If the request can not be routed or the handler fails, it will be
* turned into a valid error response before returning.
* @throws void
*/
private function handleRequest(ServerRequestInterface $request)
{
$response = ($this->handler)($request);
assert($response instanceof ResponseInterface || $response instanceof PromiseInterface || $response instanceof \Generator);
if ($response instanceof \Generator) {
if ($response->valid()) {
$response = $this->coroutine($response);
} else {
$response = $response->getReturn();
assert($response instanceof ResponseInterface);
}
}
return $response;
}
/**
* @return PromiseInterface<ResponseInterface>
*/
private function coroutine(\Generator $generator): PromiseInterface
{
$next = null;
$deferred = new Deferred();
$next = function () use ($generator, &$next, $deferred) {
if (!$generator->valid()) {
$deferred->resolve($generator->getReturn());
return;
}
$promise = $generator->current();
assert($promise instanceof PromiseInterface);
$promise->then(function ($value) use ($generator, $next) {
$generator->send($value);
$next();
}, function ($reason) use ($generator, $next) {
$generator->throw($reason);
$next();
});
};
$next();
return $deferred->promise();
}
}

View File

@@ -0,0 +1,382 @@
<?php
namespace FrameworkX;
use Psr\Container\ContainerInterface;
use Psr\Http\Message\ServerRequestInterface;
/**
* @final
*/
class Container
{
/** @var array<string,object|callable():(object|scalar|null)|scalar|null>|ContainerInterface */
private $container;
/** @var bool */
private $useProcessEnv;
/** @param array<string,callable():(object|scalar|null) | object | scalar | null>|ContainerInterface $loader */
public function __construct($loader = [])
{
/** @var mixed $loader explicit type check for mixed if user ignores parameter type */
if (!\is_array($loader) && !$loader instanceof ContainerInterface) {
throw new \TypeError(
'Argument #1 ($loader) must be of type array|Psr\Container\ContainerInterface, ' . (\is_object($loader) ? get_class($loader) : gettype($loader)) . ' given'
);
}
foreach (($loader instanceof ContainerInterface ? [] : $loader) as $name => $value) {
if (
(!\is_object($value) && !\is_scalar($value) && $value !== null) ||
(!$value instanceof $name && !$value instanceof \Closure && !\is_string($value) && \strpos($name, '\\') !== false)
) {
throw new \BadMethodCallException('Map for ' . $name . ' contains unexpected ' . (is_object($value) ? get_class($value) : gettype($value)));
}
}
$this->container = $loader;
// prefer reading environment from `$_ENV` and `$_SERVER`, only fall back to `getenv()` in thread-safe environments
$this->useProcessEnv = \ZEND_THREAD_SAFE === false || \in_array(\PHP_SAPI, ['cli', 'cli-server', 'cgi-fcgi', 'fpm-fcgi'], true);
}
/** @return mixed */
public function __invoke(ServerRequestInterface $request, callable $next = null)
{
if ($next === null) {
// You don't want to end up here. This only happens if you use the
// container as a final request handler instead of as a middleware.
// In this case, you should omit the container or add another final
// request handler behind the container in the middleware chain.
throw new \BadMethodCallException('Container should not be used as final request handler');
}
// If the container is used as a middleware, simply forward to the next
// request handler. As an additional optimization, the container would
// usually be filtered out from a middleware chain as this is a NO-OP.
return $next($request);
}
/**
* @param class-string $class
* @return callable(ServerRequestInterface,?callable=null)
* @internal
*/
public function callable(string $class): callable
{
return function (ServerRequestInterface $request, callable $next = null) use ($class) {
// Check `$class` references a valid class name that can be autoloaded
if (\is_array($this->container) && !\class_exists($class, true) && !interface_exists($class, false) && !trait_exists($class, false)) {
throw new \BadMethodCallException('Request handler class ' . $class . ' not found');
}
try {
if ($this->container instanceof ContainerInterface) {
$handler = $this->container->get($class);
} else {
$handler = $this->loadObject($class);
}
} catch (\Throwable $e) {
throw new \BadMethodCallException(
'Request handler class ' . $class . ' failed to load: ' . $e->getMessage(),
0,
$e
);
}
// Check `$handler` references a class name that is callable, i.e. has an `__invoke()` method.
// This initial version is intentionally limited to checking the method name only.
// A follow-up version will likely use reflection to check request handler argument types.
if (!is_callable($handler)) {
throw new \BadMethodCallException('Request handler class "' . $class . '" has no public __invoke() method');
}
// invoke request handler as middleware handler or final controller
if ($next === null) {
return $handler($request);
}
return $handler($request, $next);
};
}
/** @internal */
public function getEnv(string $name): ?string
{
assert(\preg_match('/^[A-Z][A-Z0-9_]+$/', $name) === 1);
if ($this->container instanceof ContainerInterface && $this->container->has($name)) {
$value = $this->container->get($name);
} elseif ($this->hasVariable($name)) {
$value = $this->loadVariable($name, 'mixed', true, 64);
} else {
return null;
}
if (!\is_string($value) && $value !== null) {
throw new \TypeError('Environment variable $' . $name . ' expected type string|null, but got ' . (\is_object($value) ? \get_class($value) : \gettype($value)));
}
return $value;
}
/** @internal */
public function getAccessLogHandler(): AccessLogHandler
{
if ($this->container instanceof ContainerInterface) {
if ($this->container->has(AccessLogHandler::class)) {
// @phpstan-ignore-next-line method return type will ensure correct type or throw `TypeError`
return $this->container->get(AccessLogHandler::class);
} else {
return new AccessLogHandler();
}
}
return $this->loadObject(AccessLogHandler::class);
}
/** @internal */
public function getErrorHandler(): ErrorHandler
{
if ($this->container instanceof ContainerInterface) {
if ($this->container->has(ErrorHandler::class)) {
// @phpstan-ignore-next-line method return type will ensure correct type or throw `TypeError`
return $this->container->get(ErrorHandler::class);
} else {
return new ErrorHandler();
}
}
return $this->loadObject(ErrorHandler::class);
}
/**
* @template T of object
* @param class-string<T> $name
* @return T
* @throws \BadMethodCallException if object of type $name can not be loaded
*/
private function loadObject(string $name, int $depth = 64) /*: object (PHP 7.2+) */
{
assert(\is_array($this->container));
if (\array_key_exists($name, $this->container)) {
if (\is_string($this->container[$name])) {
if ($depth < 1) {
throw new \BadMethodCallException('Factory for ' . $name . ' is recursive');
}
// @phpstan-ignore-next-line because type of container value is explicitly checked after getting here
$value = $this->loadObject($this->container[$name], $depth - 1);
if (!$value instanceof $name) {
throw new \BadMethodCallException('Factory for ' . $name . ' returned unexpected ' . \get_class($value));
}
$this->container[$name] = $value;
} elseif ($this->container[$name] instanceof \Closure) {
// build list of factory parameters based on parameter types
$closure = new \ReflectionFunction($this->container[$name]);
$params = $this->loadFunctionParams($closure, $depth, true);
// invoke factory with list of parameters
$value = $params === [] ? ($this->container[$name])() : ($this->container[$name])(...$params);
if (\is_string($value)) {
if ($depth < 1) {
throw new \BadMethodCallException('Factory for ' . $name . ' is recursive');
}
// @phpstan-ignore-next-line because type of container value is explicitly checked after getting here
$value = $this->loadObject($value, $depth - 1);
}
if (!$value instanceof $name) {
throw new \BadMethodCallException('Factory for ' . $name . ' returned unexpected ' . (is_object($value) ? get_class($value) : gettype($value)));
}
$this->container[$name] = $value;
} elseif (!$this->container[$name] instanceof $name) {
throw new \BadMethodCallException('Map for ' . $name . ' contains unexpected ' . (\is_object($this->container[$name]) ? \get_class($this->container[$name]) : \gettype($this->container[$name])));
}
assert($this->container[$name] instanceof $name);
return $this->container[$name];
}
// Check `$name` references a valid class name that can be autoloaded
if (!\class_exists($name, true) && !interface_exists($name, false) && !trait_exists($name, false)) {
throw new \BadMethodCallException('Class ' . $name . ' not found');
}
$class = new \ReflectionClass($name);
if (!$class->isInstantiable()) {
$modifier = 'class';
if ($class->isInterface()) {
$modifier = 'interface';
} elseif ($class->isAbstract()) {
$modifier = 'abstract class';
} elseif ($class->isTrait()) {
$modifier = 'trait';
}
throw new \BadMethodCallException('Cannot instantiate ' . $modifier . ' '. $name);
}
// build list of constructor parameters based on parameter types
$ctor = $class->getConstructor();
$params = $ctor === null ? [] : $this->loadFunctionParams($ctor, $depth, false);
// instantiate with list of parameters
// @phpstan-ignore-next-line because `$class->newInstance()` is known to return `T`
return $this->container[$name] = $params === [] ? new $name() : $class->newInstance(...$params);
}
/**
* @return list<mixed>
* @throws \BadMethodCallException if either parameter can not be loaded
*/
private function loadFunctionParams(\ReflectionFunctionAbstract $function, int $depth, bool $allowVariables): array
{
$params = [];
foreach ($function->getParameters() as $parameter) {
$params[] = $this->loadParameter($parameter, $depth, $allowVariables);
}
return $params;
}
/**
* @return mixed
* @throws \BadMethodCallException if $parameter can not be loaded
*/
private function loadParameter(\ReflectionParameter $parameter, int $depth, bool $allowVariables) /*: mixed (PHP 8.0+) */
{
assert(\is_array($this->container));
$type = $parameter->getType();
$hasDefault = $parameter->isDefaultValueAvailable() || ((!$type instanceof \ReflectionNamedType || $type->getName() !== 'mixed') && $parameter->allowsNull());
// abort for union types (PHP 8.0+) and intersection types (PHP 8.1+)
// @phpstan-ignore-next-line for PHP < 8
if ($type instanceof \ReflectionUnionType || $type instanceof \ReflectionIntersectionType) { // @codeCoverageIgnoreStart
if ($hasDefault) {
return $parameter->isDefaultValueAvailable() ? $parameter->getDefaultValue() : null;
}
throw new \BadMethodCallException(self::parameterError($parameter) . ' expects unsupported type ' . $type);
} // @codeCoverageIgnoreEnd
// load container variables if parameter name is known
assert($type === null || $type instanceof \ReflectionNamedType);
if ($allowVariables && $this->hasVariable($parameter->getName())) {
return $this->loadVariable($parameter->getName(), $type === null ? 'mixed' : $type->getName(), $parameter->allowsNull(), $depth);
}
// abort if parameter is untyped and not explicitly defined by container variable
if ($type === null) {
assert($parameter->allowsNull());
if ($parameter->isDefaultValueAvailable()) {
return $parameter->getDefaultValue();
}
throw new \BadMethodCallException(self::parameterError($parameter) . ' has no type');
}
// use default/nullable argument if not loadable as container variable or by type
assert($type instanceof \ReflectionNamedType);
if ($hasDefault && ($type->isBuiltin() || !\array_key_exists($type->getName(), $this->container))) {
return $parameter->isDefaultValueAvailable() ? $parameter->getDefaultValue() : null;
}
// abort if required container variable is not defined or for any other primitive types (array etc.)
if ($type->isBuiltin()) {
if ($allowVariables) {
throw new \BadMethodCallException(self::parameterError($parameter) . ' is not defined');
} else {
throw new \BadMethodCallException(self::parameterError($parameter) . ' expects unsupported type ' . $type->getName());
}
}
// abort for unreasonably deep nesting or recursive types
if ($depth < 1) {
throw new \BadMethodCallException(self::parameterError($parameter) . ' is recursive');
}
// @phpstan-ignore-next-line because `$type->getName()` is a `class-string` by definition
return $this->loadObject($type->getName(), $depth - 1);
}
private function hasVariable(string $name): bool
{
return (\is_array($this->container) && \array_key_exists($name, $this->container)) || (isset($_ENV[$name]) || (\is_string($_SERVER[$name] ?? null) || ($this->useProcessEnv && \getenv($name) !== false)) && \preg_match('/^[A-Z][A-Z0-9_]+$/', $name));
}
/**
* @return object|string|int|float|bool|null
* @throws \BadMethodCallException if $name is not a valid container variable
*/
private function loadVariable(string $name, string $type, bool $nullable, int $depth) /*: object|string|int|float|bool|null (PHP 8.0+) */
{
assert($this->hasVariable($name));
assert(\is_array($this->container) || !$this->container->has($name));
if (\is_array($this->container) && ($this->container[$name] ?? null) instanceof \Closure) {
if ($depth < 1) {
throw new \BadMethodCallException('Container variable $' . $name . ' is recursive');
}
// build list of factory parameters based on parameter types
$factory = $this->container[$name];
assert($factory instanceof \Closure);
$closure = new \ReflectionFunction($factory);
$params = $this->loadFunctionParams($closure, $depth - 1, true);
// invoke factory with list of parameters
$value = $params === [] ? $factory() : $factory(...$params);
if (!\is_object($value) && !\is_scalar($value) && $value !== null) {
throw new \BadMethodCallException('Container variable $' . $name . ' expected type object|scalar|null from factory, but got ' . \gettype($value));
}
$this->container[$name] = $value;
} elseif (\is_array($this->container) && \array_key_exists($name, $this->container)) {
$value = $this->container[$name];
} elseif (isset($_ENV[$name])) {
assert(\is_string($_ENV[$name]));
$value = $_ENV[$name];
} elseif (isset($_SERVER[$name])) {
assert(\is_string($_SERVER[$name]));
$value = $_SERVER[$name];
} else {
$value = \getenv($name);
assert($this->useProcessEnv && $value !== false);
}
assert(\is_object($value) || \is_scalar($value) || $value === null);
// allow null values if parameter is marked nullable or untyped or mixed
if ($nullable && $value === null) {
return null;
}
// skip type checks and allow all values if expected type is undefined or mixed (PHP 8+)
if ($type === 'mixed') {
return $value;
}
if (
(\is_object($value) && !$value instanceof $type) ||
(!\is_object($value) && !\in_array($type, ['string', 'int', 'float', 'bool'])) ||
($type === 'string' && !\is_string($value)) || ($type === 'int' && !\is_int($value)) || ($type === 'float' && !\is_float($value)) || ($type === 'bool' && !\is_bool($value))
) {
throw new \BadMethodCallException('Container variable $' . $name . ' expected type ' . $type . ', but got ' . (\is_object($value) ? \get_class($value) : \gettype($value)));
}
return $value;
}
/** @throws void */
private static function parameterError(\ReflectionParameter $parameter): string
{
$name = $parameter->getDeclaringFunction()->getShortName();
if (!$parameter->getDeclaringFunction()->isClosure() && ($class = $parameter->getDeclaringClass()) !== null) {
$name = explode("\0", $class->getName())[0] . '::' . $name;
}
return 'Argument ' . ($parameter->getPosition() + 1) . ' ($' . $parameter->getName() . ') of ' . $name . '()';
}
}

View File

@@ -0,0 +1,211 @@
<?php
namespace FrameworkX;
use FrameworkX\Io\HtmlHandler;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use React\Http\Message\Response;
use React\Promise\PromiseInterface;
/**
* @final
*/
class ErrorHandler
{
/** @var Htmlhandler */
private $html;
public function __construct()
{
$this->html = new HtmlHandler();
}
/**
* @return ResponseInterface|PromiseInterface<ResponseInterface>|\Generator
* Returns a response, a Promise which eventually fulfills with a
* response or a Generator which eventually returns a response. This
* method never throws or resolves a rejected promise. If the next
* handler fails to return a valid response, it will be turned into a
* valid error response before returning.
* @throws void
*/
public function __invoke(ServerRequestInterface $request, callable $next)
{
try {
$response = $next($request);
} catch (\Throwable $e) {
return $this->errorInvalidException($e);
}
if ($response instanceof ResponseInterface) {
return $response;
} elseif ($response instanceof PromiseInterface) {
return $response->then(function ($response) {
if ($response instanceof ResponseInterface) {
return $response;
} else {
return $this->errorInvalidResponse($response);
}
}, function ($e) {
// Promise rejected, always a `\Throwable` as of Promise v3
assert($e instanceof \Throwable || !\method_exists(PromiseInterface::class, 'catch')); // @phpstan-ignore-line
if ($e instanceof \Throwable) {
return $this->errorInvalidException($e);
} else { // @phpstan-ignore-line
// @phpstan-ignore-next-line
return $this->errorInvalidResponse(\React\Promise\reject($e)); // @codeCoverageIgnore
}
});
} elseif ($response instanceof \Generator) {
return $this->coroutine($response);
} else {
return $this->errorInvalidResponse($response);
}
}
private function coroutine(\Generator $generator): \Generator
{
do {
try {
if (!$generator->valid()) {
$response = $generator->getReturn();
if ($response instanceof ResponseInterface) {
return $response;
} else {
return $this->errorInvalidResponse($response);
}
}
} catch (\Throwable $e) {
return $this->errorInvalidException($e);
}
$promise = $generator->current();
if (!$promise instanceof PromiseInterface) {
$gref = new \ReflectionGenerator($generator);
return $this->errorInvalidCoroutine(
$promise,
$gref->getExecutingFile(),
$gref->getExecutingLine()
);
}
try {
$next = yield $promise;
} catch (\Throwable $e) {
try {
$generator->throw($e);
continue;
} catch (\Throwable $e) {
return $this->errorInvalidException($e);
}
}
try {
$generator->send($next);
} catch (\Throwable $e) {
return $this->errorInvalidException($e);
}
} while (true);
} // @codeCoverageIgnore
/** @internal */
public function requestNotFound(): ResponseInterface
{
return $this->htmlResponse(
Response::STATUS_NOT_FOUND,
'Page Not Found',
'Please check the URL in the address bar and try again.'
);
}
/**
* @internal
* @param list<string> $allowedMethods
*/
public function requestMethodNotAllowed(array $allowedMethods): ResponseInterface
{
$methods = \implode('/', \array_map(function (string $method) { return '<code>' . $method . '</code>'; }, $allowedMethods));
return $this->htmlResponse(
Response::STATUS_METHOD_NOT_ALLOWED,
'Method Not Allowed',
'Please check the URL in the address bar and try again with ' . $methods . ' request.'
)->withHeader('Allow', \implode(', ', $allowedMethods));
}
/** @internal */
public function requestProxyUnsupported(): ResponseInterface
{
return $this->htmlResponse(
Response::STATUS_BAD_REQUEST,
'Proxy Requests Not Allowed',
'Please check your settings and retry.'
);
}
private function errorInvalidException(\Throwable $e): ResponseInterface
{
$where = ' in ' . $this->where($e->getFile(), $e->getLine());
$message = '<code>' . $this->html->escape($e->getMessage()) . '</code>';
return $this->htmlResponse(
Response::STATUS_INTERNAL_SERVER_ERROR,
'Internal Server Error',
'The requested page failed to load, please try again later.',
'Expected request handler to return <code>' . ResponseInterface::class . '</code> but got uncaught <code>' . \get_class($e) . '</code> with message ' . $message . $where . '.'
);
}
/** @param mixed $value */
private function errorInvalidResponse($value): ResponseInterface
{
return $this->htmlResponse(
Response::STATUS_INTERNAL_SERVER_ERROR,
'Internal Server Error',
'The requested page failed to load, please try again later.',
'Expected request handler to return <code>' . ResponseInterface::class . '</code> but got <code>' . $this->describeType($value) . '</code>.'
);
}
/** @param mixed $value */
private function errorInvalidCoroutine($value, string $file, int $line): ResponseInterface
{
$where = ' near or before '. $this->where($file, $line) . '.';
return $this->htmlResponse(
Response::STATUS_INTERNAL_SERVER_ERROR,
'Internal Server Error',
'The requested page failed to load, please try again later.',
'Expected request handler to yield <code>' . PromiseInterface::class . '</code> but got <code>' . $this->describeType($value) . '</code>' . $where
);
}
private function where(string $file, int $line): string
{
return '<code title="See ' . $file . ' line ' . $line . '">' . \basename($file) . ':' . $line . '</code>';
}
private function htmlResponse(int $statusCode, string $title, string ...$info): ResponseInterface
{
return $this->html->statusResponse(
$statusCode,
'Error ' . $statusCode . ': ' .$title,
$title,
\implode('', \array_map(function (string $info) { return "<p>$info</p>\n"; }, $info))
);
}
/** @param mixed $value */
private function describeType($value): string
{
if ($value === null) {
return 'null';
} elseif (\is_scalar($value) && !\is_string($value)) {
return \var_export($value, true);
}
return \is_object($value) ? \get_class($value) : \gettype($value);
}
}

View File

@@ -0,0 +1,130 @@
<?php
namespace FrameworkX;
use FrameworkX\Io\HtmlHandler;
use FrameworkX\Io\RedirectHandler;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use React\Http\Message\Response;
class FilesystemHandler
{
/** @var string */
private $root;
/**
* Mapping between file extension and MIME type to send in `Content-Type` response header
*
* @var array<string,string>
*/
private $mimetypes = array(
'atom' => 'application/atom+xml',
'bz2' => 'application/x-bzip2',
'css' => 'text/css',
'gif' => 'image/gif',
'gz' => 'application/gzip',
'htm' => 'text/html',
'html' => 'text/html',
'ico' => 'image/x-icon',
'jpeg' => 'image/jpeg',
'jpg' => 'image/jpeg',
'js' => 'text/javascript',
'json' => 'application/json',
'pdf' => 'application/pdf',
'png' => 'image/png',
'rss' => 'application/rss+xml',
'svg' => 'image/svg+xml',
'tar' => 'application/x-tar',
'xml' => 'application/xml',
'zip' => 'application/zip',
);
/**
* Assign default MIME type to send in `Content-Type` response header (same as nginx/Apache)
*
* @var string
* @see self::$mimetypes
*/
private $defaultMimetype = 'text/plain';
/** @var ErrorHandler */
private $errorHandler;
/** @var HtmlHandler */
private $html;
public function __construct(string $root)
{
$this->root = $root;
$this->errorHandler = new ErrorHandler();
$this->html = new HtmlHandler();
}
public function __invoke(ServerRequestInterface $request): ResponseInterface
{
$local = $request->getAttribute('path', '');
assert(\is_string($local));
$path = \rtrim($this->root . '/' . $local, '/');
// local path should not contain "./", "../", "//" or null bytes or start with slash
$valid = !\preg_match('#(?:^|/)\.\.?(?:$|/)|^/|//|\x00#', $local);
\clearstatcache();
if ($valid && \is_dir($path)) {
if ($local !== '' && \substr($local, -1) !== '/') {
return (new RedirectHandler(\basename($path) . '/'))();
}
$response = '<strong>' . $this->html->escape($local === '' ? '/' : $local) . '</strong>' . "\n<ul>\n";
if ($local !== '') {
$response .= ' <li><a href="../">../</a></li>' . "\n";
}
$files = \scandir($path);
// @phpstan-ignore-next-line TODO handle error if directory can not be accessed
foreach ($files as $file) {
if ($file === '.' || $file === '..') {
continue;
}
$dir = \is_dir($path . '/' . $file) ? '/' : '';
$response .= ' <li><a href="' . \rawurlencode($file) . $dir . '">' . $this->html->escape($file) . $dir . '</a></li>' . "\n";
}
$response .= '</ul>' . "\n";
return Response::html(
$response
);
} elseif ($valid && \is_file($path)) {
if ($local !== '' && \substr($local, -1) === '/') {
return (new RedirectHandler('../' . \basename($path)))();
}
// Assign MIME type based on file extension (same as nginx/Apache) or fall back to given default otherwise.
// Browsers are pretty good at figuring out the correct type if no charset attribute is given.
$ext = \strtolower(\substr($path, \strrpos($path, '.') + 1));
$headers = [
'Content-Type' => $this->mimetypes[$ext] ?? $this->defaultMimetype
];
$stat = @\stat($path);
if ($stat !== false) {
$headers['Last-Modified'] = \gmdate('D, d M Y H:i:s', $stat['mtime']) . ' GMT';
if ($request->getHeaderLine('If-Modified-Since') === $headers['Last-Modified']) {
return new Response(Response::STATUS_NOT_MODIFIED);
}
}
return new Response(
Response::STATUS_OK,
$headers,
\file_get_contents($path) // @phpstan-ignore-line TODO handle error if file can not be accessed
);
} else {
return $this->errorHandler->requestNotFound();
}
}
}

View File

@@ -0,0 +1,67 @@
<?php
namespace FrameworkX\Io;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use React\Promise\Deferred;
use React\Promise\PromiseInterface;
/**
* [Internal] Fibers middleware handler to ensure each request is processed in a separate `Fiber`
*
* The `Fiber` class has been added in PHP 8.1+, so this middleware is only used
* on PHP 8.1+. On supported PHP versions, this middleware is automatically
* added to the list of middleware handlers, so there's no need to reference
* this class in application code.
*
* @internal
* @link https://framework-x.org/docs/async/fibers/
*/
class FiberHandler
{
/**
* @return ResponseInterface|PromiseInterface<ResponseInterface>|\Generator
* Returns a `ResponseInterface` from the next request handler in the
* chain. If the next request handler returns immediately, this method
* will return immediately. If the next request handler suspends the
* fiber (see `await()`), this method will return a `PromiseInterface`
* that is fulfilled with a `ResponseInterface` when the fiber is
* terminated successfully. If the next request handler returns a
* promise, this method will return a promise that follows its
* resolution. If the next request handler returns a Generator-based
* coroutine, this method returns a `Generator`. This method never
* throws or resolves a rejected promise. If the handler fails, it will
* be turned into a valid error response before returning.
* @throws void
*/
public function __invoke(ServerRequestInterface $request, callable $next)
{
$deferred = null;
$fiber = new \Fiber(function () use ($request, $next, &$deferred) {
$response = $next($request);
assert($response instanceof ResponseInterface || $response instanceof PromiseInterface || $response instanceof \Generator);
// if the next request handler returns immediately, the fiber can terminate immediately without using a Deferred
// if the next request handler suspends the fiber, we only reach this point after resuming the fiber, so the code below will have assigned a Deferred
/** @var ?Deferred<ResponseInterface> $deferred */
if ($deferred !== null) {
assert($response instanceof ResponseInterface);
$deferred->resolve($response);
}
return $response;
});
/** @throws void because the next handler will always be an `ErrorHandler` */
$fiber->start();
if ($fiber->isTerminated()) {
/** @throws void because fiber is known to have terminated successfully */
/** @var ResponseInterface|PromiseInterface<ResponseInterface>|\Generator */
return $fiber->getReturn();
}
$deferred = new Deferred();
return $deferred->promise();
}
}

View File

@@ -0,0 +1,65 @@
<?php
namespace FrameworkX\Io;
use Psr\Http\Message\ResponseInterface;
use React\Http\Message\Response;
/**
* @internal
*/
class HtmlHandler
{
public function statusResponse(int $statusCode, string $title, string $subtitle, string $info): ResponseInterface
{
$nonce = \base64_encode(\random_bytes(16));
$html = <<<HTML
<!DOCTYPE html>
<html>
<head>
<title>$title</title>
<style nonce="$nonce">
body { display: grid; justify-content: center; align-items: center; grid-auto-rows: minmax(min-content, calc(100vh - 4em)); margin: 2em; font-family: ui-sans-serif, Arial, "Noto Sans", sans-serif; }
@media (min-width: 700px) { main { display: grid; max-width: 700px; } }
h1 { margin: 0 .5em 0 0; border-right: calc(2 * max(0px, min(100vw - 700px + 1px, 1px))) solid #e3e4e7; padding-right: .5em; color: #aebdcc; font-size: 3em; }
strong { color: #111827; font-size: 3em; }
p { margin: .5em 0 0 0; grid-column: 2; color: #6b7280; }
code { padding: 0 .3em; background-color: #f5f6f9; } code span { padding: 0 .2em; border-radius: 3px; background-color: #0001; }
a { color: inherit; }
</style>
</head>
<body>
<main>
<h1>$statusCode</h1>
<strong>$subtitle</strong>
$info</main>
</body>
</html>
HTML;
return new Response(
$statusCode,
[
'Content-Type' => 'text/html; charset=utf-8',
'Content-Security-Policy' => "style-src 'nonce-$nonce'; img-src 'self'; default-src 'none'"
],
$html
);
}
public function escape(string $s): string
{
return (string) \preg_replace_callback(
'/[\x00-\x1F]+/',
function (array $match): string {
return '<span>' . \addcslashes($match[0], "\x00..\xff") . '</span>';
},
(string) \preg_replace(
'/(^| ) |(?: $)/',
'$1&nbsp;',
\htmlspecialchars($s, \ENT_NOQUOTES | \ENT_SUBSTITUTE | \ENT_DISALLOWED, 'utf-8')
)
);
}
}

View File

@@ -0,0 +1,95 @@
<?php
namespace FrameworkX\Io;
/**
* @internal
*/
class LogStreamHandler
{
/** @var ?resource */
private $stream;
/**
* @param string $path absolute log file path
* @throws \InvalidArgumentException if given `$path` is not an absolute file path
* @throws \RuntimeException if given `$path` can not be opened in append mode
*/
public function __construct(string $path)
{
if (\strpos($path, "\0") !== false || (\stripos($path, 'php://') !== 0 && !$this->isAbsolutePath($path))) {
throw new \InvalidArgumentException(
'Unable to open log file "' . \addslashes($path) . '": Invalid path given'
);
}
$errstr = '';
\set_error_handler(function (int $_, string $error) use (&$errstr): bool {
// Match errstr from PHP's warning message.
// fopen(/dev/not-a-valid-path): Failed to open stream: Permission denied
$errstr = \preg_replace('#.*: #', '', $error);
return true;
});
$stream = \fopen($path, 'ae');
// try to fstat($stream) to see if this points to /dev/null (skip on Windows)
// @codeCoverageIgnoreStart
$stat = false;
if ($stream !== false && \DIRECTORY_SEPARATOR !== '\\') {
if (\strtolower($path) === 'php://output') {
// php://output doesn't support stat, so assume php://output will go to php://stdout
$stdout = \defined('STDOUT') ? \STDOUT : \fopen('php://stdout', 'w');
if (\is_resource($stdout)) {
$stat = \fstat($stdout);
} else {
// STDOUT can not be opened => assume piping to /dev/null
$stream = null;
}
} else {
$stat = \fstat($stream);
}
// close stream if it points to /dev/null
if ($stat !== false && $stat === \stat('/dev/null')) {
$stream = null;
}
}
// @codeCoverageIgnoreEnd
\restore_error_handler();
if ($stream === false) {
throw new \RuntimeException(
'Unable to open log file "' . $path . '": ' . $errstr
);
}
$this->stream = $stream;
}
public function isDevNull(): bool
{
return $this->stream === null;
}
public function log(string $message): void
{
// nothing to do if we're writing to /dev/null
if ($this->stream === null) {
return; // @codeCoverageIgnore
}
$time = \microtime(true);
$prefix = \date('Y-m-d H:i:s', (int) $time) . \sprintf('.%03d ', (int) (($time - (int) $time) * 1e3));
$ret = \fwrite($this->stream, $prefix . $message . \PHP_EOL);
assert(\is_int($ret));
}
private function isAbsolutePath(string $path): bool
{
return \DIRECTORY_SEPARATOR !== '\\' ? \substr($path, 0, 1) === '/' : (bool) \preg_match('#^[A-Z]:[/\\\\]#i', $path);
}
}

View File

@@ -0,0 +1,40 @@
<?php
namespace FrameworkX\Io;
use Psr\Http\Message\ServerRequestInterface;
/**
* @internal
*/
class MiddlewareHandler
{
/** @var list<callable> $handlers */
private $handlers;
/** @param list<callable> $handlers */
public function __construct(array $handlers)
{
assert(count($handlers) >= 2);
$this->handlers = $handlers;
}
/** @return mixed */
public function __invoke(ServerRequestInterface $request)
{
return $this->call($request, 0);
}
/** @return mixed */
private function call(ServerRequestInterface $request, int $position)
{
if (!isset($this->handlers[$position + 2])) {
return $this->handlers[$position]($request, $this->handlers[$position + 1]);
}
return $this->handlers[$position]($request, function (ServerRequestInterface $request) use ($position) {
return $this->call($request, $position + 1);
});
}
}

View File

@@ -0,0 +1,91 @@
<?php
namespace FrameworkX\Io;
use React\EventLoop\Loop;
use React\Http\HttpServer;
use React\Socket\SocketServer;
/**
* [Internal] Powerful reactive request handler built on top of ReactPHP.
*
* This is where the magic happens: The main `App` uses this class to run
* ReactPHP's efficient HTTP server to handle incoming HTTP requests when
* executed on the command line (CLI). ReactPHP's lightweight socket server can
* listen for a large number of concurrent connections and process multiple
* incoming connections simultaneously. The long-running server process will
* continue to run until it is interrupted by a signal.
*
* Note that this is an internal class only and nothing you should usually have
* to care about. See also the `App` and `SapiHandler` for more details.
*
* @internal
*/
class ReactiveHandler
{
/** @var LogStreamHandler */
private $logger;
/** @var string */
private $listenAddress;
public function __construct(?string $listenAddress)
{
/** @throws void */
$this->logger = new LogStreamHandler('php://output');
$this->listenAddress = $listenAddress ?? '127.0.0.1:8080';
}
public function run(callable $handler): void
{
$socket = new SocketServer($this->listenAddress);
// create HTTP server, automatically start new fiber for each request on PHP 8.1+
$http = new HttpServer(...(\PHP_VERSION_ID >= 80100 ? [new FiberHandler(), $handler] : [$handler]));
$http->listen($socket);
$logger = $this->logger;
$logger->log('Listening on ' . \str_replace('tcp:', 'http:', (string) $socket->getAddress()));
$http->on('error', static function (\Exception $e) use ($logger): void {
$logger->log('HTTP error: ' . $e->getMessage());
});
// @codeCoverageIgnoreStart
try {
Loop::addSignal(\defined('SIGINT') ? \SIGINT : 2, $f1 = static function () use ($socket, $logger): void {
if (\PHP_VERSION_ID >= 70200 && \stream_isatty(\STDIN)) {
echo "\r";
}
$logger->log('Received SIGINT, stopping loop');
$socket->close();
Loop::stop();
});
Loop::addSignal(\defined('SIGTERM') ? \SIGTERM : 15, $f2 = static function () use ($socket, $logger): void {
$logger->log('Received SIGTERM, stopping loop');
$socket->close();
Loop::stop();
});
} catch (\BadMethodCallException $e) {
$logger->log('Notice: No signal handler support, installing ext-ev or ext-pcntl recommended for production use.');
}
// @codeCoverageIgnoreEnd
do {
Loop::run();
if ($socket->getAddress() !== null) {
// Fiber compatibility mode for PHP < 8.1: Restart loop as long as socket is available
$logger->log('Warning: Loop restarted. Upgrade to react/async v4 recommended for production use.');
} else {
break;
}
} while (true);
// remove signal handlers when loop stops (if registered)
Loop::removeSignal(\defined('SIGINT') ? \SIGINT : 2, $f1 ?? 'printf');
Loop::removeSignal(\defined('SIGTERM') ? \SIGTERM : 15, $f2 ?? 'printf');
}
}

View File

@@ -0,0 +1,48 @@
<?php
namespace FrameworkX\Io;
use Psr\Http\Message\ResponseInterface;
use React\Http\Message\Response;
/**
* @internal
*/
class RedirectHandler
{
/** @var string */
private $target;
/** @var int */
private $code;
/** @var string */
private $reason;
/** @var HtmlHandler */
private $html;
public function __construct(string $target, int $redirectStatusCode = Response::STATUS_FOUND)
{
if ($redirectStatusCode < 300 || $redirectStatusCode === Response::STATUS_NOT_MODIFIED || $redirectStatusCode >= 400) {
throw new \InvalidArgumentException('Invalid redirect status code given');
}
$this->target = $target;
$this->code = $redirectStatusCode;
$this->reason = \ucwords((new Response($redirectStatusCode))->getReasonPhrase()) ?: 'Redirect';
$this->html = new HtmlHandler();
}
public function __invoke(): ResponseInterface
{
$url = $this->html->escape($this->target);
return $this->html->statusResponse(
$this->code,
'Redirecting to ' . $url,
$this->reason,
"<p>Redirecting to <a href=\"$url\"><code>$url</code></a>...</p>\n"
)->withHeader('Location', $this->target);
}
}

View File

@@ -0,0 +1,116 @@
<?php
namespace FrameworkX\Io;
use FastRoute\DataGenerator\GroupCountBased as RouteGenerator;
use FastRoute\Dispatcher\GroupCountBased as RouteDispatcher;
use FastRoute\RouteCollector;
use FastRoute\RouteParser\Std as RouteParser;
use FrameworkX\AccessLogHandler;
use FrameworkX\Container;
use FrameworkX\ErrorHandler;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use React\Promise\PromiseInterface;
/**
* @internal
*/
class RouteHandler
{
/** @var RouteCollector */
private $routeCollector;
/** @var ?RouteDispatcher */
private $routeDispatcher;
/** @var ErrorHandler */
private $errorHandler;
/** @var Container */
private $container;
public function __construct(Container $container = null)
{
$this->routeCollector = new RouteCollector(new RouteParser(), new RouteGenerator());
$this->errorHandler = new ErrorHandler();
$this->container = $container ?? new Container();
}
/**
* @param string[] $methods
* @param string $route
* @param callable|class-string $handler
* @param callable|class-string ...$handlers
*/
public function map(array $methods, string $route, $handler, ...$handlers): void
{
if ($handlers) {
\array_unshift($handlers, $handler);
\end($handlers);
} else {
$handlers = [$handler];
}
$last = key($handlers);
$container = $this->container;
foreach ($handlers as $i => $handler) {
if ($handler instanceof Container && $i !== $last) {
$container = $handler;
unset($handlers[$i]);
} elseif ($handler instanceof AccessLogHandler || $handler === AccessLogHandler::class) {
throw new \TypeError('AccessLogHandler may currently only be passed as a global middleware');
} elseif (!\is_callable($handler)) {
$handlers[$i] = $container->callable($handler);
}
}
/** @var non-empty-array<callable> $handlers */
$handler = \count($handlers) > 1 ? new MiddlewareHandler(array_values($handlers)) : \reset($handlers);
$this->routeDispatcher = null;
$this->routeCollector->addRoute($methods, $route, $handler);
}
/**
* @return ResponseInterface|PromiseInterface<ResponseInterface>|\Generator
*/
public function __invoke(ServerRequestInterface $request)
{
$target = $request->getRequestTarget();
if ($target[0] !== '/' && $target !== '*') {
return $this->errorHandler->requestProxyUnsupported();
} elseif ($target !== '*') {
$target = $request->getUri()->getPath();
}
if ($this->routeDispatcher === null) {
$this->routeDispatcher = new RouteDispatcher($this->routeCollector->getData());
}
$routeInfo = $this->routeDispatcher->dispatch($request->getMethod(), $target);
assert(\is_array($routeInfo) && isset($routeInfo[0]));
// happy path: matching route found, assign route attributes and invoke request handler
if ($routeInfo[0] === \FastRoute\Dispatcher::FOUND) {
$handler = $routeInfo[1];
$vars = $routeInfo[2];
foreach ($vars as $key => $value) {
$request = $request->withAttribute($key, rawurldecode($value));
}
return $handler($request);
}
// no matching route found: report error `404 Not Found`
if ($routeInfo[0] === \FastRoute\Dispatcher::NOT_FOUND) {
return $this->errorHandler->requestNotFound();
}
// unexpected request method for route: report error `405 Method Not Allowed`
assert($routeInfo[0] === \FastRoute\Dispatcher::METHOD_NOT_ALLOWED);
assert(\is_array($routeInfo[1]) && \count($routeInfo[1]) > 0);
return $this->errorHandler->requestMethodNotAllowed($routeInfo[1]);
}
}

View File

@@ -0,0 +1,164 @@
<?php
namespace FrameworkX\Io;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use React\EventLoop\Loop;
use React\Http\Message\Response;
use React\Http\Message\ServerRequest;
use React\Promise\PromiseInterface;
use React\Stream\ReadableStreamInterface;
/**
* [Internal] Request handler for traditional PHP SAPIs.
*
* This request handler will be used when executed behind traditional PHP SAPIs
* (PHP-FPM, FastCGI, Apache, etc.). It will handle a single request and run
* until a single response is sent. This is particularly useful because it
* allows you to run the exact same app in any environment.
*
* Note that this is an internal class only and nothing you should usually have
* to care about. See also the `App` and `ReactiveHandler` for more details.
*
* @internal
*/
class SapiHandler
{
public function run(callable $handler): void
{
$request = $this->requestFromGlobals();
$response = $handler($request);
if ($response instanceof ResponseInterface) {
$this->sendResponse($response);
} elseif ($response instanceof PromiseInterface) {
/** @var PromiseInterface<ResponseInterface> $response */
$response->then(function (ResponseInterface $response): void {
$this->sendResponse($response);
});
}
Loop::run();
}
public function requestFromGlobals(): ServerRequestInterface
{
$host = null;
$headers = array();
// @codeCoverageIgnoreStart
if (\function_exists('getallheaders')) {
$headers = \getallheaders();
$host = \array_change_key_case($headers, \CASE_LOWER)['host'] ?? null;
} else {
foreach ($_SERVER as $key => $value) {
if (\strpos($key, 'HTTP_') === 0) {
$key = str_replace(' ', '-', \ucwords(\strtolower(\str_replace('_', ' ', \substr($key, 5)))));
$headers[$key] = $value;
if ($host === null && $key === 'Host') {
$host = $value;
}
}
}
}
// @codeCoverageIgnoreEnd
$target = ($_SERVER['REQUEST_URI'] ?? '/');
$url = $target;
if (($target[0] ?? '/') === '/' || $target === '*') {
$url = (($_SERVER['HTTPS'] ?? null) === 'on' ? 'https://' : 'http://') . ($host ?? 'localhost') . ($target === '*' ? '' : $target);
}
$body = file_get_contents('php://input');
assert(\is_string($body));
$request = new ServerRequest(
$_SERVER['REQUEST_METHOD'] ?? 'GET',
$url,
$headers,
$body,
substr($_SERVER['SERVER_PROTOCOL'] ?? 'http/1.1', 5),
$_SERVER
);
if ($host === null) {
$request = $request->withoutHeader('Host');
}
if (isset($target[0]) && $target[0] !== '/') {
$request = $request->withRequestTarget($target);
}
$request = $request->withParsedBody($_POST);
// Content-Length / Content-Type are special <3
if ($request->getHeaderLine('Content-Length') === '') {
$request = $request->withoutHeader('Content-Length');
}
if ($request->getHeaderLine('Content-Type') === '' && !isset($_SERVER['HTTP_CONTENT_TYPE'])) {
$request = $request->withoutHeader('Content-Type');
}
return $request;
}
/**
* @param ResponseInterface $response
*/
public function sendResponse(ResponseInterface $response): void
{
$status = $response->getStatusCode();
$body = $response->getBody();
if ($status === Response::STATUS_NO_CONTENT) {
// `204 No Content` MUST NOT include "Content-Length" response header
$response = $response->withoutHeader('Content-Length');
} elseif (!$response->hasHeader('Content-Length') && $body->getSize() !== null && ($status !== Response::STATUS_NOT_MODIFIED || $body->getSize() !== 0)) {
// automatically assign "Content-Length" response header if known and not already present
$response = $response->withHeader('Content-Length', (string) $body->getSize());
}
// remove default "Content-Type" header set by PHP (default_mimetype)
if (!$response->hasHeader('Content-Type')) {
header('Content-Type:');
header_remove('Content-Type');
}
// send all headers without applying default "; charset=utf-8" set by PHP (default_charset)
$old = ini_get('default_charset');
assert(\is_string($old));
ini_set('default_charset', '');
foreach ($response->getHeaders() as $name => $values) {
foreach ($values as $value) {
header($name . ': ' . $value, false);
}
}
ini_set('default_charset', $old);
header($_SERVER['SERVER_PROTOCOL'] . ' ' . $status . ' ' . $response->getReasonPhrase());
if (($_SERVER['REQUEST_METHOD'] ?? '') === 'HEAD' || $status === Response::STATUS_NO_CONTENT || $status === Response::STATUS_NOT_MODIFIED) {
$body->close();
return;
}
if ($body instanceof ReadableStreamInterface) {
// try to disable nginx buffering (http://nginx.org/en/docs/http/ngx_http_proxy_module.html#proxy_buffering)
if (isset($_SERVER['SERVER_SOFTWARE']) && strpos($_SERVER['SERVER_SOFTWARE'], 'nginx') === 0) {
header('X-Accel-Buffering: no');
}
// clear output buffer to show streaming output (default in cli-server)
if (\PHP_SAPI === 'cli-server') {
\ob_end_flush(); // @codeCoverageIgnore
}
// flush data whenever stream reports one data chunk
$body->on('data', function ($chunk) {
echo $chunk;
flush();
});
} else {
echo $body;
}
}
}

View File

@@ -0,0 +1 @@
vendor/

View File

@@ -0,0 +1,147 @@
# Changelog
All notable changes to this project will be documented in this file, in reverse chronological order by release.
## 1.1.5 - 2020-11-24
### Added
- [#19](https://github.com/php-fig/http-message-util/pull/19) adds support for PHP 8.
### Changed
- Nothing.
### Deprecated
- Nothing.
### Removed
- Nothing.
### Fixed
- Nothing.
## 1.1.4 - 2020-02-05
### Added
- Nothing.
### Changed
- Nothing.
### Deprecated
- Nothing.
### Removed
- [#15](https://github.com/php-fig/http-message-util/pull/15) removes the dependency on psr/http-message, as it is not technically necessary for usage of this package.
### Fixed
- Nothing.
## 1.1.3 - 2018-11-19
### Added
- [#10](https://github.com/php-fig/http-message-util/pull/10) adds the constants `StatusCodeInterface::STATUS_EARLY_HINTS` (103) and
`StatusCodeInterface::STATUS_TOO_EARLY` (425).
### Changed
- Nothing.
### Deprecated
- Nothing.
### Removed
- Nothing.
### Fixed
- Nothing.
## 1.1.2 - 2017-02-09
### Added
- [#4](https://github.com/php-fig/http-message-util/pull/4) adds the constant
`StatusCodeInterface::STATUS_MISDIRECTED_REQUEST` (421).
### Deprecated
- Nothing.
### Removed
- Nothing.
### Fixed
- Nothing.
## 1.1.1 - 2017-02-06
### Added
- [#3](https://github.com/php-fig/http-message-util/pull/3) adds the constant
`StatusCodeInterface::STATUS_IM_A_TEAPOT` (418).
### Deprecated
- Nothing.
### Removed
- Nothing.
### Fixed
- Nothing.
## 1.1.0 - 2016-09-19
### Added
- [#1](https://github.com/php-fig/http-message-util/pull/1) adds
`Fig\Http\Message\StatusCodeInterface`, with constants named after common
status reason phrases, with values indicating the status codes themselves.
### Deprecated
- Nothing.
### Removed
- Nothing.
### Fixed
- Nothing.
## 1.0.0 - 2017-08-05
### Added
- Adds `Fig\Http\Message\RequestMethodInterface`, with constants covering the
most common HTTP request methods as specified by the IETF.
### Deprecated
- Nothing.
### Removed
- Nothing.
### Fixed
- Nothing.

19
vendor/fig/http-message-util/LICENSE vendored Normal file
View File

@@ -0,0 +1,19 @@
Copyright (c) 2016 PHP Framework Interoperability Group
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

17
vendor/fig/http-message-util/README.md vendored Normal file
View File

@@ -0,0 +1,17 @@
# PSR Http Message Util
This repository holds utility classes and constants to facilitate common
operations of [PSR-7](https://www.php-fig.org/psr/psr-7/); the primary purpose is
to provide constants for referring to request methods, response status codes and
messages, and potentially common headers.
Implementation of PSR-7 interfaces is **not** within the scope of this package.
## Installation
Install by adding the package as a [Composer](https://getcomposer.org)
requirement:
```bash
$ composer require fig/http-message-util
```

View File

@@ -0,0 +1,28 @@
{
"name": "fig/http-message-util",
"description": "Utility classes and constants for use with PSR-7 (psr/http-message)",
"keywords": ["psr", "psr-7", "http", "http-message", "request", "response"],
"license": "MIT",
"authors": [
{
"name": "PHP-FIG",
"homepage": "https://www.php-fig.org/"
}
],
"require": {
"php": "^5.3 || ^7.0 || ^8.0"
},
"suggest": {
"psr/http-message": "The package containing the PSR-7 interfaces"
},
"autoload": {
"psr-4": {
"Fig\\Http\\Message\\": "src/"
}
},
"extra": {
"branch-alias": {
"dev-master": "1.1.x-dev"
}
}
}

View File

@@ -0,0 +1,34 @@
<?php
namespace Fig\Http\Message;
/**
* Defines constants for common HTTP request methods.
*
* Usage:
*
* <code>
* class RequestFactory implements RequestMethodInterface
* {
* public static function factory(
* $uri = '/',
* $method = self::METHOD_GET,
* $data = []
* ) {
* }
* }
* </code>
*/
interface RequestMethodInterface
{
const METHOD_HEAD = 'HEAD';
const METHOD_GET = 'GET';
const METHOD_POST = 'POST';
const METHOD_PUT = 'PUT';
const METHOD_PATCH = 'PATCH';
const METHOD_DELETE = 'DELETE';
const METHOD_PURGE = 'PURGE';
const METHOD_OPTIONS = 'OPTIONS';
const METHOD_TRACE = 'TRACE';
const METHOD_CONNECT = 'CONNECT';
}

View File

@@ -0,0 +1,107 @@
<?php
namespace Fig\Http\Message;
/**
* Defines constants for common HTTP status code.
*
* @see https://tools.ietf.org/html/rfc2295#section-8.1
* @see https://tools.ietf.org/html/rfc2324#section-2.3
* @see https://tools.ietf.org/html/rfc2518#section-9.7
* @see https://tools.ietf.org/html/rfc2774#section-7
* @see https://tools.ietf.org/html/rfc3229#section-10.4
* @see https://tools.ietf.org/html/rfc4918#section-11
* @see https://tools.ietf.org/html/rfc5842#section-7.1
* @see https://tools.ietf.org/html/rfc5842#section-7.2
* @see https://tools.ietf.org/html/rfc6585#section-3
* @see https://tools.ietf.org/html/rfc6585#section-4
* @see https://tools.ietf.org/html/rfc6585#section-5
* @see https://tools.ietf.org/html/rfc6585#section-6
* @see https://tools.ietf.org/html/rfc7231#section-6
* @see https://tools.ietf.org/html/rfc7238#section-3
* @see https://tools.ietf.org/html/rfc7725#section-3
* @see https://tools.ietf.org/html/rfc7540#section-9.1.2
* @see https://tools.ietf.org/html/rfc8297#section-2
* @see https://tools.ietf.org/html/rfc8470#section-7
* Usage:
*
* <code>
* class ResponseFactory implements StatusCodeInterface
* {
* public function createResponse($code = self::STATUS_OK)
* {
* }
* }
* </code>
*/
interface StatusCodeInterface
{
// Informational 1xx
const STATUS_CONTINUE = 100;
const STATUS_SWITCHING_PROTOCOLS = 101;
const STATUS_PROCESSING = 102;
const STATUS_EARLY_HINTS = 103;
// Successful 2xx
const STATUS_OK = 200;
const STATUS_CREATED = 201;
const STATUS_ACCEPTED = 202;
const STATUS_NON_AUTHORITATIVE_INFORMATION = 203;
const STATUS_NO_CONTENT = 204;
const STATUS_RESET_CONTENT = 205;
const STATUS_PARTIAL_CONTENT = 206;
const STATUS_MULTI_STATUS = 207;
const STATUS_ALREADY_REPORTED = 208;
const STATUS_IM_USED = 226;
// Redirection 3xx
const STATUS_MULTIPLE_CHOICES = 300;
const STATUS_MOVED_PERMANENTLY = 301;
const STATUS_FOUND = 302;
const STATUS_SEE_OTHER = 303;
const STATUS_NOT_MODIFIED = 304;
const STATUS_USE_PROXY = 305;
const STATUS_RESERVED = 306;
const STATUS_TEMPORARY_REDIRECT = 307;
const STATUS_PERMANENT_REDIRECT = 308;
// Client Errors 4xx
const STATUS_BAD_REQUEST = 400;
const STATUS_UNAUTHORIZED = 401;
const STATUS_PAYMENT_REQUIRED = 402;
const STATUS_FORBIDDEN = 403;
const STATUS_NOT_FOUND = 404;
const STATUS_METHOD_NOT_ALLOWED = 405;
const STATUS_NOT_ACCEPTABLE = 406;
const STATUS_PROXY_AUTHENTICATION_REQUIRED = 407;
const STATUS_REQUEST_TIMEOUT = 408;
const STATUS_CONFLICT = 409;
const STATUS_GONE = 410;
const STATUS_LENGTH_REQUIRED = 411;
const STATUS_PRECONDITION_FAILED = 412;
const STATUS_PAYLOAD_TOO_LARGE = 413;
const STATUS_URI_TOO_LONG = 414;
const STATUS_UNSUPPORTED_MEDIA_TYPE = 415;
const STATUS_RANGE_NOT_SATISFIABLE = 416;
const STATUS_EXPECTATION_FAILED = 417;
const STATUS_IM_A_TEAPOT = 418;
const STATUS_MISDIRECTED_REQUEST = 421;
const STATUS_UNPROCESSABLE_ENTITY = 422;
const STATUS_LOCKED = 423;
const STATUS_FAILED_DEPENDENCY = 424;
const STATUS_TOO_EARLY = 425;
const STATUS_UPGRADE_REQUIRED = 426;
const STATUS_PRECONDITION_REQUIRED = 428;
const STATUS_TOO_MANY_REQUESTS = 429;
const STATUS_REQUEST_HEADER_FIELDS_TOO_LARGE = 431;
const STATUS_UNAVAILABLE_FOR_LEGAL_REASONS = 451;
// Server Errors 5xx
const STATUS_INTERNAL_SERVER_ERROR = 500;
const STATUS_NOT_IMPLEMENTED = 501;
const STATUS_BAD_GATEWAY = 502;
const STATUS_SERVICE_UNAVAILABLE = 503;
const STATUS_GATEWAY_TIMEOUT = 504;
const STATUS_VERSION_NOT_SUPPORTED = 505;
const STATUS_VARIANT_ALSO_NEGOTIATES = 506;
const STATUS_INSUFFICIENT_STORAGE = 507;
const STATUS_LOOP_DETECTED = 508;
const STATUS_NOT_EXTENDED = 510;
const STATUS_NETWORK_AUTHENTICATION_REQUIRED = 511;
}

5
vendor/nikic/fast-route/.gitignore vendored Normal file
View File

@@ -0,0 +1,5 @@
/vendor/
.idea/
# ignore lock file since we have no extra dependencies
composer.lock

1
vendor/nikic/fast-route/.hhconfig vendored Normal file
View File

@@ -0,0 +1 @@
assume_php=false

20
vendor/nikic/fast-route/.travis.yml vendored Normal file
View File

@@ -0,0 +1,20 @@
sudo: false
language: php
php:
- 5.4
- 5.5
- 5.6
- 7.0
- 7.1
- 7.2
- hhvm
script:
- ./vendor/bin/phpunit
before_install:
- travis_retry composer self-update
install:
- composer install

126
vendor/nikic/fast-route/FastRoute.hhi vendored Normal file
View File

@@ -0,0 +1,126 @@
<?hh // decl
namespace FastRoute {
class BadRouteException extends \LogicException {
}
interface RouteParser {
public function parse(string $route): array<array>;
}
class RouteCollector {
public function __construct(RouteParser $routeParser, DataGenerator $dataGenerator);
public function addRoute(mixed $httpMethod, string $route, mixed $handler): void;
public function getData(): array;
}
class Route {
public function __construct(string $httpMethod, mixed $handler, string $regex, array $variables);
public function matches(string $str): bool;
}
interface DataGenerator {
public function addRoute(string $httpMethod, array $routeData, mixed $handler);
public function getData(): array;
}
interface Dispatcher {
const int NOT_FOUND = 0;
const int FOUND = 1;
const int METHOD_NOT_ALLOWED = 2;
public function dispatch(string $httpMethod, string $uri): array;
}
function simpleDispatcher(
(function(RouteCollector): void) $routeDefinitionCallback,
shape(
?'routeParser' => classname<RouteParser>,
?'dataGenerator' => classname<DataGenerator>,
?'dispatcher' => classname<Dispatcher>,
?'routeCollector' => classname<RouteCollector>,
) $options = shape()): Dispatcher;
function cachedDispatcher(
(function(RouteCollector): void) $routeDefinitionCallback,
shape(
?'routeParser' => classname<RouteParser>,
?'dataGenerator' => classname<DataGenerator>,
?'dispatcher' => classname<Dispatcher>,
?'routeCollector' => classname<RouteCollector>,
?'cacheDisabled' => bool,
?'cacheFile' => string,
) $options = shape()): Dispatcher;
}
namespace FastRoute\DataGenerator {
abstract class RegexBasedAbstract implements \FastRoute\DataGenerator {
protected abstract function getApproxChunkSize();
protected abstract function processChunk($regexToRoutesMap);
public function addRoute(string $httpMethod, array $routeData, mixed $handler): void;
public function getData(): array;
}
class CharCountBased extends RegexBasedAbstract {
protected function getApproxChunkSize(): int;
protected function processChunk(array<string, string> $regexToRoutesMap): array<string, mixed>;
}
class GroupCountBased extends RegexBasedAbstract {
protected function getApproxChunkSize(): int;
protected function processChunk(array<string, string> $regexToRoutesMap): array<string, mixed>;
}
class GroupPosBased extends RegexBasedAbstract {
protected function getApproxChunkSize(): int;
protected function processChunk(array<string, string> $regexToRoutesMap): array<string, mixed>;
}
class MarkBased extends RegexBasedAbstract {
protected function getApproxChunkSize(): int;
protected function processChunk(array<string, string> $regexToRoutesMap): array<string, mixed>;
}
}
namespace FastRoute\Dispatcher {
abstract class RegexBasedAbstract implements \FastRoute\Dispatcher {
protected abstract function dispatchVariableRoute(array<array> $routeData, string $uri): array;
public function dispatch(string $httpMethod, string $uri): array;
}
class GroupPosBased extends RegexBasedAbstract {
public function __construct(array $data);
protected function dispatchVariableRoute(array<array> $routeData, string $uri): array;
}
class GroupCountBased extends RegexBasedAbstract {
public function __construct(array $data);
protected function dispatchVariableRoute(array<array> $routeData, string $uri): array;
}
class CharCountBased extends RegexBasedAbstract {
public function __construct(array $data);
protected function dispatchVariableRoute(array<array> $routeData, string $uri): array;
}
class MarkBased extends RegexBasedAbstract {
public function __construct(array $data);
protected function dispatchVariableRoute(array<array> $routeData, string $uri): array;
}
}
namespace FastRoute\RouteParser {
class Std implements \FastRoute\RouteParser {
const string VARIABLE_REGEX = <<<'REGEX'
\{
\s* ([a-zA-Z][a-zA-Z0-9_]*) \s*
(?:
: \s* ([^{}]*(?:\{(?-1)\}[^{}]*)*)
)?
\}
REGEX;
const string DEFAULT_DISPATCH_REGEX = '[^/]+';
public function parse(string $route): array<array>;
}
}

31
vendor/nikic/fast-route/LICENSE vendored Normal file
View File

@@ -0,0 +1,31 @@
Copyright (c) 2013 by Nikita Popov.
Some rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following
disclaimer in the documentation and/or other materials provided
with the distribution.
* The names of the contributors may not be used to endorse or
promote products derived from this software without specific
prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

313
vendor/nikic/fast-route/README.md vendored Normal file
View File

@@ -0,0 +1,313 @@
FastRoute - Fast request router for PHP
=======================================
This library provides a fast implementation of a regular expression based router. [Blog post explaining how the
implementation works and why it is fast.][blog_post]
Install
-------
To install with composer:
```sh
composer require nikic/fast-route
```
Requires PHP 5.4 or newer.
Usage
-----
Here's a basic usage example:
```php
<?php
require '/path/to/vendor/autoload.php';
$dispatcher = FastRoute\simpleDispatcher(function(FastRoute\RouteCollector $r) {
$r->addRoute('GET', '/users', 'get_all_users_handler');
// {id} must be a number (\d+)
$r->addRoute('GET', '/user/{id:\d+}', 'get_user_handler');
// The /{title} suffix is optional
$r->addRoute('GET', '/articles/{id:\d+}[/{title}]', 'get_article_handler');
});
// Fetch method and URI from somewhere
$httpMethod = $_SERVER['REQUEST_METHOD'];
$uri = $_SERVER['REQUEST_URI'];
// Strip query string (?foo=bar) and decode URI
if (false !== $pos = strpos($uri, '?')) {
$uri = substr($uri, 0, $pos);
}
$uri = rawurldecode($uri);
$routeInfo = $dispatcher->dispatch($httpMethod, $uri);
switch ($routeInfo[0]) {
case FastRoute\Dispatcher::NOT_FOUND:
// ... 404 Not Found
break;
case FastRoute\Dispatcher::METHOD_NOT_ALLOWED:
$allowedMethods = $routeInfo[1];
// ... 405 Method Not Allowed
break;
case FastRoute\Dispatcher::FOUND:
$handler = $routeInfo[1];
$vars = $routeInfo[2];
// ... call $handler with $vars
break;
}
```
### Defining routes
The routes are defined by calling the `FastRoute\simpleDispatcher()` function, which accepts
a callable taking a `FastRoute\RouteCollector` instance. The routes are added by calling
`addRoute()` on the collector instance:
```php
$r->addRoute($method, $routePattern, $handler);
```
The `$method` is an uppercase HTTP method string for which a certain route should match. It
is possible to specify multiple valid methods using an array:
```php
// These two calls
$r->addRoute('GET', '/test', 'handler');
$r->addRoute('POST', '/test', 'handler');
// Are equivalent to this one call
$r->addRoute(['GET', 'POST'], '/test', 'handler');
```
By default the `$routePattern` uses a syntax where `{foo}` specifies a placeholder with name `foo`
and matching the regex `[^/]+`. To adjust the pattern the placeholder matches, you can specify
a custom pattern by writing `{bar:[0-9]+}`. Some examples:
```php
// Matches /user/42, but not /user/xyz
$r->addRoute('GET', '/user/{id:\d+}', 'handler');
// Matches /user/foobar, but not /user/foo/bar
$r->addRoute('GET', '/user/{name}', 'handler');
// Matches /user/foo/bar as well
$r->addRoute('GET', '/user/{name:.+}', 'handler');
```
Custom patterns for route placeholders cannot use capturing groups. For example `{lang:(en|de)}`
is not a valid placeholder, because `()` is a capturing group. Instead you can use either
`{lang:en|de}` or `{lang:(?:en|de)}`.
Furthermore parts of the route enclosed in `[...]` are considered optional, so that `/foo[bar]`
will match both `/foo` and `/foobar`. Optional parts are only supported in a trailing position,
not in the middle of a route.
```php
// This route
$r->addRoute('GET', '/user/{id:\d+}[/{name}]', 'handler');
// Is equivalent to these two routes
$r->addRoute('GET', '/user/{id:\d+}', 'handler');
$r->addRoute('GET', '/user/{id:\d+}/{name}', 'handler');
// Multiple nested optional parts are possible as well
$r->addRoute('GET', '/user[/{id:\d+}[/{name}]]', 'handler');
// This route is NOT valid, because optional parts can only occur at the end
$r->addRoute('GET', '/user[/{id:\d+}]/{name}', 'handler');
```
The `$handler` parameter does not necessarily have to be a callback, it could also be a controller
class name or any other kind of data you wish to associate with the route. FastRoute only tells you
which handler corresponds to your URI, how you interpret it is up to you.
#### Shorcut methods for common request methods
For the `GET`, `POST`, `PUT`, `PATCH`, `DELETE` and `HEAD` request methods shortcut methods are available. For example:
```php
$r->get('/get-route', 'get_handler');
$r->post('/post-route', 'post_handler');
```
Is equivalent to:
```php
$r->addRoute('GET', '/get-route', 'get_handler');
$r->addRoute('POST', '/post-route', 'post_handler');
```
#### Route Groups
Additionally, you can specify routes inside of a group. All routes defined inside a group will have a common prefix.
For example, defining your routes as:
```php
$r->addGroup('/admin', function (RouteCollector $r) {
$r->addRoute('GET', '/do-something', 'handler');
$r->addRoute('GET', '/do-another-thing', 'handler');
$r->addRoute('GET', '/do-something-else', 'handler');
});
```
Will have the same result as:
```php
$r->addRoute('GET', '/admin/do-something', 'handler');
$r->addRoute('GET', '/admin/do-another-thing', 'handler');
$r->addRoute('GET', '/admin/do-something-else', 'handler');
```
Nested groups are also supported, in which case the prefixes of all the nested groups are combined.
### Caching
The reason `simpleDispatcher` accepts a callback for defining the routes is to allow seamless
caching. By using `cachedDispatcher` instead of `simpleDispatcher` you can cache the generated
routing data and construct the dispatcher from the cached information:
```php
<?php
$dispatcher = FastRoute\cachedDispatcher(function(FastRoute\RouteCollector $r) {
$r->addRoute('GET', '/user/{name}/{id:[0-9]+}', 'handler0');
$r->addRoute('GET', '/user/{id:[0-9]+}', 'handler1');
$r->addRoute('GET', '/user/{name}', 'handler2');
}, [
'cacheFile' => __DIR__ . '/route.cache', /* required */
'cacheDisabled' => IS_DEBUG_ENABLED, /* optional, enabled by default */
]);
```
The second parameter to the function is an options array, which can be used to specify the cache
file location, among other things.
### Dispatching a URI
A URI is dispatched by calling the `dispatch()` method of the created dispatcher. This method
accepts the HTTP method and a URI. Getting those two bits of information (and normalizing them
appropriately) is your job - this library is not bound to the PHP web SAPIs.
The `dispatch()` method returns an array whose first element contains a status code. It is one
of `Dispatcher::NOT_FOUND`, `Dispatcher::METHOD_NOT_ALLOWED` and `Dispatcher::FOUND`. For the
method not allowed status the second array element contains a list of HTTP methods allowed for
the supplied URI. For example:
[FastRoute\Dispatcher::METHOD_NOT_ALLOWED, ['GET', 'POST']]
> **NOTE:** The HTTP specification requires that a `405 Method Not Allowed` response include the
`Allow:` header to detail available methods for the requested resource. Applications using FastRoute
should use the second array element to add this header when relaying a 405 response.
For the found status the second array element is the handler that was associated with the route
and the third array element is a dictionary of placeholder names to their values. For example:
/* Routing against GET /user/nikic/42 */
[FastRoute\Dispatcher::FOUND, 'handler0', ['name' => 'nikic', 'id' => '42']]
### Overriding the route parser and dispatcher
The routing process makes use of three components: A route parser, a data generator and a
dispatcher. The three components adhere to the following interfaces:
```php
<?php
namespace FastRoute;
interface RouteParser {
public function parse($route);
}
interface DataGenerator {
public function addRoute($httpMethod, $routeData, $handler);
public function getData();
}
interface Dispatcher {
const NOT_FOUND = 0, FOUND = 1, METHOD_NOT_ALLOWED = 2;
public function dispatch($httpMethod, $uri);
}
```
The route parser takes a route pattern string and converts it into an array of route infos, where
each route info is again an array of it's parts. The structure is best understood using an example:
/* The route /user/{id:\d+}[/{name}] converts to the following array: */
[
[
'/user/',
['id', '\d+'],
],
[
'/user/',
['id', '\d+'],
'/',
['name', '[^/]+'],
],
]
This array can then be passed to the `addRoute()` method of a data generator. After all routes have
been added the `getData()` of the generator is invoked, which returns all the routing data required
by the dispatcher. The format of this data is not further specified - it is tightly coupled to
the corresponding dispatcher.
The dispatcher accepts the routing data via a constructor and provides a `dispatch()` method, which
you're already familiar with.
The route parser can be overwritten individually (to make use of some different pattern syntax),
however the data generator and dispatcher should always be changed as a pair, as the output from
the former is tightly coupled to the input of the latter. The reason the generator and the
dispatcher are separate is that only the latter is needed when using caching (as the output of
the former is what is being cached.)
When using the `simpleDispatcher` / `cachedDispatcher` functions from above the override happens
through the options array:
```php
<?php
$dispatcher = FastRoute\simpleDispatcher(function(FastRoute\RouteCollector $r) {
/* ... */
}, [
'routeParser' => 'FastRoute\\RouteParser\\Std',
'dataGenerator' => 'FastRoute\\DataGenerator\\GroupCountBased',
'dispatcher' => 'FastRoute\\Dispatcher\\GroupCountBased',
]);
```
The above options array corresponds to the defaults. By replacing `GroupCountBased` by
`GroupPosBased` you could switch to a different dispatching strategy.
### A Note on HEAD Requests
The HTTP spec requires servers to [support both GET and HEAD methods][2616-511]:
> The methods GET and HEAD MUST be supported by all general-purpose servers
To avoid forcing users to manually register HEAD routes for each resource we fallback to matching an
available GET route for a given resource. The PHP web SAPI transparently removes the entity body
from HEAD responses so this behavior has no effect on the vast majority of users.
However, implementers using FastRoute outside the web SAPI environment (e.g. a custom server) MUST
NOT send entity bodies generated in response to HEAD requests. If you are a non-SAPI user this is
*your responsibility*; FastRoute has no purview to prevent you from breaking HTTP in such cases.
Finally, note that applications MAY always specify their own HEAD method route for a given
resource to bypass this behavior entirely.
### Credits
This library is based on a router that [Levi Morrison][levi] implemented for the Aerys server.
A large number of tests, as well as HTTP compliance considerations, were provided by [Daniel Lowrey][rdlowrey].
[2616-511]: http://www.w3.org/Protocols/rfc2616/rfc2616-sec5.html#sec5.1.1 "RFC 2616 Section 5.1.1"
[blog_post]: http://nikic.github.io/2014/02/18/Fast-request-routing-using-regular-expressions.html
[levi]: https://github.com/morrisonlevi
[rdlowrey]: https://github.com/rdlowrey

24
vendor/nikic/fast-route/composer.json vendored Normal file
View File

@@ -0,0 +1,24 @@
{
"name": "nikic/fast-route",
"description": "Fast request router for PHP",
"keywords": ["routing", "router"],
"license": "BSD-3-Clause",
"authors": [
{
"name": "Nikita Popov",
"email": "nikic@php.net"
}
],
"autoload": {
"psr-4": {
"FastRoute\\": "src/"
},
"files": ["src/functions.php"]
},
"require": {
"php": ">=5.4.0"
},
"require-dev": {
"phpunit/phpunit": "^4.8.35|~5.7"
}
}

24
vendor/nikic/fast-route/phpunit.xml vendored Normal file
View File

@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit backupGlobals="false"
backupStaticAttributes="false"
colors="true"
convertErrorsToExceptions="true"
convertNoticesToExceptions="true"
convertWarningsToExceptions="true"
processIsolation="false"
syntaxCheck="false"
bootstrap="test/bootstrap.php"
>
<testsuites>
<testsuite name="FastRoute Tests">
<directory>./test/</directory>
</testsuite>
</testsuites>
<filter>
<whitelist>
<directory>./src/</directory>
</whitelist>
</filter>
</phpunit>

28
vendor/nikic/fast-route/psalm.xml vendored Normal file
View File

@@ -0,0 +1,28 @@
<?xml version="1.0"?>
<psalm
name="Example Psalm config with recommended defaults"
stopOnFirstError="false"
useDocblockTypes="true"
totallyTyped="false"
requireVoidReturnType="false"
>
<projectFiles>
<directory name="src" />
</projectFiles>
<issueHandlers>
<LessSpecificReturnType errorLevel="info" />
<!-- level 3 issues - slightly lazy code writing, but provably low false-negatives -->
<DeprecatedMethod errorLevel="info" />
<MissingClosureReturnType errorLevel="info" />
<MissingReturnType errorLevel="info" />
<MissingPropertyType errorLevel="info" />
<InvalidDocblock errorLevel="info" />
<MisplacedRequiredParam errorLevel="info" />
<PropertyNotSetInConstructor errorLevel="info" />
<MissingConstructor errorLevel="info" />
</issueHandlers>
</psalm>

View File

@@ -0,0 +1,7 @@
<?php
namespace FastRoute;
class BadRouteException extends \LogicException
{
}

View File

@@ -0,0 +1,26 @@
<?php
namespace FastRoute;
interface DataGenerator
{
/**
* Adds a route to the data generator. The route data uses the
* same format that is returned by RouterParser::parser().
*
* The handler doesn't necessarily need to be a callable, it
* can be arbitrary data that will be returned when the route
* matches.
*
* @param string $httpMethod
* @param array $routeData
* @param mixed $handler
*/
public function addRoute($httpMethod, $routeData, $handler);
/**
* Returns dispatcher data in some unspecified format, which
* depends on the used method of dispatch.
*/
public function getData();
}

View File

@@ -0,0 +1,31 @@
<?php
namespace FastRoute\DataGenerator;
class CharCountBased extends RegexBasedAbstract
{
protected function getApproxChunkSize()
{
return 30;
}
protected function processChunk($regexToRoutesMap)
{
$routeMap = [];
$regexes = [];
$suffixLen = 0;
$suffix = '';
$count = count($regexToRoutesMap);
foreach ($regexToRoutesMap as $regex => $route) {
$suffixLen++;
$suffix .= "\t";
$regexes[] = '(?:' . $regex . '/(\t{' . $suffixLen . '})\t{' . ($count - $suffixLen) . '})';
$routeMap[$suffix] = [$route->handler, $route->variables];
}
$regex = '~^(?|' . implode('|', $regexes) . ')$~';
return ['regex' => $regex, 'suffix' => '/' . $suffix, 'routeMap' => $routeMap];
}
}

View File

@@ -0,0 +1,30 @@
<?php
namespace FastRoute\DataGenerator;
class GroupCountBased extends RegexBasedAbstract
{
protected function getApproxChunkSize()
{
return 10;
}
protected function processChunk($regexToRoutesMap)
{
$routeMap = [];
$regexes = [];
$numGroups = 0;
foreach ($regexToRoutesMap as $regex => $route) {
$numVariables = count($route->variables);
$numGroups = max($numGroups, $numVariables);
$regexes[] = $regex . str_repeat('()', $numGroups - $numVariables);
$routeMap[$numGroups + 1] = [$route->handler, $route->variables];
++$numGroups;
}
$regex = '~^(?|' . implode('|', $regexes) . ')$~';
return ['regex' => $regex, 'routeMap' => $routeMap];
}
}

View File

@@ -0,0 +1,27 @@
<?php
namespace FastRoute\DataGenerator;
class GroupPosBased extends RegexBasedAbstract
{
protected function getApproxChunkSize()
{
return 10;
}
protected function processChunk($regexToRoutesMap)
{
$routeMap = [];
$regexes = [];
$offset = 1;
foreach ($regexToRoutesMap as $regex => $route) {
$regexes[] = $regex;
$routeMap[$offset] = [$route->handler, $route->variables];
$offset += count($route->variables);
}
$regex = '~^(?:' . implode('|', $regexes) . ')$~';
return ['regex' => $regex, 'routeMap' => $routeMap];
}
}

View File

@@ -0,0 +1,27 @@
<?php
namespace FastRoute\DataGenerator;
class MarkBased extends RegexBasedAbstract
{
protected function getApproxChunkSize()
{
return 30;
}
protected function processChunk($regexToRoutesMap)
{
$routeMap = [];
$regexes = [];
$markName = 'a';
foreach ($regexToRoutesMap as $regex => $route) {
$regexes[] = $regex . '(*MARK:' . $markName . ')';
$routeMap[$markName] = [$route->handler, $route->variables];
++$markName;
}
$regex = '~^(?|' . implode('|', $regexes) . ')$~';
return ['regex' => $regex, 'routeMap' => $routeMap];
}
}

View File

@@ -0,0 +1,186 @@
<?php
namespace FastRoute\DataGenerator;
use FastRoute\BadRouteException;
use FastRoute\DataGenerator;
use FastRoute\Route;
abstract class RegexBasedAbstract implements DataGenerator
{
/** @var mixed[][] */
protected $staticRoutes = [];
/** @var Route[][] */
protected $methodToRegexToRoutesMap = [];
/**
* @return int
*/
abstract protected function getApproxChunkSize();
/**
* @return mixed[]
*/
abstract protected function processChunk($regexToRoutesMap);
public function addRoute($httpMethod, $routeData, $handler)
{
if ($this->isStaticRoute($routeData)) {
$this->addStaticRoute($httpMethod, $routeData, $handler);
} else {
$this->addVariableRoute($httpMethod, $routeData, $handler);
}
}
/**
* @return mixed[]
*/
public function getData()
{
if (empty($this->methodToRegexToRoutesMap)) {
return [$this->staticRoutes, []];
}
return [$this->staticRoutes, $this->generateVariableRouteData()];
}
/**
* @return mixed[]
*/
private function generateVariableRouteData()
{
$data = [];
foreach ($this->methodToRegexToRoutesMap as $method => $regexToRoutesMap) {
$chunkSize = $this->computeChunkSize(count($regexToRoutesMap));
$chunks = array_chunk($regexToRoutesMap, $chunkSize, true);
$data[$method] = array_map([$this, 'processChunk'], $chunks);
}
return $data;
}
/**
* @param int
* @return int
*/
private function computeChunkSize($count)
{
$numParts = max(1, round($count / $this->getApproxChunkSize()));
return (int) ceil($count / $numParts);
}
/**
* @param mixed[]
* @return bool
*/
private function isStaticRoute($routeData)
{
return count($routeData) === 1 && is_string($routeData[0]);
}
private function addStaticRoute($httpMethod, $routeData, $handler)
{
$routeStr = $routeData[0];
if (isset($this->staticRoutes[$httpMethod][$routeStr])) {
throw new BadRouteException(sprintf(
'Cannot register two routes matching "%s" for method "%s"',
$routeStr, $httpMethod
));
}
if (isset($this->methodToRegexToRoutesMap[$httpMethod])) {
foreach ($this->methodToRegexToRoutesMap[$httpMethod] as $route) {
if ($route->matches($routeStr)) {
throw new BadRouteException(sprintf(
'Static route "%s" is shadowed by previously defined variable route "%s" for method "%s"',
$routeStr, $route->regex, $httpMethod
));
}
}
}
$this->staticRoutes[$httpMethod][$routeStr] = $handler;
}
private function addVariableRoute($httpMethod, $routeData, $handler)
{
list($regex, $variables) = $this->buildRegexForRoute($routeData);
if (isset($this->methodToRegexToRoutesMap[$httpMethod][$regex])) {
throw new BadRouteException(sprintf(
'Cannot register two routes matching "%s" for method "%s"',
$regex, $httpMethod
));
}
$this->methodToRegexToRoutesMap[$httpMethod][$regex] = new Route(
$httpMethod, $handler, $regex, $variables
);
}
/**
* @param mixed[]
* @return mixed[]
*/
private function buildRegexForRoute($routeData)
{
$regex = '';
$variables = [];
foreach ($routeData as $part) {
if (is_string($part)) {
$regex .= preg_quote($part, '~');
continue;
}
list($varName, $regexPart) = $part;
if (isset($variables[$varName])) {
throw new BadRouteException(sprintf(
'Cannot use the same placeholder "%s" twice', $varName
));
}
if ($this->regexHasCapturingGroups($regexPart)) {
throw new BadRouteException(sprintf(
'Regex "%s" for parameter "%s" contains a capturing group',
$regexPart, $varName
));
}
$variables[$varName] = $varName;
$regex .= '(' . $regexPart . ')';
}
return [$regex, $variables];
}
/**
* @param string
* @return bool
*/
private function regexHasCapturingGroups($regex)
{
if (false === strpos($regex, '(')) {
// Needs to have at least a ( to contain a capturing group
return false;
}
// Semi-accurate detection for capturing groups
return (bool) preg_match(
'~
(?:
\(\?\(
| \[ [^\]\\\\]* (?: \\\\ . [^\]\\\\]* )* \]
| \\\\ .
) (*SKIP)(*FAIL) |
\(
(?!
\? (?! <(?![!=]) | P< | \' )
| \*
)
~x',
$regex
);
}
}

View File

@@ -0,0 +1,26 @@
<?php
namespace FastRoute;
interface Dispatcher
{
const NOT_FOUND = 0;
const FOUND = 1;
const METHOD_NOT_ALLOWED = 2;
/**
* Dispatches against the provided HTTP method verb and URI.
*
* Returns array with one of the following formats:
*
* [self::NOT_FOUND]
* [self::METHOD_NOT_ALLOWED, ['GET', 'OTHER_ALLOWED_METHODS']]
* [self::FOUND, $handler, ['varName' => 'value', ...]]
*
* @param string $httpMethod
* @param string $uri
*
* @return array
*/
public function dispatch($httpMethod, $uri);
}

View File

@@ -0,0 +1,31 @@
<?php
namespace FastRoute\Dispatcher;
class CharCountBased extends RegexBasedAbstract
{
public function __construct($data)
{
list($this->staticRouteMap, $this->variableRouteData) = $data;
}
protected function dispatchVariableRoute($routeData, $uri)
{
foreach ($routeData as $data) {
if (!preg_match($data['regex'], $uri . $data['suffix'], $matches)) {
continue;
}
list($handler, $varNames) = $data['routeMap'][end($matches)];
$vars = [];
$i = 0;
foreach ($varNames as $varName) {
$vars[$varName] = $matches[++$i];
}
return [self::FOUND, $handler, $vars];
}
return [self::NOT_FOUND];
}
}

View File

@@ -0,0 +1,31 @@
<?php
namespace FastRoute\Dispatcher;
class GroupCountBased extends RegexBasedAbstract
{
public function __construct($data)
{
list($this->staticRouteMap, $this->variableRouteData) = $data;
}
protected function dispatchVariableRoute($routeData, $uri)
{
foreach ($routeData as $data) {
if (!preg_match($data['regex'], $uri, $matches)) {
continue;
}
list($handler, $varNames) = $data['routeMap'][count($matches)];
$vars = [];
$i = 0;
foreach ($varNames as $varName) {
$vars[$varName] = $matches[++$i];
}
return [self::FOUND, $handler, $vars];
}
return [self::NOT_FOUND];
}
}

View File

@@ -0,0 +1,33 @@
<?php
namespace FastRoute\Dispatcher;
class GroupPosBased extends RegexBasedAbstract
{
public function __construct($data)
{
list($this->staticRouteMap, $this->variableRouteData) = $data;
}
protected function dispatchVariableRoute($routeData, $uri)
{
foreach ($routeData as $data) {
if (!preg_match($data['regex'], $uri, $matches)) {
continue;
}
// find first non-empty match
for ($i = 1; '' === $matches[$i]; ++$i);
list($handler, $varNames) = $data['routeMap'][$i];
$vars = [];
foreach ($varNames as $varName) {
$vars[$varName] = $matches[$i++];
}
return [self::FOUND, $handler, $vars];
}
return [self::NOT_FOUND];
}
}

View File

@@ -0,0 +1,31 @@
<?php
namespace FastRoute\Dispatcher;
class MarkBased extends RegexBasedAbstract
{
public function __construct($data)
{
list($this->staticRouteMap, $this->variableRouteData) = $data;
}
protected function dispatchVariableRoute($routeData, $uri)
{
foreach ($routeData as $data) {
if (!preg_match($data['regex'], $uri, $matches)) {
continue;
}
list($handler, $varNames) = $data['routeMap'][$matches['MARK']];
$vars = [];
$i = 0;
foreach ($varNames as $varName) {
$vars[$varName] = $matches[++$i];
}
return [self::FOUND, $handler, $vars];
}
return [self::NOT_FOUND];
}
}

View File

@@ -0,0 +1,88 @@
<?php
namespace FastRoute\Dispatcher;
use FastRoute\Dispatcher;
abstract class RegexBasedAbstract implements Dispatcher
{
/** @var mixed[][] */
protected $staticRouteMap = [];
/** @var mixed[] */
protected $variableRouteData = [];
/**
* @return mixed[]
*/
abstract protected function dispatchVariableRoute($routeData, $uri);
public function dispatch($httpMethod, $uri)
{
if (isset($this->staticRouteMap[$httpMethod][$uri])) {
$handler = $this->staticRouteMap[$httpMethod][$uri];
return [self::FOUND, $handler, []];
}
$varRouteData = $this->variableRouteData;
if (isset($varRouteData[$httpMethod])) {
$result = $this->dispatchVariableRoute($varRouteData[$httpMethod], $uri);
if ($result[0] === self::FOUND) {
return $result;
}
}
// For HEAD requests, attempt fallback to GET
if ($httpMethod === 'HEAD') {
if (isset($this->staticRouteMap['GET'][$uri])) {
$handler = $this->staticRouteMap['GET'][$uri];
return [self::FOUND, $handler, []];
}
if (isset($varRouteData['GET'])) {
$result = $this->dispatchVariableRoute($varRouteData['GET'], $uri);
if ($result[0] === self::FOUND) {
return $result;
}
}
}
// If nothing else matches, try fallback routes
if (isset($this->staticRouteMap['*'][$uri])) {
$handler = $this->staticRouteMap['*'][$uri];
return [self::FOUND, $handler, []];
}
if (isset($varRouteData['*'])) {
$result = $this->dispatchVariableRoute($varRouteData['*'], $uri);
if ($result[0] === self::FOUND) {
return $result;
}
}
// Find allowed methods for this URI by matching against all other HTTP methods as well
$allowedMethods = [];
foreach ($this->staticRouteMap as $method => $uriMap) {
if ($method !== $httpMethod && isset($uriMap[$uri])) {
$allowedMethods[] = $method;
}
}
foreach ($varRouteData as $method => $routeData) {
if ($method === $httpMethod) {
continue;
}
$result = $this->dispatchVariableRoute($routeData, $uri);
if ($result[0] === self::FOUND) {
$allowedMethods[] = $method;
}
}
// If there are no allowed methods the route simply does not exist
if ($allowedMethods) {
return [self::METHOD_NOT_ALLOWED, $allowedMethods];
}
return [self::NOT_FOUND];
}
}

47
vendor/nikic/fast-route/src/Route.php vendored Normal file
View File

@@ -0,0 +1,47 @@
<?php
namespace FastRoute;
class Route
{
/** @var string */
public $httpMethod;
/** @var string */
public $regex;
/** @var array */
public $variables;
/** @var mixed */
public $handler;
/**
* Constructs a route (value object).
*
* @param string $httpMethod
* @param mixed $handler
* @param string $regex
* @param array $variables
*/
public function __construct($httpMethod, $handler, $regex, $variables)
{
$this->httpMethod = $httpMethod;
$this->handler = $handler;
$this->regex = $regex;
$this->variables = $variables;
}
/**
* Tests whether this route matches the given string.
*
* @param string $str
*
* @return bool
*/
public function matches($str)
{
$regex = '~^' . $this->regex . '$~';
return (bool) preg_match($regex, $str);
}
}

View File

@@ -0,0 +1,152 @@
<?php
namespace FastRoute;
class RouteCollector
{
/** @var RouteParser */
protected $routeParser;
/** @var DataGenerator */
protected $dataGenerator;
/** @var string */
protected $currentGroupPrefix;
/**
* Constructs a route collector.
*
* @param RouteParser $routeParser
* @param DataGenerator $dataGenerator
*/
public function __construct(RouteParser $routeParser, DataGenerator $dataGenerator)
{
$this->routeParser = $routeParser;
$this->dataGenerator = $dataGenerator;
$this->currentGroupPrefix = '';
}
/**
* Adds a route to the collection.
*
* The syntax used in the $route string depends on the used route parser.
*
* @param string|string[] $httpMethod
* @param string $route
* @param mixed $handler
*/
public function addRoute($httpMethod, $route, $handler)
{
$route = $this->currentGroupPrefix . $route;
$routeDatas = $this->routeParser->parse($route);
foreach ((array) $httpMethod as $method) {
foreach ($routeDatas as $routeData) {
$this->dataGenerator->addRoute($method, $routeData, $handler);
}
}
}
/**
* Create a route group with a common prefix.
*
* All routes created in the passed callback will have the given group prefix prepended.
*
* @param string $prefix
* @param callable $callback
*/
public function addGroup($prefix, callable $callback)
{
$previousGroupPrefix = $this->currentGroupPrefix;
$this->currentGroupPrefix = $previousGroupPrefix . $prefix;
$callback($this);
$this->currentGroupPrefix = $previousGroupPrefix;
}
/**
* Adds a GET route to the collection
*
* This is simply an alias of $this->addRoute('GET', $route, $handler)
*
* @param string $route
* @param mixed $handler
*/
public function get($route, $handler)
{
$this->addRoute('GET', $route, $handler);
}
/**
* Adds a POST route to the collection
*
* This is simply an alias of $this->addRoute('POST', $route, $handler)
*
* @param string $route
* @param mixed $handler
*/
public function post($route, $handler)
{
$this->addRoute('POST', $route, $handler);
}
/**
* Adds a PUT route to the collection
*
* This is simply an alias of $this->addRoute('PUT', $route, $handler)
*
* @param string $route
* @param mixed $handler
*/
public function put($route, $handler)
{
$this->addRoute('PUT', $route, $handler);
}
/**
* Adds a DELETE route to the collection
*
* This is simply an alias of $this->addRoute('DELETE', $route, $handler)
*
* @param string $route
* @param mixed $handler
*/
public function delete($route, $handler)
{
$this->addRoute('DELETE', $route, $handler);
}
/**
* Adds a PATCH route to the collection
*
* This is simply an alias of $this->addRoute('PATCH', $route, $handler)
*
* @param string $route
* @param mixed $handler
*/
public function patch($route, $handler)
{
$this->addRoute('PATCH', $route, $handler);
}
/**
* Adds a HEAD route to the collection
*
* This is simply an alias of $this->addRoute('HEAD', $route, $handler)
*
* @param string $route
* @param mixed $handler
*/
public function head($route, $handler)
{
$this->addRoute('HEAD', $route, $handler);
}
/**
* Returns the collected route data, as provided by the data generator.
*
* @return array
*/
public function getData()
{
return $this->dataGenerator->getData();
}
}

View File

@@ -0,0 +1,37 @@
<?php
namespace FastRoute;
interface RouteParser
{
/**
* Parses a route string into multiple route data arrays.
*
* The expected output is defined using an example:
*
* For the route string "/fixedRoutePart/{varName}[/moreFixed/{varName2:\d+}]", if {varName} is interpreted as
* a placeholder and [...] is interpreted as an optional route part, the expected result is:
*
* [
* // first route: without optional part
* [
* "/fixedRoutePart/",
* ["varName", "[^/]+"],
* ],
* // second route: with optional part
* [
* "/fixedRoutePart/",
* ["varName", "[^/]+"],
* "/moreFixed/",
* ["varName2", [0-9]+"],
* ],
* ]
*
* Here one route string was converted into two route data arrays.
*
* @param string $route Route string to parse
*
* @return mixed[][] Array of route data arrays
*/
public function parse($route);
}

View File

@@ -0,0 +1,87 @@
<?php
namespace FastRoute\RouteParser;
use FastRoute\BadRouteException;
use FastRoute\RouteParser;
/**
* Parses route strings of the following form:
*
* "/user/{name}[/{id:[0-9]+}]"
*/
class Std implements RouteParser
{
const VARIABLE_REGEX = <<<'REGEX'
\{
\s* ([a-zA-Z_][a-zA-Z0-9_-]*) \s*
(?:
: \s* ([^{}]*(?:\{(?-1)\}[^{}]*)*)
)?
\}
REGEX;
const DEFAULT_DISPATCH_REGEX = '[^/]+';
public function parse($route)
{
$routeWithoutClosingOptionals = rtrim($route, ']');
$numOptionals = strlen($route) - strlen($routeWithoutClosingOptionals);
// Split on [ while skipping placeholders
$segments = preg_split('~' . self::VARIABLE_REGEX . '(*SKIP)(*F) | \[~x', $routeWithoutClosingOptionals);
if ($numOptionals !== count($segments) - 1) {
// If there are any ] in the middle of the route, throw a more specific error message
if (preg_match('~' . self::VARIABLE_REGEX . '(*SKIP)(*F) | \]~x', $routeWithoutClosingOptionals)) {
throw new BadRouteException('Optional segments can only occur at the end of a route');
}
throw new BadRouteException("Number of opening '[' and closing ']' does not match");
}
$currentRoute = '';
$routeDatas = [];
foreach ($segments as $n => $segment) {
if ($segment === '' && $n !== 0) {
throw new BadRouteException('Empty optional part');
}
$currentRoute .= $segment;
$routeDatas[] = $this->parsePlaceholders($currentRoute);
}
return $routeDatas;
}
/**
* Parses a route string that does not contain optional segments.
*
* @param string
* @return mixed[]
*/
private function parsePlaceholders($route)
{
if (!preg_match_all(
'~' . self::VARIABLE_REGEX . '~x', $route, $matches,
PREG_OFFSET_CAPTURE | PREG_SET_ORDER
)) {
return [$route];
}
$offset = 0;
$routeData = [];
foreach ($matches as $set) {
if ($set[0][1] > $offset) {
$routeData[] = substr($route, $offset, $set[0][1] - $offset);
}
$routeData[] = [
$set[1][0],
isset($set[2]) ? trim($set[2][0]) : self::DEFAULT_DISPATCH_REGEX
];
$offset = $set[0][1] + strlen($set[0][0]);
}
if ($offset !== strlen($route)) {
$routeData[] = substr($route, $offset);
}
return $routeData;
}
}

View File

@@ -0,0 +1,12 @@
<?php
namespace FastRoute;
require __DIR__ . '/functions.php';
spl_autoload_register(function ($class) {
if (strpos($class, 'FastRoute\\') === 0) {
$name = substr($class, strlen('FastRoute'));
require __DIR__ . strtr($name, '\\', DIRECTORY_SEPARATOR) . '.php';
}
});

View File

@@ -0,0 +1,74 @@
<?php
namespace FastRoute;
if (!function_exists('FastRoute\simpleDispatcher')) {
/**
* @param callable $routeDefinitionCallback
* @param array $options
*
* @return Dispatcher
*/
function simpleDispatcher(callable $routeDefinitionCallback, array $options = [])
{
$options += [
'routeParser' => 'FastRoute\\RouteParser\\Std',
'dataGenerator' => 'FastRoute\\DataGenerator\\GroupCountBased',
'dispatcher' => 'FastRoute\\Dispatcher\\GroupCountBased',
'routeCollector' => 'FastRoute\\RouteCollector',
];
/** @var RouteCollector $routeCollector */
$routeCollector = new $options['routeCollector'](
new $options['routeParser'], new $options['dataGenerator']
);
$routeDefinitionCallback($routeCollector);
return new $options['dispatcher']($routeCollector->getData());
}
/**
* @param callable $routeDefinitionCallback
* @param array $options
*
* @return Dispatcher
*/
function cachedDispatcher(callable $routeDefinitionCallback, array $options = [])
{
$options += [
'routeParser' => 'FastRoute\\RouteParser\\Std',
'dataGenerator' => 'FastRoute\\DataGenerator\\GroupCountBased',
'dispatcher' => 'FastRoute\\Dispatcher\\GroupCountBased',
'routeCollector' => 'FastRoute\\RouteCollector',
'cacheDisabled' => false,
];
if (!isset($options['cacheFile'])) {
throw new \LogicException('Must specify "cacheFile" option');
}
if (!$options['cacheDisabled'] && file_exists($options['cacheFile'])) {
$dispatchData = require $options['cacheFile'];
if (!is_array($dispatchData)) {
throw new \RuntimeException('Invalid cache file "' . $options['cacheFile'] . '"');
}
return new $options['dispatcher']($dispatchData);
}
$routeCollector = new $options['routeCollector'](
new $options['routeParser'], new $options['dataGenerator']
);
$routeDefinitionCallback($routeCollector);
/** @var RouteCollector $routeCollector */
$dispatchData = $routeCollector->getData();
if (!$options['cacheDisabled']) {
file_put_contents(
$options['cacheFile'],
'<?php return ' . var_export($dispatchData, true) . ';'
);
}
return new $options['dispatcher']($dispatchData);
}
}

View File

@@ -0,0 +1,16 @@
<?php
namespace FastRoute\Dispatcher;
class CharCountBasedTest extends DispatcherTest
{
protected function getDispatcherClass()
{
return 'FastRoute\\Dispatcher\\CharCountBased';
}
protected function getDataGeneratorClass()
{
return 'FastRoute\\DataGenerator\\CharCountBased';
}
}

View File

@@ -0,0 +1,581 @@
<?php
namespace FastRoute\Dispatcher;
use FastRoute\RouteCollector;
use PHPUnit\Framework\TestCase;
abstract class DispatcherTest extends TestCase
{
/**
* Delegate dispatcher selection to child test classes
*/
abstract protected function getDispatcherClass();
/**
* Delegate dataGenerator selection to child test classes
*/
abstract protected function getDataGeneratorClass();
/**
* Set appropriate options for the specific Dispatcher class we're testing
*/
private function generateDispatcherOptions()
{
return [
'dataGenerator' => $this->getDataGeneratorClass(),
'dispatcher' => $this->getDispatcherClass()
];
}
/**
* @dataProvider provideFoundDispatchCases
*/
public function testFoundDispatches($method, $uri, $callback, $handler, $argDict)
{
$dispatcher = \FastRoute\simpleDispatcher($callback, $this->generateDispatcherOptions());
$info = $dispatcher->dispatch($method, $uri);
$this->assertSame($dispatcher::FOUND, $info[0]);
$this->assertSame($handler, $info[1]);
$this->assertSame($argDict, $info[2]);
}
/**
* @dataProvider provideNotFoundDispatchCases
*/
public function testNotFoundDispatches($method, $uri, $callback)
{
$dispatcher = \FastRoute\simpleDispatcher($callback, $this->generateDispatcherOptions());
$routeInfo = $dispatcher->dispatch($method, $uri);
$this->assertArrayNotHasKey(1, $routeInfo,
'NOT_FOUND result must only contain a single element in the returned info array'
);
$this->assertSame($dispatcher::NOT_FOUND, $routeInfo[0]);
}
/**
* @dataProvider provideMethodNotAllowedDispatchCases
*/
public function testMethodNotAllowedDispatches($method, $uri, $callback, $availableMethods)
{
$dispatcher = \FastRoute\simpleDispatcher($callback, $this->generateDispatcherOptions());
$routeInfo = $dispatcher->dispatch($method, $uri);
$this->assertArrayHasKey(1, $routeInfo,
'METHOD_NOT_ALLOWED result must return an array of allowed methods at index 1'
);
list($routedStatus, $methodArray) = $dispatcher->dispatch($method, $uri);
$this->assertSame($dispatcher::METHOD_NOT_ALLOWED, $routedStatus);
$this->assertSame($availableMethods, $methodArray);
}
/**
* @expectedException \FastRoute\BadRouteException
* @expectedExceptionMessage Cannot use the same placeholder "test" twice
*/
public function testDuplicateVariableNameError()
{
\FastRoute\simpleDispatcher(function (RouteCollector $r) {
$r->addRoute('GET', '/foo/{test}/{test:\d+}', 'handler0');
}, $this->generateDispatcherOptions());
}
/**
* @expectedException \FastRoute\BadRouteException
* @expectedExceptionMessage Cannot register two routes matching "/user/([^/]+)" for method "GET"
*/
public function testDuplicateVariableRoute()
{
\FastRoute\simpleDispatcher(function (RouteCollector $r) {
$r->addRoute('GET', '/user/{id}', 'handler0'); // oops, forgot \d+ restriction ;)
$r->addRoute('GET', '/user/{name}', 'handler1');
}, $this->generateDispatcherOptions());
}
/**
* @expectedException \FastRoute\BadRouteException
* @expectedExceptionMessage Cannot register two routes matching "/user" for method "GET"
*/
public function testDuplicateStaticRoute()
{
\FastRoute\simpleDispatcher(function (RouteCollector $r) {
$r->addRoute('GET', '/user', 'handler0');
$r->addRoute('GET', '/user', 'handler1');
}, $this->generateDispatcherOptions());
}
/**
* @expectedException \FastRoute\BadRouteException
* @expectedExceptionMessage Static route "/user/nikic" is shadowed by previously defined variable route "/user/([^/]+)" for method "GET"
*/
public function testShadowedStaticRoute()
{
\FastRoute\simpleDispatcher(function (RouteCollector $r) {
$r->addRoute('GET', '/user/{name}', 'handler0');
$r->addRoute('GET', '/user/nikic', 'handler1');
}, $this->generateDispatcherOptions());
}
/**
* @expectedException \FastRoute\BadRouteException
* @expectedExceptionMessage Regex "(en|de)" for parameter "lang" contains a capturing group
*/
public function testCapturing()
{
\FastRoute\simpleDispatcher(function (RouteCollector $r) {
$r->addRoute('GET', '/{lang:(en|de)}', 'handler0');
}, $this->generateDispatcherOptions());
}
public function provideFoundDispatchCases()
{
$cases = [];
// 0 -------------------------------------------------------------------------------------->
$callback = function (RouteCollector $r) {
$r->addRoute('GET', '/resource/123/456', 'handler0');
};
$method = 'GET';
$uri = '/resource/123/456';
$handler = 'handler0';
$argDict = [];
$cases[] = [$method, $uri, $callback, $handler, $argDict];
// 1 -------------------------------------------------------------------------------------->
$callback = function (RouteCollector $r) {
$r->addRoute('GET', '/handler0', 'handler0');
$r->addRoute('GET', '/handler1', 'handler1');
$r->addRoute('GET', '/handler2', 'handler2');
};
$method = 'GET';
$uri = '/handler2';
$handler = 'handler2';
$argDict = [];
$cases[] = [$method, $uri, $callback, $handler, $argDict];
// 2 -------------------------------------------------------------------------------------->
$callback = function (RouteCollector $r) {
$r->addRoute('GET', '/user/{name}/{id:[0-9]+}', 'handler0');
$r->addRoute('GET', '/user/{id:[0-9]+}', 'handler1');
$r->addRoute('GET', '/user/{name}', 'handler2');
};
$method = 'GET';
$uri = '/user/rdlowrey';
$handler = 'handler2';
$argDict = ['name' => 'rdlowrey'];
$cases[] = [$method, $uri, $callback, $handler, $argDict];
// 3 -------------------------------------------------------------------------------------->
// reuse $callback from #2
$method = 'GET';
$uri = '/user/12345';
$handler = 'handler1';
$argDict = ['id' => '12345'];
$cases[] = [$method, $uri, $callback, $handler, $argDict];
// 4 -------------------------------------------------------------------------------------->
// reuse $callback from #3
$method = 'GET';
$uri = '/user/NaN';
$handler = 'handler2';
$argDict = ['name' => 'NaN'];
$cases[] = [$method, $uri, $callback, $handler, $argDict];
// 5 -------------------------------------------------------------------------------------->
// reuse $callback from #4
$method = 'GET';
$uri = '/user/rdlowrey/12345';
$handler = 'handler0';
$argDict = ['name' => 'rdlowrey', 'id' => '12345'];
$cases[] = [$method, $uri, $callback, $handler, $argDict];
// 6 -------------------------------------------------------------------------------------->
$callback = function (RouteCollector $r) {
$r->addRoute('GET', '/user/{id:[0-9]+}', 'handler0');
$r->addRoute('GET', '/user/12345/extension', 'handler1');
$r->addRoute('GET', '/user/{id:[0-9]+}.{extension}', 'handler2');
};
$method = 'GET';
$uri = '/user/12345.svg';
$handler = 'handler2';
$argDict = ['id' => '12345', 'extension' => 'svg'];
$cases[] = [$method, $uri, $callback, $handler, $argDict];
// 7 ----- Test GET method fallback on HEAD route miss ------------------------------------>
$callback = function (RouteCollector $r) {
$r->addRoute('GET', '/user/{name}', 'handler0');
$r->addRoute('GET', '/user/{name}/{id:[0-9]+}', 'handler1');
$r->addRoute('GET', '/static0', 'handler2');
$r->addRoute('GET', '/static1', 'handler3');
$r->addRoute('HEAD', '/static1', 'handler4');
};
$method = 'HEAD';
$uri = '/user/rdlowrey';
$handler = 'handler0';
$argDict = ['name' => 'rdlowrey'];
$cases[] = [$method, $uri, $callback, $handler, $argDict];
// 8 ----- Test GET method fallback on HEAD route miss ------------------------------------>
// reuse $callback from #7
$method = 'HEAD';
$uri = '/user/rdlowrey/1234';
$handler = 'handler1';
$argDict = ['name' => 'rdlowrey', 'id' => '1234'];
$cases[] = [$method, $uri, $callback, $handler, $argDict];
// 9 ----- Test GET method fallback on HEAD route miss ------------------------------------>
// reuse $callback from #8
$method = 'HEAD';
$uri = '/static0';
$handler = 'handler2';
$argDict = [];
$cases[] = [$method, $uri, $callback, $handler, $argDict];
// 10 ---- Test existing HEAD route used if available (no fallback) ----------------------->
// reuse $callback from #9
$method = 'HEAD';
$uri = '/static1';
$handler = 'handler4';
$argDict = [];
$cases[] = [$method, $uri, $callback, $handler, $argDict];
// 11 ---- More specified routes are not shadowed by less specific of another method ------>
$callback = function (RouteCollector $r) {
$r->addRoute('GET', '/user/{name}', 'handler0');
$r->addRoute('POST', '/user/{name:[a-z]+}', 'handler1');
};
$method = 'POST';
$uri = '/user/rdlowrey';
$handler = 'handler1';
$argDict = ['name' => 'rdlowrey'];
$cases[] = [$method, $uri, $callback, $handler, $argDict];
// 12 ---- Handler of more specific routes is used, if it occurs first -------------------->
$callback = function (RouteCollector $r) {
$r->addRoute('GET', '/user/{name}', 'handler0');
$r->addRoute('POST', '/user/{name:[a-z]+}', 'handler1');
$r->addRoute('POST', '/user/{name}', 'handler2');
};
$method = 'POST';
$uri = '/user/rdlowrey';
$handler = 'handler1';
$argDict = ['name' => 'rdlowrey'];
$cases[] = [$method, $uri, $callback, $handler, $argDict];
// 13 ---- Route with constant suffix ----------------------------------------------------->
$callback = function (RouteCollector $r) {
$r->addRoute('GET', '/user/{name}', 'handler0');
$r->addRoute('GET', '/user/{name}/edit', 'handler1');
};
$method = 'GET';
$uri = '/user/rdlowrey/edit';
$handler = 'handler1';
$argDict = ['name' => 'rdlowrey'];
$cases[] = [$method, $uri, $callback, $handler, $argDict];
// 14 ---- Handle multiple methods with the same handler ---------------------------------->
$callback = function (RouteCollector $r) {
$r->addRoute(['GET', 'POST'], '/user', 'handlerGetPost');
$r->addRoute(['DELETE'], '/user', 'handlerDelete');
$r->addRoute([], '/user', 'handlerNone');
};
$argDict = [];
$cases[] = ['GET', '/user', $callback, 'handlerGetPost', $argDict];
$cases[] = ['POST', '/user', $callback, 'handlerGetPost', $argDict];
$cases[] = ['DELETE', '/user', $callback, 'handlerDelete', $argDict];
// 17 ----
$callback = function (RouteCollector $r) {
$r->addRoute('POST', '/user.json', 'handler0');
$r->addRoute('GET', '/{entity}.json', 'handler1');
};
$cases[] = ['GET', '/user.json', $callback, 'handler1', ['entity' => 'user']];
// 18 ----
$callback = function (RouteCollector $r) {
$r->addRoute('GET', '', 'handler0');
};
$cases[] = ['GET', '', $callback, 'handler0', []];
// 19 ----
$callback = function (RouteCollector $r) {
$r->addRoute('HEAD', '/a/{foo}', 'handler0');
$r->addRoute('GET', '/b/{foo}', 'handler1');
};
$cases[] = ['HEAD', '/b/bar', $callback, 'handler1', ['foo' => 'bar']];
// 20 ----
$callback = function (RouteCollector $r) {
$r->addRoute('HEAD', '/a', 'handler0');
$r->addRoute('GET', '/b', 'handler1');
};
$cases[] = ['HEAD', '/b', $callback, 'handler1', []];
// 21 ----
$callback = function (RouteCollector $r) {
$r->addRoute('GET', '/foo', 'handler0');
$r->addRoute('HEAD', '/{bar}', 'handler1');
};
$cases[] = ['HEAD', '/foo', $callback, 'handler1', ['bar' => 'foo']];
// 22 ----
$callback = function (RouteCollector $r) {
$r->addRoute('*', '/user', 'handler0');
$r->addRoute('*', '/{user}', 'handler1');
$r->addRoute('GET', '/user', 'handler2');
};
$cases[] = ['GET', '/user', $callback, 'handler2', []];
// 23 ----
$callback = function (RouteCollector $r) {
$r->addRoute('*', '/user', 'handler0');
$r->addRoute('GET', '/user', 'handler1');
};
$cases[] = ['POST', '/user', $callback, 'handler0', []];
// 24 ----
$cases[] = ['HEAD', '/user', $callback, 'handler1', []];
// 25 ----
$callback = function (RouteCollector $r) {
$r->addRoute('GET', '/{bar}', 'handler0');
$r->addRoute('*', '/foo', 'handler1');
};
$cases[] = ['GET', '/foo', $callback, 'handler0', ['bar' => 'foo']];
// 26 ----
$callback = function(RouteCollector $r) {
$r->addRoute('GET', '/user', 'handler0');
$r->addRoute('*', '/{foo:.*}', 'handler1');
};
$cases[] = ['POST', '/bar', $callback, 'handler1', ['foo' => 'bar']];
// x -------------------------------------------------------------------------------------->
return $cases;
}
public function provideNotFoundDispatchCases()
{
$cases = [];
// 0 -------------------------------------------------------------------------------------->
$callback = function (RouteCollector $r) {
$r->addRoute('GET', '/resource/123/456', 'handler0');
};
$method = 'GET';
$uri = '/not-found';
$cases[] = [$method, $uri, $callback];
// 1 -------------------------------------------------------------------------------------->
// reuse callback from #0
$method = 'POST';
$uri = '/not-found';
$cases[] = [$method, $uri, $callback];
// 2 -------------------------------------------------------------------------------------->
// reuse callback from #1
$method = 'PUT';
$uri = '/not-found';
$cases[] = [$method, $uri, $callback];
// 3 -------------------------------------------------------------------------------------->
$callback = function (RouteCollector $r) {
$r->addRoute('GET', '/handler0', 'handler0');
$r->addRoute('GET', '/handler1', 'handler1');
$r->addRoute('GET', '/handler2', 'handler2');
};
$method = 'GET';
$uri = '/not-found';
$cases[] = [$method, $uri, $callback];
// 4 -------------------------------------------------------------------------------------->
$callback = function (RouteCollector $r) {
$r->addRoute('GET', '/user/{name}/{id:[0-9]+}', 'handler0');
$r->addRoute('GET', '/user/{id:[0-9]+}', 'handler1');
$r->addRoute('GET', '/user/{name}', 'handler2');
};
$method = 'GET';
$uri = '/not-found';
$cases[] = [$method, $uri, $callback];
// 5 -------------------------------------------------------------------------------------->
// reuse callback from #4
$method = 'GET';
$uri = '/user/rdlowrey/12345/not-found';
$cases[] = [$method, $uri, $callback];
// 6 -------------------------------------------------------------------------------------->
// reuse callback from #5
$method = 'HEAD';
$cases[] = [$method, $uri, $callback];
// x -------------------------------------------------------------------------------------->
return $cases;
}
public function provideMethodNotAllowedDispatchCases()
{
$cases = [];
// 0 -------------------------------------------------------------------------------------->
$callback = function (RouteCollector $r) {
$r->addRoute('GET', '/resource/123/456', 'handler0');
};
$method = 'POST';
$uri = '/resource/123/456';
$allowedMethods = ['GET'];
$cases[] = [$method, $uri, $callback, $allowedMethods];
// 1 -------------------------------------------------------------------------------------->
$callback = function (RouteCollector $r) {
$r->addRoute('GET', '/resource/123/456', 'handler0');
$r->addRoute('POST', '/resource/123/456', 'handler1');
$r->addRoute('PUT', '/resource/123/456', 'handler2');
$r->addRoute('*', '/', 'handler3');
};
$method = 'DELETE';
$uri = '/resource/123/456';
$allowedMethods = ['GET', 'POST', 'PUT'];
$cases[] = [$method, $uri, $callback, $allowedMethods];
// 2 -------------------------------------------------------------------------------------->
$callback = function (RouteCollector $r) {
$r->addRoute('GET', '/user/{name}/{id:[0-9]+}', 'handler0');
$r->addRoute('POST', '/user/{name}/{id:[0-9]+}', 'handler1');
$r->addRoute('PUT', '/user/{name}/{id:[0-9]+}', 'handler2');
$r->addRoute('PATCH', '/user/{name}/{id:[0-9]+}', 'handler3');
};
$method = 'DELETE';
$uri = '/user/rdlowrey/42';
$allowedMethods = ['GET', 'POST', 'PUT', 'PATCH'];
$cases[] = [$method, $uri, $callback, $allowedMethods];
// 3 -------------------------------------------------------------------------------------->
$callback = function (RouteCollector $r) {
$r->addRoute('POST', '/user/{name}', 'handler1');
$r->addRoute('PUT', '/user/{name:[a-z]+}', 'handler2');
$r->addRoute('PATCH', '/user/{name:[a-z]+}', 'handler3');
};
$method = 'GET';
$uri = '/user/rdlowrey';
$allowedMethods = ['POST', 'PUT', 'PATCH'];
$cases[] = [$method, $uri, $callback, $allowedMethods];
// 4 -------------------------------------------------------------------------------------->
$callback = function (RouteCollector $r) {
$r->addRoute(['GET', 'POST'], '/user', 'handlerGetPost');
$r->addRoute(['DELETE'], '/user', 'handlerDelete');
$r->addRoute([], '/user', 'handlerNone');
};
$cases[] = ['PUT', '/user', $callback, ['GET', 'POST', 'DELETE']];
// 5
$callback = function (RouteCollector $r) {
$r->addRoute('POST', '/user.json', 'handler0');
$r->addRoute('GET', '/{entity}.json', 'handler1');
};
$cases[] = ['PUT', '/user.json', $callback, ['POST', 'GET']];
// x -------------------------------------------------------------------------------------->
return $cases;
}
}

View File

@@ -0,0 +1,16 @@
<?php
namespace FastRoute\Dispatcher;
class GroupCountBasedTest extends DispatcherTest
{
protected function getDispatcherClass()
{
return 'FastRoute\\Dispatcher\\GroupCountBased';
}
protected function getDataGeneratorClass()
{
return 'FastRoute\\DataGenerator\\GroupCountBased';
}
}

View File

@@ -0,0 +1,16 @@
<?php
namespace FastRoute\Dispatcher;
class GroupPosBasedTest extends DispatcherTest
{
protected function getDispatcherClass()
{
return 'FastRoute\\Dispatcher\\GroupPosBased';
}
protected function getDataGeneratorClass()
{
return 'FastRoute\\DataGenerator\\GroupPosBased';
}
}

View File

@@ -0,0 +1,24 @@
<?php
namespace FastRoute\Dispatcher;
class MarkBasedTest extends DispatcherTest
{
public function setUp()
{
preg_match('/(*MARK:A)a/', 'a', $matches);
if (!isset($matches['MARK'])) {
$this->markTestSkipped('PHP 5.6 required for MARK support');
}
}
protected function getDispatcherClass()
{
return 'FastRoute\\Dispatcher\\MarkBased';
}
protected function getDataGeneratorClass()
{
return 'FastRoute\\DataGenerator\\MarkBased';
}
}

View File

@@ -0,0 +1,44 @@
<?php
namespace FastRoute;
use PHPUnit\Framework\TestCase;
class HackTypecheckerTest extends TestCase
{
const SERVER_ALREADY_RUNNING_CODE = 77;
public function testTypechecks($recurse = true)
{
if (!defined('HHVM_VERSION')) {
$this->markTestSkipped('HHVM only');
}
if (!version_compare(HHVM_VERSION, '3.9.0', '>=')) {
$this->markTestSkipped('classname<T> requires HHVM 3.9+');
}
// The typechecker recurses the whole tree, so it makes sure
// that everything in fixtures/ is valid when this runs.
$output = [];
$exit_code = null;
exec(
'hh_server --check ' . escapeshellarg(__DIR__ . '/../../') . ' 2>&1',
$output,
$exit_code
);
if ($exit_code === self::SERVER_ALREADY_RUNNING_CODE) {
$this->assertTrue(
$recurse,
'Typechecker still running after running hh_client stop'
);
// Server already running - 3.10 => 3.11 regression:
// https://github.com/facebook/hhvm/issues/6646
exec('hh_client stop 2>/dev/null');
$this->testTypechecks(/* recurse = */ false);
return;
}
$this->assertSame(0, $exit_code, implode("\n", $output));
}
}

View File

@@ -0,0 +1,29 @@
<?hh
namespace FastRoute\TestFixtures;
function all_options_simple(): \FastRoute\Dispatcher {
return \FastRoute\simpleDispatcher(
$collector ==> {},
shape(
'routeParser' => \FastRoute\RouteParser\Std::class,
'dataGenerator' => \FastRoute\DataGenerator\GroupCountBased::class,
'dispatcher' => \FastRoute\Dispatcher\GroupCountBased::class,
'routeCollector' => \FastRoute\RouteCollector::class,
),
);
}
function all_options_cached(): \FastRoute\Dispatcher {
return \FastRoute\cachedDispatcher(
$collector ==> {},
shape(
'routeParser' => \FastRoute\RouteParser\Std::class,
'dataGenerator' => \FastRoute\DataGenerator\GroupCountBased::class,
'dispatcher' => \FastRoute\Dispatcher\GroupCountBased::class,
'routeCollector' => \FastRoute\RouteCollector::class,
'cacheFile' => '/dev/null',
'cacheDisabled' => false,
),
);
}

View File

@@ -0,0 +1,11 @@
<?hh
namespace FastRoute\TestFixtures;
function empty_options_simple(): \FastRoute\Dispatcher {
return \FastRoute\simpleDispatcher($collector ==> {}, shape());
}
function empty_options_cached(): \FastRoute\Dispatcher {
return \FastRoute\cachedDispatcher($collector ==> {}, shape());
}

View File

@@ -0,0 +1,11 @@
<?hh
namespace FastRoute\TestFixtures;
function no_options_simple(): \FastRoute\Dispatcher {
return \FastRoute\simpleDispatcher($collector ==> {});
}
function no_options_cached(): \FastRoute\Dispatcher {
return \FastRoute\cachedDispatcher($collector ==> {});
}

View File

@@ -0,0 +1,108 @@
<?php
namespace FastRoute;
use PHPUnit\Framework\TestCase;
class RouteCollectorTest extends TestCase
{
public function testShortcuts()
{
$r = new DummyRouteCollector();
$r->delete('/delete', 'delete');
$r->get('/get', 'get');
$r->head('/head', 'head');
$r->patch('/patch', 'patch');
$r->post('/post', 'post');
$r->put('/put', 'put');
$expected = [
['DELETE', '/delete', 'delete'],
['GET', '/get', 'get'],
['HEAD', '/head', 'head'],
['PATCH', '/patch', 'patch'],
['POST', '/post', 'post'],
['PUT', '/put', 'put'],
];
$this->assertSame($expected, $r->routes);
}
public function testGroups()
{
$r = new DummyRouteCollector();
$r->delete('/delete', 'delete');
$r->get('/get', 'get');
$r->head('/head', 'head');
$r->patch('/patch', 'patch');
$r->post('/post', 'post');
$r->put('/put', 'put');
$r->addGroup('/group-one', function (DummyRouteCollector $r) {
$r->delete('/delete', 'delete');
$r->get('/get', 'get');
$r->head('/head', 'head');
$r->patch('/patch', 'patch');
$r->post('/post', 'post');
$r->put('/put', 'put');
$r->addGroup('/group-two', function (DummyRouteCollector $r) {
$r->delete('/delete', 'delete');
$r->get('/get', 'get');
$r->head('/head', 'head');
$r->patch('/patch', 'patch');
$r->post('/post', 'post');
$r->put('/put', 'put');
});
});
$r->addGroup('/admin', function (DummyRouteCollector $r) {
$r->get('-some-info', 'admin-some-info');
});
$r->addGroup('/admin-', function (DummyRouteCollector $r) {
$r->get('more-info', 'admin-more-info');
});
$expected = [
['DELETE', '/delete', 'delete'],
['GET', '/get', 'get'],
['HEAD', '/head', 'head'],
['PATCH', '/patch', 'patch'],
['POST', '/post', 'post'],
['PUT', '/put', 'put'],
['DELETE', '/group-one/delete', 'delete'],
['GET', '/group-one/get', 'get'],
['HEAD', '/group-one/head', 'head'],
['PATCH', '/group-one/patch', 'patch'],
['POST', '/group-one/post', 'post'],
['PUT', '/group-one/put', 'put'],
['DELETE', '/group-one/group-two/delete', 'delete'],
['GET', '/group-one/group-two/get', 'get'],
['HEAD', '/group-one/group-two/head', 'head'],
['PATCH', '/group-one/group-two/patch', 'patch'],
['POST', '/group-one/group-two/post', 'post'],
['PUT', '/group-one/group-two/put', 'put'],
['GET', '/admin-some-info', 'admin-some-info'],
['GET', '/admin-more-info', 'admin-more-info'],
];
$this->assertSame($expected, $r->routes);
}
}
class DummyRouteCollector extends RouteCollector
{
public $routes = [];
public function __construct()
{
}
public function addRoute($method, $route, $handler)
{
$route = $this->currentGroupPrefix . $route;
$this->routes[] = [$method, $route, $handler];
}
}

View File

@@ -0,0 +1,154 @@
<?php
namespace FastRoute\RouteParser;
use PHPUnit\Framework\TestCase;
class StdTest extends TestCase
{
/** @dataProvider provideTestParse */
public function testParse($routeString, $expectedRouteDatas)
{
$parser = new Std();
$routeDatas = $parser->parse($routeString);
$this->assertSame($expectedRouteDatas, $routeDatas);
}
/** @dataProvider provideTestParseError */
public function testParseError($routeString, $expectedExceptionMessage)
{
$parser = new Std();
$this->setExpectedException('FastRoute\\BadRouteException', $expectedExceptionMessage);
$parser->parse($routeString);
}
public function provideTestParse()
{
return [
[
'/test',
[
['/test'],
]
],
[
'/test/{param}',
[
['/test/', ['param', '[^/]+']],
]
],
[
'/te{ param }st',
[
['/te', ['param', '[^/]+'], 'st']
]
],
[
'/test/{param1}/test2/{param2}',
[
['/test/', ['param1', '[^/]+'], '/test2/', ['param2', '[^/]+']]
]
],
[
'/test/{param:\d+}',
[
['/test/', ['param', '\d+']]
]
],
[
'/test/{ param : \d{1,9} }',
[
['/test/', ['param', '\d{1,9}']]
]
],
[
'/test[opt]',
[
['/test'],
['/testopt'],
]
],
[
'/test[/{param}]',
[
['/test'],
['/test/', ['param', '[^/]+']],
]
],
[
'/{param}[opt]',
[
['/', ['param', '[^/]+']],
['/', ['param', '[^/]+'], 'opt']
]
],
[
'/test[/{name}[/{id:[0-9]+}]]',
[
['/test'],
['/test/', ['name', '[^/]+']],
['/test/', ['name', '[^/]+'], '/', ['id', '[0-9]+']],
]
],
[
'',
[
[''],
]
],
[
'[test]',
[
[''],
['test'],
]
],
[
'/{foo-bar}',
[
['/', ['foo-bar', '[^/]+']]
]
],
[
'/{_foo:.*}',
[
['/', ['_foo', '.*']]
]
],
];
}
public function provideTestParseError()
{
return [
[
'/test[opt',
"Number of opening '[' and closing ']' does not match"
],
[
'/test[opt[opt2]',
"Number of opening '[' and closing ']' does not match"
],
[
'/testopt]',
"Number of opening '[' and closing ']' does not match"
],
[
'/test[]',
'Empty optional part'
],
[
'/test[[opt]]',
'Empty optional part'
],
[
'[[test]]',
'Empty optional part'
],
[
'/test[/opt]/required',
'Optional segments can only occur at the end of a route'
],
];
}
}

View File

@@ -0,0 +1,11 @@
<?php
require_once __DIR__ . '/../src/functions.php';
spl_autoload_register(function ($class) {
if (strpos($class, 'FastRoute\\') === 0) {
$dir = strcasecmp(substr($class, -4), 'Test') ? 'src/' : 'test/';
$name = substr($class, strlen('FastRoute'));
require __DIR__ . '/../' . $dir . strtr($name, '\\', DIRECTORY_SEPARATOR) . '.php';
}
});

36
vendor/psr/http-message/CHANGELOG.md vendored Normal file
View File

@@ -0,0 +1,36 @@
# Changelog
All notable changes to this project will be documented in this file, in reverse chronological order by release.
## 1.0.1 - 2016-08-06
### Added
- Nothing.
### Deprecated
- Nothing.
### Removed
- Nothing.
### Fixed
- Updated all `@return self` annotation references in interfaces to use
`@return static`, which more closelly follows the semantics of the
specification.
- Updated the `MessageInterface::getHeaders()` return annotation to use the
value `string[][]`, indicating the format is a nested array of strings.
- Updated the `@link` annotation for `RequestInterface::withRequestTarget()`
to point to the correct section of RFC 7230.
- Updated the `ServerRequestInterface::withUploadedFiles()` parameter annotation
to add the parameter name (`$uploadedFiles`).
- Updated a `@throws` annotation for the `UploadedFileInterface::moveTo()`
method to correctly reference the method parameter (it was referencing an
incorrect parameter name previously).
## 1.0.0 - 2016-05-18
Initial stable release; reflects accepted PSR-7 specification.

19
vendor/psr/http-message/LICENSE vendored Normal file
View File

@@ -0,0 +1,19 @@
Copyright (c) 2014 PHP Framework Interoperability Group
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

16
vendor/psr/http-message/README.md vendored Normal file
View File

@@ -0,0 +1,16 @@
PSR Http Message
================
This repository holds all interfaces/classes/traits related to
[PSR-7](http://www.php-fig.org/psr/psr-7/).
Note that this is not a HTTP message implementation of its own. It is merely an
interface that describes a HTTP message. See the specification for more details.
Usage
-----
Before reading the usage guide we recommend reading the PSR-7 interfaces method list:
* [`PSR-7 Interfaces Method List`](docs/PSR7-Interfaces.md)
* [`PSR-7 Usage Guide`](docs/PSR7-Usage.md)

26
vendor/psr/http-message/composer.json vendored Normal file
View File

@@ -0,0 +1,26 @@
{
"name": "psr/http-message",
"description": "Common interface for HTTP messages",
"keywords": ["psr", "psr-7", "http", "http-message", "request", "response"],
"homepage": "https://github.com/php-fig/http-message",
"license": "MIT",
"authors": [
{
"name": "PHP-FIG",
"homepage": "http://www.php-fig.org/"
}
],
"require": {
"php": "^7.2 || ^8.0"
},
"autoload": {
"psr-4": {
"Psr\\Http\\Message\\": "src/"
}
},
"extra": {
"branch-alias": {
"dev-master": "1.1.x-dev"
}
}
}

View File

@@ -0,0 +1,130 @@
# Interfaces
The purpose of this list is to help in finding the methods when working with PSR-7. This can be considered as a cheatsheet for PSR-7 interfaces.
The interfaces defined in PSR-7 are the following:
| Class Name | Description |
|---|---|
| [Psr\Http\Message\MessageInterface](http://www.php-fig.org/psr/psr-7/#psrhttpmessagemessageinterface) | Representation of a HTTP message |
| [Psr\Http\Message\RequestInterface](http://www.php-fig.org/psr/psr-7/#psrhttpmessagerequestinterface) | Representation of an outgoing, client-side request. |
| [Psr\Http\Message\ServerRequestInterface](http://www.php-fig.org/psr/psr-7/#psrhttpmessageserverrequestinterface) | Representation of an incoming, server-side HTTP request. |
| [Psr\Http\Message\ResponseInterface](http://www.php-fig.org/psr/psr-7/#psrhttpmessageresponseinterface) | Representation of an outgoing, server-side response. |
| [Psr\Http\Message\StreamInterface](http://www.php-fig.org/psr/psr-7/#psrhttpmessagestreaminterface) | Describes a data stream |
| [Psr\Http\Message\UriInterface](http://www.php-fig.org/psr/psr-7/#psrhttpmessageuriinterface) | Value object representing a URI. |
| [Psr\Http\Message\UploadedFileInterface](http://www.php-fig.org/psr/psr-7/#psrhttpmessageuploadedfileinterface) | Value object representing a file uploaded through an HTTP request. |
## `Psr\Http\Message\MessageInterface` Methods
| Method Name | Description | Notes |
|------------------------------------| ----------- | ----- |
| `getProtocolVersion()` | Retrieve HTTP protocol version | 1.0 or 1.1 |
| `withProtocolVersion($version)` | Returns new message instance with given HTTP protocol version | |
| `getHeaders()` | Retrieve all HTTP Headers | [Request Header List](https://en.wikipedia.org/wiki/List_of_HTTP_header_fields#Request_fields), [Response Header List](https://en.wikipedia.org/wiki/List_of_HTTP_header_fields#Response_fields) |
| `hasHeader($name)` | Checks if HTTP Header with given name exists | |
| `getHeader($name)` | Retrieves a array with the values for a single header | |
| `getHeaderLine($name)` | Retrieves a comma-separated string of the values for a single header | |
| `withHeader($name, $value)` | Returns new message instance with given HTTP Header | if the header existed in the original instance, replaces the header value from the original message with the value provided when creating the new instance. |
| `withAddedHeader($name, $value)` | Returns new message instance with appended value to given header | If header already exists value will be appended, if not a new header will be created |
| `withoutHeader($name)` | Removes HTTP Header with given name| |
| `getBody()` | Retrieves the HTTP Message Body | Returns object implementing `StreamInterface`|
| `withBody(StreamInterface $body)` | Returns new message instance with given HTTP Message Body | |
## `Psr\Http\Message\RequestInterface` Methods
Same methods as `Psr\Http\Message\MessageInterface` + the following methods:
| Method Name | Description | Notes |
|------------------------------------| ----------- | ----- |
| `getRequestTarget()` | Retrieves the message's request target | origin-form, absolute-form, authority-form, asterisk-form ([RFC7230](https://www.rfc-editor.org/rfc/rfc7230.txt)) |
| `withRequestTarget($requestTarget)` | Return a new message instance with the specific request-target | |
| `getMethod()` | Retrieves the HTTP method of the request. | GET, HEAD, POST, PUT, DELETE, CONNECT, OPTIONS, TRACE (defined in [RFC7231](https://tools.ietf.org/html/rfc7231)), PATCH (defined in [RFC5789](https://tools.ietf.org/html/rfc5789)) |
| `withMethod($method)` | Returns a new message instance with the provided HTTP method | |
| `getUri()` | Retrieves the URI instance | |
| `withUri(UriInterface $uri, $preserveHost = false)` | Returns a new message instance with the provided URI | |
## `Psr\Http\Message\ServerRequestInterface` Methods
Same methods as `Psr\Http\Message\RequestInterface` + the following methods:
| Method Name | Description | Notes |
|------------------------------------| ----------- | ----- |
| `getServerParams() ` | Retrieve server parameters | Typically derived from `$_SERVER` |
| `getCookieParams()` | Retrieves cookies sent by the client to the server. | Typically derived from `$_COOKIES` |
| `withCookieParams(array $cookies)` | Returns a new request instance with the specified cookies | |
| `withQueryParams(array $query)` | Returns a new request instance with the specified query string arguments | |
| `getUploadedFiles()` | Retrieve normalized file upload data | |
| `withUploadedFiles(array $uploadedFiles)` | Returns a new request instance with the specified uploaded files | |
| `getParsedBody()` | Retrieve any parameters provided in the request body | |
| `withParsedBody($data)` | Returns a new request instance with the specified body parameters | |
| `getAttributes()` | Retrieve attributes derived from the request | |
| `getAttribute($name, $default = null)` | Retrieve a single derived request attribute | |
| `withAttribute($name, $value)` | Returns a new request instance with the specified derived request attribute | |
| `withoutAttribute($name)` | Returns a new request instance that without the specified derived request attribute | |
## `Psr\Http\Message\ResponseInterface` Methods:
Same methods as `Psr\Http\Message\MessageInterface` + the following methods:
| Method Name | Description | Notes |
|------------------------------------| ----------- | ----- |
| `getStatusCode()` | Gets the response status code. | |
| `withStatus($code, $reasonPhrase = '')` | Returns a new response instance with the specified status code and, optionally, reason phrase. | |
| `getReasonPhrase()` | Gets the response reason phrase associated with the status code. | |
## `Psr\Http\Message\StreamInterface` Methods
| Method Name | Description | Notes |
|------------------------------------| ----------- | ----- |
| `__toString()` | Reads all data from the stream into a string, from the beginning to end. | |
| `close()` | Closes the stream and any underlying resources. | |
| `detach()` | Separates any underlying resources from the stream. | |
| `getSize()` | Get the size of the stream if known. | |
| `eof()` | Returns true if the stream is at the end of the stream.| |
| `isSeekable()` | Returns whether or not the stream is seekable. | |
| `seek($offset, $whence = SEEK_SET)` | Seek to a position in the stream. | |
| `rewind()` | Seek to the beginning of the stream. | |
| `isWritable()` | Returns whether or not the stream is writable. | |
| `write($string)` | Write data to the stream. | |
| `isReadable()` | Returns whether or not the stream is readable. | |
| `read($length)` | Read data from the stream. | |
| `getContents()` | Returns the remaining contents in a string | |
| `getMetadata($key = null)()` | Get stream metadata as an associative array or retrieve a specific key. | |
## `Psr\Http\Message\UriInterface` Methods
| Method Name | Description | Notes |
|------------------------------------| ----------- | ----- |
| `getScheme()` | Retrieve the scheme component of the URI. | |
| `getAuthority()` | Retrieve the authority component of the URI. | |
| `getUserInfo()` | Retrieve the user information component of the URI. | |
| `getHost()` | Retrieve the host component of the URI. | |
| `getPort()` | Retrieve the port component of the URI. | |
| `getPath()` | Retrieve the path component of the URI. | |
| `getQuery()` | Retrieve the query string of the URI. | |
| `getFragment()` | Retrieve the fragment component of the URI. | |
| `withScheme($scheme)` | Return an instance with the specified scheme. | |
| `withUserInfo($user, $password = null)` | Return an instance with the specified user information. | |
| `withHost($host)` | Return an instance with the specified host. | |
| `withPort($port)` | Return an instance with the specified port. | |
| `withPath($path)` | Return an instance with the specified path. | |
| `withQuery($query)` | Return an instance with the specified query string. | |
| `withFragment($fragment)` | Return an instance with the specified URI fragment. | |
| `__toString()` | Return the string representation as a URI reference. | |
## `Psr\Http\Message\UploadedFileInterface` Methods
| Method Name | Description | Notes |
|------------------------------------| ----------- | ----- |
| `getStream()` | Retrieve a stream representing the uploaded file. | |
| `moveTo($targetPath)` | Move the uploaded file to a new location. | |
| `getSize()` | Retrieve the file size. | |
| `getError()` | Retrieve the error associated with the uploaded file. | |
| `getClientFilename()` | Retrieve the filename sent by the client. | |
| `getClientMediaType()` | Retrieve the media type sent by the client. | |
> `RequestInterface`, `ServerRequestInterface`, `ResponseInterface` extend `MessageInterface` because the `Request` and the `Response` are `HTTP Messages`.
> When using `ServerRequestInterface`, both `RequestInterface` and `Psr\Http\Message\MessageInterface` methods are considered.

View File

@@ -0,0 +1,159 @@
### PSR-7 Usage
All PSR-7 applications comply with these interfaces
They were created to establish a standard between middleware implementations.
> `RequestInterface`, `ServerRequestInterface`, `ResponseInterface` extend `MessageInterface` because the `Request` and the `Response` are `HTTP Messages`.
> When using `ServerRequestInterface`, both `RequestInterface` and `Psr\Http\Message\MessageInterface` methods are considered.
The following examples will illustrate how basic operations are done in PSR-7.
##### Examples
For this examples to work (at least) a PSR-7 implementation package is required. (eg: zendframework/zend-diactoros, guzzlehttp/psr7, slim/slim, etc)
All PSR-7 implementations should have the same behaviour.
The following will be assumed:
`$request` is an object of `Psr\Http\Message\RequestInterface` and
`$response` is an object implementing `Psr\Http\Message\RequestInterface`
### Working with HTTP Headers
#### Adding headers to response:
```php
$response->withHeader('My-Custom-Header', 'My Custom Message');
```
#### Appending values to headers
```php
$response->withAddedHeader('My-Custom-Header', 'The second message');
```
#### Checking if header exists:
```php
$request->hasHeader('My-Custom-Header'); // will return false
$response->hasHeader('My-Custom-Header'); // will return true
```
> Note: My-Custom-Header was only added in the Response
#### Getting comma-separated values from a header (also applies to request)
```php
// getting value from request headers
$request->getHeaderLine('Content-Type'); // will return: "text/html; charset=UTF-8"
// getting value from response headers
$response->getHeaderLine('My-Custom-Header'); // will return: "My Custom Message; The second message"
```
#### Getting array of value from a header (also applies to request)
```php
// getting value from request headers
$request->getHeader('Content-Type'); // will return: ["text/html", "charset=UTF-8"]
// getting value from response headers
$response->getHeader('My-Custom-Header'); // will return: ["My Custom Message", "The second message"]
```
#### Removing headers from HTTP Messages
```php
// removing a header from Request, removing deprecated "Content-MD5" header
$request->withoutHeader('Content-MD5');
// removing a header from Response
// effect: the browser won't know the size of the stream
// the browser will download the stream till it ends
$response->withoutHeader('Content-Length');
```
### Working with HTTP Message Body
When working with the PSR-7 there are two methods of implementation:
#### 1. Getting the body separately
> This method makes the body handling easier to understand and is useful when repeatedly calling body methods. (You only call `getBody()` once). Using this method mistakes like `$response->write()` are also prevented.
```php
$body = $response->getBody();
// operations on body, eg. read, write, seek
// ...
// replacing the old body
$response->withBody($body);
// this last statement is optional as we working with objects
// in this case the "new" body is same with the "old" one
// the $body variable has the same value as the one in $request, only the reference is passed
```
#### 2. Working directly on response
> This method is useful when only performing few operations as the `$request->getBody()` statement fragment is required
```php
$response->getBody()->write('hello');
```
### Getting the body contents
The following snippet gets the contents of a stream contents.
> Note: Streams must be rewinded, if content was written into streams, it will be ignored when calling `getContents()` because the stream pointer is set to the last character, which is `\0` - meaning end of stream.
```php
$body = $response->getBody();
$body->rewind(); // or $body->seek(0);
$bodyText = $body->getContents();
```
> Note: If `$body->seek(1)` is called before `$body->getContents()`, the first character will be ommited as the starting pointer is set to `1`, not `0`. This is why using `$body->rewind()` is recommended.
### Append to body
```php
$response->getBody()->write('Hello'); // writing directly
$body = $request->getBody(); // which is a `StreamInterface`
$body->write('xxxxx');
```
### Prepend to body
Prepending is different when it comes to streams. The content must be copied before writing the content to be prepended.
The following example will explain the behaviour of streams.
```php
// assuming our response is initially empty
$body = $repsonse->getBody();
// writing the string "abcd"
$body->write('abcd');
// seeking to start of stream
$body->seek(0);
// writing 'ef'
$body->write('ef'); // at this point the stream contains "efcd"
```
#### Prepending by rewriting separately
```php
// assuming our response body stream only contains: "abcd"
$body = $response->getBody();
$body->rewind();
$contents = $body->getContents(); // abcd
// seeking the stream to beginning
$body->rewind();
$body->write('ef'); // stream contains "efcd"
$body->write($contents); // stream contains "efabcd"
```
> Note: `getContents()` seeks the stream while reading it, therefore if the second `rewind()` method call was not present the stream would have resulted in `abcdefabcd` because the `write()` method appends to stream if not preceeded by `rewind()` or `seek(0)`.
#### Prepending by using contents as a string
```php
$body = $response->getBody();
$body->rewind();
$contents = $body->getContents(); // efabcd
$contents = 'ef'.$contents;
$body->rewind();
$body->write($contents);
```

View File

@@ -0,0 +1,189 @@
<?php
declare(strict_types=1);
namespace Psr\Http\Message;
/**
* HTTP messages consist of requests from a client to a server and responses
* from a server to a client. This interface defines the methods common to
* each.
*
* Messages are considered immutable; all methods that might change state MUST
* be implemented such that they retain the internal state of the current
* message and return an instance that contains the changed state.
*
* @link http://www.ietf.org/rfc/rfc7230.txt
* @link http://www.ietf.org/rfc/rfc7231.txt
*/
interface MessageInterface
{
/**
* Retrieves the HTTP protocol version as a string.
*
* The string MUST contain only the HTTP version number (e.g., "1.1", "1.0").
*
* @return string HTTP protocol version.
*/
public function getProtocolVersion();
/**
* Return an instance with the specified HTTP protocol version.
*
* The version string MUST contain only the HTTP version number (e.g.,
* "1.1", "1.0").
*
* This method MUST be implemented in such a way as to retain the
* immutability of the message, and MUST return an instance that has the
* new protocol version.
*
* @param string $version HTTP protocol version
* @return static
*/
public function withProtocolVersion(string $version);
/**
* Retrieves all message header values.
*
* The keys represent the header name as it will be sent over the wire, and
* each value is an array of strings associated with the header.
*
* // Represent the headers as a string
* foreach ($message->getHeaders() as $name => $values) {
* echo $name . ": " . implode(", ", $values);
* }
*
* // Emit headers iteratively:
* foreach ($message->getHeaders() as $name => $values) {
* foreach ($values as $value) {
* header(sprintf('%s: %s', $name, $value), false);
* }
* }
*
* While header names are not case-sensitive, getHeaders() will preserve the
* exact case in which headers were originally specified.
*
* @return string[][] Returns an associative array of the message's headers. Each
* key MUST be a header name, and each value MUST be an array of strings
* for that header.
*/
public function getHeaders();
/**
* Checks if a header exists by the given case-insensitive name.
*
* @param string $name Case-insensitive header field name.
* @return bool Returns true if any header names match the given header
* name using a case-insensitive string comparison. Returns false if
* no matching header name is found in the message.
*/
public function hasHeader(string $name);
/**
* Retrieves a message header value by the given case-insensitive name.
*
* This method returns an array of all the header values of the given
* case-insensitive header name.
*
* If the header does not appear in the message, this method MUST return an
* empty array.
*
* @param string $name Case-insensitive header field name.
* @return string[] An array of string values as provided for the given
* header. If the header does not appear in the message, this method MUST
* return an empty array.
*/
public function getHeader(string $name);
/**
* Retrieves a comma-separated string of the values for a single header.
*
* This method returns all of the header values of the given
* case-insensitive header name as a string concatenated together using
* a comma.
*
* NOTE: Not all header values may be appropriately represented using
* comma concatenation. For such headers, use getHeader() instead
* and supply your own delimiter when concatenating.
*
* If the header does not appear in the message, this method MUST return
* an empty string.
*
* @param string $name Case-insensitive header field name.
* @return string A string of values as provided for the given header
* concatenated together using a comma. If the header does not appear in
* the message, this method MUST return an empty string.
*/
public function getHeaderLine(string $name);
/**
* Return an instance with the provided value replacing the specified header.
*
* While header names are case-insensitive, the casing of the header will
* be preserved by this function, and returned from getHeaders().
*
* This method MUST be implemented in such a way as to retain the
* immutability of the message, and MUST return an instance that has the
* new and/or updated header and value.
*
* @param string $name Case-insensitive header field name.
* @param string|string[] $value Header value(s).
* @return static
* @throws \InvalidArgumentException for invalid header names or values.
*/
public function withHeader(string $name, $value);
/**
* Return an instance with the specified header appended with the given value.
*
* Existing values for the specified header will be maintained. The new
* value(s) will be appended to the existing list. If the header did not
* exist previously, it will be added.
*
* This method MUST be implemented in such a way as to retain the
* immutability of the message, and MUST return an instance that has the
* new header and/or value.
*
* @param string $name Case-insensitive header field name to add.
* @param string|string[] $value Header value(s).
* @return static
* @throws \InvalidArgumentException for invalid header names or values.
*/
public function withAddedHeader(string $name, $value);
/**
* Return an instance without the specified header.
*
* Header resolution MUST be done without case-sensitivity.
*
* This method MUST be implemented in such a way as to retain the
* immutability of the message, and MUST return an instance that removes
* the named header.
*
* @param string $name Case-insensitive header field name to remove.
* @return static
*/
public function withoutHeader(string $name);
/**
* Gets the body of the message.
*
* @return StreamInterface Returns the body as a stream.
*/
public function getBody();
/**
* Return an instance with the specified message body.
*
* The body MUST be a StreamInterface object.
*
* This method MUST be implemented in such a way as to retain the
* immutability of the message, and MUST return a new instance that has the
* new body stream.
*
* @param StreamInterface $body Body.
* @return static
* @throws \InvalidArgumentException When the body is not valid.
*/
public function withBody(StreamInterface $body);
}

View File

@@ -0,0 +1,131 @@
<?php
declare(strict_types=1);
namespace Psr\Http\Message;
/**
* Representation of an outgoing, client-side request.
*
* Per the HTTP specification, this interface includes properties for
* each of the following:
*
* - Protocol version
* - HTTP method
* - URI
* - Headers
* - Message body
*
* During construction, implementations MUST attempt to set the Host header from
* a provided URI if no Host header is provided.
*
* Requests are considered immutable; all methods that might change state MUST
* be implemented such that they retain the internal state of the current
* message and return an instance that contains the changed state.
*/
interface RequestInterface extends MessageInterface
{
/**
* Retrieves the message's request target.
*
* Retrieves the message's request-target either as it will appear (for
* clients), as it appeared at request (for servers), or as it was
* specified for the instance (see withRequestTarget()).
*
* In most cases, this will be the origin-form of the composed URI,
* unless a value was provided to the concrete implementation (see
* withRequestTarget() below).
*
* If no URI is available, and no request-target has been specifically
* provided, this method MUST return the string "/".
*
* @return string
*/
public function getRequestTarget();
/**
* Return an instance with the specific request-target.
*
* If the request needs a non-origin-form request-target — e.g., for
* specifying an absolute-form, authority-form, or asterisk-form —
* this method may be used to create an instance with the specified
* request-target, verbatim.
*
* This method MUST be implemented in such a way as to retain the
* immutability of the message, and MUST return an instance that has the
* changed request target.
*
* @link http://tools.ietf.org/html/rfc7230#section-5.3 (for the various
* request-target forms allowed in request messages)
* @param string $requestTarget
* @return static
*/
public function withRequestTarget(string $requestTarget);
/**
* Retrieves the HTTP method of the request.
*
* @return string Returns the request method.
*/
public function getMethod();
/**
* Return an instance with the provided HTTP method.
*
* While HTTP method names are typically all uppercase characters, HTTP
* method names are case-sensitive and thus implementations SHOULD NOT
* modify the given string.
*
* This method MUST be implemented in such a way as to retain the
* immutability of the message, and MUST return an instance that has the
* changed request method.
*
* @param string $method Case-sensitive method.
* @return static
* @throws \InvalidArgumentException for invalid HTTP methods.
*/
public function withMethod(string $method);
/**
* Retrieves the URI instance.
*
* This method MUST return a UriInterface instance.
*
* @link http://tools.ietf.org/html/rfc3986#section-4.3
* @return UriInterface Returns a UriInterface instance
* representing the URI of the request.
*/
public function getUri();
/**
* Returns an instance with the provided URI.
*
* This method MUST update the Host header of the returned request by
* default if the URI contains a host component. If the URI does not
* contain a host component, any pre-existing Host header MUST be carried
* over to the returned request.
*
* You can opt-in to preserving the original state of the Host header by
* setting `$preserveHost` to `true`. When `$preserveHost` is set to
* `true`, this method interacts with the Host header in the following ways:
*
* - If the Host header is missing or empty, and the new URI contains
* a host component, this method MUST update the Host header in the returned
* request.
* - If the Host header is missing or empty, and the new URI does not contain a
* host component, this method MUST NOT update the Host header in the returned
* request.
* - If a Host header is present and non-empty, this method MUST NOT update
* the Host header in the returned request.
*
* This method MUST be implemented in such a way as to retain the
* immutability of the message, and MUST return an instance that has the
* new UriInterface instance.
*
* @link http://tools.ietf.org/html/rfc3986#section-4.3
* @param UriInterface $uri New request URI to use.
* @param bool $preserveHost Preserve the original state of the Host header.
* @return static
*/
public function withUri(UriInterface $uri, bool $preserveHost = false);
}

View File

@@ -0,0 +1,70 @@
<?php
declare(strict_types=1);
namespace Psr\Http\Message;
/**
* Representation of an outgoing, server-side response.
*
* Per the HTTP specification, this interface includes properties for
* each of the following:
*
* - Protocol version
* - Status code and reason phrase
* - Headers
* - Message body
*
* Responses are considered immutable; all methods that might change state MUST
* be implemented such that they retain the internal state of the current
* message and return an instance that contains the changed state.
*/
interface ResponseInterface extends MessageInterface
{
/**
* Gets the response status code.
*
* The status code is a 3-digit integer result code of the server's attempt
* to understand and satisfy the request.
*
* @return int Status code.
*/
public function getStatusCode();
/**
* Return an instance with the specified status code and, optionally, reason phrase.
*
* If no reason phrase is specified, implementations MAY choose to default
* to the RFC 7231 or IANA recommended reason phrase for the response's
* status code.
*
* This method MUST be implemented in such a way as to retain the
* immutability of the message, and MUST return an instance that has the
* updated status and reason phrase.
*
* @link http://tools.ietf.org/html/rfc7231#section-6
* @link http://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml
* @param int $code The 3-digit integer result code to set.
* @param string $reasonPhrase The reason phrase to use with the
* provided status code; if none is provided, implementations MAY
* use the defaults as suggested in the HTTP specification.
* @return static
* @throws \InvalidArgumentException For invalid status code arguments.
*/
public function withStatus(int $code, string $reasonPhrase = '');
/**
* Gets the response reason phrase associated with the status code.
*
* Because a reason phrase is not a required element in a response
* status line, the reason phrase value MAY be null. Implementations MAY
* choose to return the default RFC 7231 recommended reason phrase (or those
* listed in the IANA HTTP Status Code Registry) for the response's
* status code.
*
* @link http://tools.ietf.org/html/rfc7231#section-6
* @link http://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml
* @return string Reason phrase; must return an empty string if none present.
*/
public function getReasonPhrase();
}

View File

@@ -0,0 +1,263 @@
<?php
declare(strict_types=1);
namespace Psr\Http\Message;
/**
* Representation of an incoming, server-side HTTP request.
*
* Per the HTTP specification, this interface includes properties for
* each of the following:
*
* - Protocol version
* - HTTP method
* - URI
* - Headers
* - Message body
*
* Additionally, it encapsulates all data as it has arrived to the
* application from the CGI and/or PHP environment, including:
*
* - The values represented in $_SERVER.
* - Any cookies provided (generally via $_COOKIE)
* - Query string arguments (generally via $_GET, or as parsed via parse_str())
* - Upload files, if any (as represented by $_FILES)
* - Deserialized body parameters (generally from $_POST)
*
* $_SERVER values MUST be treated as immutable, as they represent application
* state at the time of request; as such, no methods are provided to allow
* modification of those values. The other values provide such methods, as they
* can be restored from $_SERVER or the request body, and may need treatment
* during the application (e.g., body parameters may be deserialized based on
* content type).
*
* Additionally, this interface recognizes the utility of introspecting a
* request to derive and match additional parameters (e.g., via URI path
* matching, decrypting cookie values, deserializing non-form-encoded body
* content, matching authorization headers to users, etc). These parameters
* are stored in an "attributes" property.
*
* Requests are considered immutable; all methods that might change state MUST
* be implemented such that they retain the internal state of the current
* message and return an instance that contains the changed state.
*/
interface ServerRequestInterface extends RequestInterface
{
/**
* Retrieve server parameters.
*
* Retrieves data related to the incoming request environment,
* typically derived from PHP's $_SERVER superglobal. The data IS NOT
* REQUIRED to originate from $_SERVER.
*
* @return array
*/
public function getServerParams();
/**
* Retrieve cookies.
*
* Retrieves cookies sent by the client to the server.
*
* The data MUST be compatible with the structure of the $_COOKIE
* superglobal.
*
* @return array
*/
public function getCookieParams();
/**
* Return an instance with the specified cookies.
*
* The data IS NOT REQUIRED to come from the $_COOKIE superglobal, but MUST
* be compatible with the structure of $_COOKIE. Typically, this data will
* be injected at instantiation.
*
* This method MUST NOT update the related Cookie header of the request
* instance, nor related values in the server params.
*
* This method MUST be implemented in such a way as to retain the
* immutability of the message, and MUST return an instance that has the
* updated cookie values.
*
* @param array $cookies Array of key/value pairs representing cookies.
* @return static
*/
public function withCookieParams(array $cookies);
/**
* Retrieve query string arguments.
*
* Retrieves the deserialized query string arguments, if any.
*
* Note: the query params might not be in sync with the URI or server
* params. If you need to ensure you are only getting the original
* values, you may need to parse the query string from `getUri()->getQuery()`
* or from the `QUERY_STRING` server param.
*
* @return array
*/
public function getQueryParams();
/**
* Return an instance with the specified query string arguments.
*
* These values SHOULD remain immutable over the course of the incoming
* request. They MAY be injected during instantiation, such as from PHP's
* $_GET superglobal, or MAY be derived from some other value such as the
* URI. In cases where the arguments are parsed from the URI, the data
* MUST be compatible with what PHP's parse_str() would return for
* purposes of how duplicate query parameters are handled, and how nested
* sets are handled.
*
* Setting query string arguments MUST NOT change the URI stored by the
* request, nor the values in the server params.
*
* This method MUST be implemented in such a way as to retain the
* immutability of the message, and MUST return an instance that has the
* updated query string arguments.
*
* @param array $query Array of query string arguments, typically from
* $_GET.
* @return static
*/
public function withQueryParams(array $query);
/**
* Retrieve normalized file upload data.
*
* This method returns upload metadata in a normalized tree, with each leaf
* an instance of Psr\Http\Message\UploadedFileInterface.
*
* These values MAY be prepared from $_FILES or the message body during
* instantiation, or MAY be injected via withUploadedFiles().
*
* @return array An array tree of UploadedFileInterface instances; an empty
* array MUST be returned if no data is present.
*/
public function getUploadedFiles();
/**
* Create a new instance with the specified uploaded files.
*
* This method MUST be implemented in such a way as to retain the
* immutability of the message, and MUST return an instance that has the
* updated body parameters.
*
* @param array $uploadedFiles An array tree of UploadedFileInterface instances.
* @return static
* @throws \InvalidArgumentException if an invalid structure is provided.
*/
public function withUploadedFiles(array $uploadedFiles);
/**
* Retrieve any parameters provided in the request body.
*
* If the request Content-Type is either application/x-www-form-urlencoded
* or multipart/form-data, and the request method is POST, this method MUST
* return the contents of $_POST.
*
* Otherwise, this method may return any results of deserializing
* the request body content; as parsing returns structured content, the
* potential types MUST be arrays or objects only. A null value indicates
* the absence of body content.
*
* @return null|array|object The deserialized body parameters, if any.
* These will typically be an array or object.
*/
public function getParsedBody();
/**
* Return an instance with the specified body parameters.
*
* These MAY be injected during instantiation.
*
* If the request Content-Type is either application/x-www-form-urlencoded
* or multipart/form-data, and the request method is POST, use this method
* ONLY to inject the contents of $_POST.
*
* The data IS NOT REQUIRED to come from $_POST, but MUST be the results of
* deserializing the request body content. Deserialization/parsing returns
* structured data, and, as such, this method ONLY accepts arrays or objects,
* or a null value if nothing was available to parse.
*
* As an example, if content negotiation determines that the request data
* is a JSON payload, this method could be used to create a request
* instance with the deserialized parameters.
*
* This method MUST be implemented in such a way as to retain the
* immutability of the message, and MUST return an instance that has the
* updated body parameters.
*
* @param null|array|object $data The deserialized body data. This will
* typically be in an array or object.
* @return static
* @throws \InvalidArgumentException if an unsupported argument type is
* provided.
*/
public function withParsedBody($data);
/**
* Retrieve attributes derived from the request.
*
* The request "attributes" may be used to allow injection of any
* parameters derived from the request: e.g., the results of path
* match operations; the results of decrypting cookies; the results of
* deserializing non-form-encoded message bodies; etc. Attributes
* will be application and request specific, and CAN be mutable.
*
* @return array Attributes derived from the request.
*/
public function getAttributes();
/**
* Retrieve a single derived request attribute.
*
* Retrieves a single derived request attribute as described in
* getAttributes(). If the attribute has not been previously set, returns
* the default value as provided.
*
* This method obviates the need for a hasAttribute() method, as it allows
* specifying a default value to return if the attribute is not found.
*
* @see getAttributes()
* @param string $name The attribute name.
* @param mixed $default Default value to return if the attribute does not exist.
* @return mixed
*/
public function getAttribute(string $name, $default = null);
/**
* Return an instance with the specified derived request attribute.
*
* This method allows setting a single derived request attribute as
* described in getAttributes().
*
* This method MUST be implemented in such a way as to retain the
* immutability of the message, and MUST return an instance that has the
* updated attribute.
*
* @see getAttributes()
* @param string $name The attribute name.
* @param mixed $value The value of the attribute.
* @return static
*/
public function withAttribute(string $name, $value);
/**
* Return an instance that removes the specified derived request attribute.
*
* This method allows removing a single derived request attribute as
* described in getAttributes().
*
* This method MUST be implemented in such a way as to retain the
* immutability of the message, and MUST return an instance that removes
* the attribute.
*
* @see getAttributes()
* @param string $name The attribute name.
* @return static
*/
public function withoutAttribute(string $name);
}

View File

@@ -0,0 +1,160 @@
<?php
declare(strict_types=1);
namespace Psr\Http\Message;
/**
* Describes a data stream.
*
* Typically, an instance will wrap a PHP stream; this interface provides
* a wrapper around the most common operations, including serialization of
* the entire stream to a string.
*/
interface StreamInterface
{
/**
* Reads all data from the stream into a string, from the beginning to end.
*
* This method MUST attempt to seek to the beginning of the stream before
* reading data and read the stream until the end is reached.
*
* Warning: This could attempt to load a large amount of data into memory.
*
* This method MUST NOT raise an exception in order to conform with PHP's
* string casting operations.
*
* @see http://php.net/manual/en/language.oop5.magic.php#object.tostring
* @return string
*/
public function __toString();
/**
* Closes the stream and any underlying resources.
*
* @return void
*/
public function close();
/**
* Separates any underlying resources from the stream.
*
* After the stream has been detached, the stream is in an unusable state.
*
* @return resource|null Underlying PHP stream, if any
*/
public function detach();
/**
* Get the size of the stream if known.
*
* @return int|null Returns the size in bytes if known, or null if unknown.
*/
public function getSize();
/**
* Returns the current position of the file read/write pointer
*
* @return int Position of the file pointer
* @throws \RuntimeException on error.
*/
public function tell();
/**
* Returns true if the stream is at the end of the stream.
*
* @return bool
*/
public function eof();
/**
* Returns whether or not the stream is seekable.
*
* @return bool
*/
public function isSeekable();
/**
* Seek to a position in the stream.
*
* @link http://www.php.net/manual/en/function.fseek.php
* @param int $offset Stream offset
* @param int $whence Specifies how the cursor position will be calculated
* based on the seek offset. Valid values are identical to the built-in
* PHP $whence values for `fseek()`. SEEK_SET: Set position equal to
* offset bytes SEEK_CUR: Set position to current location plus offset
* SEEK_END: Set position to end-of-stream plus offset.
* @throws \RuntimeException on failure.
*/
public function seek(int $offset, int $whence = SEEK_SET);
/**
* Seek to the beginning of the stream.
*
* If the stream is not seekable, this method will raise an exception;
* otherwise, it will perform a seek(0).
*
* @see seek()
* @link http://www.php.net/manual/en/function.fseek.php
* @throws \RuntimeException on failure.
*/
public function rewind();
/**
* Returns whether or not the stream is writable.
*
* @return bool
*/
public function isWritable();
/**
* Write data to the stream.
*
* @param string $string The string that is to be written.
* @return int Returns the number of bytes written to the stream.
* @throws \RuntimeException on failure.
*/
public function write(string $string);
/**
* Returns whether or not the stream is readable.
*
* @return bool
*/
public function isReadable();
/**
* Read data from the stream.
*
* @param int $length Read up to $length bytes from the object and return
* them. Fewer than $length bytes may be returned if underlying stream
* call returns fewer bytes.
* @return string Returns the data read from the stream, or an empty string
* if no bytes are available.
* @throws \RuntimeException if an error occurs.
*/
public function read(int $length);
/**
* Returns the remaining contents in a string
*
* @return string
* @throws \RuntimeException if unable to read or an error occurs while
* reading.
*/
public function getContents();
/**
* Get stream metadata as an associative array or retrieve a specific key.
*
* The keys returned are identical to the keys returned from PHP's
* stream_get_meta_data() function.
*
* @link http://php.net/manual/en/function.stream-get-meta-data.php
* @param string|null $key Specific metadata to retrieve.
* @return array|mixed|null Returns an associative array if no key is
* provided. Returns a specific key value if a key is provided and the
* value is found, or null if the key is not found.
*/
public function getMetadata(?string $key = null);
}

View File

@@ -0,0 +1,125 @@
<?php
declare(strict_types=1);
namespace Psr\Http\Message;
/**
* Value object representing a file uploaded through an HTTP request.
*
* Instances of this interface are considered immutable; all methods that
* might change state MUST be implemented such that they retain the internal
* state of the current instance and return an instance that contains the
* changed state.
*/
interface UploadedFileInterface
{
/**
* Retrieve a stream representing the uploaded file.
*
* This method MUST return a StreamInterface instance, representing the
* uploaded file. The purpose of this method is to allow utilizing native PHP
* stream functionality to manipulate the file upload, such as
* stream_copy_to_stream() (though the result will need to be decorated in a
* native PHP stream wrapper to work with such functions).
*
* If the moveTo() method has been called previously, this method MUST raise
* an exception.
*
* @return StreamInterface Stream representation of the uploaded file.
* @throws \RuntimeException in cases when no stream is available or can be
* created.
*/
public function getStream();
/**
* Move the uploaded file to a new location.
*
* Use this method as an alternative to move_uploaded_file(). This method is
* guaranteed to work in both SAPI and non-SAPI environments.
* Implementations must determine which environment they are in, and use the
* appropriate method (move_uploaded_file(), rename(), or a stream
* operation) to perform the operation.
*
* $targetPath may be an absolute path, or a relative path. If it is a
* relative path, resolution should be the same as used by PHP's rename()
* function.
*
* The original file or stream MUST be removed on completion.
*
* If this method is called more than once, any subsequent calls MUST raise
* an exception.
*
* When used in an SAPI environment where $_FILES is populated, when writing
* files via moveTo(), is_uploaded_file() and move_uploaded_file() SHOULD be
* used to ensure permissions and upload status are verified correctly.
*
* If you wish to move to a stream, use getStream(), as SAPI operations
* cannot guarantee writing to stream destinations.
*
* @see http://php.net/is_uploaded_file
* @see http://php.net/move_uploaded_file
* @param string $targetPath Path to which to move the uploaded file.
* @throws \InvalidArgumentException if the $targetPath specified is invalid.
* @throws \RuntimeException on any error during the move operation, or on
* the second or subsequent call to the method.
*/
public function moveTo(string $targetPath);
/**
* Retrieve the file size.
*
* Implementations SHOULD return the value stored in the "size" key of
* the file in the $_FILES array if available, as PHP calculates this based
* on the actual size transmitted.
*
* @return int|null The file size in bytes or null if unknown.
*/
public function getSize();
/**
* Retrieve the error associated with the uploaded file.
*
* The return value MUST be one of PHP's UPLOAD_ERR_XXX constants.
*
* If the file was uploaded successfully, this method MUST return
* UPLOAD_ERR_OK.
*
* Implementations SHOULD return the value stored in the "error" key of
* the file in the $_FILES array.
*
* @see http://php.net/manual/en/features.file-upload.errors.php
* @return int One of PHP's UPLOAD_ERR_XXX constants.
*/
public function getError();
/**
* Retrieve the filename sent by the client.
*
* Do not trust the value returned by this method. A client could send
* a malicious filename with the intention to corrupt or hack your
* application.
*
* Implementations SHOULD return the value stored in the "name" key of
* the file in the $_FILES array.
*
* @return string|null The filename sent by the client or null if none
* was provided.
*/
public function getClientFilename();
/**
* Retrieve the media type sent by the client.
*
* Do not trust the value returned by this method. A client could send
* a malicious media type with the intention to corrupt or hack your
* application.
*
* Implementations SHOULD return the value stored in the "type" key of
* the file in the $_FILES array.
*
* @return string|null The media type sent by the client or null if none
* was provided.
*/
public function getClientMediaType();
}

View File

@@ -0,0 +1,326 @@
<?php
declare(strict_types=1);
namespace Psr\Http\Message;
/**
* Value object representing a URI.
*
* This interface is meant to represent URIs according to RFC 3986 and to
* provide methods for most common operations. Additional functionality for
* working with URIs can be provided on top of the interface or externally.
* Its primary use is for HTTP requests, but may also be used in other
* contexts.
*
* Instances of this interface are considered immutable; all methods that
* might change state MUST be implemented such that they retain the internal
* state of the current instance and return an instance that contains the
* changed state.
*
* Typically the Host header will be also be present in the request message.
* For server-side requests, the scheme will typically be discoverable in the
* server parameters.
*
* @link http://tools.ietf.org/html/rfc3986 (the URI specification)
*/
interface UriInterface
{
/**
* Retrieve the scheme component of the URI.
*
* If no scheme is present, this method MUST return an empty string.
*
* The value returned MUST be normalized to lowercase, per RFC 3986
* Section 3.1.
*
* The trailing ":" character is not part of the scheme and MUST NOT be
* added.
*
* @see https://tools.ietf.org/html/rfc3986#section-3.1
* @return string The URI scheme.
*/
public function getScheme();
/**
* Retrieve the authority component of the URI.
*
* If no authority information is present, this method MUST return an empty
* string.
*
* The authority syntax of the URI is:
*
* <pre>
* [user-info@]host[:port]
* </pre>
*
* If the port component is not set or is the standard port for the current
* scheme, it SHOULD NOT be included.
*
* @see https://tools.ietf.org/html/rfc3986#section-3.2
* @return string The URI authority, in "[user-info@]host[:port]" format.
*/
public function getAuthority();
/**
* Retrieve the user information component of the URI.
*
* If no user information is present, this method MUST return an empty
* string.
*
* If a user is present in the URI, this will return that value;
* additionally, if the password is also present, it will be appended to the
* user value, with a colon (":") separating the values.
*
* The trailing "@" character is not part of the user information and MUST
* NOT be added.
*
* @return string The URI user information, in "username[:password]" format.
*/
public function getUserInfo();
/**
* Retrieve the host component of the URI.
*
* If no host is present, this method MUST return an empty string.
*
* The value returned MUST be normalized to lowercase, per RFC 3986
* Section 3.2.2.
*
* @see http://tools.ietf.org/html/rfc3986#section-3.2.2
* @return string The URI host.
*/
public function getHost();
/**
* Retrieve the port component of the URI.
*
* If a port is present, and it is non-standard for the current scheme,
* this method MUST return it as an integer. If the port is the standard port
* used with the current scheme, this method SHOULD return null.
*
* If no port is present, and no scheme is present, this method MUST return
* a null value.
*
* If no port is present, but a scheme is present, this method MAY return
* the standard port for that scheme, but SHOULD return null.
*
* @return null|int The URI port.
*/
public function getPort();
/**
* Retrieve the path component of the URI.
*
* The path can either be empty or absolute (starting with a slash) or
* rootless (not starting with a slash). Implementations MUST support all
* three syntaxes.
*
* Normally, the empty path "" and absolute path "/" are considered equal as
* defined in RFC 7230 Section 2.7.3. But this method MUST NOT automatically
* do this normalization because in contexts with a trimmed base path, e.g.
* the front controller, this difference becomes significant. It's the task
* of the user to handle both "" and "/".
*
* The value returned MUST be percent-encoded, but MUST NOT double-encode
* any characters. To determine what characters to encode, please refer to
* RFC 3986, Sections 2 and 3.3.
*
* As an example, if the value should include a slash ("/") not intended as
* delimiter between path segments, that value MUST be passed in encoded
* form (e.g., "%2F") to the instance.
*
* @see https://tools.ietf.org/html/rfc3986#section-2
* @see https://tools.ietf.org/html/rfc3986#section-3.3
* @return string The URI path.
*/
public function getPath();
/**
* Retrieve the query string of the URI.
*
* If no query string is present, this method MUST return an empty string.
*
* The leading "?" character is not part of the query and MUST NOT be
* added.
*
* The value returned MUST be percent-encoded, but MUST NOT double-encode
* any characters. To determine what characters to encode, please refer to
* RFC 3986, Sections 2 and 3.4.
*
* As an example, if a value in a key/value pair of the query string should
* include an ampersand ("&") not intended as a delimiter between values,
* that value MUST be passed in encoded form (e.g., "%26") to the instance.
*
* @see https://tools.ietf.org/html/rfc3986#section-2
* @see https://tools.ietf.org/html/rfc3986#section-3.4
* @return string The URI query string.
*/
public function getQuery();
/**
* Retrieve the fragment component of the URI.
*
* If no fragment is present, this method MUST return an empty string.
*
* The leading "#" character is not part of the fragment and MUST NOT be
* added.
*
* The value returned MUST be percent-encoded, but MUST NOT double-encode
* any characters. To determine what characters to encode, please refer to
* RFC 3986, Sections 2 and 3.5.
*
* @see https://tools.ietf.org/html/rfc3986#section-2
* @see https://tools.ietf.org/html/rfc3986#section-3.5
* @return string The URI fragment.
*/
public function getFragment();
/**
* Return an instance with the specified scheme.
*
* This method MUST retain the state of the current instance, and return
* an instance that contains the specified scheme.
*
* Implementations MUST support the schemes "http" and "https" case
* insensitively, and MAY accommodate other schemes if required.
*
* An empty scheme is equivalent to removing the scheme.
*
* @param string $scheme The scheme to use with the new instance.
* @return static A new instance with the specified scheme.
* @throws \InvalidArgumentException for invalid or unsupported schemes.
*/
public function withScheme(string $scheme);
/**
* Return an instance with the specified user information.
*
* This method MUST retain the state of the current instance, and return
* an instance that contains the specified user information.
*
* Password is optional, but the user information MUST include the
* user; an empty string for the user is equivalent to removing user
* information.
*
* @param string $user The user name to use for authority.
* @param null|string $password The password associated with $user.
* @return static A new instance with the specified user information.
*/
public function withUserInfo(string $user, ?string $password = null);
/**
* Return an instance with the specified host.
*
* This method MUST retain the state of the current instance, and return
* an instance that contains the specified host.
*
* An empty host value is equivalent to removing the host.
*
* @param string $host The hostname to use with the new instance.
* @return static A new instance with the specified host.
* @throws \InvalidArgumentException for invalid hostnames.
*/
public function withHost(string $host);
/**
* Return an instance with the specified port.
*
* This method MUST retain the state of the current instance, and return
* an instance that contains the specified port.
*
* Implementations MUST raise an exception for ports outside the
* established TCP and UDP port ranges.
*
* A null value provided for the port is equivalent to removing the port
* information.
*
* @param null|int $port The port to use with the new instance; a null value
* removes the port information.
* @return static A new instance with the specified port.
* @throws \InvalidArgumentException for invalid ports.
*/
public function withPort(?int $port);
/**
* Return an instance with the specified path.
*
* This method MUST retain the state of the current instance, and return
* an instance that contains the specified path.
*
* The path can either be empty or absolute (starting with a slash) or
* rootless (not starting with a slash). Implementations MUST support all
* three syntaxes.
*
* If the path is intended to be domain-relative rather than path relative then
* it must begin with a slash ("/"). Paths not starting with a slash ("/")
* are assumed to be relative to some base path known to the application or
* consumer.
*
* Users can provide both encoded and decoded path characters.
* Implementations ensure the correct encoding as outlined in getPath().
*
* @param string $path The path to use with the new instance.
* @return static A new instance with the specified path.
* @throws \InvalidArgumentException for invalid paths.
*/
public function withPath(string $path);
/**
* Return an instance with the specified query string.
*
* This method MUST retain the state of the current instance, and return
* an instance that contains the specified query string.
*
* Users can provide both encoded and decoded query characters.
* Implementations ensure the correct encoding as outlined in getQuery().
*
* An empty query string value is equivalent to removing the query string.
*
* @param string $query The query string to use with the new instance.
* @return static A new instance with the specified query string.
* @throws \InvalidArgumentException for invalid query strings.
*/
public function withQuery(string $query);
/**
* Return an instance with the specified URI fragment.
*
* This method MUST retain the state of the current instance, and return
* an instance that contains the specified URI fragment.
*
* Users can provide both encoded and decoded fragment characters.
* Implementations ensure the correct encoding as outlined in getFragment().
*
* An empty fragment value is equivalent to removing the fragment.
*
* @param string $fragment The fragment to use with the new instance.
* @return static A new instance with the specified fragment.
*/
public function withFragment(string $fragment);
/**
* Return the string representation as a URI reference.
*
* Depending on which components of the URI are present, the resulting
* string is either a full URI or relative reference according to RFC 3986,
* Section 4.1. The method concatenates the various components of the URI,
* using the appropriate delimiters:
*
* - If a scheme is present, it MUST be suffixed by ":".
* - If an authority is present, it MUST be prefixed by "//".
* - The path can be concatenated without delimiters. But there are two
* cases where the path has to be adjusted to make the URI reference
* valid as PHP does not allow to throw an exception in __toString():
* - If the path is rootless and an authority is present, the path MUST
* be prefixed by "/".
* - If the path is starting with more than one "/" and no authority is
* present, the starting slashes MUST be reduced to one.
* - If a query is present, it MUST be prefixed by "?".
* - If a fragment is present, it MUST be prefixed by "#".
*
* @see http://tools.ietf.org/html/rfc3986#section-4.1
* @return string
*/
public function __toString();
}

112
vendor/react/async/CHANGELOG.md vendored Normal file
View File

@@ -0,0 +1,112 @@
# Changelog
## 4.3.0 (2024-06-04)
* Feature: Improve performance by avoiding unneeded references in `FiberMap`.
(#88 by @clue)
* Feature: Improve PHP 8.4+ support by avoiding implicitly nullable type declarations.
(#87 by @clue)
* Improve type safety for test environment.
(#86 by @SimonFrings)
## 4.2.0 (2023-11-22)
* Feature: Add Promise v3 template types for all public functions.
(#40 by @WyriHaximus and @clue)
All our public APIs now use Promise v3 template types to guide IDEs and static
analysis tools (like PHPStan), helping with proper type usage and improving
code quality:
```php
assertType('bool', await(resolve(true)));
assertType('PromiseInterface<bool>', async(fn(): bool => true)());
assertType('PromiseInterface<bool>', coroutine(fn(): bool => true));
```
* Feature: Full PHP 8.3 compatibility.
(#81 by @clue)
* Update test suite to avoid unhandled promise rejections.
(#79 by @clue)
## 4.1.0 (2023-06-22)
* Feature: Add new `delay()` function to delay program execution.
(#69 and #78 by @clue)
```php
echo 'a';
Loop::addTimer(1.0, function () {
echo 'b';
});
React\Async\delay(3.0);
echo 'c';
// prints "a" at t=0.0s
// prints "b" at t=1.0s
// prints "c" at t=3.0s
```
* Update test suite, add PHPStan with `max` level and report failed assertions.
(#66 and #76 by @clue and #61 and #73 by @WyriHaximus)
## 4.0.0 (2022-07-11)
A major new feature release, see [**release announcement**](https://clue.engineering/2022/announcing-reactphp-async).
* We'd like to emphasize that this component is production ready and battle-tested.
We plan to support all long-term support (LTS) releases for at least 24 months,
so you have a rock-solid foundation to build on top of.
* The v4 release will be the way forward for this package. However, we will still
actively support v3 and v2 to provide a smooth upgrade path for those not yet
on PHP 8.1+. If you're using an older PHP version, you may use either version
which all provide a compatible API but may not take advantage of newer language
features. You may target multiple versions at the same time to support a wider range of
PHP versions:
* [`4.x` branch](https://github.com/reactphp/async/tree/4.x) (PHP 8.1+)
* [`3.x` branch](https://github.com/reactphp/async/tree/3.x) (PHP 7.1+)
* [`2.x` branch](https://github.com/reactphp/async/tree/2.x) (PHP 5.3+)
This update involves some major new features and a minor BC break over the
`v3.0.0` release. We've tried hard to avoid BC breaks where possible and
minimize impact otherwise. We expect that most consumers of this package will be
affected by BC breaks, but updating should take no longer than a few minutes.
See below for more details:
* Feature / BC break: Require PHP 8.1+ and add `mixed` type declarations.
(#14 by @clue)
* Feature: Add Fiber-based `async()` and `await()` functions.
(#15, #18, #19 and #20 by @WyriHaximus and #26, #28, #30, #32, #34, #55 and #57 by @clue)
* Project maintenance, rename `main` branch to `4.x` and update installation instructions.
(#29 by @clue)
The following changes had to be ported to this release due to our branching
strategy, but also appeared in the `v3.0.0` release:
* Feature: Support iterable type for `parallel()` + `series()` + `waterfall()`.
(#49 by @clue)
* Feature: Forward compatibility with upcoming Promise v3.
(#48 by @clue)
* Minor documentation improvements.
(#36 by @SimonFrings and #51 by @nhedger)
## 3.0.0 (2022-07-11)
See [`3.x` CHANGELOG](https://github.com/reactphp/async/blob/3.x/CHANGELOG.md) for more details.
## 2.0.0 (2022-07-11)
See [`2.x` CHANGELOG](https://github.com/reactphp/async/blob/2.x/CHANGELOG.md) for more details.
## 1.0.0 (2013-02-07)
* First tagged release

19
vendor/react/async/LICENSE vendored Normal file
View File

@@ -0,0 +1,19 @@
Copyright (c) 2012 Christian Lück, Cees-Jan Kiewiet, Jan Sorgalla, Chris Boden, Igor Wiedler
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is furnished
to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

672
vendor/react/async/README.md vendored Normal file
View File

@@ -0,0 +1,672 @@
# Async Utilities
[![CI status](https://github.com/reactphp/async/workflows/CI/badge.svg)](https://github.com/reactphp/async/actions)
[![installs on Packagist](https://img.shields.io/packagist/dt/react/async?color=blue&label=installs%20on%20Packagist)](https://packagist.org/packages/react/async)
Async utilities and fibers for [ReactPHP](https://reactphp.org/).
This library allows you to manage async control flow. It provides a number of
combinators for [Promise](https://github.com/reactphp/promise)-based APIs.
Instead of nesting or chaining promise callbacks, you can declare them as a
list, which is resolved sequentially in an async manner.
React/Async will not automagically change blocking code to be async. You need
to have an actual event loop and non-blocking libraries interacting with that
event loop for it to work. As long as you have a Promise-based API that runs in
an event loop, it can be used with this library.
**Table of Contents**
* [Usage](#usage)
* [async()](#async)
* [await()](#await)
* [coroutine()](#coroutine)
* [delay()](#delay)
* [parallel()](#parallel)
* [series()](#series)
* [waterfall()](#waterfall)
* [Todo](#todo)
* [Install](#install)
* [Tests](#tests)
* [License](#license)
## Usage
This lightweight library consists only of a few simple functions.
All functions reside under the `React\Async` namespace.
The below examples refer to all functions with their fully-qualified names like this:
```php
React\Async\await();
```
As of PHP 5.6+ you can also import each required function into your code like this:
```php
use function React\Async\await;
await();
```
Alternatively, you can also use an import statement similar to this:
```php
use React\Async;
Async\await();
```
### async()
The `async(callable():(PromiseInterface<T>|T) $function): (callable():PromiseInterface<T>)` function can be used to
return an async function for a function that uses [`await()`](#await) internally.
This function is specifically designed to complement the [`await()` function](#await).
The [`await()` function](#await) can be considered *blocking* from the
perspective of the calling code. You can avoid this blocking behavior by
wrapping it in an `async()` function call. Everything inside this function
will still be blocked, but everything outside this function can be executed
asynchronously without blocking:
```php
Loop::addTimer(0.5, React\Async\async(function () {
echo 'a';
React\Async\await(React\Promise\Timer\sleep(1.0));
echo 'c';
}));
Loop::addTimer(1.0, function () {
echo 'b';
});
// prints "a" at t=0.5s
// prints "b" at t=1.0s
// prints "c" at t=1.5s
```
See also the [`await()` function](#await) for more details.
Note that this function only works in tandem with the [`await()` function](#await).
In particular, this function does not "magically" make any blocking function
non-blocking:
```php
Loop::addTimer(0.5, React\Async\async(function () {
echo 'a';
sleep(1); // broken: using PHP's blocking sleep() for demonstration purposes
echo 'c';
}));
Loop::addTimer(1.0, function () {
echo 'b';
});
// prints "a" at t=0.5s
// prints "c" at t=1.5s: Correct timing, but wrong order
// prints "b" at t=1.5s: Triggered too late because it was blocked
```
As an alternative, you should always make sure to use this function in tandem
with the [`await()` function](#await) and an async API returning a promise
as shown in the previous example.
The `async()` function is specifically designed for cases where it is used
as a callback (such as an event loop timer, event listener, or promise
callback). For this reason, it returns a new function wrapping the given
`$function` instead of directly invoking it and returning its value.
```php
use function React\Async\async;
Loop::addTimer(1.0, async(function () { }));
$connection->on('close', async(function () { }));
$stream->on('data', async(function ($data) { }));
$promise->then(async(function (int $result) { }));
```
You can invoke this wrapping function to invoke the given `$function` with
any arguments given as-is. The function will always return a Promise which
will be fulfilled with whatever your `$function` returns. Likewise, it will
return a promise that will be rejected if you throw an `Exception` or
`Throwable` from your `$function`. This allows you to easily create
Promise-based functions:
```php
$promise = React\Async\async(function (): int {
$browser = new React\Http\Browser();
$urls = [
'https://example.com/alice',
'https://example.com/bob'
];
$bytes = 0;
foreach ($urls as $url) {
$response = React\Async\await($browser->get($url));
assert($response instanceof Psr\Http\Message\ResponseInterface);
$bytes += $response->getBody()->getSize();
}
return $bytes;
})();
$promise->then(function (int $bytes) {
echo 'Total size: ' . $bytes . PHP_EOL;
}, function (Exception $e) {
echo 'Error: ' . $e->getMessage() . PHP_EOL;
});
```
The previous example uses [`await()`](#await) inside a loop to highlight how
this vastly simplifies consuming asynchronous operations. At the same time,
this naive example does not leverage concurrent execution, as it will
essentially "await" between each operation. In order to take advantage of
concurrent execution within the given `$function`, you can "await" multiple
promises by using a single [`await()`](#await) together with Promise-based
primitives like this:
```php
$promise = React\Async\async(function (): int {
$browser = new React\Http\Browser();
$urls = [
'https://example.com/alice',
'https://example.com/bob'
];
$promises = [];
foreach ($urls as $url) {
$promises[] = $browser->get($url);
}
try {
$responses = React\Async\await(React\Promise\all($promises));
} catch (Exception $e) {
foreach ($promises as $promise) {
$promise->cancel();
}
throw $e;
}
$bytes = 0;
foreach ($responses as $response) {
assert($response instanceof Psr\Http\Message\ResponseInterface);
$bytes += $response->getBody()->getSize();
}
return $bytes;
})();
$promise->then(function (int $bytes) {
echo 'Total size: ' . $bytes . PHP_EOL;
}, function (Exception $e) {
echo 'Error: ' . $e->getMessage() . PHP_EOL;
});
```
The returned promise is implemented in such a way that it can be cancelled
when it is still pending. Cancelling a pending promise will cancel any awaited
promises inside that fiber or any nested fibers. As such, the following example
will only output `ab` and cancel the pending [`delay()`](#delay).
The [`await()`](#await) calls in this example would throw a `RuntimeException`
from the cancelled [`delay()`](#delay) call that bubbles up through the fibers.
```php
$promise = async(static function (): int {
echo 'a';
await(async(static function (): void {
echo 'b';
delay(2);
echo 'c';
})());
echo 'd';
return time();
})();
$promise->cancel();
await($promise);
```
### await()
The `await(PromiseInterface<T> $promise): T` function can be used to
block waiting for the given `$promise` to be fulfilled.
```php
$result = React\Async\await($promise);
```
This function will only return after the given `$promise` has settled, i.e.
either fulfilled or rejected. While the promise is pending, this function
can be considered *blocking* from the perspective of the calling code.
You can avoid this blocking behavior by wrapping it in an [`async()` function](#async)
call. Everything inside this function will still be blocked, but everything
outside this function can be executed asynchronously without blocking:
```php
Loop::addTimer(0.5, React\Async\async(function () {
echo 'a';
React\Async\await(React\Promise\Timer\sleep(1.0));
echo 'c';
}));
Loop::addTimer(1.0, function () {
echo 'b';
});
// prints "a" at t=0.5s
// prints "b" at t=1.0s
// prints "c" at t=1.5s
```
See also the [`async()` function](#async) for more details.
Once the promise is fulfilled, this function will return whatever the promise
resolved to.
Once the promise is rejected, this will throw whatever the promise rejected
with. If the promise did not reject with an `Exception` or `Throwable`, then
this function will throw an `UnexpectedValueException` instead.
```php
try {
$result = React\Async\await($promise);
// promise successfully fulfilled with $result
echo 'Result: ' . $result;
} catch (Throwable $e) {
// promise rejected with $e
echo 'Error: ' . $e->getMessage();
}
```
### coroutine()
The `coroutine(callable(mixed ...$args):(\Generator|PromiseInterface<T>|T) $function, mixed ...$args): PromiseInterface<T>` function can be used to
execute a Generator-based coroutine to "await" promises.
```php
React\Async\coroutine(function () {
$browser = new React\Http\Browser();
try {
$response = yield $browser->get('https://example.com/');
assert($response instanceof Psr\Http\Message\ResponseInterface);
echo $response->getBody();
} catch (Exception $e) {
echo 'Error: ' . $e->getMessage() . PHP_EOL;
}
});
```
Using Generator-based coroutines is an alternative to directly using the
underlying promise APIs. For many use cases, this makes using promise-based
APIs much simpler, as it resembles a synchronous code flow more closely.
The above example performs the equivalent of directly using the promise APIs:
```php
$browser = new React\Http\Browser();
$browser->get('https://example.com/')->then(function (Psr\Http\Message\ResponseInterface $response) {
echo $response->getBody();
}, function (Exception $e) {
echo 'Error: ' . $e->getMessage() . PHP_EOL;
});
```
The `yield` keyword can be used to "await" a promise resolution. Internally,
it will turn the entire given `$function` into a [`Generator`](https://www.php.net/manual/en/class.generator.php).
This allows the execution to be interrupted and resumed at the same place
when the promise is fulfilled. The `yield` statement returns whatever the
promise is fulfilled with. If the promise is rejected, it will throw an
`Exception` or `Throwable`.
The `coroutine()` function will always return a Promise which will be
fulfilled with whatever your `$function` returns. Likewise, it will return
a promise that will be rejected if you throw an `Exception` or `Throwable`
from your `$function`. This allows you to easily create Promise-based
functions:
```php
$promise = React\Async\coroutine(function () {
$browser = new React\Http\Browser();
$urls = [
'https://example.com/alice',
'https://example.com/bob'
];
$bytes = 0;
foreach ($urls as $url) {
$response = yield $browser->get($url);
assert($response instanceof Psr\Http\Message\ResponseInterface);
$bytes += $response->getBody()->getSize();
}
return $bytes;
});
$promise->then(function (int $bytes) {
echo 'Total size: ' . $bytes . PHP_EOL;
}, function (Exception $e) {
echo 'Error: ' . $e->getMessage() . PHP_EOL;
});
```
The previous example uses a `yield` statement inside a loop to highlight how
this vastly simplifies consuming asynchronous operations. At the same time,
this naive example does not leverage concurrent execution, as it will
essentially "await" between each operation. In order to take advantage of
concurrent execution within the given `$function`, you can "await" multiple
promises by using a single `yield` together with Promise-based primitives
like this:
```php
$promise = React\Async\coroutine(function () {
$browser = new React\Http\Browser();
$urls = [
'https://example.com/alice',
'https://example.com/bob'
];
$promises = [];
foreach ($urls as $url) {
$promises[] = $browser->get($url);
}
try {
$responses = yield React\Promise\all($promises);
} catch (Exception $e) {
foreach ($promises as $promise) {
$promise->cancel();
}
throw $e;
}
$bytes = 0;
foreach ($responses as $response) {
assert($response instanceof Psr\Http\Message\ResponseInterface);
$bytes += $response->getBody()->getSize();
}
return $bytes;
});
$promise->then(function (int $bytes) {
echo 'Total size: ' . $bytes . PHP_EOL;
}, function (Exception $e) {
echo 'Error: ' . $e->getMessage() . PHP_EOL;
});
```
### delay()
The `delay(float $seconds): void` function can be used to
delay program execution for duration given in `$seconds`.
```php
React\Async\delay($seconds);
```
This function will only return after the given number of `$seconds` have
elapsed. If there are no other events attached to this loop, it will behave
similar to PHP's [`sleep()` function](https://www.php.net/manual/en/function.sleep.php).
```php
echo 'a';
React\Async\delay(1.0);
echo 'b';
// prints "a" at t=0.0s
// prints "b" at t=1.0s
```
Unlike PHP's [`sleep()` function](https://www.php.net/manual/en/function.sleep.php),
this function may not necessarily halt execution of the entire process thread.
Instead, it allows the event loop to run any other events attached to the
same loop until the delay returns:
```php
echo 'a';
Loop::addTimer(1.0, function (): void {
echo 'b';
});
React\Async\delay(3.0);
echo 'c';
// prints "a" at t=0.0s
// prints "b" at t=1.0s
// prints "c" at t=3.0s
```
This behavior is especially useful if you want to delay the program execution
of a particular routine, such as when building a simple polling or retry
mechanism:
```php
try {
something();
} catch (Throwable) {
// in case of error, retry after a short delay
React\Async\delay(1.0);
something();
}
```
Because this function only returns after some time has passed, it can be
considered *blocking* from the perspective of the calling code. You can avoid
this blocking behavior by wrapping it in an [`async()` function](#async) call.
Everything inside this function will still be blocked, but everything outside
this function can be executed asynchronously without blocking:
```php
Loop::addTimer(0.5, React\Async\async(function (): void {
echo 'a';
React\Async\delay(1.0);
echo 'c';
}));
Loop::addTimer(1.0, function (): void {
echo 'b';
});
// prints "a" at t=0.5s
// prints "b" at t=1.0s
// prints "c" at t=1.5s
```
See also the [`async()` function](#async) for more details.
Internally, the `$seconds` argument will be used as a timer for the loop so that
it keeps running until this timer triggers. This implies that if you pass a
really small (or negative) value, it will still start a timer and will thus
trigger at the earliest possible time in the future.
The function is implemented in such a way that it can be cancelled when it is
running inside an [`async()` function](#async). Cancelling the resulting
promise will clean up any pending timers and throw a `RuntimeException` from
the pending delay which in turn would reject the resulting promise.
```php
$promise = async(function (): void {
echo 'a';
delay(3.0);
echo 'b';
})();
Loop::addTimer(2.0, function () use ($promise): void {
$promise->cancel();
});
// prints "a" at t=0.0s
// rejects $promise at t=2.0
// never prints "b"
```
### parallel()
The `parallel(iterable<callable():PromiseInterface<T>> $tasks): PromiseInterface<array<T>>` function can be used
like this:
```php
<?php
use React\EventLoop\Loop;
use React\Promise\Promise;
React\Async\parallel([
function () {
return new Promise(function ($resolve) {
Loop::addTimer(1, function () use ($resolve) {
$resolve('Slept for a whole second');
});
});
},
function () {
return new Promise(function ($resolve) {
Loop::addTimer(1, function () use ($resolve) {
$resolve('Slept for another whole second');
});
});
},
function () {
return new Promise(function ($resolve) {
Loop::addTimer(1, function () use ($resolve) {
$resolve('Slept for yet another whole second');
});
});
},
])->then(function (array $results) {
foreach ($results as $result) {
var_dump($result);
}
}, function (Exception $e) {
echo 'Error: ' . $e->getMessage() . PHP_EOL;
});
```
### series()
The `series(iterable<callable():PromiseInterface<T>> $tasks): PromiseInterface<array<T>>` function can be used
like this:
```php
<?php
use React\EventLoop\Loop;
use React\Promise\Promise;
React\Async\series([
function () {
return new Promise(function ($resolve) {
Loop::addTimer(1, function () use ($resolve) {
$resolve('Slept for a whole second');
});
});
},
function () {
return new Promise(function ($resolve) {
Loop::addTimer(1, function () use ($resolve) {
$resolve('Slept for another whole second');
});
});
},
function () {
return new Promise(function ($resolve) {
Loop::addTimer(1, function () use ($resolve) {
$resolve('Slept for yet another whole second');
});
});
},
])->then(function (array $results) {
foreach ($results as $result) {
var_dump($result);
}
}, function (Exception $e) {
echo 'Error: ' . $e->getMessage() . PHP_EOL;
});
```
### waterfall()
The `waterfall(iterable<callable(mixed=):PromiseInterface<T>> $tasks): PromiseInterface<T>` function can be used
like this:
```php
<?php
use React\EventLoop\Loop;
use React\Promise\Promise;
$addOne = function ($prev = 0) {
return new Promise(function ($resolve) use ($prev) {
Loop::addTimer(1, function () use ($prev, $resolve) {
$resolve($prev + 1);
});
});
};
React\Async\waterfall([
$addOne,
$addOne,
$addOne
])->then(function ($prev) {
echo "Final result is $prev\n";
}, function (Exception $e) {
echo 'Error: ' . $e->getMessage() . PHP_EOL;
});
```
## Todo
* Implement queue()
## Install
The recommended way to install this library is [through Composer](https://getcomposer.org/).
[New to Composer?](https://getcomposer.org/doc/00-intro.md)
This project follows [SemVer](https://semver.org/).
This will install the latest supported version from this branch:
```bash
composer require react/async:^4.3
```
See also the [CHANGELOG](CHANGELOG.md) for details about version upgrades.
This project aims to run on any platform and thus does not require any PHP
extensions and supports running on PHP 8.1+.
It's *highly recommended to use the latest supported PHP version* for this project.
We're committed to providing long-term support (LTS) options and to provide a
smooth upgrade path. If you're using an older PHP version, you may use the
[`3.x` branch](https://github.com/reactphp/async/tree/3.x) (PHP 7.1+) or
[`2.x` branch](https://github.com/reactphp/async/tree/2.x) (PHP 5.3+) which both
provide a compatible API but do not take advantage of newer language features.
You may target multiple versions at the same time to support a wider range of
PHP versions like this:
```bash
composer require "react/async:^4 || ^3 || ^2"
```
## Tests
To run the test suite, you first need to clone this repo and then install all
dependencies [through Composer](https://getcomposer.org/):
```bash
composer install
```
To run the test suite, go to the project root and run:
```bash
vendor/bin/phpunit
```
On top of this, we use PHPStan on max level to ensure type safety across the project:
```bash
vendor/bin/phpstan
```
## License
MIT, see [LICENSE file](LICENSE).
This project is heavily influenced by [async.js](https://github.com/caolan/async).

50
vendor/react/async/composer.json vendored Normal file
View File

@@ -0,0 +1,50 @@
{
"name": "react/async",
"description": "Async utilities and fibers for ReactPHP",
"keywords": ["async", "ReactPHP"],
"license": "MIT",
"authors": [
{
"name": "Christian Lück",
"homepage": "https://clue.engineering/",
"email": "christian@clue.engineering"
},
{
"name": "Cees-Jan Kiewiet",
"homepage": "https://wyrihaximus.net/",
"email": "reactphp@ceesjankiewiet.nl"
},
{
"name": "Jan Sorgalla",
"homepage": "https://sorgalla.com/",
"email": "jsorgalla@gmail.com"
},
{
"name": "Chris Boden",
"homepage": "https://cboden.dev/",
"email": "cboden@gmail.com"
}
],
"require": {
"php": ">=8.1",
"react/event-loop": "^1.2",
"react/promise": "^3.2 || ^2.8 || ^1.2.1"
},
"require-dev": {
"phpstan/phpstan": "1.10.39",
"phpunit/phpunit": "^9.6"
},
"autoload": {
"psr-4": {
"React\\Async\\": "src/"
},
"files": [
"src/functions_include.php"
]
},
"autoload-dev": {
"psr-4": {
"React\\Tests\\Async\\": "tests/"
}
}
}

33
vendor/react/async/src/FiberFactory.php vendored Normal file
View File

@@ -0,0 +1,33 @@
<?php
namespace React\Async;
/**
* This factory its only purpose is interoperability. Where with
* event loops one could simply wrap another event loop. But with fibers
* that has become impossible and as such we provide this factory and the
* FiberInterface.
*
* Usage is not documented and as such not supported and might chang without
* notice. Use at your own risk.
*
* @internal
*/
final class FiberFactory
{
private static ?\Closure $factory = null;
public static function create(): FiberInterface
{
return (self::factory())();
}
public static function factory(?\Closure $factory = null): \Closure
{
if ($factory !== null) {
self::$factory = $factory;
}
return self::$factory ?? static fn (): FiberInterface => new SimpleFiber();
}
}

View File

@@ -0,0 +1,23 @@
<?php
namespace React\Async;
/**
* This interface its only purpose is interoperability. Where with
* event loops one could simply wrap another event loop. But with fibers
* that has become impossible and as such we provide this interface and the
* FiberFactory.
*
* Usage is not documented and as such not supported and might chang without
* notice. Use at your own risk.
*
* @internal
*/
interface FiberInterface
{
public function resume(mixed $value): void;
public function throw(\Throwable $throwable): void;
public function suspend(): mixed;
}

42
vendor/react/async/src/FiberMap.php vendored Normal file
View File

@@ -0,0 +1,42 @@
<?php
namespace React\Async;
use React\Promise\PromiseInterface;
/**
* @internal
*
* @template T
*/
final class FiberMap
{
/** @var array<int,PromiseInterface<T>> */
private static array $map = [];
/**
* @param \Fiber<mixed,mixed,mixed,mixed> $fiber
* @param PromiseInterface<T> $promise
*/
public static function setPromise(\Fiber $fiber, PromiseInterface $promise): void
{
self::$map[\spl_object_id($fiber)] = $promise;
}
/**
* @param \Fiber<mixed,mixed,mixed,mixed> $fiber
*/
public static function unsetPromise(\Fiber $fiber): void
{
unset(self::$map[\spl_object_id($fiber)]);
}
/**
* @param \Fiber<mixed,mixed,mixed,mixed> $fiber
* @return ?PromiseInterface<T>
*/
public static function getPromise(\Fiber $fiber): ?PromiseInterface
{
return self::$map[\spl_object_id($fiber)] ?? null;
}
}

79
vendor/react/async/src/SimpleFiber.php vendored Normal file
View File

@@ -0,0 +1,79 @@
<?php
namespace React\Async;
use React\EventLoop\Loop;
/**
* @internal
*/
final class SimpleFiber implements FiberInterface
{
/** @var ?\Fiber<void,void,void,callable(): mixed> */
private static ?\Fiber $scheduler = null;
private static ?\Closure $suspend = null;
/** @var ?\Fiber<mixed,mixed,mixed,mixed> */
private ?\Fiber $fiber = null;
public function __construct()
{
$this->fiber = \Fiber::getCurrent();
}
public function resume(mixed $value): void
{
if ($this->fiber !== null) {
$this->fiber->resume($value);
} else {
self::$suspend = static fn() => $value;
}
if (self::$suspend !== null && \Fiber::getCurrent() === self::$scheduler) {
$suspend = self::$suspend;
self::$suspend = null;
\Fiber::suspend($suspend);
}
}
public function throw(\Throwable $throwable): void
{
if ($this->fiber !== null) {
$this->fiber->throw($throwable);
} else {
self::$suspend = static fn() => throw $throwable;
}
if (self::$suspend !== null && \Fiber::getCurrent() === self::$scheduler) {
$suspend = self::$suspend;
self::$suspend = null;
\Fiber::suspend($suspend);
}
}
public function suspend(): mixed
{
if ($this->fiber === null) {
if (self::$scheduler === null || self::$scheduler->isTerminated()) {
self::$scheduler = new \Fiber(static fn() => Loop::run());
// Run event loop to completion on shutdown.
\register_shutdown_function(static function (): void {
assert(self::$scheduler instanceof \Fiber);
if (self::$scheduler->isSuspended()) {
self::$scheduler->resume();
}
});
}
$ret = (self::$scheduler->isStarted() ? self::$scheduler->resume() : self::$scheduler->start());
assert(\is_callable($ret));
return $ret();
}
return \Fiber::suspend();
}
}

846
vendor/react/async/src/functions.php vendored Normal file
View File

@@ -0,0 +1,846 @@
<?php
namespace React\Async;
use React\EventLoop\Loop;
use React\EventLoop\TimerInterface;
use React\Promise\Deferred;
use React\Promise\Promise;
use React\Promise\PromiseInterface;
use function React\Promise\reject;
use function React\Promise\resolve;
/**
* Return an async function for a function that uses [`await()`](#await) internally.
*
* This function is specifically designed to complement the [`await()` function](#await).
* The [`await()` function](#await) can be considered *blocking* from the
* perspective of the calling code. You can avoid this blocking behavior by
* wrapping it in an `async()` function call. Everything inside this function
* will still be blocked, but everything outside this function can be executed
* asynchronously without blocking:
*
* ```php
* Loop::addTimer(0.5, React\Async\async(function () {
* echo 'a';
* React\Async\await(React\Promise\Timer\sleep(1.0));
* echo 'c';
* }));
*
* Loop::addTimer(1.0, function () {
* echo 'b';
* });
*
* // prints "a" at t=0.5s
* // prints "b" at t=1.0s
* // prints "c" at t=1.5s
* ```
*
* See also the [`await()` function](#await) for more details.
*
* Note that this function only works in tandem with the [`await()` function](#await).
* In particular, this function does not "magically" make any blocking function
* non-blocking:
*
* ```php
* Loop::addTimer(0.5, React\Async\async(function () {
* echo 'a';
* sleep(1); // broken: using PHP's blocking sleep() for demonstration purposes
* echo 'c';
* }));
*
* Loop::addTimer(1.0, function () {
* echo 'b';
* });
*
* // prints "a" at t=0.5s
* // prints "c" at t=1.5s: Correct timing, but wrong order
* // prints "b" at t=1.5s: Triggered too late because it was blocked
* ```
*
* As an alternative, you should always make sure to use this function in tandem
* with the [`await()` function](#await) and an async API returning a promise
* as shown in the previous example.
*
* The `async()` function is specifically designed for cases where it is used
* as a callback (such as an event loop timer, event listener, or promise
* callback). For this reason, it returns a new function wrapping the given
* `$function` instead of directly invoking it and returning its value.
*
* ```php
* use function React\Async\async;
*
* Loop::addTimer(1.0, async(function () { … }));
* $connection->on('close', async(function () { … }));
* $stream->on('data', async(function ($data) { … }));
* $promise->then(async(function (int $result) { … }));
* ```
*
* You can invoke this wrapping function to invoke the given `$function` with
* any arguments given as-is. The function will always return a Promise which
* will be fulfilled with whatever your `$function` returns. Likewise, it will
* return a promise that will be rejected if you throw an `Exception` or
* `Throwable` from your `$function`. This allows you to easily create
* Promise-based functions:
*
* ```php
* $promise = React\Async\async(function (): int {
* $browser = new React\Http\Browser();
* $urls = [
* 'https://example.com/alice',
* 'https://example.com/bob'
* ];
*
* $bytes = 0;
* foreach ($urls as $url) {
* $response = React\Async\await($browser->get($url));
* assert($response instanceof Psr\Http\Message\ResponseInterface);
* $bytes += $response->getBody()->getSize();
* }
* return $bytes;
* })();
*
* $promise->then(function (int $bytes) {
* echo 'Total size: ' . $bytes . PHP_EOL;
* }, function (Exception $e) {
* echo 'Error: ' . $e->getMessage() . PHP_EOL;
* });
* ```
*
* The previous example uses [`await()`](#await) inside a loop to highlight how
* this vastly simplifies consuming asynchronous operations. At the same time,
* this naive example does not leverage concurrent execution, as it will
* essentially "await" between each operation. In order to take advantage of
* concurrent execution within the given `$function`, you can "await" multiple
* promises by using a single [`await()`](#await) together with Promise-based
* primitives like this:
*
* ```php
* $promise = React\Async\async(function (): int {
* $browser = new React\Http\Browser();
* $urls = [
* 'https://example.com/alice',
* 'https://example.com/bob'
* ];
*
* $promises = [];
* foreach ($urls as $url) {
* $promises[] = $browser->get($url);
* }
*
* try {
* $responses = React\Async\await(React\Promise\all($promises));
* } catch (Exception $e) {
* foreach ($promises as $promise) {
* $promise->cancel();
* }
* throw $e;
* }
*
* $bytes = 0;
* foreach ($responses as $response) {
* assert($response instanceof Psr\Http\Message\ResponseInterface);
* $bytes += $response->getBody()->getSize();
* }
* return $bytes;
* })();
*
* $promise->then(function (int $bytes) {
* echo 'Total size: ' . $bytes . PHP_EOL;
* }, function (Exception $e) {
* echo 'Error: ' . $e->getMessage() . PHP_EOL;
* });
* ```
*
* The returned promise is implemented in such a way that it can be cancelled
* when it is still pending. Cancelling a pending promise will cancel any awaited
* promises inside that fiber or any nested fibers. As such, the following example
* will only output `ab` and cancel the pending [`delay()`](#delay).
* The [`await()`](#await) calls in this example would throw a `RuntimeException`
* from the cancelled [`delay()`](#delay) call that bubbles up through the fibers.
*
* ```php
* $promise = async(static function (): int {
* echo 'a';
* await(async(static function (): void {
* echo 'b';
* delay(2);
* echo 'c';
* })());
* echo 'd';
*
* return time();
* })();
*
* $promise->cancel();
* await($promise);
* ```
*
* @template T
* @template A1 (any number of function arguments, see https://github.com/phpstan/phpstan/issues/8214)
* @template A2
* @template A3
* @template A4
* @template A5
* @param callable(A1,A2,A3,A4,A5): (PromiseInterface<T>|T) $function
* @return callable(A1=,A2=,A3=,A4=,A5=): PromiseInterface<T>
* @since 4.0.0
* @see coroutine()
*/
function async(callable $function): callable
{
return static function (mixed ...$args) use ($function): PromiseInterface {
$fiber = null;
/** @var PromiseInterface<T> $promise*/
$promise = new Promise(function (callable $resolve, callable $reject) use ($function, $args, &$fiber): void {
$fiber = new \Fiber(function () use ($resolve, $reject, $function, $args, &$fiber): void {
try {
$resolve($function(...$args));
} catch (\Throwable $exception) {
$reject($exception);
} finally {
assert($fiber instanceof \Fiber);
FiberMap::unsetPromise($fiber);
}
});
$fiber->start();
}, function () use (&$fiber): void {
assert($fiber instanceof \Fiber);
$promise = FiberMap::getPromise($fiber);
if ($promise instanceof PromiseInterface && \method_exists($promise, 'cancel')) {
$promise->cancel();
}
});
$lowLevelFiber = \Fiber::getCurrent();
if ($lowLevelFiber !== null) {
FiberMap::setPromise($lowLevelFiber, $promise);
}
return $promise;
};
}
/**
* Block waiting for the given `$promise` to be fulfilled.
*
* ```php
* $result = React\Async\await($promise);
* ```
*
* This function will only return after the given `$promise` has settled, i.e.
* either fulfilled or rejected. While the promise is pending, this function
* can be considered *blocking* from the perspective of the calling code.
* You can avoid this blocking behavior by wrapping it in an [`async()` function](#async)
* call. Everything inside this function will still be blocked, but everything
* outside this function can be executed asynchronously without blocking:
*
* ```php
* Loop::addTimer(0.5, React\Async\async(function () {
* echo 'a';
* React\Async\await(React\Promise\Timer\sleep(1.0));
* echo 'c';
* }));
*
* Loop::addTimer(1.0, function () {
* echo 'b';
* });
*
* // prints "a" at t=0.5s
* // prints "b" at t=1.0s
* // prints "c" at t=1.5s
* ```
*
* See also the [`async()` function](#async) for more details.
*
* Once the promise is fulfilled, this function will return whatever the promise
* resolved to.
*
* Once the promise is rejected, this will throw whatever the promise rejected
* with. If the promise did not reject with an `Exception` or `Throwable`, then
* this function will throw an `UnexpectedValueException` instead.
*
* ```php
* try {
* $result = React\Async\await($promise);
* // promise successfully fulfilled with $result
* echo 'Result: ' . $result;
* } catch (Throwable $e) {
* // promise rejected with $e
* echo 'Error: ' . $e->getMessage();
* }
* ```
*
* @template T
* @param PromiseInterface<T> $promise
* @return T returns whatever the promise resolves to
* @throws \Exception when the promise is rejected with an `Exception`
* @throws \Throwable when the promise is rejected with a `Throwable`
* @throws \UnexpectedValueException when the promise is rejected with an unexpected value (Promise API v1 or v2 only)
*/
function await(PromiseInterface $promise): mixed
{
$fiber = null;
$resolved = false;
$rejected = false;
/** @var T $resolvedValue */
$resolvedValue = null;
$rejectedThrowable = null;
$lowLevelFiber = \Fiber::getCurrent();
$promise->then(
function (mixed $value) use (&$resolved, &$resolvedValue, &$fiber, $lowLevelFiber): void {
if ($lowLevelFiber !== null) {
FiberMap::unsetPromise($lowLevelFiber);
}
/** @var ?\Fiber<mixed,mixed,mixed,mixed> $fiber */
if ($fiber === null) {
$resolved = true;
/** @var T $resolvedValue */
$resolvedValue = $value;
return;
}
$fiber->resume($value);
},
function (mixed $throwable) use (&$rejected, &$rejectedThrowable, &$fiber, $lowLevelFiber): void {
if ($lowLevelFiber !== null) {
FiberMap::unsetPromise($lowLevelFiber);
}
if (!$throwable instanceof \Throwable) {
$throwable = new \UnexpectedValueException(
'Promise rejected with unexpected value of type ' . (is_object($throwable) ? get_class($throwable) : gettype($throwable)) /** @phpstan-ignore-line */
);
// avoid garbage references by replacing all closures in call stack.
// what a lovely piece of code!
$r = new \ReflectionProperty('Exception', 'trace');
$trace = $r->getValue($throwable);
assert(\is_array($trace));
// Exception trace arguments only available when zend.exception_ignore_args is not set
// @codeCoverageIgnoreStart
foreach ($trace as $ti => $one) {
if (isset($one['args'])) {
foreach ($one['args'] as $ai => $arg) {
if ($arg instanceof \Closure) {
$trace[$ti]['args'][$ai] = 'Object(' . \get_class($arg) . ')';
}
}
}
}
// @codeCoverageIgnoreEnd
$r->setValue($throwable, $trace);
}
if ($fiber === null) {
$rejected = true;
$rejectedThrowable = $throwable;
return;
}
$fiber->throw($throwable);
}
);
if ($resolved) {
return $resolvedValue;
}
if ($rejected) {
assert($rejectedThrowable instanceof \Throwable);
throw $rejectedThrowable;
}
if ($lowLevelFiber !== null) {
FiberMap::setPromise($lowLevelFiber, $promise);
}
$fiber = FiberFactory::create();
return $fiber->suspend();
}
/**
* Delay program execution for duration given in `$seconds`.
*
* ```php
* React\Async\delay($seconds);
* ```
*
* This function will only return after the given number of `$seconds` have
* elapsed. If there are no other events attached to this loop, it will behave
* similar to PHP's [`sleep()` function](https://www.php.net/manual/en/function.sleep.php).
*
* ```php
* echo 'a';
* React\Async\delay(1.0);
* echo 'b';
*
* // prints "a" at t=0.0s
* // prints "b" at t=1.0s
* ```
*
* Unlike PHP's [`sleep()` function](https://www.php.net/manual/en/function.sleep.php),
* this function may not necessarily halt execution of the entire process thread.
* Instead, it allows the event loop to run any other events attached to the
* same loop until the delay returns:
*
* ```php
* echo 'a';
* Loop::addTimer(1.0, function (): void {
* echo 'b';
* });
* React\Async\delay(3.0);
* echo 'c';
*
* // prints "a" at t=0.0s
* // prints "b" at t=1.0s
* // prints "c" at t=3.0s
* ```
*
* This behavior is especially useful if you want to delay the program execution
* of a particular routine, such as when building a simple polling or retry
* mechanism:
*
* ```php
* try {
* something();
* } catch (Throwable) {
* // in case of error, retry after a short delay
* React\Async\delay(1.0);
* something();
* }
* ```
*
* Because this function only returns after some time has passed, it can be
* considered *blocking* from the perspective of the calling code. You can avoid
* this blocking behavior by wrapping it in an [`async()` function](#async) call.
* Everything inside this function will still be blocked, but everything outside
* this function can be executed asynchronously without blocking:
*
* ```php
* Loop::addTimer(0.5, React\Async\async(function (): void {
* echo 'a';
* React\Async\delay(1.0);
* echo 'c';
* }));
*
* Loop::addTimer(1.0, function (): void {
* echo 'b';
* });
*
* // prints "a" at t=0.5s
* // prints "b" at t=1.0s
* // prints "c" at t=1.5s
* ```
*
* See also the [`async()` function](#async) for more details.
*
* Internally, the `$seconds` argument will be used as a timer for the loop so that
* it keeps running until this timer triggers. This implies that if you pass a
* really small (or negative) value, it will still start a timer and will thus
* trigger at the earliest possible time in the future.
*
* The function is implemented in such a way that it can be cancelled when it is
* running inside an [`async()` function](#async). Cancelling the resulting
* promise will clean up any pending timers and throw a `RuntimeException` from
* the pending delay which in turn would reject the resulting promise.
*
* ```php
* $promise = async(function (): void {
* echo 'a';
* delay(3.0);
* echo 'b';
* })();
*
* Loop::addTimer(2.0, function () use ($promise): void {
* $promise->cancel();
* });
*
* // prints "a" at t=0.0s
* // rejects $promise at t=2.0
* // never prints "b"
* ```
*
* @return void
* @throws \RuntimeException when the function is cancelled inside an `async()` function
* @see async()
* @uses await()
*/
function delay(float $seconds): void
{
/** @var ?TimerInterface $timer */
$timer = null;
await(new Promise(function (callable $resolve) use ($seconds, &$timer): void {
$timer = Loop::addTimer($seconds, fn() => $resolve(null));
}, function () use (&$timer): void {
assert($timer instanceof TimerInterface);
Loop::cancelTimer($timer);
throw new \RuntimeException('Delay cancelled');
}));
}
/**
* Execute a Generator-based coroutine to "await" promises.
*
* ```php
* React\Async\coroutine(function () {
* $browser = new React\Http\Browser();
*
* try {
* $response = yield $browser->get('https://example.com/');
* assert($response instanceof Psr\Http\Message\ResponseInterface);
* echo $response->getBody();
* } catch (Exception $e) {
* echo 'Error: ' . $e->getMessage() . PHP_EOL;
* }
* });
* ```
*
* Using Generator-based coroutines is an alternative to directly using the
* underlying promise APIs. For many use cases, this makes using promise-based
* APIs much simpler, as it resembles a synchronous code flow more closely.
* The above example performs the equivalent of directly using the promise APIs:
*
* ```php
* $browser = new React\Http\Browser();
*
* $browser->get('https://example.com/')->then(function (Psr\Http\Message\ResponseInterface $response) {
* echo $response->getBody();
* }, function (Exception $e) {
* echo 'Error: ' . $e->getMessage() . PHP_EOL;
* });
* ```
*
* The `yield` keyword can be used to "await" a promise resolution. Internally,
* it will turn the entire given `$function` into a [`Generator`](https://www.php.net/manual/en/class.generator.php).
* This allows the execution to be interrupted and resumed at the same place
* when the promise is fulfilled. The `yield` statement returns whatever the
* promise is fulfilled with. If the promise is rejected, it will throw an
* `Exception` or `Throwable`.
*
* The `coroutine()` function will always return a Promise which will be
* fulfilled with whatever your `$function` returns. Likewise, it will return
* a promise that will be rejected if you throw an `Exception` or `Throwable`
* from your `$function`. This allows you to easily create Promise-based
* functions:
*
* ```php
* $promise = React\Async\coroutine(function () {
* $browser = new React\Http\Browser();
* $urls = [
* 'https://example.com/alice',
* 'https://example.com/bob'
* ];
*
* $bytes = 0;
* foreach ($urls as $url) {
* $response = yield $browser->get($url);
* assert($response instanceof Psr\Http\Message\ResponseInterface);
* $bytes += $response->getBody()->getSize();
* }
* return $bytes;
* });
*
* $promise->then(function (int $bytes) {
* echo 'Total size: ' . $bytes . PHP_EOL;
* }, function (Exception $e) {
* echo 'Error: ' . $e->getMessage() . PHP_EOL;
* });
* ```
*
* The previous example uses a `yield` statement inside a loop to highlight how
* this vastly simplifies consuming asynchronous operations. At the same time,
* this naive example does not leverage concurrent execution, as it will
* essentially "await" between each operation. In order to take advantage of
* concurrent execution within the given `$function`, you can "await" multiple
* promises by using a single `yield` together with Promise-based primitives
* like this:
*
* ```php
* $promise = React\Async\coroutine(function () {
* $browser = new React\Http\Browser();
* $urls = [
* 'https://example.com/alice',
* 'https://example.com/bob'
* ];
*
* $promises = [];
* foreach ($urls as $url) {
* $promises[] = $browser->get($url);
* }
*
* try {
* $responses = yield React\Promise\all($promises);
* } catch (Exception $e) {
* foreach ($promises as $promise) {
* $promise->cancel();
* }
* throw $e;
* }
*
* $bytes = 0;
* foreach ($responses as $response) {
* assert($response instanceof Psr\Http\Message\ResponseInterface);
* $bytes += $response->getBody()->getSize();
* }
* return $bytes;
* });
*
* $promise->then(function (int $bytes) {
* echo 'Total size: ' . $bytes . PHP_EOL;
* }, function (Exception $e) {
* echo 'Error: ' . $e->getMessage() . PHP_EOL;
* });
* ```
*
* @template T
* @template TYield
* @template A1 (any number of function arguments, see https://github.com/phpstan/phpstan/issues/8214)
* @template A2
* @template A3
* @template A4
* @template A5
* @param callable(A1, A2, A3, A4, A5):(\Generator<mixed, PromiseInterface<TYield>, TYield, PromiseInterface<T>|T>|PromiseInterface<T>|T) $function
* @param mixed ...$args Optional list of additional arguments that will be passed to the given `$function` as is
* @return PromiseInterface<T>
* @since 3.0.0
*/
function coroutine(callable $function, mixed ...$args): PromiseInterface
{
try {
$generator = $function(...$args);
} catch (\Throwable $e) {
return reject($e);
}
if (!$generator instanceof \Generator) {
return resolve($generator);
}
$promise = null;
/** @var Deferred<T> $deferred*/
$deferred = new Deferred(function () use (&$promise) {
/** @var ?PromiseInterface<T> $promise */
if ($promise instanceof PromiseInterface && \method_exists($promise, 'cancel')) {
$promise->cancel();
}
$promise = null;
});
/** @var callable $next */
$next = function () use ($deferred, $generator, &$next, &$promise) {
try {
if (!$generator->valid()) {
$next = null;
$deferred->resolve($generator->getReturn());
return;
}
} catch (\Throwable $e) {
$next = null;
$deferred->reject($e);
return;
}
$promise = $generator->current();
if (!$promise instanceof PromiseInterface) {
$next = null;
$deferred->reject(new \UnexpectedValueException(
'Expected coroutine to yield ' . PromiseInterface::class . ', but got ' . (is_object($promise) ? get_class($promise) : gettype($promise))
));
return;
}
/** @var PromiseInterface<TYield> $promise */
assert($next instanceof \Closure);
$promise->then(function ($value) use ($generator, $next) {
$generator->send($value);
$next();
}, function (\Throwable $reason) use ($generator, $next) {
$generator->throw($reason);
$next();
})->then(null, function (\Throwable $reason) use ($deferred, &$next) {
$next = null;
$deferred->reject($reason);
});
};
$next();
return $deferred->promise();
}
/**
* @template T
* @param iterable<callable():(PromiseInterface<T>|T)> $tasks
* @return PromiseInterface<array<T>>
*/
function parallel(iterable $tasks): PromiseInterface
{
/** @var array<int,PromiseInterface<T>> $pending */
$pending = [];
/** @var Deferred<array<T>> $deferred */
$deferred = new Deferred(function () use (&$pending) {
foreach ($pending as $promise) {
if ($promise instanceof PromiseInterface && \method_exists($promise, 'cancel')) {
$promise->cancel();
}
}
$pending = [];
});
$results = [];
$continue = true;
$taskErrback = function ($error) use (&$pending, $deferred, &$continue) {
$continue = false;
$deferred->reject($error);
foreach ($pending as $promise) {
if ($promise instanceof PromiseInterface && \method_exists($promise, 'cancel')) {
$promise->cancel();
}
}
$pending = [];
};
foreach ($tasks as $i => $task) {
$taskCallback = function ($result) use (&$results, &$pending, &$continue, $i, $deferred) {
$results[$i] = $result;
unset($pending[$i]);
if (!$pending && !$continue) {
$deferred->resolve($results);
}
};
$promise = \call_user_func($task);
assert($promise instanceof PromiseInterface);
$pending[$i] = $promise;
$promise->then($taskCallback, $taskErrback);
if (!$continue) {
break;
}
}
$continue = false;
if (!$pending) {
$deferred->resolve($results);
}
/** @var PromiseInterface<array<T>> Remove once defining `Deferred()` above is supported by PHPStan, see https://github.com/phpstan/phpstan/issues/11032 */
return $deferred->promise();
}
/**
* @template T
* @param iterable<callable():(PromiseInterface<T>|T)> $tasks
* @return PromiseInterface<array<T>>
*/
function series(iterable $tasks): PromiseInterface
{
$pending = null;
/** @var Deferred<array<T>> $deferred */
$deferred = new Deferred(function () use (&$pending) {
/** @var ?PromiseInterface<T> $pending */
if ($pending instanceof PromiseInterface && \method_exists($pending, 'cancel')) {
$pending->cancel();
}
$pending = null;
});
$results = [];
if ($tasks instanceof \IteratorAggregate) {
$tasks = $tasks->getIterator();
assert($tasks instanceof \Iterator);
}
$taskCallback = function ($result) use (&$results, &$next) {
$results[] = $result;
/** @var \Closure $next */
$next();
};
$next = function () use (&$tasks, $taskCallback, $deferred, &$results, &$pending) {
if ($tasks instanceof \Iterator ? !$tasks->valid() : !$tasks) {
$deferred->resolve($results);
return;
}
if ($tasks instanceof \Iterator) {
$task = $tasks->current();
$tasks->next();
} else {
assert(\is_array($tasks));
$task = \array_shift($tasks);
}
assert(\is_callable($task));
$promise = \call_user_func($task);
assert($promise instanceof PromiseInterface);
$pending = $promise;
$promise->then($taskCallback, array($deferred, 'reject'));
};
$next();
/** @var PromiseInterface<array<T>> Remove once defining `Deferred()` above is supported by PHPStan, see https://github.com/phpstan/phpstan/issues/11032 */
return $deferred->promise();
}
/**
* @template T
* @param iterable<(callable():(PromiseInterface<T>|T))|(callable(mixed):(PromiseInterface<T>|T))> $tasks
* @return PromiseInterface<($tasks is non-empty-array|\Traversable ? T : null)>
*/
function waterfall(iterable $tasks): PromiseInterface
{
$pending = null;
/** @var Deferred<T> $deferred*/
$deferred = new Deferred(function () use (&$pending) {
/** @var ?PromiseInterface<T> $pending */
if ($pending instanceof PromiseInterface && \method_exists($pending, 'cancel')) {
$pending->cancel();
}
$pending = null;
});
if ($tasks instanceof \IteratorAggregate) {
$tasks = $tasks->getIterator();
assert($tasks instanceof \Iterator);
}
/** @var callable $next */
$next = function ($value = null) use (&$tasks, &$next, $deferred, &$pending) {
if ($tasks instanceof \Iterator ? !$tasks->valid() : !$tasks) {
$deferred->resolve($value);
return;
}
if ($tasks instanceof \Iterator) {
$task = $tasks->current();
$tasks->next();
} else {
assert(\is_array($tasks));
$task = \array_shift($tasks);
}
assert(\is_callable($task));
$promise = \call_user_func_array($task, func_get_args());
assert($promise instanceof PromiseInterface);
$pending = $promise;
$promise->then($next, array($deferred, 'reject'));
};
$next();
return $deferred->promise();
}

View File

@@ -0,0 +1,9 @@
<?php
namespace React\Async;
// @codeCoverageIgnoreStart
if (!\function_exists(__NAMESPACE__ . '\\parallel')) {
require __DIR__ . '/functions.php';
}
// @codeCoverageIgnoreEnd

96
vendor/react/cache/CHANGELOG.md vendored Normal file
View File

@@ -0,0 +1,96 @@
# Changelog
## 1.2.0 (2022-11-30)
* Feature: Support PHP 8.1 and PHP 8.2.
(#47 by @SimonFrings and #52 by @WyriHaximus)
* Minor documentation improvements.
(#48 by @SimonFrings and #51 by @nhedger)
* Update test suite and use GitHub actions for continuous integration (CI).
(#45 and #49 by @SimonFrings and #54 by @clue)
## 1.1.0 (2020-09-18)
* Feature: Forward compatibility with react/promise 3.
(#39 by @WyriHaximus)
* Add `.gitattributes` to exclude dev files from exports.
(#40 by @reedy)
* Improve test suite, update to support PHP 8 and PHPUnit 9.3.
(#41 and #43 by @SimonFrings and #42 by @WyriHaximus)
## 1.0.0 (2019-07-11)
* First stable LTS release, now following [SemVer](https://semver.org/).
We'd like to emphasize that this component is production ready and battle-tested.
We plan to support all long-term support (LTS) releases for at least 24 months,
so you have a rock-solid foundation to build on top of.
> Contains no other changes, so it's actually fully compatible with the v0.6.0 release.
## 0.6.0 (2019-07-04)
* Feature / BC break: Add support for `getMultiple()`, `setMultiple()`, `deleteMultiple()`, `clear()` and `has()`
supporting multiple cache items (inspired by PSR-16).
(#32 by @krlv and #37 by @clue)
* Documentation for TTL precision with millisecond accuracy or below and
use high-resolution timer for cache TTL on PHP 7.3+.
(#35 and #38 by @clue)
* Improve API documentation and allow legacy HHVM to fail in Travis CI config.
(#34 and #36 by @clue)
* Prefix all global functions calls with \ to skip the look up and resolve process and go straight to the global function.
(#31 by @WyriHaximus)
## 0.5.0 (2018-06-25)
* Improve documentation by describing what is expected of a class implementing `CacheInterface`.
(#21, #22, #23, #27 by @WyriHaximus)
* Implemented (optional) Least Recently Used (LRU) cache algorithm for `ArrayCache`.
(#26 by @clue)
* Added support for cache expiration (TTL).
(#29 by @clue and @WyriHaximus)
* Renamed `remove` to `delete` making it more in line with `PSR-16`.
(#30 by @clue)
## 0.4.2 (2017-12-20)
* Improve documentation with usage and installation instructions
(#10 by @clue)
* Improve test suite by adding PHPUnit to `require-dev` and
add forward compatibility with PHPUnit 5 and PHPUnit 6 and
sanitize Composer autoload paths
(#14 by @shaunbramley and #12 and #18 by @clue)
## 0.4.1 (2016-02-25)
* Repository maintenance, split off from main repo, improve test suite and documentation
* First class support for PHP7 and HHVM (#9 by @clue)
* Adjust compatibility to 5.3 (#7 by @clue)
## 0.4.0 (2014-02-02)
* BC break: Bump minimum PHP version to PHP 5.4, remove 5.3 specific hacks
* BC break: Update to React/Promise 2.0
* Dependency: Autoloading and filesystem structure now PSR-4 instead of PSR-0
## 0.3.2 (2013-05-10)
* Version bump
## 0.3.0 (2013-04-14)
* Version bump
## 0.2.6 (2012-12-26)
* Feature: New cache component, used by DNS

21
vendor/react/cache/LICENSE vendored Normal file
View File

@@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2012 Christian Lück, Cees-Jan Kiewiet, Jan Sorgalla, Chris Boden, Igor Wiedler
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is furnished
to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

367
vendor/react/cache/README.md vendored Normal file
View File

@@ -0,0 +1,367 @@
# Cache
[![CI status](https://github.com/reactphp/cache/actions/workflows/ci.yml/badge.svg)](https://github.com/reactphp/cache/actions)
[![installs on Packagist](https://img.shields.io/packagist/dt/react/cache?color=blue&label=installs%20on%20Packagist)](https://packagist.org/packages/react/cache)
Async, [Promise](https://github.com/reactphp/promise)-based cache interface
for [ReactPHP](https://reactphp.org/).
The cache component provides a
[Promise](https://github.com/reactphp/promise)-based
[`CacheInterface`](#cacheinterface) and an in-memory [`ArrayCache`](#arraycache)
implementation of that.
This allows consumers to type hint against the interface and third parties to
provide alternate implementations.
This project is heavily inspired by
[PSR-16: Common Interface for Caching Libraries](https://www.php-fig.org/psr/psr-16/),
but uses an interface more suited for async, non-blocking applications.
**Table of Contents**
* [Usage](#usage)
* [CacheInterface](#cacheinterface)
* [get()](#get)
* [set()](#set)
* [delete()](#delete)
* [getMultiple()](#getmultiple)
* [setMultiple()](#setmultiple)
* [deleteMultiple()](#deletemultiple)
* [clear()](#clear)
* [has()](#has)
* [ArrayCache](#arraycache)
* [Common usage](#common-usage)
* [Fallback get](#fallback-get)
* [Fallback-get-and-set](#fallback-get-and-set)
* [Install](#install)
* [Tests](#tests)
* [License](#license)
## Usage
### CacheInterface
The `CacheInterface` describes the main interface of this component.
This allows consumers to type hint against the interface and third parties to
provide alternate implementations.
#### get()
The `get(string $key, mixed $default = null): PromiseInterface<mixed>` method can be used to
retrieve an item from the cache.
This method will resolve with the cached value on success or with the
given `$default` value when no item can be found or when an error occurs.
Similarly, an expired cache item (once the time-to-live is expired) is
considered a cache miss.
```php
$cache
->get('foo')
->then('var_dump');
```
This example fetches the value of the key `foo` and passes it to the
`var_dump` function. You can use any of the composition provided by
[promises](https://github.com/reactphp/promise).
#### set()
The `set(string $key, mixed $value, ?float $ttl = null): PromiseInterface<bool>` method can be used to
store an item in the cache.
This method will resolve with `true` on success or `false` when an error
occurs. If the cache implementation has to go over the network to store
it, it may take a while.
The optional `$ttl` parameter sets the maximum time-to-live in seconds
for this cache item. If this parameter is omitted (or `null`), the item
will stay in the cache for as long as the underlying implementation
supports. Trying to access an expired cache item results in a cache miss,
see also [`get()`](#get).
```php
$cache->set('foo', 'bar', 60);
```
This example eventually sets the value of the key `foo` to `bar`. If it
already exists, it is overridden.
This interface does not enforce any particular TTL resolution, so special
care may have to be taken if you rely on very high precision with
millisecond accuracy or below. Cache implementations SHOULD work on a
best effort basis and SHOULD provide at least second accuracy unless
otherwise noted. Many existing cache implementations are known to provide
microsecond or millisecond accuracy, but it's generally not recommended
to rely on this high precision.
This interface suggests that cache implementations SHOULD use a monotonic
time source if available. Given that a monotonic time source is only
available as of PHP 7.3 by default, cache implementations MAY fall back
to using wall-clock time.
While this does not affect many common use cases, this is an important
distinction for programs that rely on a high time precision or on systems
that are subject to discontinuous time adjustments (time jumps).
This means that if you store a cache item with a TTL of 30s and then
adjust your system time forward by 20s, the cache item SHOULD still
expire in 30s.
#### delete()
The `delete(string $key): PromiseInterface<bool>` method can be used to
delete an item from the cache.
This method will resolve with `true` on success or `false` when an error
occurs. When no item for `$key` is found in the cache, it also resolves
to `true`. If the cache implementation has to go over the network to
delete it, it may take a while.
```php
$cache->delete('foo');
```
This example eventually deletes the key `foo` from the cache. As with
`set()`, this may not happen instantly and a promise is returned to
provide guarantees whether or not the item has been removed from cache.
#### getMultiple()
The `getMultiple(string[] $keys, mixed $default = null): PromiseInterface<array>` method can be used to
retrieve multiple cache items by their unique keys.
This method will resolve with an array of cached values on success or with the
given `$default` value when an item can not be found or when an error occurs.
Similarly, an expired cache item (once the time-to-live is expired) is
considered a cache miss.
```php
$cache->getMultiple(array('name', 'age'))->then(function (array $values) {
$name = $values['name'] ?? 'User';
$age = $values['age'] ?? 'n/a';
echo $name . ' is ' . $age . PHP_EOL;
});
```
This example fetches the cache items for the `name` and `age` keys and
prints some example output. You can use any of the composition provided
by [promises](https://github.com/reactphp/promise).
#### setMultiple()
The `setMultiple(array $values, ?float $ttl = null): PromiseInterface<bool>` method can be used to
persist a set of key => value pairs in the cache, with an optional TTL.
This method will resolve with `true` on success or `false` when an error
occurs. If the cache implementation has to go over the network to store
it, it may take a while.
The optional `$ttl` parameter sets the maximum time-to-live in seconds
for these cache items. If this parameter is omitted (or `null`), these items
will stay in the cache for as long as the underlying implementation
supports. Trying to access an expired cache items results in a cache miss,
see also [`getMultiple()`](#getmultiple).
```php
$cache->setMultiple(array('foo' => 1, 'bar' => 2), 60);
```
This example eventually sets the list of values - the key `foo` to `1` value
and the key `bar` to `2`. If some of the keys already exist, they are overridden.
#### deleteMultiple()
The `setMultiple(string[] $keys): PromiseInterface<bool>` method can be used to
delete multiple cache items in a single operation.
This method will resolve with `true` on success or `false` when an error
occurs. When no items for `$keys` are found in the cache, it also resolves
to `true`. If the cache implementation has to go over the network to
delete it, it may take a while.
```php
$cache->deleteMultiple(array('foo', 'bar, 'baz'));
```
This example eventually deletes keys `foo`, `bar` and `baz` from the cache.
As with `setMultiple()`, this may not happen instantly and a promise is returned to
provide guarantees whether or not the item has been removed from cache.
#### clear()
The `clear(): PromiseInterface<bool>` method can be used to
wipe clean the entire cache.
This method will resolve with `true` on success or `false` when an error
occurs. If the cache implementation has to go over the network to
delete it, it may take a while.
```php
$cache->clear();
```
This example eventually deletes all keys from the cache. As with `deleteMultiple()`,
this may not happen instantly and a promise is returned to provide guarantees
whether or not all the items have been removed from cache.
#### has()
The `has(string $key): PromiseInterface<bool>` method can be used to
determine whether an item is present in the cache.
This method will resolve with `true` on success or `false` when no item can be found
or when an error occurs. Similarly, an expired cache item (once the time-to-live
is expired) is considered a cache miss.
```php
$cache
->has('foo')
->then('var_dump');
```
This example checks if the value of the key `foo` is set in the cache and passes
the result to the `var_dump` function. You can use any of the composition provided by
[promises](https://github.com/reactphp/promise).
NOTE: It is recommended that has() is only to be used for cache warming type purposes
and not to be used within your live applications operations for get/set, as this method
is subject to a race condition where your has() will return true and immediately after,
another script can remove it making the state of your app out of date.
### ArrayCache
The `ArrayCache` provides an in-memory implementation of the [`CacheInterface`](#cacheinterface).
```php
$cache = new ArrayCache();
$cache->set('foo', 'bar');
```
Its constructor accepts an optional `?int $limit` parameter to limit the
maximum number of entries to store in the LRU cache. If you add more
entries to this instance, it will automatically take care of removing
the one that was least recently used (LRU).
For example, this snippet will overwrite the first value and only store
the last two entries:
```php
$cache = new ArrayCache(2);
$cache->set('foo', '1');
$cache->set('bar', '2');
$cache->set('baz', '3');
```
This cache implementation is known to rely on wall-clock time to schedule
future cache expiration times when using any version before PHP 7.3,
because a monotonic time source is only available as of PHP 7.3 (`hrtime()`).
While this does not affect many common use cases, this is an important
distinction for programs that rely on a high time precision or on systems
that are subject to discontinuous time adjustments (time jumps).
This means that if you store a cache item with a TTL of 30s on PHP < 7.3
and then adjust your system time forward by 20s, the cache item may
expire in 10s. See also [`set()`](#set) for more details.
## Common usage
### Fallback get
A common use case of caches is to attempt fetching a cached value and as a
fallback retrieve it from the original data source if not found. Here is an
example of that:
```php
$cache
->get('foo')
->then(function ($result) {
if ($result === null) {
return getFooFromDb();
}
return $result;
})
->then('var_dump');
```
First an attempt is made to retrieve the value of `foo`. A callback function is
registered that will call `getFooFromDb` when the resulting value is null.
`getFooFromDb` is a function (can be any PHP callable) that will be called if the
key does not exist in the cache.
`getFooFromDb` can handle the missing key by returning a promise for the
actual value from the database (or any other data source). As a result, this
chain will correctly fall back, and provide the value in both cases.
### Fallback get and set
To expand on the fallback get example, often you want to set the value on the
cache after fetching it from the data source.
```php
$cache
->get('foo')
->then(function ($result) {
if ($result === null) {
return $this->getAndCacheFooFromDb();
}
return $result;
})
->then('var_dump');
public function getAndCacheFooFromDb()
{
return $this->db
->get('foo')
->then(array($this, 'cacheFooFromDb'));
}
public function cacheFooFromDb($foo)
{
$this->cache->set('foo', $foo);
return $foo;
}
```
By using chaining you can easily conditionally cache the value if it is
fetched from the database.
## Install
The recommended way to install this library is [through Composer](https://getcomposer.org).
[New to Composer?](https://getcomposer.org/doc/00-intro.md)
This project follows [SemVer](https://semver.org/).
This will install the latest supported version:
```bash
composer require react/cache:^1.2
```
See also the [CHANGELOG](CHANGELOG.md) for details about version upgrades.
This project aims to run on any platform and thus does not require any PHP
extensions and supports running on legacy PHP 5.3 through current PHP 8+ and
HHVM.
It's *highly recommended to use PHP 7+* for this project.
## Tests
To run the test suite, you first need to clone this repo and then install all
dependencies [through Composer](https://getcomposer.org):
```bash
composer install
```
To run the test suite, go to the project root and run:
```bash
vendor/bin/phpunit
```
## License
MIT, see [LICENSE file](LICENSE).

45
vendor/react/cache/composer.json vendored Normal file
View File

@@ -0,0 +1,45 @@
{
"name": "react/cache",
"description": "Async, Promise-based cache interface for ReactPHP",
"keywords": ["cache", "caching", "promise", "ReactPHP"],
"license": "MIT",
"authors": [
{
"name": "Christian Lück",
"homepage": "https://clue.engineering/",
"email": "christian@clue.engineering"
},
{
"name": "Cees-Jan Kiewiet",
"homepage": "https://wyrihaximus.net/",
"email": "reactphp@ceesjankiewiet.nl"
},
{
"name": "Jan Sorgalla",
"homepage": "https://sorgalla.com/",
"email": "jsorgalla@gmail.com"
},
{
"name": "Chris Boden",
"homepage": "https://cboden.dev/",
"email": "cboden@gmail.com"
}
],
"require": {
"php": ">=5.3.0",
"react/promise": "^3.0 || ^2.0 || ^1.1"
},
"require-dev": {
"phpunit/phpunit": "^9.5 || ^5.7 || ^4.8.35"
},
"autoload": {
"psr-4": {
"React\\Cache\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"React\\Tests\\Cache\\": "tests/"
}
}
}

181
vendor/react/cache/src/ArrayCache.php vendored Normal file
View File

@@ -0,0 +1,181 @@
<?php
namespace React\Cache;
use React\Promise;
use React\Promise\PromiseInterface;
class ArrayCache implements CacheInterface
{
private $limit;
private $data = array();
private $expires = array();
private $supportsHighResolution;
/**
* The `ArrayCache` provides an in-memory implementation of the [`CacheInterface`](#cacheinterface).
*
* ```php
* $cache = new ArrayCache();
*
* $cache->set('foo', 'bar');
* ```
*
* Its constructor accepts an optional `?int $limit` parameter to limit the
* maximum number of entries to store in the LRU cache. If you add more
* entries to this instance, it will automatically take care of removing
* the one that was least recently used (LRU).
*
* For example, this snippet will overwrite the first value and only store
* the last two entries:
*
* ```php
* $cache = new ArrayCache(2);
*
* $cache->set('foo', '1');
* $cache->set('bar', '2');
* $cache->set('baz', '3');
* ```
*
* This cache implementation is known to rely on wall-clock time to schedule
* future cache expiration times when using any version before PHP 7.3,
* because a monotonic time source is only available as of PHP 7.3 (`hrtime()`).
* While this does not affect many common use cases, this is an important
* distinction for programs that rely on a high time precision or on systems
* that are subject to discontinuous time adjustments (time jumps).
* This means that if you store a cache item with a TTL of 30s on PHP < 7.3
* and then adjust your system time forward by 20s, the cache item may
* expire in 10s. See also [`set()`](#set) for more details.
*
* @param int|null $limit maximum number of entries to store in the LRU cache
*/
public function __construct($limit = null)
{
$this->limit = $limit;
// prefer high-resolution timer, available as of PHP 7.3+
$this->supportsHighResolution = \function_exists('hrtime');
}
public function get($key, $default = null)
{
// delete key if it is already expired => below will detect this as a cache miss
if (isset($this->expires[$key]) && $this->now() - $this->expires[$key] > 0) {
unset($this->data[$key], $this->expires[$key]);
}
if (!\array_key_exists($key, $this->data)) {
return Promise\resolve($default);
}
// remove and append to end of array to keep track of LRU info
$value = $this->data[$key];
unset($this->data[$key]);
$this->data[$key] = $value;
return Promise\resolve($value);
}
public function set($key, $value, $ttl = null)
{
// unset before setting to ensure this entry will be added to end of array (LRU info)
unset($this->data[$key]);
$this->data[$key] = $value;
// sort expiration times if TTL is given (first will expire first)
unset($this->expires[$key]);
if ($ttl !== null) {
$this->expires[$key] = $this->now() + $ttl;
\asort($this->expires);
}
// ensure size limit is not exceeded or remove first entry from array
if ($this->limit !== null && \count($this->data) > $this->limit) {
// first try to check if there's any expired entry
// expiration times are sorted, so we can simply look at the first one
\reset($this->expires);
$key = \key($this->expires);
// check to see if the first in the list of expiring keys is already expired
// if the first key is not expired, we have to overwrite by using LRU info
if ($key === null || $this->now() - $this->expires[$key] < 0) {
\reset($this->data);
$key = \key($this->data);
}
unset($this->data[$key], $this->expires[$key]);
}
return Promise\resolve(true);
}
public function delete($key)
{
unset($this->data[$key], $this->expires[$key]);
return Promise\resolve(true);
}
public function getMultiple(array $keys, $default = null)
{
$values = array();
foreach ($keys as $key) {
$values[$key] = $this->get($key, $default);
}
return Promise\all($values);
}
public function setMultiple(array $values, $ttl = null)
{
foreach ($values as $key => $value) {
$this->set($key, $value, $ttl);
}
return Promise\resolve(true);
}
public function deleteMultiple(array $keys)
{
foreach ($keys as $key) {
unset($this->data[$key], $this->expires[$key]);
}
return Promise\resolve(true);
}
public function clear()
{
$this->data = array();
$this->expires = array();
return Promise\resolve(true);
}
public function has($key)
{
// delete key if it is already expired
if (isset($this->expires[$key]) && $this->now() - $this->expires[$key] > 0) {
unset($this->data[$key], $this->expires[$key]);
}
if (!\array_key_exists($key, $this->data)) {
return Promise\resolve(false);
}
// remove and append to end of array to keep track of LRU info
$value = $this->data[$key];
unset($this->data[$key]);
$this->data[$key] = $value;
return Promise\resolve(true);
}
/**
* @return float
*/
private function now()
{
return $this->supportsHighResolution ? \hrtime(true) * 1e-9 : \microtime(true);
}
}

View File

@@ -0,0 +1,194 @@
<?php
namespace React\Cache;
use React\Promise\PromiseInterface;
interface CacheInterface
{
/**
* Retrieves an item from the cache.
*
* This method will resolve with the cached value on success or with the
* given `$default` value when no item can be found or when an error occurs.
* Similarly, an expired cache item (once the time-to-live is expired) is
* considered a cache miss.
*
* ```php
* $cache
* ->get('foo')
* ->then('var_dump');
* ```
*
* This example fetches the value of the key `foo` and passes it to the
* `var_dump` function. You can use any of the composition provided by
* [promises](https://github.com/reactphp/promise).
*
* @param string $key
* @param mixed $default Default value to return for cache miss or null if not given.
* @return PromiseInterface<mixed>
*/
public function get($key, $default = null);
/**
* Stores an item in the cache.
*
* This method will resolve with `true` on success or `false` when an error
* occurs. If the cache implementation has to go over the network to store
* it, it may take a while.
*
* The optional `$ttl` parameter sets the maximum time-to-live in seconds
* for this cache item. If this parameter is omitted (or `null`), the item
* will stay in the cache for as long as the underlying implementation
* supports. Trying to access an expired cache item results in a cache miss,
* see also [`get()`](#get).
*
* ```php
* $cache->set('foo', 'bar', 60);
* ```
*
* This example eventually sets the value of the key `foo` to `bar`. If it
* already exists, it is overridden.
*
* This interface does not enforce any particular TTL resolution, so special
* care may have to be taken if you rely on very high precision with
* millisecond accuracy or below. Cache implementations SHOULD work on a
* best effort basis and SHOULD provide at least second accuracy unless
* otherwise noted. Many existing cache implementations are known to provide
* microsecond or millisecond accuracy, but it's generally not recommended
* to rely on this high precision.
*
* This interface suggests that cache implementations SHOULD use a monotonic
* time source if available. Given that a monotonic time source is only
* available as of PHP 7.3 by default, cache implementations MAY fall back
* to using wall-clock time.
* While this does not affect many common use cases, this is an important
* distinction for programs that rely on a high time precision or on systems
* that are subject to discontinuous time adjustments (time jumps).
* This means that if you store a cache item with a TTL of 30s and then
* adjust your system time forward by 20s, the cache item SHOULD still
* expire in 30s.
*
* @param string $key
* @param mixed $value
* @param ?float $ttl
* @return PromiseInterface<bool> Returns a promise which resolves to `true` on success or `false` on error
*/
public function set($key, $value, $ttl = null);
/**
* Deletes an item from the cache.
*
* This method will resolve with `true` on success or `false` when an error
* occurs. When no item for `$key` is found in the cache, it also resolves
* to `true`. If the cache implementation has to go over the network to
* delete it, it may take a while.
*
* ```php
* $cache->delete('foo');
* ```
*
* This example eventually deletes the key `foo` from the cache. As with
* `set()`, this may not happen instantly and a promise is returned to
* provide guarantees whether or not the item has been removed from cache.
*
* @param string $key
* @return PromiseInterface<bool> Returns a promise which resolves to `true` on success or `false` on error
*/
public function delete($key);
/**
* Retrieves multiple cache items by their unique keys.
*
* This method will resolve with an array of cached values on success or with the
* given `$default` value when an item can not be found or when an error occurs.
* Similarly, an expired cache item (once the time-to-live is expired) is
* considered a cache miss.
*
* ```php
* $cache->getMultiple(array('name', 'age'))->then(function (array $values) {
* $name = $values['name'] ?? 'User';
* $age = $values['age'] ?? 'n/a';
*
* echo $name . ' is ' . $age . PHP_EOL;
* });
* ```
*
* This example fetches the cache items for the `name` and `age` keys and
* prints some example output. You can use any of the composition provided
* by [promises](https://github.com/reactphp/promise).
*
* @param string[] $keys A list of keys that can obtained in a single operation.
* @param mixed $default Default value to return for keys that do not exist.
* @return PromiseInterface<array> Returns a promise which resolves to an `array` of cached values
*/
public function getMultiple(array $keys, $default = null);
/**
* Persists a set of key => value pairs in the cache, with an optional TTL.
*
* This method will resolve with `true` on success or `false` when an error
* occurs. If the cache implementation has to go over the network to store
* it, it may take a while.
*
* The optional `$ttl` parameter sets the maximum time-to-live in seconds
* for these cache items. If this parameter is omitted (or `null`), these items
* will stay in the cache for as long as the underlying implementation
* supports. Trying to access an expired cache items results in a cache miss,
* see also [`get()`](#get).
*
* ```php
* $cache->setMultiple(array('foo' => 1, 'bar' => 2), 60);
* ```
*
* This example eventually sets the list of values - the key `foo` to 1 value
* and the key `bar` to 2. If some of the keys already exist, they are overridden.
*
* @param array $values A list of key => value pairs for a multiple-set operation.
* @param ?float $ttl Optional. The TTL value of this item.
* @return PromiseInterface<bool> Returns a promise which resolves to `true` on success or `false` on error
*/
public function setMultiple(array $values, $ttl = null);
/**
* Deletes multiple cache items in a single operation.
*
* @param string[] $keys A list of string-based keys to be deleted.
* @return PromiseInterface<bool> Returns a promise which resolves to `true` on success or `false` on error
*/
public function deleteMultiple(array $keys);
/**
* Wipes clean the entire cache.
*
* @return PromiseInterface<bool> Returns a promise which resolves to `true` on success or `false` on error
*/
public function clear();
/**
* Determines whether an item is present in the cache.
*
* This method will resolve with `true` on success or `false` when no item can be found
* or when an error occurs. Similarly, an expired cache item (once the time-to-live
* is expired) is considered a cache miss.
*
* ```php
* $cache
* ->has('foo')
* ->then('var_dump');
* ```
*
* This example checks if the value of the key `foo` is set in the cache and passes
* the result to the `var_dump` function. You can use any of the composition provided by
* [promises](https://github.com/reactphp/promise).
*
* NOTE: It is recommended that has() is only to be used for cache warming type purposes
* and not to be used within your live applications operations for get/set, as this method
* is subject to a race condition where your has() will return true and immediately after,
* another script can remove it making the state of your app out of date.
*
* @param string $key The cache item key.
* @return PromiseInterface<bool> Returns a promise which resolves to `true` on success or `false` on error
*/
public function has($key);
}

452
vendor/react/dns/CHANGELOG.md vendored Normal file
View File

@@ -0,0 +1,452 @@
# Changelog
## 1.13.0 (2024-06-13)
* Feature: Improve PHP 8.4+ support by avoiding implicitly nullable type declarations.
(#224 by @WyriHaximus)
## 1.12.0 (2023-11-29)
* Feature: Full PHP 8.3 compatibility.
(#217 by @sergiy-petrov)
* Update test environment and avoid unhandled promise rejections.
(#215, #216 and #218 by @clue)
## 1.11.0 (2023-06-02)
* Feature: Include timeout logic to avoid dependency on reactphp/promise-timer.
(#213 by @clue)
* Improve test suite and project setup and report failed assertions.
(#210 by @clue, #212 by @WyriHaximus and #209 and #211 by @SimonFrings)
## 1.10.0 (2022-09-08)
* Feature: Full support for PHP 8.2 release.
(#201 by @clue and #207 by @WyriHaximus)
* Feature: Optimize forward compatibility with Promise v3, avoid hitting autoloader.
(#202 by @clue)
* Feature / Fix: Improve error reporting when custom error handler is used.
(#197 by @clue)
* Fix: Fix invalid references in exception stack trace.
(#191 by @clue)
* Minor documentation improvements.
(#195 by @SimonFrings and #203 by @nhedger)
* Improve test suite, update to use default loop and new reactphp/async package.
(#204, #205 and #206 by @clue and #196 by @SimonFrings)
## 1.9.0 (2021-12-20)
* Feature: Full support for PHP 8.1 release and prepare PHP 8.2 compatibility
by refactoring `Parser` to avoid assigning dynamic properties.
(#188 and #186 by @clue and #184 by @SimonFrings)
* Feature: Avoid dependency on `ext-filter`.
(#185 by @clue)
* Feature / Fix: Skip invalid nameserver entries from `resolv.conf` and ignore IPv6 zone IDs.
(#187 by @clue)
* Feature / Fix: Reduce socket read chunk size for queries over TCP/IP.
(#189 by @clue)
## 1.8.0 (2021-07-11)
A major new feature release, see [**release announcement**](https://clue.engineering/2021/announcing-reactphp-default-loop).
* Feature: Simplify usage by supporting new [default loop](https://reactphp.org/event-loop/#loop).
(#182 by @clue)
```php
// old (still supported)
$factory = new React\Dns\Resolver\Factory();
$resolver = $factory->create($config, $loop);
// new (using default loop)
$factory = new React\Dns\Resolver\Factory();
$resolver = $factory->create($config);
```
## 1.7.0 (2021-06-25)
* Feature: Update DNS `Factory` to accept complete `Config` object.
Add new `FallbackExecutor` and use fallback DNS servers when `Config` lists multiple servers.
(#179 and #180 by @clue)
```php
// old (still supported)
$config = React\Dns\Config\Config::loadSystemConfigBlocking();
$server = $config->nameservers ? reset($config->nameservers) : '8.8.8.8';
$resolver = $factory->create($server, $loop);
// new
$config = React\Dns\Config\Config::loadSystemConfigBlocking();
if (!$config->nameservers) {
$config->nameservers[] = '8.8.8.8';
}
$resolver = $factory->create($config, $loop);
```
## 1.6.0 (2021-06-21)
* Feature: Add support for legacy `SPF` record type.
(#178 by @akondas and @clue)
* Fix: Fix integer overflow for TCP/IP chunk size on 32 bit platforms.
(#177 by @clue)
## 1.5.0 (2021-03-05)
* Feature: Improve error reporting when query fails, include domain and query type and DNS server address where applicable.
(#174 by @clue)
* Feature: Improve error handling when sending data to DNS server fails (macOS).
(#171 and #172 by @clue)
* Fix: Improve DNS response parser to limit recursion for compressed labels.
(#169 by @clue)
* Improve test suite, use GitHub actions for continuous integration (CI).
(#170 by @SimonFrings)
## 1.4.0 (2020-09-18)
* Feature: Support upcoming PHP 8.
(#168 by @clue)
* Improve test suite and update to PHPUnit 9.3.
(#164 by @clue, #165 and #166 by @SimonFrings and #167 by @WyriHaximus)
## 1.3.0 (2020-07-10)
* Feature: Forward compatibility with react/promise v3.
(#153 by @WyriHaximus)
* Feature: Support parsing `OPT` records (EDNS0).
(#157 by @clue)
* Fix: Avoid PHP warnings due to lack of args in exception trace on PHP 7.4.
(#160 by @clue)
* Improve test suite and add `.gitattributes` to exclude dev files from exports.
Run tests on PHPUnit 9 and PHP 7.4 and clean up test suite.
(#154 by @reedy, #156 by @clue and #163 by @SimonFrings)
## 1.2.0 (2019-08-15)
* Feature: Add `TcpTransportExecutor` to send DNS queries over TCP/IP connection,
add `SelectiveTransportExecutor` to retry with TCP if UDP is truncated and
automatically select transport protocol when no explicit `udp://` or `tcp://` scheme is given in `Factory`.
(#145, #146, #147 and #148 by @clue)
* Feature: Support escaping literal dots and special characters in domain names.
(#144 by @clue)
## 1.1.0 (2019-07-18)
* Feature: Support parsing `CAA` and `SSHFP` records.
(#141 and #142 by @clue)
* Feature: Add `ResolverInterface` as common interface for `Resolver` class.
(#139 by @clue)
* Fix: Add missing private property definitions and
remove unneeded dependency on `react/stream`.
(#140 and #143 by @clue)
## 1.0.0 (2019-07-11)
* First stable LTS release, now following [SemVer](https://semver.org/).
We'd like to emphasize that this component is production ready and battle-tested.
We plan to support all long-term support (LTS) releases for at least 24 months,
so you have a rock-solid foundation to build on top of.
This update involves a number of BC breaks due to dropped support for
deprecated functionality and some internal API cleanup. We've tried hard to
avoid BC breaks where possible and minimize impact otherwise. We expect that
most consumers of this package will actually not be affected by any BC
breaks, see below for more details:
* BC break: Delete all deprecated APIs, use `Query` objects for `Message` questions
instead of nested arrays and increase code coverage to 100%.
(#130 by @clue)
* BC break: Move `$nameserver` from `ExecutorInterface` to `UdpTransportExecutor`,
remove advanced/internal `UdpTransportExecutor` args for `Parser`/`BinaryDumper` and
add API documentation for `ExecutorInterface`.
(#135, #137 and #138 by @clue)
* BC break: Replace `HeaderBag` attributes with simple `Message` properties.
(#132 by @clue)
* BC break: Mark all `Record` attributes as required, add documentation vs `Query`.
(#136 by @clue)
* BC break: Mark all classes as final to discourage inheritance
(#134 by @WyriHaximus)
## 0.4.19 (2019-07-10)
* Feature: Avoid garbage references when DNS resolution rejects on legacy PHP <= 5.6.
(#133 by @clue)
## 0.4.18 (2019-09-07)
* Feature / Fix: Implement `CachingExecutor` using cache TTL, deprecate old `CachedExecutor`,
respect TTL from response records when caching and do not cache truncated responses.
(#129 by @clue)
* Feature: Limit cache size to 256 last responses by default.
(#127 by @clue)
* Feature: Cooperatively resolve hosts to avoid running same query concurrently.
(#125 by @clue)
## 0.4.17 (2019-04-01)
* Feature: Support parsing `authority` and `additional` records from DNS response.
(#123 by @clue)
* Feature: Support dumping records as part of outgoing binary DNS message.
(#124 by @clue)
* Feature: Forward compatibility with upcoming Cache v0.6 and Cache v1.0
(#121 by @clue)
* Improve test suite to add forward compatibility with PHPUnit 7,
test against PHP 7.3 and use legacy PHPUnit 5 on legacy HHVM.
(#122 by @clue)
## 0.4.16 (2018-11-11)
* Feature: Improve promise cancellation for DNS lookup retries and clean up any garbage references.
(#118 by @clue)
* Fix: Reject parsing malformed DNS response messages such as incomplete DNS response messages,
malformed record data or malformed compressed domain name labels.
(#115 and #117 by @clue)
* Fix: Fix interpretation of TTL as UINT32 with most significant bit unset.
(#116 by @clue)
* Fix: Fix caching advanced MX/SRV/TXT/SOA structures.
(#112 by @clue)
## 0.4.15 (2018-07-02)
* Feature: Add `resolveAll()` method to support custom query types in `Resolver`.
(#110 by @clue and @WyriHaximus)
```php
$resolver->resolveAll('reactphp.org', Message::TYPE_AAAA)->then(function ($ips) {
echo 'IPv6 addresses for reactphp.org ' . implode(', ', $ips) . PHP_EOL;
});
```
* Feature: Support parsing `NS`, `TXT`, `MX`, `SOA` and `SRV` records.
(#104, #105, #106, #107 and #108 by @clue)
* Feature: Add support for `Message::TYPE_ANY` and parse unknown types as binary data.
(#104 by @clue)
* Feature: Improve error messages for failed queries and improve documentation.
(#109 by @clue)
* Feature: Add reverse DNS lookup example.
(#111 by @clue)
## 0.4.14 (2018-06-26)
* Feature: Add `UdpTransportExecutor`, validate incoming DNS response messages
to avoid cache poisoning attacks and deprecate legacy `Executor`.
(#101 and #103 by @clue)
* Feature: Forward compatibility with Cache 0.5
(#102 by @clue)
* Deprecate legacy `Query::$currentTime` and binary parser data attributes to clean up and simplify API.
(#99 by @clue)
## 0.4.13 (2018-02-27)
* Add `Config::loadSystemConfigBlocking()` to load default system config
and support parsing DNS config on all supported platforms
(`/etc/resolv.conf` on Unix/Linux/Mac and WMIC on Windows)
(#92, #93, #94 and #95 by @clue)
```php
$config = Config::loadSystemConfigBlocking();
$server = $config->nameservers ? reset($config->nameservers) : '8.8.8.8';
```
* Remove unneeded cyclic dependency on react/socket
(#96 by @clue)
## 0.4.12 (2018-01-14)
* Improve test suite by adding forward compatibility with PHPUnit 6,
test against PHP 7.2, fix forward compatibility with upcoming EventLoop releases,
add test group to skip integration tests relying on internet connection
and add minor documentation improvements.
(#85 and #87 by @carusogabriel, #88 and #89 by @clue and #83 by @jsor)
## 0.4.11 (2017-08-25)
* Feature: Support resolving from default hosts file
(#75, #76 and #77 by @clue)
This means that resolving hosts such as `localhost` will now work as
expected across all platforms with no changes required:
```php
$resolver->resolve('localhost')->then(function ($ip) {
echo 'IP: ' . $ip;
});
```
The new `HostsExecutor` exists for advanced usage and is otherwise used
internally for this feature.
## 0.4.10 (2017-08-10)
* Feature: Forward compatibility with EventLoop v1.0 and v0.5 and
lock minimum dependencies and work around circular dependency for tests
(#70 and #71 by @clue)
* Fix: Work around DNS timeout issues for Windows users
(#74 by @clue)
* Documentation and examples for advanced usage
(#66 by @WyriHaximus)
* Remove broken TCP code, do not retry with invalid TCP query
(#73 by @clue)
* Improve test suite by fixing HHVM build for now again and ignore future HHVM build errors and
lock Travis distro so new defaults will not break the build and
fix failing tests for PHP 7.1
(#68 by @WyriHaximus and #69 and #72 by @clue)
## 0.4.9 (2017-05-01)
* Feature: Forward compatibility with upcoming Socket v1.0 and v0.8
(#61 by @clue)
## 0.4.8 (2017-04-16)
* Feature: Add support for the AAAA record type to the protocol parser
(#58 by @othillo)
* Feature: Add support for the PTR record type to the protocol parser
(#59 by @othillo)
## 0.4.7 (2017-03-31)
* Feature: Forward compatibility with upcoming Socket v0.6 and v0.7 component
(#57 by @clue)
## 0.4.6 (2017-03-11)
* Fix: Fix DNS timeout issues for Windows users and add forward compatibility
with Stream v0.5 and upcoming v0.6
(#53 by @clue)
* Improve test suite by adding PHPUnit to `require-dev`
(#54 by @clue)
## 0.4.5 (2017-03-02)
* Fix: Ensure we ignore the case of the answer
(#51 by @WyriHaximus)
* Feature: Add `TimeoutExecutor` and simplify internal APIs to allow internal
code re-use for upcoming versions.
(#48 and #49 by @clue)
## 0.4.4 (2017-02-13)
* Fix: Fix handling connection and stream errors
(#45 by @clue)
* Feature: Add examples and forward compatibility with upcoming Socket v0.5 component
(#46 and #47 by @clue)
## 0.4.3 (2016-07-31)
* Feature: Allow for cache adapter injection (#38 by @WyriHaximus)
```php
$factory = new React\Dns\Resolver\Factory();
$cache = new MyCustomCacheInstance();
$resolver = $factory->createCached('8.8.8.8', $loop, $cache);
```
* Feature: Support Promise cancellation (#35 by @clue)
```php
$promise = $resolver->resolve('reactphp.org');
$promise->cancel();
```
## 0.4.2 (2016-02-24)
* Repository maintenance, split off from main repo, improve test suite and documentation
* First class support for PHP7 and HHVM (#34 by @clue)
* Adjust compatibility to 5.3 (#30 by @clue)
## 0.4.1 (2014-04-13)
* Bug fix: Fixed PSR-4 autoload path (@marcj/WyriHaximus)
## 0.4.0 (2014-02-02)
* BC break: Bump minimum PHP version to PHP 5.4, remove 5.3 specific hacks
* BC break: Update to React/Promise 2.0
* Bug fix: Properly resolve CNAME aliases
* Dependency: Autoloading and filesystem structure now PSR-4 instead of PSR-0
* Bump React dependencies to v0.4
## 0.3.2 (2013-05-10)
* Feature: Support default port for IPv6 addresses (@clue)
## 0.3.0 (2013-04-14)
* Bump React dependencies to v0.3
## 0.2.6 (2012-12-26)
* Feature: New cache component, used by DNS
## 0.2.5 (2012-11-26)
* Version bump
## 0.2.4 (2012-11-18)
* Feature: Change to promise-based API (@jsor)
## 0.2.3 (2012-11-14)
* Version bump
## 0.2.2 (2012-10-28)
* Feature: DNS executor timeout handling (@arnaud-lb)
* Feature: DNS retry executor (@arnaud-lb)
## 0.2.1 (2012-10-14)
* Minor adjustments to DNS parser
## 0.2.0 (2012-09-10)
* Feature: DNS resolver

21
vendor/react/dns/LICENSE vendored Normal file
View File

@@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2012 Christian Lück, Cees-Jan Kiewiet, Jan Sorgalla, Chris Boden, Igor Wiedler
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is furnished
to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

453
vendor/react/dns/README.md vendored Normal file
View File

@@ -0,0 +1,453 @@
# DNS
[![CI status](https://github.com/reactphp/dns/actions/workflows/ci.yml/badge.svg)](https://github.com/reactphp/dns/actions)
[![installs on Packagist](https://img.shields.io/packagist/dt/react/dns?color=blue&label=installs%20on%20Packagist)](https://packagist.org/packages/react/dns)
Async DNS resolver for [ReactPHP](https://reactphp.org/).
The main point of the DNS component is to provide async DNS resolution.
However, it is really a toolkit for working with DNS messages, and could
easily be used to create a DNS server.
**Table of contents**
* [Basic usage](#basic-usage)
* [Caching](#caching)
* [Custom cache adapter](#custom-cache-adapter)
* [ResolverInterface](#resolverinterface)
* [resolve()](#resolve)
* [resolveAll()](#resolveall)
* [Advanced usage](#advanced-usage)
* [UdpTransportExecutor](#udptransportexecutor)
* [TcpTransportExecutor](#tcptransportexecutor)
* [SelectiveTransportExecutor](#selectivetransportexecutor)
* [HostsFileExecutor](#hostsfileexecutor)
* [Install](#install)
* [Tests](#tests)
* [License](#license)
* [References](#references)
## Basic usage
The most basic usage is to just create a resolver through the resolver
factory. All you need to give it is a nameserver, then you can start resolving
names, baby!
```php
$config = React\Dns\Config\Config::loadSystemConfigBlocking();
if (!$config->nameservers) {
$config->nameservers[] = '8.8.8.8';
}
$factory = new React\Dns\Resolver\Factory();
$dns = $factory->create($config);
$dns->resolve('igor.io')->then(function ($ip) {
echo "Host: $ip\n";
});
```
See also the [first example](examples).
The `Config` class can be used to load the system default config. This is an
operation that may access the filesystem and block. Ideally, this method should
thus be executed only once before the loop starts and not repeatedly while it is
running.
Note that this class may return an *empty* configuration if the system config
can not be loaded. As such, you'll likely want to apply a default nameserver
as above if none can be found.
> Note that the factory loads the hosts file from the filesystem once when
creating the resolver instance.
Ideally, this method should thus be executed only once before the loop starts
and not repeatedly while it is running.
But there's more.
## Caching
You can cache results by configuring the resolver to use a `CachedExecutor`:
```php
$config = React\Dns\Config\Config::loadSystemConfigBlocking();
if (!$config->nameservers) {
$config->nameservers[] = '8.8.8.8';
}
$factory = new React\Dns\Resolver\Factory();
$dns = $factory->createCached($config);
$dns->resolve('igor.io')->then(function ($ip) {
echo "Host: $ip\n";
});
...
$dns->resolve('igor.io')->then(function ($ip) {
echo "Host: $ip\n";
});
```
If the first call returns before the second, only one query will be executed.
The second result will be served from an in memory cache.
This is particularly useful for long running scripts where the same hostnames
have to be looked up multiple times.
See also the [third example](examples).
### Custom cache adapter
By default, the above will use an in memory cache.
You can also specify a custom cache implementing [`CacheInterface`](https://github.com/reactphp/cache) to handle the record cache instead:
```php
$cache = new React\Cache\ArrayCache();
$factory = new React\Dns\Resolver\Factory();
$dns = $factory->createCached('8.8.8.8', null, $cache);
```
See also the wiki for possible [cache implementations](https://github.com/reactphp/react/wiki/Users#cache-implementations).
## ResolverInterface
<a id="resolver"><!-- legacy reference --></a>
### resolve()
The `resolve(string $domain): PromiseInterface<string>` method can be used to
resolve the given $domain name to a single IPv4 address (type `A` query).
```php
$resolver->resolve('reactphp.org')->then(function ($ip) {
echo 'IP for reactphp.org is ' . $ip . PHP_EOL;
});
```
This is one of the main methods in this package. It sends a DNS query
for the given $domain name to your DNS server and returns a single IP
address on success.
If the DNS server sends a DNS response message that contains more than
one IP address for this query, it will randomly pick one of the IP
addresses from the response. If you want the full list of IP addresses
or want to send a different type of query, you should use the
[`resolveAll()`](#resolveall) method instead.
If the DNS server sends a DNS response message that indicates an error
code, this method will reject with a `RecordNotFoundException`. Its
message and code can be used to check for the response code.
If the DNS communication fails and the server does not respond with a
valid response message, this message will reject with an `Exception`.
Pending DNS queries can be cancelled by cancelling its pending promise like so:
```php
$promise = $resolver->resolve('reactphp.org');
$promise->cancel();
```
### resolveAll()
The `resolveAll(string $host, int $type): PromiseInterface<array>` method can be used to
resolve all record values for the given $domain name and query $type.
```php
$resolver->resolveAll('reactphp.org', Message::TYPE_A)->then(function ($ips) {
echo 'IPv4 addresses for reactphp.org ' . implode(', ', $ips) . PHP_EOL;
});
$resolver->resolveAll('reactphp.org', Message::TYPE_AAAA)->then(function ($ips) {
echo 'IPv6 addresses for reactphp.org ' . implode(', ', $ips) . PHP_EOL;
});
```
This is one of the main methods in this package. It sends a DNS query
for the given $domain name to your DNS server and returns a list with all
record values on success.
If the DNS server sends a DNS response message that contains one or more
records for this query, it will return a list with all record values
from the response. You can use the `Message::TYPE_*` constants to control
which type of query will be sent. Note that this method always returns a
list of record values, but each record value type depends on the query
type. For example, it returns the IPv4 addresses for type `A` queries,
the IPv6 addresses for type `AAAA` queries, the hostname for type `NS`,
`CNAME` and `PTR` queries and structured data for other queries. See also
the `Record` documentation for more details.
If the DNS server sends a DNS response message that indicates an error
code, this method will reject with a `RecordNotFoundException`. Its
message and code can be used to check for the response code.
If the DNS communication fails and the server does not respond with a
valid response message, this message will reject with an `Exception`.
Pending DNS queries can be cancelled by cancelling its pending promise like so:
```php
$promise = $resolver->resolveAll('reactphp.org', Message::TYPE_AAAA);
$promise->cancel();
```
## Advanced Usage
### UdpTransportExecutor
The `UdpTransportExecutor` can be used to
send DNS queries over a UDP transport.
This is the main class that sends a DNS query to your DNS server and is used
internally by the `Resolver` for the actual message transport.
For more advanced usages one can utilize this class directly.
The following example looks up the `IPv6` address for `igor.io`.
```php
$executor = new UdpTransportExecutor('8.8.8.8:53');
$executor->query(
new Query($name, Message::TYPE_AAAA, Message::CLASS_IN)
)->then(function (Message $message) {
foreach ($message->answers as $answer) {
echo 'IPv6: ' . $answer->data . PHP_EOL;
}
}, 'printf');
```
See also the [fourth example](examples).
Note that this executor does not implement a timeout, so you will very likely
want to use this in combination with a `TimeoutExecutor` like this:
```php
$executor = new TimeoutExecutor(
new UdpTransportExecutor($nameserver),
3.0
);
```
Also note that this executor uses an unreliable UDP transport and that it
does not implement any retry logic, so you will likely want to use this in
combination with a `RetryExecutor` like this:
```php
$executor = new RetryExecutor(
new TimeoutExecutor(
new UdpTransportExecutor($nameserver),
3.0
)
);
```
Note that this executor is entirely async and as such allows you to execute
any number of queries concurrently. You should probably limit the number of
concurrent queries in your application or you're very likely going to face
rate limitations and bans on the resolver end. For many common applications,
you may want to avoid sending the same query multiple times when the first
one is still pending, so you will likely want to use this in combination with
a `CoopExecutor` like this:
```php
$executor = new CoopExecutor(
new RetryExecutor(
new TimeoutExecutor(
new UdpTransportExecutor($nameserver),
3.0
)
)
);
```
> Internally, this class uses PHP's UDP sockets and does not take advantage
of [react/datagram](https://github.com/reactphp/datagram) purely for
organizational reasons to avoid a cyclic dependency between the two
packages. Higher-level components should take advantage of the Datagram
component instead of reimplementing this socket logic from scratch.
### TcpTransportExecutor
The `TcpTransportExecutor` class can be used to
send DNS queries over a TCP/IP stream transport.
This is one of the main classes that send a DNS query to your DNS server.
For more advanced usages one can utilize this class directly.
The following example looks up the `IPv6` address for `reactphp.org`.
```php
$executor = new TcpTransportExecutor('8.8.8.8:53');
$executor->query(
new Query($name, Message::TYPE_AAAA, Message::CLASS_IN)
)->then(function (Message $message) {
foreach ($message->answers as $answer) {
echo 'IPv6: ' . $answer->data . PHP_EOL;
}
}, 'printf');
```
See also [example #92](examples).
Note that this executor does not implement a timeout, so you will very likely
want to use this in combination with a `TimeoutExecutor` like this:
```php
$executor = new TimeoutExecutor(
new TcpTransportExecutor($nameserver),
3.0
);
```
Unlike the `UdpTransportExecutor`, this class uses a reliable TCP/IP
transport, so you do not necessarily have to implement any retry logic.
Note that this executor is entirely async and as such allows you to execute
queries concurrently. The first query will establish a TCP/IP socket
connection to the DNS server which will be kept open for a short period.
Additional queries will automatically reuse this existing socket connection
to the DNS server, will pipeline multiple requests over this single
connection and will keep an idle connection open for a short period. The
initial TCP/IP connection overhead may incur a slight delay if you only send
occasional queries when sending a larger number of concurrent queries over
an existing connection, it becomes increasingly more efficient and avoids
creating many concurrent sockets like the UDP-based executor. You may still
want to limit the number of (concurrent) queries in your application or you
may be facing rate limitations and bans on the resolver end. For many common
applications, you may want to avoid sending the same query multiple times
when the first one is still pending, so you will likely want to use this in
combination with a `CoopExecutor` like this:
```php
$executor = new CoopExecutor(
new TimeoutExecutor(
new TcpTransportExecutor($nameserver),
3.0
)
);
```
> Internally, this class uses PHP's TCP/IP sockets and does not take advantage
of [react/socket](https://github.com/reactphp/socket) purely for
organizational reasons to avoid a cyclic dependency between the two
packages. Higher-level components should take advantage of the Socket
component instead of reimplementing this socket logic from scratch.
### SelectiveTransportExecutor
The `SelectiveTransportExecutor` class can be used to
Send DNS queries over a UDP or TCP/IP stream transport.
This class will automatically choose the correct transport protocol to send
a DNS query to your DNS server. It will always try to send it over the more
efficient UDP transport first. If this query yields a size related issue
(truncated messages), it will retry over a streaming TCP/IP transport.
For more advanced usages one can utilize this class directly.
The following example looks up the `IPv6` address for `reactphp.org`.
```php
$executor = new SelectiveTransportExecutor($udpExecutor, $tcpExecutor);
$executor->query(
new Query($name, Message::TYPE_AAAA, Message::CLASS_IN)
)->then(function (Message $message) {
foreach ($message->answers as $answer) {
echo 'IPv6: ' . $answer->data . PHP_EOL;
}
}, 'printf');
```
Note that this executor only implements the logic to select the correct
transport for the given DNS query. Implementing the correct transport logic,
implementing timeouts and any retry logic is left up to the given executors,
see also [`UdpTransportExecutor`](#udptransportexecutor) and
[`TcpTransportExecutor`](#tcptransportexecutor) for more details.
Note that this executor is entirely async and as such allows you to execute
any number of queries concurrently. You should probably limit the number of
concurrent queries in your application or you're very likely going to face
rate limitations and bans on the resolver end. For many common applications,
you may want to avoid sending the same query multiple times when the first
one is still pending, so you will likely want to use this in combination with
a `CoopExecutor` like this:
```php
$executor = new CoopExecutor(
new SelectiveTransportExecutor(
$datagramExecutor,
$streamExecutor
)
);
```
### HostsFileExecutor
Note that the above `UdpTransportExecutor` class always performs an actual DNS query.
If you also want to take entries from your hosts file into account, you may
use this code:
```php
$hosts = \React\Dns\Config\HostsFile::loadFromPathBlocking();
$executor = new UdpTransportExecutor('8.8.8.8:53');
$executor = new HostsFileExecutor($hosts, $executor);
$executor->query(
new Query('localhost', Message::TYPE_A, Message::CLASS_IN)
);
```
## Install
The recommended way to install this library is [through Composer](https://getcomposer.org/).
[New to Composer?](https://getcomposer.org/doc/00-intro.md)
This project follows [SemVer](https://semver.org/).
This will install the latest supported version:
```bash
composer require react/dns:^1.13
```
See also the [CHANGELOG](CHANGELOG.md) for details about version upgrades.
This project aims to run on any platform and thus does not require any PHP
extensions and supports running on legacy PHP 5.3 through current PHP 8+ and
HHVM.
It's *highly recommended to use the latest supported PHP version* for this project.
## Tests
To run the test suite, you first need to clone this repo and then install all
dependencies [through Composer](https://getcomposer.org/):
```bash
composer install
```
To run the test suite, go to the project root and run:
```bash
vendor/bin/phpunit
```
The test suite also contains a number of functional integration tests that rely
on a stable internet connection.
If you do not want to run these, they can simply be skipped like this:
```bash
vendor/bin/phpunit --exclude-group internet
```
## License
MIT, see [LICENSE file](LICENSE).
## References
* [RFC 1034](https://tools.ietf.org/html/rfc1034) Domain Names - Concepts and Facilities
* [RFC 1035](https://tools.ietf.org/html/rfc1035) Domain Names - Implementation and Specification

49
vendor/react/dns/composer.json vendored Normal file
View File

@@ -0,0 +1,49 @@
{
"name": "react/dns",
"description": "Async DNS resolver for ReactPHP",
"keywords": ["dns", "dns-resolver", "ReactPHP", "async"],
"license": "MIT",
"authors": [
{
"name": "Christian Lück",
"homepage": "https://clue.engineering/",
"email": "christian@clue.engineering"
},
{
"name": "Cees-Jan Kiewiet",
"homepage": "https://wyrihaximus.net/",
"email": "reactphp@ceesjankiewiet.nl"
},
{
"name": "Jan Sorgalla",
"homepage": "https://sorgalla.com/",
"email": "jsorgalla@gmail.com"
},
{
"name": "Chris Boden",
"homepage": "https://cboden.dev/",
"email": "cboden@gmail.com"
}
],
"require": {
"php": ">=5.3.0",
"react/cache": "^1.0 || ^0.6 || ^0.5",
"react/event-loop": "^1.2",
"react/promise": "^3.2 || ^2.7 || ^1.2.1"
},
"require-dev": {
"phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36",
"react/async": "^4.3 || ^3 || ^2",
"react/promise-timer": "^1.11"
},
"autoload": {
"psr-4": {
"React\\Dns\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"React\\Tests\\Dns\\": "tests/"
}
}
}

View File

@@ -0,0 +1,7 @@
<?php
namespace React\Dns;
final class BadServerException extends \Exception
{
}

137
vendor/react/dns/src/Config/Config.php vendored Normal file
View File

@@ -0,0 +1,137 @@
<?php
namespace React\Dns\Config;
use RuntimeException;
final class Config
{
/**
* Loads the system DNS configuration
*
* Note that this method may block while loading its internal files and/or
* commands and should thus be used with care! While this should be
* relatively fast for most systems, it remains unknown if this may block
* under certain circumstances. In particular, this method should only be
* executed before the loop starts, not while it is running.
*
* Note that this method will try to access its files and/or commands and
* try to parse its output. Currently, this will only parse valid nameserver
* entries from its output and will ignore all other output without
* complaining.
*
* Note that the previous section implies that this may return an empty
* `Config` object if no valid nameserver entries can be found.
*
* @return self
* @codeCoverageIgnore
*/
public static function loadSystemConfigBlocking()
{
// Use WMIC output on Windows
if (DIRECTORY_SEPARATOR === '\\') {
return self::loadWmicBlocking();
}
// otherwise (try to) load from resolv.conf
try {
return self::loadResolvConfBlocking();
} catch (RuntimeException $ignored) {
// return empty config if parsing fails (file not found)
return new self();
}
}
/**
* Loads a resolv.conf file (from the given path or default location)
*
* Note that this method blocks while loading the given path and should
* thus be used with care! While this should be relatively fast for normal
* resolv.conf files, this may be an issue if this file is located on a slow
* device or contains an excessive number of entries. In particular, this
* method should only be executed before the loop starts, not while it is
* running.
*
* Note that this method will throw if the given file can not be loaded,
* such as if it is not readable or does not exist. In particular, this file
* is not available on Windows.
*
* Currently, this will only parse valid "nameserver X" lines from the
* given file contents. Lines can be commented out with "#" and ";" and
* invalid lines will be ignored without complaining. See also
* `man resolv.conf` for more details.
*
* Note that the previous section implies that this may return an empty
* `Config` object if no valid "nameserver X" lines can be found. See also
* `man resolv.conf` which suggests that the DNS server on the localhost
* should be used in this case. This is left up to higher level consumers
* of this API.
*
* @param ?string $path (optional) path to resolv.conf file or null=load default location
* @return self
* @throws RuntimeException if the path can not be loaded (does not exist)
*/
public static function loadResolvConfBlocking($path = null)
{
if ($path === null) {
$path = '/etc/resolv.conf';
}
$contents = @file_get_contents($path);
if ($contents === false) {
throw new RuntimeException('Unable to load resolv.conf file "' . $path . '"');
}
$matches = array();
preg_match_all('/^nameserver\s+(\S+)\s*$/m', $contents, $matches);
$config = new self();
foreach ($matches[1] as $ip) {
// remove IPv6 zone ID (`fe80::1%lo0` => `fe80:1`)
if (strpos($ip, ':') !== false && ($pos = strpos($ip, '%')) !== false) {
$ip = substr($ip, 0, $pos);
}
if (@inet_pton($ip) !== false) {
$config->nameservers[] = $ip;
}
}
return $config;
}
/**
* Loads the DNS configurations from Windows's WMIC (from the given command or default command)
*
* Note that this method blocks while loading the given command and should
* thus be used with care! While this should be relatively fast for normal
* WMIC commands, it remains unknown if this may block under certain
* circumstances. In particular, this method should only be executed before
* the loop starts, not while it is running.
*
* Note that this method will only try to execute the given command try to
* parse its output, irrespective of whether this command exists. In
* particular, this command is only available on Windows. Currently, this
* will only parse valid nameserver entries from the command output and will
* ignore all other output without complaining.
*
* Note that the previous section implies that this may return an empty
* `Config` object if no valid nameserver entries can be found.
*
* @param ?string $command (advanced) should not be given (NULL) unless you know what you're doing
* @return self
* @link https://ss64.com/nt/wmic.html
*/
public static function loadWmicBlocking($command = null)
{
$contents = shell_exec($command === null ? 'wmic NICCONFIG get "DNSServerSearchOrder" /format:CSV' : $command);
preg_match_all('/(?<=[{;,"])([\da-f.:]{4,})(?=[};,"])/i', $contents, $matches);
$config = new self();
$config->nameservers = $matches[1];
return $config;
}
public $nameservers = array();
}

Some files were not shown because too many files have changed in this diff Show More