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 /.gitignore export-ignore
/phpcs.xml export-ignore /phpcs.xml export-ignore
/phpstan.neon export-ignore /phpstan.neon export-ignore
/phpstan-baseline.neon export-ignore
/phpunit.xml export-ignore /phpunit.xml export-ignore

2
.gitignore vendored

@ -4,8 +4,6 @@ composer.phar
composer.lock composer.lock
.phpunit.result.cache .phpunit.result.cache
coverage/ coverage/
.vscode/settings.json
*.sublime* *.sublime*
.vscode/
clover.xml clover.xml
phpcs.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: 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) ![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) ![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? # What is Flight?

@ -364,34 +364,36 @@ class Engine
/** /**
* Processes each routes middleware. * Processes each routes middleware.
* *
* @param array<int, callable> $middleware Middleware attached to the route. * @param Route $route The route to process the middleware for.
* @param array<mixed> $params `$route->params`.
* @param 'before'|'after' $event_name If this is the before or after method. * @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; $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; $middleware_object = false;
if ($event_name === $this->dispatcher::FILTER_BEFORE) { if ($event_name === Dispatcher::FILTER_BEFORE) {
// can be a callable or a class // can be a callable or a class
$middleware_object = (is_callable($middleware) === true $middleware_object = (is_callable($middleware) === true
? $middleware ? $middleware
: (method_exists($middleware, $this->dispatcher::FILTER_BEFORE) === true : (method_exists($middleware, Dispatcher::FILTER_BEFORE) === true
? [$middleware, $this->dispatcher::FILTER_BEFORE] ? [$middleware, Dispatcher::FILTER_BEFORE]
: false : false
) )
); );
} elseif ($event_name === $this->dispatcher::FILTER_AFTER) { } elseif ($event_name === Dispatcher::FILTER_AFTER) {
// must be an object. No functions allowed here // must be an object. No functions allowed here
if ( if (
is_object($middleware) === true is_object($middleware) === true
&& !($middleware instanceof Closure) && !($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; 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(); ob_start();
} }
// It's assumed if you don't declare before, that it will be assumed as the before method // It's assumed if you don't declare before, that it will be assumed as the before method
$middleware_result = $middleware_object($params); $middleware_result = $middleware_object($params);
if ($this->response()->v2_output_buffering === false) { if ($use_v3_output_buffering === true) {
$this->response()->write(ob_get_clean()); $this->response()->write(ob_get_clean());
} }
@ -462,21 +468,36 @@ class Engine
$params[] = $route; $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 // Run any before middlewares
if (count($route->middleware) > 0) { if (count($route->middleware) > 0) {
$at_least_one_middleware_failed = $this->processMiddleware( $at_least_one_middleware_failed = $this->processMiddleware($route, 'before');
$route->middleware,
$route->params,
'before'
);
if ($at_least_one_middleware_failed === true) { if ($at_least_one_middleware_failed === true) {
$failed_middleware_check = true; $failed_middleware_check = true;
break; 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(); ob_start();
} }
@ -486,18 +507,14 @@ class Engine
$params $params
); );
if ($response->v2_output_buffering === false) { if ($use_v3_output_buffering === true) {
$response->write(ob_get_clean()); $response->write(ob_get_clean());
} }
// Run any before middlewares // Run any before middlewares
if (count($route->middleware) > 0) { if (count($route->middleware) > 0) {
// process the middleware in reverse order now // process the middleware in reverse order now
$at_least_one_middleware_failed = $this->processMiddleware( $at_least_one_middleware_failed = $this->processMiddleware($route, 'after');
array_reverse($route->middleware),
$route->params,
'after'
);
if ($at_least_one_middleware_failed === true) { if ($at_least_one_middleware_failed === true) {
$failed_middleware_check = true; $failed_middleware_check = true;
@ -750,8 +767,10 @@ class Engine
$this->response() $this->response()
->status($code) ->status($code)
->header('Content-Type', 'application/json; charset=' . $charset) ->header('Content-Type', 'application/json; charset=' . $charset)
->write($json) ->write($json);
->send(); if ($this->response()->v2_output_buffering === true) {
$this->response()->send();
}
} }
/** /**
@ -780,8 +799,10 @@ class Engine
$this->response() $this->response()
->status($code) ->status($code)
->header('Content-Type', 'application/javascript; charset=' . $charset) ->header('Content-Type', 'application/javascript; charset=' . $charset)
->write($callback . '(' . $json . ');') ->write($callback . '(' . $json . ');');
->send(); if ($this->response()->v2_output_buffering === true) {
$this->response()->send();
}
} }
/** /**

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

@ -63,10 +63,20 @@ class Route
/** /**
* The middleware to be applied to the route * The middleware to be applied to the route
* *
* @var array<int,callable|object> * @var array<int, callable|object>
*/ */
public array $middleware = []; 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. * Constructor.
* *
@ -176,7 +186,7 @@ class Route
/** /**
* Hydrates the route url with the given parameters * 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 public function hydrateUrl(array $params = []): string
{ {
@ -209,9 +219,7 @@ class Route
/** /**
* Sets the route middleware * Sets the route middleware
* *
* @param array<int,callable>|callable $middleware * @param array<int, callable>|callable $middleware
*
* @return self
*/ */
public function addMiddleware($middleware): self public function addMiddleware($middleware): self
{ {
@ -222,4 +230,19 @@ class Route
} }
return $this; 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: includes:
- vendor/phpstan/phpstan/conf/bleedingEdge.neon - vendor/phpstan/phpstan/conf/bleedingEdge.neon
- phpstan-baseline.neon
parameters: parameters:
level: 6 level: 6

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

@ -278,4 +278,30 @@ class FlightTest extends TestCase
Flight::start(); Flight::start();
$this->assertEquals('hooked before starttest', Flight::response()->getBody()); $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 // Test 1: Root route
Flight::route('/', function () { Flight::route('/', function () {
echo '<span id="infotext">Route text:</span> Root route works!'; 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 () { Flight::route('/querytestpath', function () {
echo '<span id="infotext">Route text:</span> This ir query route<br>'; 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::route('/template/@name', function ($name) {
Flight::render('template.phtml', ['name' => $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::set('flight.views.path', './');
Flight::map('error', function (Throwable $error) { 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 '<pre style="border: 2px solid red; padding: 21px; background: lightgray; font-weight: bold;">';
echo str_replace(getenv('PWD'), "***CLASSIFIED*****", $error->getTraceAsString()); echo str_replace(getenv('PWD'), "***CLASSIFIED*****", $error->getTraceAsString());
echo "</pre>"; echo "</pre>";
@ -164,6 +197,11 @@ echo '
<li><a href="/querytestpath?test=1&variable2=uuid&variable3=tester">Query path</a></li> <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="/postpage">Post method test page - should be 404</a></li>
<li><a href="' . Flight::getUrl('final_group') . '">Mega group</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>'; </ul>';
Flight::before('start', function ($params) { Flight::before('start', function ($params) {
echo '<div id="container">'; echo '<div id="container">';

@ -72,6 +72,11 @@ class LayoutMiddleware
<li><a href="/postpage">404 Not Found</a></li> <li><a href="/postpage">404 Not Found</a></li>
<li><a href="{$final_route}">Mega group</a></li> <li><a href="{$final_route}">Mega group</a></li>
<li><a href="/error">Error</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> </ul>
HTML; HTML;
echo '<div id="container">'; echo '<div id="container">';

@ -9,9 +9,7 @@ declare(strict_types=1);
* @author Kristaps Muižnieks https://github.com/krmu * @author Kristaps Muižnieks https://github.com/krmu
*/ */
require_once file_exists(__DIR__ . '/../../vendor/autoload.php') require file_exists(__DIR__ . '/../../vendor/autoload.php') ? __DIR__ . '/../../vendor/autoload.php' : __DIR__ . '/../../flight/autoload.php';
? __DIR__ . '/../../vendor/autoload.php'
: __DIR__ . '/../../flight/autoload.php';
Flight::set('flight.content_length', false); Flight::set('flight.content_length', false);
Flight::set('flight.views.path', './'); Flight::set('flight.views.path', './');
@ -25,10 +23,13 @@ Flight::group('', function () {
// Test 1: Root route // Test 1: Root route
Flight::route('/', function () { Flight::route('/', function () {
echo '<span id="infotext">Route text:</span> Root route works!'; 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 () { Flight::route('/querytestpath', function () {
echo '<span id="infotext">Route text:</span> This ir query route<br>'; echo '<span id="infotext">Route text:</span> This is query route<br>';
echo "I got such query parameters:<pre>"; echo "Query parameters:<pre>";
print_r(Flight::request()->query); print_r(Flight::request()->query);
echo "</pre>"; echo "</pre>";
}, false, "querytestpath"); }, false, "querytestpath");
@ -97,23 +98,48 @@ Flight::group('', function () {
Flight::route('/error', function () { Flight::route('/error', function () {
trigger_error('This is a successful error'); trigger_error('This is a successful error');
}); });
}, [new LayoutMiddleware()]);
Flight::map('error', function (Throwable $e) { // Test 10: Halt
$styles = join(';', [ Flight::route('/halt', function () {
'border: 2px solid red', Flight::halt(400, 'Halt worked successfully');
'padding: 21px', });
'background: lightgray',
'font-weight: bold' // 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( 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->getMessage(),
$e->getCode(), $e->getCode(),
str_replace(getenv('PWD'), '***CONFIDENTIAL***', $e->getTraceAsString()) str_replace(getenv('PWD'), '***CONFIDENTIAL***', $e->getTraceAsString())
); );
echo "<br><a href='/'>Go back</a>"; echo "<br><a href='/'>Go back</a>";
}); });
Flight::map('notFound', function () { Flight::map('notFound', function () {

Loading…
Cancel
Save