+
This commit is contained in:
2
vendor/clue/framework-x/.github/FUNDING.yml
vendored
Normal file
2
vendor/clue/framework-x/.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
github: clue
|
||||
custom: https://clue.engineering/support
|
||||
373
vendor/clue/framework-x/CHANGELOG.md
vendored
Normal file
373
vendor/clue/framework-x/CHANGELOG.md
vendored
Normal 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
21
vendor/clue/framework-x/LICENSE
vendored
Normal 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
147
vendor/clue/framework-x/README.md
vendored
Normal file
@@ -0,0 +1,147 @@
|
||||
# Framework X
|
||||
|
||||
[](https://github.com/clue-access/framework-x/actions)
|
||||
[](#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
40
vendor/clue/framework-x/composer.json
vendored
Normal 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"
|
||||
]
|
||||
}
|
||||
}
|
||||
140
vendor/clue/framework-x/src/AccessLogHandler.php
vendored
Normal file
140
vendor/clue/framework-x/src/AccessLogHandler.php
vendored
Normal 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
354
vendor/clue/framework-x/src/App.php
vendored
Normal 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();
|
||||
}
|
||||
}
|
||||
382
vendor/clue/framework-x/src/Container.php
vendored
Normal file
382
vendor/clue/framework-x/src/Container.php
vendored
Normal 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 . '()';
|
||||
}
|
||||
}
|
||||
211
vendor/clue/framework-x/src/ErrorHandler.php
vendored
Normal file
211
vendor/clue/framework-x/src/ErrorHandler.php
vendored
Normal 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);
|
||||
}
|
||||
}
|
||||
130
vendor/clue/framework-x/src/FilesystemHandler.php
vendored
Normal file
130
vendor/clue/framework-x/src/FilesystemHandler.php
vendored
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
67
vendor/clue/framework-x/src/Io/FiberHandler.php
vendored
Normal file
67
vendor/clue/framework-x/src/Io/FiberHandler.php
vendored
Normal 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();
|
||||
}
|
||||
}
|
||||
65
vendor/clue/framework-x/src/Io/HtmlHandler.php
vendored
Normal file
65
vendor/clue/framework-x/src/Io/HtmlHandler.php
vendored
Normal 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 ',
|
||||
\htmlspecialchars($s, \ENT_NOQUOTES | \ENT_SUBSTITUTE | \ENT_DISALLOWED, 'utf-8')
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
95
vendor/clue/framework-x/src/Io/LogStreamHandler.php
vendored
Normal file
95
vendor/clue/framework-x/src/Io/LogStreamHandler.php
vendored
Normal 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);
|
||||
}
|
||||
}
|
||||
40
vendor/clue/framework-x/src/Io/MiddlewareHandler.php
vendored
Normal file
40
vendor/clue/framework-x/src/Io/MiddlewareHandler.php
vendored
Normal 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);
|
||||
});
|
||||
}
|
||||
}
|
||||
91
vendor/clue/framework-x/src/Io/ReactiveHandler.php
vendored
Normal file
91
vendor/clue/framework-x/src/Io/ReactiveHandler.php
vendored
Normal 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');
|
||||
}
|
||||
}
|
||||
48
vendor/clue/framework-x/src/Io/RedirectHandler.php
vendored
Normal file
48
vendor/clue/framework-x/src/Io/RedirectHandler.php
vendored
Normal 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);
|
||||
}
|
||||
}
|
||||
116
vendor/clue/framework-x/src/Io/RouteHandler.php
vendored
Normal file
116
vendor/clue/framework-x/src/Io/RouteHandler.php
vendored
Normal 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]);
|
||||
}
|
||||
}
|
||||
164
vendor/clue/framework-x/src/Io/SapiHandler.php
vendored
Normal file
164
vendor/clue/framework-x/src/Io/SapiHandler.php
vendored
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user