Merge branch 'master' into dispatcher-refactor

pull/567/head
fadrian06 11 months ago
commit 2474371e35

1
.gitattributes vendored

@ -5,4 +5,5 @@
/.gitignore export-ignore
/phpcs.xml export-ignore
/phpstan.neon export-ignore
/phpstan-baseline.neon export-ignore
/phpunit.xml export-ignore

2
.gitignore vendored

@ -4,8 +4,6 @@ composer.phar
composer.lock
.phpunit.result.cache
coverage/
.vscode/settings.json
*.sublime*
.vscode/
clover.xml
phpcs.xml

@ -0,0 +1,5 @@
{
"php.suggest.basic": false,
"editor.detectIndentation": false,
"editor.insertSpaces": true
}

@ -1,7 +1,6 @@
![PHPStan: enabled](https://user-images.githubusercontent.com/104888/50957476-9c4acb80-14be-11e9-88ce-6447364dc1bb.png)
![PHPStan: level 6](https://img.shields.io/badge/PHPStan-level%206-brightgreen.svg?style=flat)
![Matrix](https://img.shields.io/matrix/flight-php-framework%3Amatrix.org?server_fqdn=matrix.org&style=social&logo=matrix)
[![Hit Count](https://hits.dwyl.com/flightphp/core.svg?style=flat-square&show=unique)](http://hits.dwyl.com/flightphp/core)
# What is Flight?

@ -364,34 +364,36 @@ class Engine
/**
* Processes each routes middleware.
*
* @param array<int, callable> $middleware Middleware attached to the route.
* @param array<mixed> $params `$route->params`.
* @param Route $route The route to process the middleware for.
* @param 'before'|'after' $event_name If this is the before or after method.
*/
protected function processMiddleware(array $middleware, array $params, string $event_name): bool
protected function processMiddleware(Route $route, string $event_name): bool
{
$at_least_one_middleware_failed = false;
foreach ($middleware as $middleware) {
$middlewares = $event_name === Dispatcher::FILTER_BEFORE ? $route->middleware : array_reverse($route->middleware);
$params = $route->params;
foreach ($middlewares as $middleware) {
$middleware_object = false;
if ($event_name === $this->dispatcher::FILTER_BEFORE) {
if ($event_name === Dispatcher::FILTER_BEFORE) {
// can be a callable or a class
$middleware_object = (is_callable($middleware) === true
? $middleware
: (method_exists($middleware, $this->dispatcher::FILTER_BEFORE) === true
? [$middleware, $this->dispatcher::FILTER_BEFORE]
: (method_exists($middleware, Dispatcher::FILTER_BEFORE) === true
? [$middleware, Dispatcher::FILTER_BEFORE]
: false
)
);
} elseif ($event_name === $this->dispatcher::FILTER_AFTER) {
} elseif ($event_name === Dispatcher::FILTER_AFTER) {
// must be an object. No functions allowed here
if (
is_object($middleware) === true
&& !($middleware instanceof Closure)
&& method_exists($middleware, $this->dispatcher::FILTER_AFTER) === true
&& method_exists($middleware, Dispatcher::FILTER_AFTER) === true
) {
$middleware_object = [$middleware, $this->dispatcher::FILTER_AFTER];
$middleware_object = [$middleware, Dispatcher::FILTER_AFTER];
}
}
@ -399,14 +401,18 @@ class Engine
continue;
}
if ($this->response()->v2_output_buffering === false) {
$use_v3_output_buffering =
$this->response()->v2_output_buffering === false &&
$route->is_streamed === false;
if ($use_v3_output_buffering === true) {
ob_start();
}
// It's assumed if you don't declare before, that it will be assumed as the before method
$middleware_result = $middleware_object($params);
if ($this->response()->v2_output_buffering === false) {
if ($use_v3_output_buffering === true) {
$this->response()->write(ob_get_clean());
}
@ -462,21 +468,36 @@ class Engine
$params[] = $route;
}
// If this route is to be streamed, we need to output the headers now
if ($route->is_streamed === true) {
$response->status($route->streamed_headers['status']);
unset($route->streamed_headers['status']);
$response->header('X-Accel-Buffering', 'no');
$response->header('Connection', 'close');
foreach ($route->streamed_headers as $header => $value) {
$response->header($header, $value);
}
// We obviously don't know the content length right now. This must be false.
$response->content_length = false;
$response->sendHeaders();
$response->markAsSent();
}
// Run any before middlewares
if (count($route->middleware) > 0) {
$at_least_one_middleware_failed = $this->processMiddleware(
$route->middleware,
$route->params,
'before'
);
$at_least_one_middleware_failed = $this->processMiddleware($route, 'before');
if ($at_least_one_middleware_failed === true) {
$failed_middleware_check = true;
break;
}
}
if ($response->v2_output_buffering === false) {
$use_v3_output_buffering =
$this->response()->v2_output_buffering === false &&
$route->is_streamed === false;
if ($use_v3_output_buffering === true) {
ob_start();
}
@ -486,18 +507,14 @@ class Engine
$params
);
if ($response->v2_output_buffering === false) {
if ($use_v3_output_buffering === true) {
$response->write(ob_get_clean());
}
// Run any before middlewares
if (count($route->middleware) > 0) {
// process the middleware in reverse order now
$at_least_one_middleware_failed = $this->processMiddleware(
array_reverse($route->middleware),
$route->params,
'after'
);
$at_least_one_middleware_failed = $this->processMiddleware($route, 'after');
if ($at_least_one_middleware_failed === true) {
$failed_middleware_check = true;
@ -750,8 +767,10 @@ class Engine
$this->response()
->status($code)
->header('Content-Type', 'application/json; charset=' . $charset)
->write($json)
->send();
->write($json);
if ($this->response()->v2_output_buffering === true) {
$this->response()->send();
}
}
/**
@ -780,8 +799,10 @@ class Engine
$this->response()
->status($code)
->header('Content-Type', 'application/javascript; charset=' . $charset)
->write($callback . '(' . $json . ');')
->send();
->write($callback . '(' . $json . ');');
if ($this->response()->v2_output_buffering === true) {
$this->response()->send();
}
}
/**

@ -389,6 +389,14 @@ class Response
return $this->sent;
}
/**
* Marks the response as sent.
*/
public function markAsSent(): void
{
$this->sent = true;
}
/**
* Sends a HTTP response.
*/

@ -63,10 +63,20 @@ class Route
/**
* The middleware to be applied to the route
*
* @var array<int,callable|object>
* @var array<int, callable|object>
*/
public array $middleware = [];
/** Whether the response for this route should be streamed. */
public bool $is_streamed = false;
/**
* If this route is streamed, the headers to be sent before the response.
*
* @var array<string, mixed>
*/
public array $streamed_headers = [];
/**
* Constructor.
*
@ -176,7 +186,7 @@ class Route
/**
* Hydrates the route url with the given parameters
*
* @param array<string,mixed> $params the parameters to pass to the route
* @param array<string, mixed> $params the parameters to pass to the route
*/
public function hydrateUrl(array $params = []): string
{
@ -209,9 +219,7 @@ class Route
/**
* Sets the route middleware
*
* @param array<int,callable>|callable $middleware
*
* @return self
* @param array<int, callable>|callable $middleware
*/
public function addMiddleware($middleware): self
{
@ -222,4 +230,19 @@ class Route
}
return $this;
}
/**
* This will allow the response for this route to be streamed.
*
* @param array<string, mixed> $headers a key value of headers to set before the stream starts.
*
* @return $this
*/
public function streamWithHeaders(array $headers): self
{
$this->is_streamed = true;
$this->streamed_headers = $headers;
return $this;
}
}

@ -0,0 +1,6 @@
parameters:
ignoreErrors:
-
message: "#^Parameter \\#2 \\$callback of method flight\\\\core\\\\Dispatcher\\:\\:set\\(\\) expects Closure\\(\\)\\: mixed, array\\{\\$this\\(flight\\\\Engine\\), literal\\-string&non\\-falsy\\-string\\} given\\.$#"
count: 1
path: flight/Engine.php

@ -1,5 +1,6 @@
includes:
- vendor/phpstan/phpstan/conf/bleedingEdge.neon
- phpstan-baseline.neon
parameters:
level: 6

@ -5,6 +5,7 @@ declare(strict_types=1);
namespace tests;
use Exception;
use Flight;
use flight\Engine;
use flight\net\Response;
use PHPUnit\Framework\TestCase;
@ -307,6 +308,16 @@ class EngineTest extends TestCase
{
$engine = new Engine();
$engine->json(['key1' => 'value1', 'key2' => 'value2']);
$this->assertEquals('application/json; charset=utf-8', $engine->response()->headers()['Content-Type']);
$this->assertEquals(200, $engine->response()->status());
$this->assertEquals('{"key1":"value1","key2":"value2"}', $engine->response()->getBody());
}
public function testJsonV2OutputBuffering()
{
$engine = new Engine();
$engine->response()->v2_output_buffering = true;
$engine->json(['key1' => 'value1', 'key2' => 'value2']);
$this->expectOutputString('{"key1":"value1","key2":"value2"}');
$this->assertEquals('application/json; charset=utf-8', $engine->response()->headers()['Content-Type']);
$this->assertEquals(200, $engine->response()->status());
@ -317,6 +328,17 @@ class EngineTest extends TestCase
$engine = new Engine();
$engine->request()->query['jsonp'] = 'whatever';
$engine->jsonp(['key1' => 'value1', 'key2' => 'value2']);
$this->assertEquals('application/javascript; charset=utf-8', $engine->response()->headers()['Content-Type']);
$this->assertEquals(200, $engine->response()->status());
$this->assertEquals('whatever({"key1":"value1","key2":"value2"});', $engine->response()->getBody());
}
public function testJsonPV2OutputBuffering()
{
$engine = new Engine();
$engine->response()->v2_output_buffering = true;
$engine->request()->query['jsonp'] = 'whatever';
$engine->jsonp(['key1' => 'value1', 'key2' => 'value2']);
$this->expectOutputString('whatever({"key1":"value1","key2":"value2"});');
$this->assertEquals('application/javascript; charset=utf-8', $engine->response()->headers()['Content-Type']);
$this->assertEquals(200, $engine->response()->status());
@ -326,6 +348,16 @@ class EngineTest extends TestCase
{
$engine = new Engine();
$engine->jsonp(['key1' => 'value1', 'key2' => 'value2']);
$this->assertEquals('({"key1":"value1","key2":"value2"});', $engine->response()->getBody());
$this->assertEquals('application/javascript; charset=utf-8', $engine->response()->headers()['Content-Type']);
$this->assertEquals(200, $engine->response()->status());
}
public function testJsonpBadParamV2OutputBuffering()
{
$engine = new Engine();
$engine->response()->v2_output_buffering = true;
$engine->jsonp(['key1' => 'value1', 'key2' => 'value2']);
$this->expectOutputString('({"key1":"value1","key2":"value2"});');
$this->assertEquals('application/javascript; charset=utf-8', $engine->response()->headers()['Content-Type']);
$this->assertEquals(200, $engine->response()->status());

@ -278,4 +278,30 @@ class FlightTest extends TestCase
Flight::start();
$this->assertEquals('hooked before starttest', Flight::response()->getBody());
}
public function testStreamRoute()
{
$response_mock = new class extends Response {
public function setRealHeader(string $header_string, bool $replace = true, int $response_code = 0): Response
{
return $this;
}
};
$mock_response_class_name = get_class($response_mock);
Flight::register('response', $mock_response_class_name);
Flight::route('/stream', function () {
echo 'stream';
})->streamWithHeaders(['Content-Type' => 'text/plain', 'X-Test' => 'test', 'status' => 200 ]);
Flight::request()->url = '/stream';
$this->expectOutputString('stream');
Flight::start();
$this->assertEquals('', Flight::response()->getBody());
$this->assertEquals([
'Content-Type' => 'text/plain',
'X-Test' => 'test',
'X-Accel-Buffering' => 'no',
'Connection' => 'close'
], Flight::response()->getHeaders());
$this->assertEquals(200, Flight::response()->status());
}
}

@ -0,0 +1,9 @@
#!/bin/bash
# Run all tests
composer lint
composer beautify
composer phpcs
composer test-coverage
xdg-open http://localhost:8000
composer test-server

@ -20,6 +20,9 @@ Flight::set('flight.v2.output_buffering', true);
// Test 1: Root route
Flight::route('/', function () {
echo '<span id="infotext">Route text:</span> Root route works!';
if (Flight::request()->query->redirected) {
echo '<br>Redirected from /redirect route successfully!';
}
});
Flight::route('/querytestpath', function () {
echo '<span id="infotext">Route text:</span> This ir query route<br>';
@ -93,9 +96,39 @@ Flight::route('/protected', function () {
Flight::route('/template/@name', function ($name) {
Flight::render('template.phtml', ['name' => $name]);
});
// Test 8: Throw an error
Flight::route('/error', function () {
trigger_error('This is a successful error');
});
// Test 9: JSON output (should not output any other html)
Flight::route('/json', function () {
echo "\n\n\n\n\n";
Flight::json(['message' => 'JSON renders successfully!']);
echo "\n\n\n\n\n";
});
// Test 13: JSONP output (should not output any other html)
Flight::route('/jsonp', function () {
echo "\n\n\n\n\n";
Flight::jsonp(['message' => 'JSONP renders successfully!'], 'jsonp');
echo "\n\n\n\n\n";
});
// Test 10: Halt
Flight::route('/halt', function () {
Flight::halt(400, 'Halt worked successfully');
});
// Test 11: Redirect
Flight::route('/redirect', function () {
Flight::redirect('/?redirected=1');
});
Flight::set('flight.views.path', './');
Flight::map('error', function (Throwable $error) {
echo "<h1> An error occurred, mapped error method worked, error bellow </h1>";
echo "<h1> An error occurred, mapped error method worked, error below </h1>";
echo '<pre style="border: 2px solid red; padding: 21px; background: lightgray; font-weight: bold;">';
echo str_replace(getenv('PWD'), "***CLASSIFIED*****", $error->getTraceAsString());
echo "</pre>";
@ -164,6 +197,11 @@ echo '
<li><a href="/querytestpath?test=1&variable2=uuid&variable3=tester">Query path</a></li>
<li><a href="/postpage">Post method test page - should be 404</a></li>
<li><a href="' . Flight::getUrl('final_group') . '">Mega group</a></li>
<li><a href="/error">Error</a></li>
<li><a href="/json">JSON</a></li>
<li><a href="/json?jsonp=myjson">JSONP</a></li>
<li><a href="/halt">Halt</a></li>
<li><a href="/redirect">Redirect</a></li>
</ul>';
Flight::before('start', function ($params) {
echo '<div id="container">';

@ -72,6 +72,11 @@ class LayoutMiddleware
<li><a href="/postpage">404 Not Found</a></li>
<li><a href="{$final_route}">Mega group</a></li>
<li><a href="/error">Error</a></li>
<li><a href="/json">JSON</a></li>
<li><a href="/json?jsonp=myjson">JSONP</a></li>
<li><a href="/halt">Halt</a></li>
<li><a href="/redirect">Redirect</a></li>
<li><a href="/streamResponse">Stream</a></li>
</ul>
HTML;
echo '<div id="container">';

@ -9,9 +9,7 @@ declare(strict_types=1);
* @author Kristaps Muižnieks https://github.com/krmu
*/
require_once file_exists(__DIR__ . '/../../vendor/autoload.php')
? __DIR__ . '/../../vendor/autoload.php'
: __DIR__ . '/../../flight/autoload.php';
require file_exists(__DIR__ . '/../../vendor/autoload.php') ? __DIR__ . '/../../vendor/autoload.php' : __DIR__ . '/../../flight/autoload.php';
Flight::set('flight.content_length', false);
Flight::set('flight.views.path', './');
@ -25,10 +23,13 @@ Flight::group('', function () {
// Test 1: Root route
Flight::route('/', function () {
echo '<span id="infotext">Route text:</span> Root route works!';
if (Flight::request()->query->redirected) {
echo '<br>Redirected from /redirect route successfully!';
}
});
Flight::route('/querytestpath', function () {
echo '<span id="infotext">Route text:</span> This ir query route<br>';
echo "I got such query parameters:<pre>";
echo '<span id="infotext">Route text:</span> This is query route<br>';
echo "Query parameters:<pre>";
print_r(Flight::request()->query);
echo "</pre>";
}, false, "querytestpath");
@ -97,23 +98,48 @@ Flight::group('', function () {
Flight::route('/error', function () {
trigger_error('This is a successful error');
});
}, [new LayoutMiddleware()]);
Flight::map('error', function (Throwable $e) {
$styles = join(';', [
'border: 2px solid red',
'padding: 21px',
'background: lightgray',
'font-weight: bold'
]);
// Test 10: Halt
Flight::route('/halt', function () {
Flight::halt(400, 'Halt worked successfully');
});
// Test 11: Redirect
Flight::route('/redirect', function () {
Flight::redirect('/?redirected=1');
});
// Test 12: Redirect with status code
Flight::route('/streamResponse', function () {
echo "Streaming a response";
for ($i = 1; $i <= 50; $i++) {
echo ".";
usleep(50000);
ob_flush();
}
echo "is successful!!";
})->streamWithHeaders(['Content-Type' => 'text/html', 'status' => 200 ]);
}, [ new LayoutMiddleware() ]);
// Test 9: JSON output (should not output any other html)
Flight::route('/json', function () {
Flight::json(['message' => 'JSON renders successfully!']);
});
// Test 13: JSONP output (should not output any other html)
Flight::route('/jsonp', function () {
Flight::jsonp(['message' => 'JSONP renders successfully!'], 'jsonp');
});
Flight::map('error', function (Throwable $e) {
echo sprintf(
"<h1>500 Internal Server Error</h1><h3>%s (%s)</h3><pre style=\"$styles\">%s</pre>",
'<h1>500 Internal Server Error</h1>' .
'<h3>%s (%s)</h3>' .
'<pre style="border: 2px solid red; padding: 21px; background: lightgray; font-weight: bold;">%s</pre>',
$e->getMessage(),
$e->getCode(),
str_replace(getenv('PWD'), '***CONFIDENTIAL***', $e->getTraceAsString())
);
echo "<br><a href='/'>Go back</a>";
});
Flight::map('notFound', function () {

Loading…
Cancel
Save