From fcfe8adc5d05d4ffa091ad179df659ab5a5852f6 Mon Sep 17 00:00:00 2001 From: fadrian06 Date: Sun, 18 Feb 2024 17:04:49 -0400 Subject: [PATCH 01/43] Upgraded dev dependencies --- composer.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/composer.json b/composer.json index b45d101..ba3dcc0 100644 --- a/composer.json +++ b/composer.json @@ -41,11 +41,11 @@ }, "require-dev": { "ext-pdo_sqlite": "*", - "phpunit/phpunit": "^9.5", - "phpstan/phpstan": "^1.10", "phpstan/extension-installer": "^1.3", + "phpstan/phpstan": "^1.10", + "phpunit/phpunit": "^9.5", "rregeer/phpunit-coverage-check": "^0.3.1", - "squizlabs/php_codesniffer": "^3.8" + "squizlabs/php_codesniffer": "^3.9" }, "config": { "allow-plugins": { From 3fba60ca7fc943d55b905b7cf21fe7bf6e3ec7a0 Mon Sep 17 00:00:00 2001 From: fadrian06 Date: Sun, 18 Feb 2024 20:34:18 -0400 Subject: [PATCH 02/43] Apply some fixes of phpcs --- flight/Engine.php | 10 ++++++++-- flight/Flight.php | 6 ++---- index.php | 6 ++++-- phpcs.xml | 8 ++++++-- 4 files changed, 20 insertions(+), 10 deletions(-) diff --git a/flight/Engine.php b/flight/Engine.php index ffd51e4..89b6b72 100644 --- a/flight/Engine.php +++ b/flight/Engine.php @@ -27,7 +27,8 @@ use flight\net\Route; * # Core methods * @method void start() Starts engine * @method void stop() Stops framework and outputs current response - * @method void halt(int $code = 200, string $message = '', bool $actuallyExit = true) Stops processing and returns a given response. + * @method void halt(int $code = 200, string $message = '', bool $actuallyExit = true) + * Stops processing and returns a given response. * * # Routing * @method Route route(string $pattern, callable $callback, bool $pass_route = false, string $alias = '') @@ -464,7 +465,12 @@ class Engine // 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->middleware, + $route->params, + 'before' + ); + if ($at_least_one_middleware_failed === true) { $failed_middleware_check = true; break; diff --git a/flight/Flight.php b/flight/Flight.php index 6e29781..4609c60 100644 --- a/flight/Flight.php +++ b/flight/Flight.php @@ -10,8 +10,6 @@ use flight\net\Router; use flight\template\View; use flight\net\Route; -require_once __DIR__ . '/autoload.php'; - /** * The Flight class is a static representation of the framework. * @@ -140,6 +138,8 @@ class Flight */ public static function __callStatic(string $name, array $params) { + require_once __DIR__ . '/autoload.php'; + return Dispatcher::invokeMethod([self::app(), $name], $params); } @@ -147,8 +147,6 @@ class Flight public static function app(): Engine { if (!self::$initialized) { - require_once __DIR__ . '/autoload.php'; - self::setEngine(new Engine()); self::$initialized = true; } diff --git a/index.php b/index.php index 5a21ed6..65ea154 100644 --- a/index.php +++ b/index.php @@ -1,7 +1,9 @@ - + - @@ -39,6 +38,11 @@ + + + + + flight/ tests/ tests/views/* From 253c86482eca09fbce4e76559c823bd0cbad9c06 Mon Sep 17 00:00:00 2001 From: fadrian06 Date: Sun, 18 Feb 2024 21:49:59 -0400 Subject: [PATCH 03/43] Fixed yoda comparisons --- flight/Engine.php | 58 ++++++++++++++-------------- flight/net/Request.php | 57 +++++++++++++-------------- flight/net/Response.php | 11 ++++-- flight/net/Route.php | 27 ++++++------- flight/net/Router.php | 4 +- flight/template/View.php | 4 +- flight/util/Collection.php | 6 +-- flight/util/ReturnTypeWillChange.php | 1 - phpcs.xml | 27 +++++++------ tests/RedirectTest.php | 8 +--- 10 files changed, 100 insertions(+), 103 deletions(-) diff --git a/flight/Engine.php b/flight/Engine.php index 89b6b72..e3af4a1 100644 --- a/flight/Engine.php +++ b/flight/Engine.php @@ -67,9 +67,7 @@ use flight\net\Route; */ class Engine { - /** - * @var array List of methods that can be extended in the Engine class. - */ + /** @var array List of methods that can be extended in the Engine class. */ private const MAPPABLE_METHODS = [ 'start', 'stop', 'route', 'halt', 'error', 'notFound', 'render', 'redirect', 'etag', 'lastModified', 'json', 'jsonp', @@ -298,7 +296,7 @@ class Engine */ public function get(?string $key = null) { - if (null === $key) { + if ($key === null) { return $this->vars; } @@ -344,8 +342,9 @@ class Engine */ public function clear(?string $key = null): void { - if (null === $key) { + if ($key === null) { $this->vars = []; + return; } @@ -367,7 +366,7 @@ class Engine * * @param array $middleware Middleware attached to the route. * @param array $params `$route->params`. - * @param string $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 { @@ -376,23 +375,23 @@ class Engine foreach ($middleware as $middleware) { $middleware_object = false; - if ($event_name === 'before') { + if ($event_name === $this->dispatcher::FILTER_BEFORE) { // can be a callable or a class $middleware_object = (is_callable($middleware) === true ? $middleware - : (method_exists($middleware, 'before') === true - ? [$middleware, 'before'] + : (method_exists($middleware, $this->dispatcher::FILTER_BEFORE) === true + ? [$middleware, $this->dispatcher::FILTER_BEFORE] : false ) ); - } elseif ($event_name === 'after') { + } elseif ($event_name === $this->dispatcher::FILTER_AFTER) { // must be an object. No functions allowed here if ( is_object($middleware) === true && !($middleware instanceof Closure) - && method_exists($middleware, 'after') === true + && method_exists($middleware, $this->dispatcher::FILTER_AFTER) === true ) { - $middleware_object = [$middleware, 'after']; + $middleware_object = [$middleware, $this->dispatcher::FILTER_AFTER]; } } @@ -532,9 +531,7 @@ class Engine public function _error(Throwable $e): void { $msg = sprintf( - '

500 Internal Server Error

' . - '

%s (%s)

' . - '
%s
', + '

500 Internal Server Error

%s (%s)

%s
', $e->getMessage(), $e->getCode(), $e->getTraceAsString() @@ -565,7 +562,7 @@ class Engine $response = $this->response(); if (!$response->sent()) { - if (null !== $code) { + if ($code !== null) { $response->status($code); } @@ -633,8 +630,12 @@ class Engine * @param callable $callback Callback function * @param bool $pass_route Pass the matching route object to the callback */ - public function _patch(string $pattern, callable $callback, bool $pass_route = false, string $route_alias = ''): void - { + public function _patch( + string $pattern, + callable $callback, + bool $pass_route = false, + string $route_alias = '' + ): void { $this->router()->map('PATCH ' . $pattern, $callback, $pass_route, $route_alias); } @@ -645,8 +646,12 @@ class Engine * @param callable $callback Callback function * @param bool $pass_route Pass the matching route object to the callback */ - public function _delete(string $pattern, callable $callback, bool $pass_route = false, string $route_alias = ''): void - { + public function _delete( + string $pattern, + callable $callback, + bool $pass_route = false, + string $route_alias = '' + ): void { $this->router()->map('DELETE ' . $pattern, $callback, $pass_route, $route_alias); } @@ -688,14 +693,10 @@ class Engine */ public function _redirect(string $url, int $code = 303): void { - $base = $this->get('flight.base_url'); - - if (null === $base) { - $base = $this->request()->base; - } + $base = $this->get('flight.base_url') ?? $this->request()->base; // Append base url to redirect url - if ('/' !== $base && false === strpos($url, '://')) { + if ($base !== '/' && strpos($url, '://') === false) { $url = $base . preg_replace('#/+#', '/', '/' . $url); } @@ -717,8 +718,9 @@ class Engine */ public function _render(string $file, ?array $data = null, ?string $key = null): void { - if (null !== $key) { + if ($key !== null) { $this->view()->set($key, $this->view()->fetch($file, $data)); + return; } @@ -790,7 +792,7 @@ class Engine */ public function _etag(string $id, string $type = 'strong'): void { - $id = (('weak' === $type) ? 'W/' : '') . $id; + $id = (($type === 'weak') ? 'W/' : '') . $id; $this->response()->header('ETag', '"' . str_replace('"', '\"', $id) . '"'); diff --git a/flight/net/Request.php b/flight/net/Request.php index eb932c0..b322ae3 100644 --- a/flight/net/Request.php +++ b/flight/net/Request.php @@ -151,7 +151,7 @@ class Request 'method' => self::getMethod(), 'referrer' => self::getVar('HTTP_REFERER'), 'ip' => self::getVar('REMOTE_ADDR'), - 'ajax' => 'XMLHttpRequest' === self::getVar('HTTP_X_REQUESTED_WITH'), + 'ajax' => self::getVar('HTTP_X_REQUESTED_WITH') === 'XMLHttpRequest', 'scheme' => self::getScheme(), 'user_agent' => self::getVar('HTTP_USER_AGENT'), 'type' => self::getVar('CONTENT_TYPE'), @@ -160,7 +160,7 @@ class Request 'data' => new Collection($_POST), 'cookies' => new Collection($_COOKIE), 'files' => new Collection($_FILES), - 'secure' => 'https' === self::getScheme(), + 'secure' => self::getScheme() === 'https', 'accept' => self::getVar('HTTP_ACCEPT'), 'proxy_ip' => self::getProxyIpAddress(), 'host' => self::getVar('HTTP_HOST'), @@ -188,7 +188,7 @@ class Request // This rewrites the url in case the public url and base directories match // (such as installing on a subdirectory in a web server) // @see testInitUrlSameAsBaseDirectory - if ('/' !== $this->base && '' !== $this->base && 0 === strpos($this->url, $this->base)) { + if ($this->base !== '/' && $this->base !== '' && strpos($this->url, $this->base) === 0) { $this->url = substr($this->url, \strlen($this->base)); } @@ -203,9 +203,10 @@ class Request } // Check for JSON input - if (0 === strpos($this->type, 'application/json')) { + if (strpos($this->type, 'application/json') === 0) { $body = $this->getBody(); - if ('' !== $body) { + + if ($body !== '') { $data = json_decode($body, true); if (is_array($data)) { $this->data->setData($data); @@ -225,14 +226,17 @@ class Request { $body = $this->body; - if ('' !== $body) { + if ($body !== '') { return $body; } - $method = self::getMethod(); - - if ('POST' === $method || 'PUT' === $method || 'DELETE' === $method || 'PATCH' === $method) { - $body = file_get_contents($this->stream_path); + switch (self::getMethod()) { + case 'POST': + case 'PUT': + case 'DELETE': + case 'PATCH': + $body = file_get_contents($this->stream_path); + break; } $this->body = $body; @@ -277,7 +281,8 @@ class Request foreach ($forwarded as $key) { if (\array_key_exists($key, $_SERVER)) { sscanf($_SERVER[$key], '%[^,]', $ip); - if (false !== filter_var($ip, \FILTER_VALIDATE_IP, $flags)) { + + if (filter_var($ip, \FILTER_VALIDATE_IP, $flags) !== false) { return $ip; } } @@ -321,13 +326,15 @@ class Request public static function getHeaders(): array { $headers = []; + foreach ($_SERVER as $key => $value) { - if (0 === strpos($key, 'HTTP_')) { + if (strpos($key, 'HTTP_') === 0) { // converts headers like HTTP_CUSTOM_HEADER to Custom-Header $key = str_replace(' ', '-', ucwords(str_replace('_', ' ', strtolower(substr($key, 5))))); $headers[$key] = $value; } } + return $headers; } @@ -336,10 +343,8 @@ class Request * * @param string $header Header name. Can be caps, lowercase, or mixed. * @param string $default Default value if the header does not exist - * - * @return string */ - public static function header(string $header, $default = '') + public static function header(string $header, $default = ''): string { return self::getHeader($header, $default); } @@ -354,21 +359,13 @@ class Request return self::getHeaders(); } - /** - * Gets the full request URL. - * - * @return string URL - */ + /** Gets the full request URL. */ public function getFullUrl(): string { return $this->scheme . '://' . $this->host . $this->url; } - /** - * Grabs the scheme and host. Does not end with a / - * - * @return string - */ + /** Grabs the scheme and host. Does not end with a / */ public function getBaseUrl(): string { return $this->scheme . '://' . $this->host; @@ -396,18 +393,18 @@ class Request /** * Gets the URL Scheme * - * @return string 'http'|'https' + * @return 'http'|'https' */ public static function getScheme(): string { if ( - (isset($_SERVER['HTTPS']) && 'on' === strtolower($_SERVER['HTTPS'])) + (isset($_SERVER['HTTPS']) && strtolower($_SERVER['HTTPS']) === 'on') || - (isset($_SERVER['HTTP_X_FORWARDED_PROTO']) && 'https' === $_SERVER['HTTP_X_FORWARDED_PROTO']) + (isset($_SERVER['HTTP_X_FORWARDED_PROTO']) && $_SERVER['HTTP_X_FORWARDED_PROTO'] === 'https') || - (isset($_SERVER['HTTP_FRONT_END_HTTPS']) && 'on' === $_SERVER['HTTP_FRONT_END_HTTPS']) + (isset($_SERVER['HTTP_FRONT_END_HTTPS']) && $_SERVER['HTTP_FRONT_END_HTTPS'] === 'on') || - (isset($_SERVER['REQUEST_SCHEME']) && 'https' === $_SERVER['REQUEST_SCHEME']) + (isset($_SERVER['REQUEST_SCHEME']) && $_SERVER['REQUEST_SCHEME'] === 'https') ) { return 'https'; } diff --git a/flight/net/Response.php b/flight/net/Response.php index 761d1a6..291e84e 100644 --- a/flight/net/Response.php +++ b/flight/net/Response.php @@ -139,7 +139,7 @@ class Response */ public function status(?int $code = null) { - if (null === $code) { + if ($code === null) { return $this->status; } @@ -263,19 +263,22 @@ class Response */ public function cache($expires): self { - if (false === $expires) { + if ($expires === false) { $this->headers['Expires'] = 'Mon, 26 Jul 1997 05:00:00 GMT'; + $this->headers['Cache-Control'] = [ 'no-store, no-cache, must-revalidate', 'post-check=0, pre-check=0', 'max-age=0', ]; + $this->headers['Pragma'] = 'no-cache'; } else { $expires = \is_int($expires) ? $expires : strtotime($expires); $this->headers['Expires'] = gmdate('D, d M Y H:i:s', $expires) . ' GMT'; $this->headers['Cache-Control'] = 'max-age=' . ($expires - time()); - if (isset($this->headers['Pragma']) && 'no-cache' == $this->headers['Pragma']) { + + if (isset($this->headers['Pragma']) && $this->headers['Pragma'] === 'no-cache') { unset($this->headers['Pragma']); } } @@ -291,7 +294,7 @@ class Response public function sendHeaders(): self { // Send status code header - if (false !== strpos(\PHP_SAPI, 'cgi')) { + if (strpos(\PHP_SAPI, 'cgi') !== false) { // @codeCoverageIgnoreStart $this->setRealHeader( sprintf( diff --git a/flight/net/Route.php b/flight/net/Route.php index 2070cc6..e73906b 100644 --- a/flight/net/Route.php +++ b/flight/net/Route.php @@ -95,7 +95,7 @@ class Route public function matchUrl(string $url, bool $case_sensitive = false): bool { // Wildcard or exact match - if ('*' === $this->pattern || $this->pattern === $url) { + if ($this->pattern === '*' || $this->pattern === $url) { return true; } @@ -110,8 +110,9 @@ class Route for ($i = 0; $i < $len; $i++) { if ($url[$i] === '/') { - $n++; + ++$n; } + if ($n === $count) { break; } @@ -136,24 +137,20 @@ class Route $regex ); - if ('/' === $last_char) { // Fix trailing slash - $regex .= '?'; - } else { // Allow trailing slash - $regex .= '/?'; - } + $regex .= $last_char === '/' ? '?' : '/?'; // Attempt to match route and named parameters - if (preg_match('#^' . $regex . '(?:\?[\s\S]*)?$#' . (($case_sensitive) ? '' : 'i'), $url, $matches)) { - foreach ($ids as $k => $v) { - $this->params[$k] = (\array_key_exists($k, $matches)) ? urldecode($matches[$k]) : null; - } - - $this->regex = $regex; + if (!preg_match('#^' . $regex . '(?:\?[\s\S]*)?$#' . (($case_sensitive) ? '' : 'i'), $url, $matches)) { + return false; + } - return true; + foreach (array_keys($ids) as $k) { + $this->params[$k] = (\array_key_exists($k, $matches)) ? urldecode($matches[$k]) : null; } - return false; + $this->regex = $regex; + + return true; } /** diff --git a/flight/net/Router.php b/flight/net/Router.php index 895b337..2387baa 100644 --- a/flight/net/Router.php +++ b/flight/net/Router.php @@ -96,7 +96,7 @@ class Router $methods = ['*']; - if (false !== strpos($url, ' ')) { + if (strpos($url, ' ') !== false) { [$method, $url] = explode(' ', $url, 2); $url = trim($url); $methods = explode('|', $method); @@ -211,10 +211,12 @@ class Router public function route(Request $request) { $url_decoded = urldecode($request->url); + while ($route = $this->current()) { if ($route->matchMethod($request->method) && $route->matchUrl($url_decoded, $this->case_sensitive)) { return $route; } + $this->next(); } diff --git a/flight/template/View.php b/flight/template/View.php index d1bc07f..c43a35f 100644 --- a/flight/template/View.php +++ b/flight/template/View.php @@ -88,7 +88,7 @@ class View */ public function clear(?string $key = null): self { - if (null === $key) { + if ($key === null) { $this->vars = []; } else { unset($this->vars[$key]); @@ -169,7 +169,7 @@ class View $is_windows = \strtoupper(\substr(PHP_OS, 0, 3)) === 'WIN'; - if (('/' == \substr($file, 0, 1)) || ($is_windows === true && ':' == \substr($file, 1, 1))) { + if ((\substr($file, 0, 1) === '/') || ($is_windows && \substr($file, 1, 1) === ':')) { return $file; } diff --git a/flight/util/Collection.php b/flight/util/Collection.php index 6ffe0b5..e17ed37 100644 --- a/flight/util/Collection.php +++ b/flight/util/Collection.php @@ -95,7 +95,7 @@ class Collection implements ArrayAccess, Iterator, Countable, JsonSerializable #[\ReturnTypeWillChange] public function offsetSet($offset, $value): void { - if (null === $offset) { + if ($offset === null) { $this->data[] = $value; } else { $this->data[$offset] = $value; @@ -166,9 +166,7 @@ class Collection implements ArrayAccess, Iterator, Countable, JsonSerializable */ public function valid(): bool { - $key = key($this->data); - - return null !== $key; + return key($this->data) !== null; } /** diff --git a/flight/util/ReturnTypeWillChange.php b/flight/util/ReturnTypeWillChange.php index 31a929b..1eba39e 100644 --- a/flight/util/ReturnTypeWillChange.php +++ b/flight/util/ReturnTypeWillChange.php @@ -3,7 +3,6 @@ declare(strict_types=1); // This file is only here so that the PHP8 attribute for doesn't throw an error in files -// phpcs:ignoreFile PSR1.Classes.ClassDeclaration.MissingNamespace class ReturnTypeWillChange { } diff --git a/phpcs.xml b/phpcs.xml index 8f8acb6..bafe06d 100644 --- a/phpcs.xml +++ b/phpcs.xml @@ -18,31 +18,34 @@ + - + + + + - - - - - - - - - - -
+ + + + + + + + + + flight/ tests/ tests/views/* diff --git a/tests/RedirectTest.php b/tests/RedirectTest.php index e44186e..07698eb 100644 --- a/tests/RedirectTest.php +++ b/tests/RedirectTest.php @@ -21,7 +21,7 @@ class RedirectTest extends TestCase public function getBaseUrl($base, $url) { - if ('/' !== $base && false === strpos($url, '://')) { + if ($base !== '/' && strpos($url, '://') === false) { $url = preg_replace('#/+#', '/', $base . '/' . $url); } @@ -67,11 +67,7 @@ class RedirectTest extends TestCase public function testBaseOverride() { $url = 'login'; - if (null !== $this->app->get('flight.base_url')) { - $base = $this->app->get('flight.base_url'); - } else { - $base = $this->app->request()->base; - } + $base = $this->app->get('flight.base_url') ?? $this->app->request()->base; self::assertEquals('/testdir/login', $this->getBaseUrl($base, $url)); } From 86df1cb2bd20075357092a69a6f7e9186fcda611 Mon Sep 17 00:00:00 2001 From: fadrian06 Date: Sun, 18 Feb 2024 22:11:29 -0400 Subject: [PATCH 04/43] Resolved phpcs Generic standard rules --- tests/RouterTest.php | 10 ++++++---- tests/server/index.php | 18 +++++++++++++----- 2 files changed, 19 insertions(+), 9 deletions(-) diff --git a/tests/RouterTest.php b/tests/RouterTest.php index 1b52ae1..84322c2 100644 --- a/tests/RouterTest.php +++ b/tests/RouterTest.php @@ -262,12 +262,14 @@ class RouterTest extends TestCase { $this->router->map('GET /api/intune/hey', [$this, 'ok']); + $error_description = 'error_description=AADSTS65004%3a+User+declined+to+consent+to+access+the'; + $error_description .= '+app.%0d%0aTrace+ID%3a+747c0cc1-ccbd-4e53-8e2f-48812eb24100%0d%0a'; + $error_description .= 'Correlation+ID%3a+362e3cb3-20ef-400b-904e-9983bd989184%0d%0a'; + $error_description .= 'Timestamp%3a+2022-09-08+09%3a58%3a12Z'; + $query_params = [ 'error=access_denied', - 'error_description=AADSTS65004%3a+User+declined+to+consent+to+access+the' - . '+app.%0d%0aTrace+ID%3a+747c0cc1-ccbd-4e53-8e2f-48812eb24100%0d%0a' - . 'Correlation+ID%3a+362e3cb3-20ef-400b-904e-9983bd989184%0d%0a' - . 'Timestamp%3a+2022-09-08+09%3a58%3a12Z', + $error_description, 'error_uri=https%3a%2f%2flogin.microsoftonline.com%2ferror%3fcode%3d65004', 'admin_consent=True', 'state=x2EUE0fcSj#' diff --git a/tests/server/index.php b/tests/server/index.php index b3d5230..c13e359 100644 --- a/tests/server/index.php +++ b/tests/server/index.php @@ -9,7 +9,9 @@ declare(strict_types=1); * @author Kristaps Muižnieks https://github.com/krmu */ - require file_exists(__DIR__ . '/../../vendor/autoload.php') ? __DIR__ . '/../../vendor/autoload.php' : __DIR__ . '/../../flight/autoload.php'; +require_once 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', './'); @@ -95,17 +97,23 @@ Flight::group('', function () { Flight::route('/error', function () { trigger_error('This is a successful error'); }); -}, [ new LayoutMiddleware() ]); +}, [new LayoutMiddleware()]); Flight::map('error', function (Throwable $e) { + $styles = join(';', [ + 'border: 2px solid red', + 'padding: 21px', + 'background: lightgray', + 'font-weight: bold' + ]); + echo sprintf( - '

500 Internal Server Error

' . - '

%s (%s)

' . - '
%s
', + "

500 Internal Server Error

%s (%s)

%s
", $e->getMessage(), $e->getCode(), str_replace(getenv('PWD'), '***CONFIDENTIAL***', $e->getTraceAsString()) ); + echo "
Go back"; }); Flight::map('notFound', function () { From 26de40b25556d2a070d7dd4caeb83a3351daa0fe Mon Sep 17 00:00:00 2001 From: fadrian06 Date: Mon, 19 Feb 2024 03:05:57 -0400 Subject: [PATCH 05/43] Created phpcs.xml.dist with automatic local config creation script --- .gitignore | 1 + composer.json | 3 ++- phpcs.xml => phpcs.xml.dist | 0 3 files changed, 3 insertions(+), 1 deletion(-) rename phpcs.xml => phpcs.xml.dist (100%) diff --git a/.gitignore b/.gitignore index fd379ad..6ca7489 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,4 @@ coverage/ *.sublime* .vscode/ clover.xml +phpcs.xml diff --git a/composer.json b/composer.json index ba3dcc0..6c61fde 100644 --- a/composer.json +++ b/composer.json @@ -62,7 +62,8 @@ "test-coverage:win": "del clover.xml && phpunit --coverage-html=coverage --coverage-clover=clover.xml && coverage-check clover.xml 100", "lint": "phpstan --no-progress -cphpstan.neon", "beautify": "phpcbf --standard=phpcs.xml", - "phpcs": "phpcs --standard=phpcs.xml -n" + "phpcs": "phpcs --standard=phpcs.xml -n", + "post-install-cmd": ["php -r \"if (!file_exists('phpcs.xml')) copy('phpcs.xml.dist', 'phpcs.xml');\""] }, "suggest": { "latte/latte": "Latte template engine", diff --git a/phpcs.xml b/phpcs.xml.dist similarity index 100% rename from phpcs.xml rename to phpcs.xml.dist From 4b4f07ea56e59e38481a36cec1df2ff5fcd35a1e Mon Sep 17 00:00:00 2001 From: n0nag0n Date: Sat, 23 Mar 2024 12:49:09 -0600 Subject: [PATCH 06/43] added comment, removed unused file reference. --- flight/Flight.php | 5 ++++- tests/DocExamplesTest.php | 18 ++++++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/flight/Flight.php b/flight/Flight.php index 0a42489..a04ad80 100644 --- a/flight/Flight.php +++ b/flight/Flight.php @@ -2,7 +2,6 @@ declare(strict_types=1); -use flight\core\Dispatcher; use flight\Engine; use flight\net\Request; use flight\net\Response; @@ -24,6 +23,10 @@ require_once __DIR__ . '/autoload.php'; * @method static void stop(?int $code = null) Stops the framework and sends a response. * @method static void halt(int $code = 200, string $message = '', bool $actuallyExit = true) * Stop the framework with an optional status code and message. + * @method static void register(string $name, string $class, array $params = [], ?callable $callback = null) + * Registers a class to a framework method. + * @method static void unregister(string $methodName) + * Unregisters a class to a framework method. * @method static void registerContainerHandler(callable|object $containerHandler) Registers a container handler. * * # Routing diff --git a/tests/DocExamplesTest.php b/tests/DocExamplesTest.php index 1518363..2bba482 100644 --- a/tests/DocExamplesTest.php +++ b/tests/DocExamplesTest.php @@ -74,4 +74,22 @@ class DocExamplesTest extends TestCase Flight::app()->handleException(new Exception('Error')); $this->expectOutputString('Custom: Error'); } + + public function testGetRouterStatically() + { + $router = Flight::router(); + Flight::request()->method = 'GET'; + Flight::request()->url = '/'; + + $router->get( + '/', + function () { + Flight::response()->write('from resp '); + } + ); + + Flight::start(); + + $this->expectOutputString('from resp '); + } } From 43300d5758f3c7d190167a192cfd89e8a1f0e757 Mon Sep 17 00:00:00 2001 From: Austin Collier Date: Fri, 29 Mar 2024 22:19:24 -0600 Subject: [PATCH 07/43] added ability to load classes with _ in them --- flight/core/Loader.php | 22 ++++++++++++++++++-- flight/util/ReturnTypeWillChange.php | 9 -------- tests/EngineTest.php | 10 +++++++-- tests/LoaderTest.php | 13 ++++++++++++ tests/run_all_tests.sh | 31 +++++++++++++++++++++------- tests/server/LayoutMiddleware.php | 1 + tests/server/Pascal_Snake_Case.php | 11 ++++++++++ tests/server/index.php | 8 +++---- 8 files changed, 81 insertions(+), 24 deletions(-) delete mode 100644 flight/util/ReturnTypeWillChange.php create mode 100644 tests/server/Pascal_Snake_Case.php diff --git a/flight/core/Loader.php b/flight/core/Loader.php index 9792949..1824b9c 100644 --- a/flight/core/Loader.php +++ b/flight/core/Loader.php @@ -25,6 +25,11 @@ class Loader */ protected array $classes = []; + /** + * If this is disabled, classes can load with underscores + */ + protected static bool $v2ClassLoading = true; + /** * Class instances. * @@ -190,14 +195,14 @@ class Loader */ public static function loadClass(string $class): void { - $classFile = str_replace(['\\', '_'], '/', $class) . '.php'; + $replace_chars = self::$v2ClassLoading === true ? ['\\', '_'] : ['\\']; + $classFile = str_replace($replace_chars, '/', $class) . '.php'; foreach (self::$dirs as $dir) { $filePath = "$dir/$classFile"; if (file_exists($filePath)) { require_once $filePath; - return; } } @@ -220,4 +225,17 @@ class Loader } } } + + + /** + * Sets the value for V2 class loading. + * + * @param bool $value The value to set for V2 class loading. + * + * @return void + */ + public static function setV2ClassLoading(bool $value): void + { + self::$v2ClassLoading = $value; + } } diff --git a/flight/util/ReturnTypeWillChange.php b/flight/util/ReturnTypeWillChange.php deleted file mode 100644 index 31a929b..0000000 --- a/flight/util/ReturnTypeWillChange.php +++ /dev/null @@ -1,9 +0,0 @@ -route('/container', Container::class.'->testThePdoWrapper'); $engine->request()->url = '/container'; - $this->expectException(ErrorException::class); - $this->expectExceptionMessageMatches("/Passing null to parameter/"); + // php 7.4 will throw a PDO exception, but php 8 will throw an ErrorException + if(version_compare(PHP_VERSION, '8.0.0', '<')) { + $this->expectException(PDOException::class); + $this->expectExceptionMessageMatches("/invalid data source name/"); + } else { + $this->expectException(ErrorException::class); + $this->expectExceptionMessageMatches("/Passing null to parameter/"); + } $engine->start(); } diff --git a/tests/LoaderTest.php b/tests/LoaderTest.php index 44a89d0..9b6047c 100644 --- a/tests/LoaderTest.php +++ b/tests/LoaderTest.php @@ -152,4 +152,17 @@ class LoaderTest extends TestCase __DIR__ . '/classes' ], $loader->getDirectories()); } + + public function testV2ClassLoading() + { + $loader = new class extends Loader { + public static function getV2ClassLoading() + { + return self::$v2ClassLoading; + } + }; + $this->assertTrue($loader::getV2ClassLoading()); + $loader::setV2ClassLoading(false); + $this->assertFalse($loader::getV2ClassLoading()); + } } diff --git a/tests/run_all_tests.sh b/tests/run_all_tests.sh index d38bf0d..72ec8ba 100644 --- a/tests/run_all_tests.sh +++ b/tests/run_all_tests.sh @@ -1,9 +1,26 @@ #!/bin/bash -# Run all tests -composer lint -composer beautify -composer phpcs -composer test-coverage -xdg-open http://localhost:8000 -composer test-server \ No newline at end of file +php_versions=("php7.4" "php8.0" "php8.1" "php8.2" "php8.3") + +count=${#php_versions[@]} + + +echo "Prettifying code first" +vendor/bin/phpcbf --standard=phpcs.xml + +set -e +for ((i = 0; i < count; i++)); do + if type "${php_versions[$i]}" &> /dev/null; then + echo "Running tests for ${php_versions[$i]}" + echo " ${php_versions[$i]} vendor/bin/phpunit" + ${php_versions[$i]} vendor/bin/phpunit + + echo "Running PHPStan" + echo " ${php_versions[$i]} vendor/bin/phpstan" + ${php_versions[$i]} vendor/bin/phpstan + + echo "Running PHPCS" + echo " ${php_versions[$i]} vendor/bin/phpcs --standard=phpcs.xml -n" + ${php_versions[$i]} vendor/bin/phpcs --standard=phpcs.xml -n + fi +done \ No newline at end of file diff --git a/tests/server/LayoutMiddleware.php b/tests/server/LayoutMiddleware.php index 500cbd7..b89c4e0 100644 --- a/tests/server/LayoutMiddleware.php +++ b/tests/server/LayoutMiddleware.php @@ -83,6 +83,7 @@ class LayoutMiddleware
  • UTF8 URL w/ Param
  • Dice Container
  • No Container Registered
  • +
  • Pascal_Snake_Case
  • HTML; echo '
    '; diff --git a/tests/server/Pascal_Snake_Case.php b/tests/server/Pascal_Snake_Case.php new file mode 100644 index 0000000..bbba0d2 --- /dev/null +++ b/tests/server/Pascal_Snake_Case.php @@ -0,0 +1,11 @@ +testUi'); Flight::route('/dice', Container::class . '->testThePdoWrapper'); + Flight::route('/Pascal_Snake_Case', Pascal_Snake_Case::class . '->doILoad'); }, [ new LayoutMiddleware() ]); // Test 9: JSON output (should not output any other html) From e322135d9c8d0947e55bb3f73f98673fefa637a9 Mon Sep 17 00:00:00 2001 From: fadrian06 Date: Mon, 1 Apr 2024 18:45:37 -0400 Subject: [PATCH 08/43] Add symfony/polyfill-80 --- composer.json | 2 ++ flight/core/Dispatcher.php | 31 ++++++++++++++----------------- 2 files changed, 16 insertions(+), 17 deletions(-) diff --git a/composer.json b/composer.json index 2be3e4b..e30b937 100644 --- a/composer.json +++ b/composer.json @@ -25,6 +25,8 @@ "require": { "php": "^7.4|^8.0|^8.1|^8.2|^8.3", "ext-json": "*" + "ext-json": "*", + "symfony/polyfill-php80": "^1.29" }, "autoload": { "files": [ diff --git a/flight/core/Dispatcher.php b/flight/core/Dispatcher.php index 6a167a8..dd202be 100644 --- a/flight/core/Dispatcher.php +++ b/flight/core/Dispatcher.php @@ -251,7 +251,10 @@ class Dispatcher */ public function execute($callback, array &$params = []) { - if (is_string($callback) === true && (strpos($callback, '->') !== false || strpos($callback, '::') !== false)) { + if ( + is_string($callback) + && (str_contains($callback, '->') || str_contains($callback, '::')) + ) { $callback = $this->parseStringClassAndMethod($callback); } @@ -327,27 +330,21 @@ class Dispatcher } [$class, $method] = $func; - $resolvedClass = null; - // Only execute the container handler if it's not a Flight class - if ( - $this->containerHandler !== null && - ( - ( - is_object($class) === true && - strpos(get_class($class), 'flight\\') === false - ) || - is_string($class) === true - ) - ) { - $containerHandler = $this->containerHandler; - $resolvedClass = $this->resolveContainerClass($containerHandler, $class, $params); - if ($resolvedClass !== null) { + $mustUseTheContainer = $this->containerHandler && ( + (is_object($class) && !str_starts_with(get_class($class), 'flight\\')) + || is_string($class) + ); + + if ($mustUseTheContainer) { + $resolvedClass = $this->resolveContainerClass($class, $params); + + if ($resolvedClass) { $class = $resolvedClass; } } - $this->verifyValidClassCallable($class, $method, $resolvedClass); + $this->verifyValidClassCallable($class, $method, $resolvedClass ?? null); // Class is a string, and method exists, create the object by hand and inject only the Engine if (is_string($class) === true) { From d931b41b464d3d1551e9acdeb6fe815ae2e04043 Mon Sep 17 00:00:00 2001 From: fadrian06 Date: Mon, 1 Apr 2024 18:51:54 -0400 Subject: [PATCH 09/43] Improved docblocks --- flight/core/Dispatcher.php | 103 +++++++++++++++++-------------------- 1 file changed, 47 insertions(+), 56 deletions(-) diff --git a/flight/core/Dispatcher.php b/flight/core/Dispatcher.php index dd202be..f9c1b68 100644 --- a/flight/core/Dispatcher.php +++ b/flight/core/Dispatcher.php @@ -4,7 +4,6 @@ declare(strict_types=1); namespace flight\core; -use Closure; use Exception; use flight\Engine; use InvalidArgumentException; @@ -31,13 +30,13 @@ class Dispatcher /** @var ?Engine $engine Engine instance */ protected ?Engine $engine = null; - /** @var array Mapped events. */ + /** @var array Mapped events. */ protected array $events = []; /** * Method filters. * - * @var array &$params, mixed &$output): (void|false)>>> + * @var array &$params, mixed &$output): (void|false)>>> */ protected array $filters = []; @@ -68,11 +67,11 @@ class Dispatcher /** * Dispatches an event. * - * @param string $name Event name + * @param string $name Event name. * @param array $params Callback parameters. * * @return mixed Output of callback - * @throws Exception If event name isn't found or if event throws an `Exception` + * @throws Exception If event name isn't found or if event throws an `Exception`. */ public function run(string $name, array $params = []) { @@ -110,7 +109,7 @@ class Dispatcher $requestedMethod = $this->get($eventName); if ($requestedMethod === null) { - throw new Exception("Event '{$eventName}' isn't found."); + throw new Exception("Event '$eventName' isn't found."); } return $this->execute($requestedMethod, $params); @@ -138,8 +137,8 @@ class Dispatcher /** * Assigns a callback to an event. * - * @param string $name Event name - * @param Closure(): (void|mixed) $callback Callback function + * @param string $name Event name. + * @param callable(): (void|mixed) $callback Callback function. * * @return $this */ @@ -153,9 +152,9 @@ class Dispatcher /** * Gets an assigned callback. * - * @param string $name Event name + * @param string $name Event name. * - * @return null|(Closure(): (void|mixed)) $callback Callback function + * @return null|(callable(): (void|mixed)) $callback Callback function. */ public function get(string $name): ?callable { @@ -165,9 +164,9 @@ class Dispatcher /** * Checks if an event has been set. * - * @param string $name Event name + * @param string $name Event name. * - * @return bool Event status + * @return bool If event exists or doesn't exists. */ public function has(string $name): bool { @@ -177,7 +176,7 @@ class Dispatcher /** * Clears an event. If no name is given, all events will be removed. * - * @param ?string $name Event name + * @param ?string $name Event name. */ public function clear(?string $name = null): void { @@ -196,8 +195,8 @@ class Dispatcher * Hooks a callback to an event. * * @param string $name Event name - * @param 'before'|'after' $type Filter type - * @param Closure(array &$params, string &$output): (void|false) $callback + * @param 'before'|'after' $type Filter type. + * @param callable(array &$params, string &$output): (void|false) $callback * * @return $this */ @@ -217,10 +216,10 @@ class Dispatcher /** * Executes a chain of method filters. * - * @param array &$params, mixed &$output): (void|false)> $filters - * Chain of filters- - * @param array $params Method parameters - * @param mixed $output Method output + * @param array &$params, mixed &$output): (void|false)> $filters + * Chain of filters. + * @param array $params Method parameters. + * @param mixed $output Method output. * * @throws Exception If an event throws an `Exception` or if `$filters` contains an invalid filter. */ @@ -242,11 +241,11 @@ class Dispatcher /** * Executes a callback function. * - * @param callable-string|(Closure(): mixed)|array{class-string|object, string} $callback - * Callback function - * @param array $params Function parameters + * @param callable-string|(callable(): mixed)|array{class-string|object, string} $callback + * Callback function. + * @param array $params Function parameters. * - * @return mixed Function results + * @return mixed Function results. * @throws Exception If `$callback` also throws an `Exception`. */ public function execute($callback, array &$params = []) @@ -266,28 +265,26 @@ class Dispatcher * * @param string $classAndMethod Class and method * - * @return array{class-string|object, string} Class and method + * @return array{0: class-string|object, 1: string} Class and method */ public function parseStringClassAndMethod(string $classAndMethod): array { - $class_parts = explode('->', $classAndMethod); - if (count($class_parts) === 1) { - $class_parts = explode('::', $class_parts[0]); - } + $classParts = explode('->', $classAndMethod); - $class = $class_parts[0]; - $method = $class_parts[1]; + if (count($classParts) === 1) { + $classParts = explode('::', $classParts[0]); + } - return [ $class, $method ]; + return $classParts; } /** * Calls a function. * - * @param callable $func Name of function to call - * @param array &$params Function parameters + * @param callable $func Name of function to call. + * @param array &$params Function parameters. * - * @return mixed Function results + * @return mixed Function results. * @deprecated 3.7.0 Use invokeCallable instead */ public function callFunction(callable $func, array &$params = []) @@ -298,12 +295,12 @@ class Dispatcher /** * Invokes a method. * - * @param array{class-string|object, string} $func Class method - * @param array &$params Class method parameters + * @param array{0: class-string|object, 1: string} $func Class method. + * @param array &$params Class method parameters. * - * @return mixed Function results + * @return mixed Function results. * @throws TypeError For nonexistent class name. - * @deprecated 3.7.0 Use invokeCallable instead + * @deprecated 3.7.0 Use invokeCallable instead. */ public function invokeMethod(array $func, array &$params = []) { @@ -313,12 +310,12 @@ class Dispatcher /** * Invokes a callable (anonymous function or Class->method). * - * @param array{class-string|object, string}|Callable $func Class method - * @param array &$params Class method parameters + * @param array{0: class-string|object, 1: string}|callable $func Class method. + * @param array &$params Class method parameters. * - * @return mixed Function results + * @return mixed Function results. * @throws TypeError For nonexistent class name. - * @throws InvalidArgumentException If the constructor requires parameters + * @throws InvalidArgumentException If the constructor requires parameters. * @version 3.7.0 */ public function invokeCallable($func, array &$params = []) @@ -357,10 +354,10 @@ class Dispatcher /** * Handles invalid callback types. * - * @param callable-string|(Closure(): mixed)|array{class-string|object, string} $callback - * Callback function + * @param callable-string|(callable(): mixed)|array{0: class-string|object, 1: string} $callback + * Callback function. * - * @throws InvalidArgumentException If `$callback` is an invalid type + * @throws InvalidArgumentException If `$callback` is an invalid type. */ protected function verifyValidFunction($callback): void { @@ -378,13 +375,11 @@ class Dispatcher /** * Verifies if the provided class and method are valid callable. * - * @param string|object $class The class name. + * @param class-string|object $class The class name. * @param string $method The method name. * @param object|null $resolvedClass The resolved class. * * @throws Exception If the class or method is not found. - * - * @return void */ protected function verifyValidClassCallable($class, $method, $resolvedClass): void { @@ -413,10 +408,10 @@ class Dispatcher * Resolves the container class. * * @param callable|object $container_handler Dependency injection container - * @param class-string $class Class name - * @param array &$params Class constructor parameters + * @param class-string $class Class name. + * @param array &$params Class constructor parameters. * - * @return object Class object + * @return ?object Class object. */ protected function resolveContainerClass($container_handler, $class, array &$params) { @@ -451,11 +446,7 @@ class Dispatcher return $class_object; } - /** - * Because this could throw an exception in the middle of an output buffer, - * - * @return void - */ + /** Because this could throw an exception in the middle of an output buffer, */ protected function fixOutputBuffering(): void { // Cause PHPUnit has 1 level of output buffering by default From 4616f521cdb612841366cafc56cd876051206f74 Mon Sep 17 00:00:00 2001 From: fadrian06 Date: Mon, 1 Apr 2024 18:55:26 -0400 Subject: [PATCH 10/43] Dispatcher refactor --- flight/core/Dispatcher.php | 30 +++++++++++++----------------- 1 file changed, 13 insertions(+), 17 deletions(-) diff --git a/flight/core/Dispatcher.php b/flight/core/Dispatcher.php index f9c1b68..8f1610d 100644 --- a/flight/core/Dispatcher.php +++ b/flight/core/Dispatcher.php @@ -22,12 +22,11 @@ class Dispatcher { public const FILTER_BEFORE = 'before'; public const FILTER_AFTER = 'after'; - private const FILTER_TYPES = [self::FILTER_BEFORE, self::FILTER_AFTER]; - /** @var mixed $containerException Exception message if thrown by setting the container as a callable method */ - protected $containerException = null; + /** Exception message if thrown by setting the container as a callable method. */ + protected ?Exception $containerException = null; - /** @var ?Engine $engine Engine instance */ + /** @var ?Engine $engine Engine instance. */ protected ?Engine $engine = null; /** @var array Mapped events. */ @@ -187,8 +186,7 @@ class Dispatcher return; } - $this->events = []; - $this->filters = []; + $this->reset(); } /** @@ -202,8 +200,10 @@ class Dispatcher */ public function hook(string $name, string $type, callable $callback): self { - if (!in_array($type, self::FILTER_TYPES, true)) { - $noticeMessage = "Invalid filter type '$type', use " . join('|', self::FILTER_TYPES); + static $filterTypes = [self::FILTER_BEFORE, self::FILTER_AFTER]; + + if (!in_array($type, $filterTypes, true)) { + $noticeMessage = "Invalid filter type '$type', use " . join('|', $filterTypes); trigger_error($noticeMessage, E_USER_NOTICE); } @@ -321,8 +321,9 @@ class Dispatcher public function invokeCallable($func, array &$params = []) { // If this is a directly callable function, call it - if (is_array($func) === false) { + if (!is_array($func)) { $this->verifyValidFunction($func); + return call_user_func_array($func, $params); } @@ -344,11 +345,11 @@ class Dispatcher $this->verifyValidClassCallable($class, $method, $resolvedClass ?? null); // Class is a string, and method exists, create the object by hand and inject only the Engine - if (is_string($class) === true) { + if (is_string($class)) { $class = new $class($this->engine); } - return call_user_func_array([ $class, $method ], $params); + return call_user_func_array([$class, $method], $params); } /** @@ -361,12 +362,7 @@ class Dispatcher */ protected function verifyValidFunction($callback): void { - $isInvalidFunctionName = ( - is_string($callback) - && !function_exists($callback) - ); - - if ($isInvalidFunctionName) { + if (is_string($callback) && !function_exists($callback)) { throw new InvalidArgumentException('Invalid callback specified.'); } } From 43b689fa6cb1c4ea7e585b8d25aebf7e340d77bb Mon Sep 17 00:00:00 2001 From: fadrian06 Date: Mon, 1 Apr 2024 18:57:29 -0400 Subject: [PATCH 11/43] Add Short After Filter Syntax with one parameter --- flight/core/Dispatcher.php | 11 ++++++++++ tests/DispatcherTest.php | 41 ++++++++++++++++++++++++++++---------- 2 files changed, 42 insertions(+), 10 deletions(-) diff --git a/flight/core/Dispatcher.php b/flight/core/Dispatcher.php index 8f1610d..f08d1f0 100644 --- a/flight/core/Dispatcher.php +++ b/flight/core/Dispatcher.php @@ -7,6 +7,7 @@ namespace flight\core; use Exception; use flight\Engine; use InvalidArgumentException; +use ReflectionFunction; use TypeError; /** @@ -208,6 +209,16 @@ class Dispatcher trigger_error($noticeMessage, E_USER_NOTICE); } + if ($type === self::FILTER_AFTER) { + $callbackInfo = new ReflectionFunction($callback); + $parametersNumber = $callbackInfo->getNumberOfParameters(); + + if ($parametersNumber === 1) { + /** @disregard &$params in after filters are deprecated. */ + $callback = fn (array &$params, &$output) => $callback($output); + } + } + $this->filters[$name][$type][] = $callback; return $this; diff --git a/tests/DispatcherTest.php b/tests/DispatcherTest.php index a755666..418897d 100644 --- a/tests/DispatcherTest.php +++ b/tests/DispatcherTest.php @@ -37,9 +37,7 @@ class DispatcherTest extends TestCase public function testFunctionMapping(): void { - $this->dispatcher->set('map2', function (): string { - return 'hello'; - }); + $this->dispatcher->set('map2', fn (): string => 'hello'); $this->assertSame('hello', $this->dispatcher->run('map2')); } @@ -61,6 +59,9 @@ class DispatcherTest extends TestCase ->set('map-event', $customFunction) ->set('map-event-2', $anotherFunction); + $this->assertTrue($this->dispatcher->has('map-event')); + $this->assertTrue($this->dispatcher->has('map-event-2')); + $this->dispatcher->clear(); $this->assertFalse($this->dispatcher->has('map-event')); @@ -76,6 +77,9 @@ class DispatcherTest extends TestCase ->set('map-event', $customFunction) ->set('map-event-2', $anotherFunction); + $this->assertTrue($this->dispatcher->has('map-event')); + $this->assertTrue($this->dispatcher->has('map-event-2')); + $this->dispatcher->clear('map-event'); $this->assertFalse($this->dispatcher->has('map-event')); @@ -105,9 +109,7 @@ class DispatcherTest extends TestCase public function testBeforeAndAfter(): void { - $this->dispatcher->set('hello', function (string $name): string { - return "Hello, $name!"; - }); + $this->dispatcher->set('hello', fn (string $name): string => "Hello, $name!"); $this->dispatcher ->hook('hello', Dispatcher::FILTER_BEFORE, function (array &$params): void { @@ -124,6 +126,25 @@ class DispatcherTest extends TestCase $this->assertSame('Hello, Fred! Have a nice day!', $result); } + public function testBeforeAndAfterWithShortAfterFilterSyntax(): void + { + $this->dispatcher->set('hello', fn (string $name): string => "Hello, $name!"); + + $this->dispatcher + ->hook('hello', Dispatcher::FILTER_BEFORE, function (array &$params): void { + // Manipulate the parameter + $params[0] = 'Fred'; + }) + ->hook('hello', Dispatcher::FILTER_AFTER, function (string &$output): void { + // Manipulate the output + $output .= ' Have a nice day!'; + }); + + $result = $this->dispatcher->run('hello', ['Bob']); + + $this->assertSame('Hello, Fred! Have a nice day!', $result); + } + public function testInvalidCallback(): void { $this->expectException(Exception::class); @@ -245,7 +266,7 @@ class DispatcherTest extends TestCase public function testInvokeMethod(): void { $class = new TesterClass('param1', 'param2', 'param3', 'param4', 'param5', 'param6'); - $result = $this->dispatcher->invokeMethod([ $class, 'instanceMethod' ]); + $result = $this->dispatcher->invokeMethod([$class, 'instanceMethod']); $this->assertSame('param1', $class->param2); } @@ -271,7 +292,7 @@ class DispatcherTest extends TestCase public function testExecuteStringClassNoConstructArraySyntax(): void { - $result = $this->dispatcher->execute([ Hello::class, 'sayHi' ]); + $result = $this->dispatcher->execute([Hello::class, 'sayHi']); $this->assertSame('hello', $result); } @@ -298,7 +319,7 @@ class DispatcherTest extends TestCase $engine = new Engine(); $engine->set('test_me_out', 'You got it boss!'); $this->dispatcher->setEngine($engine); - $result = $this->dispatcher->execute([ ContainerDefault::class, 'testTheContainer' ]); + $result = $this->dispatcher->execute([ContainerDefault::class, 'testTheContainer']); $this->assertSame('You got it boss!', $result); } @@ -306,6 +327,6 @@ class DispatcherTest extends TestCase { $this->expectException(TypeError::class); $this->expectExceptionMessageMatches('#tests\\\\classes\\\\ContainerDefault::__construct\(\).+flight\\\\Engine, null given#'); - $result = $this->dispatcher->execute([ ContainerDefault::class, 'testTheContainer' ]); + $result = $this->dispatcher->execute([ContainerDefault::class, 'testTheContainer']); } } From 389ebfd32978c9556367c76f2d121bf52dd507ba Mon Sep 17 00:00:00 2001 From: fadrian06 Date: Mon, 1 Apr 2024 18:59:23 -0400 Subject: [PATCH 12/43] Add PSR/Container dependency for type hint and expose the API --- composer.json | 2 +- flight/core/Dispatcher.php | 70 ++++++++++++++++++++------------------ 2 files changed, 38 insertions(+), 34 deletions(-) diff --git a/composer.json b/composer.json index e30b937..df9bfb0 100644 --- a/composer.json +++ b/composer.json @@ -24,8 +24,8 @@ ], "require": { "php": "^7.4|^8.0|^8.1|^8.2|^8.3", - "ext-json": "*" "ext-json": "*", + "psr/container": "^2.0", "symfony/polyfill-php80": "^1.29" }, "autoload": { diff --git a/flight/core/Dispatcher.php b/flight/core/Dispatcher.php index f08d1f0..77184e7 100644 --- a/flight/core/Dispatcher.php +++ b/flight/core/Dispatcher.php @@ -7,6 +7,7 @@ namespace flight\core; use Exception; use flight\Engine; use InvalidArgumentException; +use Psr\Container\ContainerInterface; use ReflectionFunction; use TypeError; @@ -43,20 +44,24 @@ class Dispatcher /** * This is a container for the dependency injection. * - * @var callable|object|null + * @var null|ContainerInterface|(callable(string $classString, array $params): (null|object)) */ protected $containerHandler = null; /** * Sets the dependency injection container handler. * - * @param callable|object $containerHandler Dependency injection container - * - * @return void + * @param ContainerInterface|(callable(string $classString, array $params): (null|object)) $containerHandler + * Dependency injection container. */ public function setContainerHandler($containerHandler): void { - $this->containerHandler = $containerHandler; + if ( + $containerHandler instanceof ContainerInterface + || is_callable($containerHandler) + ) { + $this->containerHandler = $containerHandler; + } } public function setEngine(Engine $engine): void @@ -390,67 +395,66 @@ class Dispatcher */ protected function verifyValidClassCallable($class, $method, $resolvedClass): void { - $final_exception = null; + $exception = null; // Final check to make sure it's actually a class and a method, or throw an error - if (is_object($class) === false && class_exists($class) === false) { - $final_exception = new Exception("Class '$class' not found. Is it being correctly autoloaded with Flight::path()?"); + if (!is_object($class) && !class_exists($class)) { + $exception = new Exception("Class '$class' not found. Is it being correctly autoloaded with Flight::path()?"); - // If this tried to resolve a class in a container and failed somehow, throw the exception - } elseif (isset($resolvedClass) === false && $this->containerException !== null) { - $final_exception = $this->containerException; + // If this tried to resolve a class in a container and failed somehow, throw the exception + } elseif (!$resolvedClass && $this->containerException) { + $exception = $this->containerException; - // Class is there, but no method - } elseif (is_object($class) === true && method_exists($class, $method) === false) { - $final_exception = new Exception("Class found, but method '" . get_class($class) . "::$method' not found."); + // Class is there, but no method + } elseif (is_object($class) && !method_exists($class, $method)) { + $classNamespace = get_class($class); + $exception = new Exception("Class found, but method '$classNamespace::$method' not found."); } - if ($final_exception !== null) { + if ($exception) { $this->fixOutputBuffering(); - throw $final_exception; + + throw $exception; } } /** * Resolves the container class. * - * @param callable|object $container_handler Dependency injection container * @param class-string $class Class name. * @param array &$params Class constructor parameters. * * @return ?object Class object. */ - protected function resolveContainerClass($container_handler, $class, array &$params) + protected function resolveContainerClass(string $class, array &$params) { - $class_object = null; - // PSR-11 if ( - is_object($container_handler) === true && - method_exists($container_handler, 'has') === true && - $container_handler->has($class) + $this->containerHandler instanceof ContainerInterface + && $this->containerHandler->has($class) ) { - $class_object = call_user_func([$container_handler, 'get'], $class); + return $this->containerHandler->get($class); + } // Just a callable where you configure the behavior (Dice, PHP-DI, etc.) - } elseif (is_callable($container_handler) === true) { - // This is to catch all the error that could be thrown by whatever container you are using + if (is_callable($this->containerHandler)) { + /* This is to catch all the error that could be thrown by whatever + container you are using */ try { - $class_object = call_user_func($container_handler, $class, $params); - } catch (Exception $e) { - // could not resolve a class for some reason - $class_object = null; + return ($this->containerHandler)($class, $params); + // could not resolve a class for some reason + } catch (Exception $exception) { // If the container throws an exception, we need to catch it // and store it somewhere. If we just let it throw itself, it // doesn't properly close the output buffers and can cause other // issues. - // This is thrown in the verifyValidClassCallable method - $this->containerException = $e; + // This is thrown in the verifyValidClassCallable method. + $this->containerException = $exception; } } - return $class_object; + return null; } /** Because this could throw an exception in the middle of an output buffer, */ From 598c71d01c2c0b74838d0397973f6579fbdac8cd Mon Sep 17 00:00:00 2001 From: fadrian06 Date: Mon, 1 Apr 2024 19:18:50 -0400 Subject: [PATCH 13/43] Restored phpcs.dist.xml > phpcs.xml script --- composer.json | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/composer.json b/composer.json index df9bfb0..8fdf8cc 100644 --- a/composer.json +++ b/composer.json @@ -66,7 +66,10 @@ "test-coverage:win": "del clover.xml && phpunit --coverage-html=coverage --coverage-clover=clover.xml && coverage-check clover.xml 100", "lint": "phpstan --no-progress -cphpstan.neon", "beautify": "phpcbf --standard=phpcs.xml", - "phpcs": "phpcs --standard=phpcs.xml -n" + "phpcs": "phpcs --standard=phpcs.xml -n", + "post-install-cmd": [ + "php -r \"if (!file_exists('phpcs.xml')) copy('phpcs.xml.dist', 'phpcs.xml');\"" + ] }, "suggest": { "latte/latte": "Latte template engine", From 0c843674d16bbcb02f6c8deaa23cfb144b2c41dd Mon Sep 17 00:00:00 2001 From: n0nag0n Date: Tue, 2 Apr 2024 09:39:27 -0600 Subject: [PATCH 14/43] Create CONTRIBUTING.md --- .vscode/CONTRIBUTING.md | 52 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 .vscode/CONTRIBUTING.md diff --git a/.vscode/CONTRIBUTING.md b/.vscode/CONTRIBUTING.md new file mode 100644 index 0000000..0e35347 --- /dev/null +++ b/.vscode/CONTRIBUTING.md @@ -0,0 +1,52 @@ +## Contributing to the Flight Framework + +Thanks for being willing to contribute to the Flight! The goal of Flight is to keep the implementation of things simple and free of outside dependencies. +You should only bring in the depedencies you want in your project right? Right. + +### Overarching Guidelines + +Flight aims to be simple and fast. Anything that compromises either of those two things will be heavily scrutinized and/or rejected. Other things to consider when making a contribution: + +* **Dependencies** - We strive to be dependency free in Flight. Yes even polyfills, yes even `Interface` only repos like `psr/container`. The fewer dependencies, the fewer your exposed security areas. + +* **Coding Standards** - We use PSR1 coding standards enforced by PHPCS. PHPStan is at level 6. Spaces are enforced instead of tabs. We prefer `===` instead of truthy or falsey statements. + +* **Core functionality vs Plugin** - Have a conversation with us in the [chatroom](https://matrix.to/#/!cTfwPXhpkTXPXwVmxY:matrix.org?via=matrix.org&via=leitstelle511.net&via=integrations.ems.host) to know if your idea is worth makes sense in the framework or in a plugin. + +#### **Did you find a bug?** + +* **Do not open up a GitHub issue if the bug is a security vulnerability**. Instead contact maintainers directly via email to safely pass in the information + +* **Ensure the bug was not already reported** by searching on GitHub under [Issues](https://github.com/flightphp/core/issues). + +* If you're unable to find an open issue addressing the problem, [open a new one](https://github.com/flightphp/core/issues/new). Be sure to include a **title and clear description**, as much relevant information as possible, and a **code sample** or an **executable test case** demonstrating the expected behavior that is not occurring. + +#### **Did you write a patch that fixes a bug?** + +* Open a new GitHub pull request with the patch. + +* Ensure the PR description clearly describes the problem and solution. Include the relevant issue number if applicable. + +#### **Did you fix whitespace, format code, or make a purely cosmetic patch?** + +Changes that are cosmetic in nature and do not add anything substantial to the stability, functionality, or testability of Flight will generally not be accepted. + +#### **Do you intend to add a new feature or change an existing one?** + +* Hop into the [chatroom](https://matrix.to/#/!cTfwPXhpkTXPXwVmxY:matrix.org?via=matrix.org&via=leitstelle511.net&via=integrations.ems.host) for Flight and let's have a conversation about the feature you want to add. It could be amazing, or it might make more sense as an extension/plugin. If you create a PR without having a conversation with maintainers, it likely will be closed without review. + +* Do not open an issue on GitHub until you have collected positive feedback about the change. GitHub issues are primarily intended for bug reports and fixes. + +#### **Do you have questions about the source code?** + +* Ask any question about how to use Flight in the in the [Flight Matrix chat room](https://matrix.to/#/!cTfwPXhpkTXPXwVmxY:matrix.org?via=matrix.org&via=leitstelle511.net&via=integrations.ems.host). + +#### **Do you want to contribute to the Flight documentation?** + +* Please read [Contributing to the Flight Documentation](https://docs.flightphp.com). + +Flight is a volunteer effort. We encourage you to pitch in and join! + +Thanks! :heart: :heart: :heart: + +Flight Team From 4add06271a9485391e6230a8895d97b4106da50d Mon Sep 17 00:00:00 2001 From: n0nag0n Date: Tue, 2 Apr 2024 09:41:37 -0600 Subject: [PATCH 15/43] Added Contributing Guidelines --- .vscode/CONTRIBUTING.md => CONTRIBUTING.md | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename .vscode/CONTRIBUTING.md => CONTRIBUTING.md (100%) diff --git a/.vscode/CONTRIBUTING.md b/CONTRIBUTING.md similarity index 100% rename from .vscode/CONTRIBUTING.md rename to CONTRIBUTING.md From d6ca2595d7a7346cb699cc53648546f8e5feb42b Mon Sep 17 00:00:00 2001 From: n0nag0n Date: Wed, 3 Apr 2024 08:56:10 -0600 Subject: [PATCH 16/43] Update CONTRIBUTING.md --- CONTRIBUTING.md | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 0e35347..4b1af6d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -7,15 +7,21 @@ You should only bring in the depedencies you want in your project right? Right. Flight aims to be simple and fast. Anything that compromises either of those two things will be heavily scrutinized and/or rejected. Other things to consider when making a contribution: -* **Dependencies** - We strive to be dependency free in Flight. Yes even polyfills, yes even `Interface` only repos like `psr/container`. The fewer dependencies, the fewer your exposed security areas. +* **Dependencies** - We strive to be dependency free in Flight. Yes even polyfills, yes even `Interface` only repos like `psr/container`. The fewer dependencies, the fewer your exposed attack vectors. -* **Coding Standards** - We use PSR1 coding standards enforced by PHPCS. PHPStan is at level 6. Spaces are enforced instead of tabs. We prefer `===` instead of truthy or falsey statements. +* **Coding Standards** - We use PSR1 coding standards enforced by PHPCS. Some standards that either need additional configuration or need to be manually done are: + * PHPStan is at level 6. + * `===` instead of truthy or falsey statements like `==` or `!is_array()`. + +* **PHP 7.4 Focused** - We do not make PHP 8+ focused enhancements on the framework as the focus is maintaining PHP 7.4. * **Core functionality vs Plugin** - Have a conversation with us in the [chatroom](https://matrix.to/#/!cTfwPXhpkTXPXwVmxY:matrix.org?via=matrix.org&via=leitstelle511.net&via=integrations.ems.host) to know if your idea is worth makes sense in the framework or in a plugin. +* **Testing** - Until automated testing is put into place, any PRs must pass unit testing in PHP 7.4 and PHP 8.2+. Additionally you need to run `composer test-server` and `composer test-server-v2` and ensure all the header links work correctly. + #### **Did you find a bug?** -* **Do not open up a GitHub issue if the bug is a security vulnerability**. Instead contact maintainers directly via email to safely pass in the information +* **Do not open up a GitHub issue if the bug is a security vulnerability**. Instead contact maintainers directly via email to safely pass in the information related to the security vuln. * **Ensure the bug was not already reported** by searching on GitHub under [Issues](https://github.com/flightphp/core/issues). @@ -43,7 +49,7 @@ Changes that are cosmetic in nature and do not add anything substantial to the s #### **Do you want to contribute to the Flight documentation?** -* Please read [Contributing to the Flight Documentation](https://docs.flightphp.com). +* Please see the [Flight Documentation repo on GitHub](https://github.com/flightphp/docs). Flight is a volunteer effort. We encourage you to pitch in and join! From 99a7d75440b4cc3f80e8d9b98630091b2595a16d Mon Sep 17 00:00:00 2001 From: fadrian06 Date: Wed, 3 Apr 2024 17:39:55 -0400 Subject: [PATCH 17/43] Removed PSR/Container --- composer.json | 1 - flight/core/Dispatcher.php | 20 +++++++++++++++----- phpstan-baseline.neon | 6 ------ 3 files changed, 15 insertions(+), 12 deletions(-) diff --git a/composer.json b/composer.json index 8fdf8cc..9d8f6e8 100644 --- a/composer.json +++ b/composer.json @@ -25,7 +25,6 @@ "require": { "php": "^7.4|^8.0|^8.1|^8.2|^8.3", "ext-json": "*", - "psr/container": "^2.0", "symfony/polyfill-php80": "^1.29" }, "autoload": { diff --git a/flight/core/Dispatcher.php b/flight/core/Dispatcher.php index 77184e7..5604d69 100644 --- a/flight/core/Dispatcher.php +++ b/flight/core/Dispatcher.php @@ -44,24 +44,34 @@ class Dispatcher /** * This is a container for the dependency injection. * - * @var null|ContainerInterface|(callable(string $classString, array $params): (null|object)) + * @var null|ContainerInterface|(callable(string $classString, array $params): (null|object)) */ protected $containerHandler = null; /** * Sets the dependency injection container handler. * - * @param ContainerInterface|(callable(string $classString, array $params): (null|object)) $containerHandler + * @param ContainerInterface|(callable(string $classString, array $params): (null|object)) $containerHandler * Dependency injection container. + * + * @throws InvalidArgumentException If $containerHandler is not a `callable` or instance of `Psr\Container\ContainerInterface`. */ public function setContainerHandler($containerHandler): void { + $containerInterfaceNS = '\Psr\Container\ContainerInterface'; + if ( - $containerHandler instanceof ContainerInterface + is_a($containerHandler, $containerInterfaceNS) || is_callable($containerHandler) ) { $this->containerHandler = $containerHandler; + + return; } + + throw new InvalidArgumentException( + "\$containerHandler must be of type callable or instance $containerInterfaceNS" + ); } public function setEngine(Engine $engine): void @@ -200,7 +210,7 @@ class Dispatcher * * @param string $name Event name * @param 'before'|'after' $type Filter type. - * @param callable(array &$params, string &$output): (void|false) $callback + * @param callable(array &$params, mixed &$output): (void|false)|callable(mixed &$output): (void|false) $callback * * @return $this */ @@ -430,7 +440,7 @@ class Dispatcher { // PSR-11 if ( - $this->containerHandler instanceof ContainerInterface + is_a($this->containerHandler, '\Psr\Container\ContainerInterface') && $this->containerHandler->has($class) ) { return $this->containerHandler->get($class); diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 9fa597c..e69de29 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -1,6 +0,0 @@ -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 From 8f3f6b5c74efa1dd873bdb13bd4c6c93bc3abb70 Mon Sep 17 00:00:00 2001 From: fadrian06 Date: Wed, 3 Apr 2024 17:51:16 -0400 Subject: [PATCH 18/43] Make explicit comparisons --- composer.json | 3 +-- flight/core/Dispatcher.php | 20 ++++++++++---------- 2 files changed, 11 insertions(+), 12 deletions(-) diff --git a/composer.json b/composer.json index 9d8f6e8..9384c12 100644 --- a/composer.json +++ b/composer.json @@ -24,8 +24,7 @@ ], "require": { "php": "^7.4|^8.0|^8.1|^8.2|^8.3", - "ext-json": "*", - "symfony/polyfill-php80": "^1.29" + "ext-json": "*" }, "autoload": { "files": [ diff --git a/flight/core/Dispatcher.php b/flight/core/Dispatcher.php index 5604d69..fd3eac7 100644 --- a/flight/core/Dispatcher.php +++ b/flight/core/Dispatcher.php @@ -277,8 +277,8 @@ class Dispatcher public function execute($callback, array &$params = []) { if ( - is_string($callback) - && (str_contains($callback, '->') || str_contains($callback, '::')) + is_string($callback) === true + && (strpos($callback, '->') !== false || strpos($callback, '::') !== false) ) { $callback = $this->parseStringClassAndMethod($callback); } @@ -347,7 +347,7 @@ class Dispatcher public function invokeCallable($func, array &$params = []) { // If this is a directly callable function, call it - if (!is_array($func)) { + if (is_array($func) === false) { $this->verifyValidFunction($func); return call_user_func_array($func, $params); @@ -355,12 +355,12 @@ class Dispatcher [$class, $method] = $func; - $mustUseTheContainer = $this->containerHandler && ( - (is_object($class) && !str_starts_with(get_class($class), 'flight\\')) + $mustUseTheContainer = $this->containerHandler !== null && ( + (is_object($class) === true && strpos(get_class($class), 'flight\\') === false) || is_string($class) ); - if ($mustUseTheContainer) { + if ($mustUseTheContainer === true) { $resolvedClass = $this->resolveContainerClass($class, $params); if ($resolvedClass) { @@ -408,20 +408,20 @@ class Dispatcher $exception = null; // Final check to make sure it's actually a class and a method, or throw an error - if (!is_object($class) && !class_exists($class)) { + if (is_object($class) === false && class_exists($class) === false) { $exception = new Exception("Class '$class' not found. Is it being correctly autoloaded with Flight::path()?"); // If this tried to resolve a class in a container and failed somehow, throw the exception - } elseif (!$resolvedClass && $this->containerException) { + } elseif (!$resolvedClass && $this->containerException !== null) { $exception = $this->containerException; // Class is there, but no method - } elseif (is_object($class) && !method_exists($class, $method)) { + } elseif (is_object($class) === true && method_exists($class, $method) === false) { $classNamespace = get_class($class); $exception = new Exception("Class found, but method '$classNamespace::$method' not found."); } - if ($exception) { + if ($exception !== null) { $this->fixOutputBuffering(); throw $exception; From 414f605f67c0c36f93eddd9bf0d718ff0bee17dc Mon Sep 17 00:00:00 2001 From: fadrian06 Date: Wed, 3 Apr 2024 18:53:15 -0400 Subject: [PATCH 19/43] More abstraction to Dispatcher::containerException typehint --- flight/core/Dispatcher.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/flight/core/Dispatcher.php b/flight/core/Dispatcher.php index fd3eac7..1d40f95 100644 --- a/flight/core/Dispatcher.php +++ b/flight/core/Dispatcher.php @@ -9,6 +9,7 @@ use flight\Engine; use InvalidArgumentException; use Psr\Container\ContainerInterface; use ReflectionFunction; +use Throwable; use TypeError; /** @@ -26,7 +27,7 @@ class Dispatcher public const FILTER_AFTER = 'after'; /** Exception message if thrown by setting the container as a callable method. */ - protected ?Exception $containerException = null; + protected ?Throwable $containerException = null; /** @var ?Engine $engine Engine instance. */ protected ?Engine $engine = null; From a1228a849221005627dbb6f86062fbef70151d50 Mon Sep 17 00:00:00 2001 From: n0nag0n Date: Wed, 3 Apr 2024 21:21:48 -0600 Subject: [PATCH 20/43] added missing test from dispatcher refactor --- tests/EngineTest.php | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/tests/EngineTest.php b/tests/EngineTest.php index 5c32968..8da7b0c 100644 --- a/tests/EngineTest.php +++ b/tests/EngineTest.php @@ -11,6 +11,7 @@ use flight\Engine; use flight\net\Request; use flight\net\Response; use flight\util\Collection; +use InvalidArgumentException; use PDOException; use PHPUnit\Framework\TestCase; use tests\classes\Container; @@ -681,6 +682,14 @@ class EngineTest extends TestCase $this->expectOutputString('before456before123OKafter123456after123'); } + public function testContainerBadClass() { + $engine = new Engine(); + + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage("\$containerHandler must be of type callable or instance \\Psr\\Container\\ContainerInterface"); + $engine->registerContainerHandler('BadClass'); + } + public function testContainerDice() { $engine = new Engine(); $dice = new \Dice\Dice(); From 0cf320659140e88c8aa706b6c9ad027ba170f4f5 Mon Sep 17 00:00:00 2001 From: n0nag0n Date: Wed, 3 Apr 2024 21:28:50 -0600 Subject: [PATCH 21/43] updated badges --- README.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 4961468..6e437bb 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,8 @@ -![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) +[![Version](http://poser.pugx.org/flightphp/core/version)](https://packagist.org/packages/flightphp/core) +[![Total Downloads](http://poser.pugx.org/flightphp/core/downloads)](https://packagist.org/packages/flightphp/core) +![PHPStan: Level 6](https://img.shields.io/badge/PHPStan-level%206-brightgreen.svg?style=flat) +[![License](http://poser.pugx.org/flightphp/core/license)](https://packagist.org/packages/flightphp/core) +[![PHP Version Require](http://poser.pugx.org/flightphp/core/require/php)](https://packagist.org/packages/flightphp/core) ![Matrix](https://img.shields.io/matrix/flight-php-framework%3Amatrix.org?server_fqdn=matrix.org&style=social&logo=matrix) # What is Flight? From c41a578e9c0bfafa05860f8b099279446ddd3645 Mon Sep 17 00:00:00 2001 From: n0nag0n Date: Wed, 3 Apr 2024 21:31:08 -0600 Subject: [PATCH 22/43] swapped out monthly downloads --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 6e437bb..a749fdb 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ [![Version](http://poser.pugx.org/flightphp/core/version)](https://packagist.org/packages/flightphp/core) -[![Total Downloads](http://poser.pugx.org/flightphp/core/downloads)](https://packagist.org/packages/flightphp/core) +[![Monthly Downloads](http://poser.pugx.org/flightphp/core/d/monthly)](https://packagist.org/packages/flightphp/core) ![PHPStan: Level 6](https://img.shields.io/badge/PHPStan-level%206-brightgreen.svg?style=flat) [![License](http://poser.pugx.org/flightphp/core/license)](https://packagist.org/packages/flightphp/core) [![PHP Version Require](http://poser.pugx.org/flightphp/core/require/php)](https://packagist.org/packages/flightphp/core) From 4d79dea91bd4a52024cf1d1ed0a648538e605f08 Mon Sep 17 00:00:00 2001 From: n0nag0n Date: Sun, 7 Apr 2024 18:20:28 -0600 Subject: [PATCH 23/43] Update issue templates --- .github/ISSUE_TEMPLATE/bug_report.md | 36 +++++++++++++++++++++++ .github/ISSUE_TEMPLATE/feature_request.md | 20 +++++++++++++ 2 files changed, 56 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/bug_report.md create mode 100644 .github/ISSUE_TEMPLATE/feature_request.md diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..8199389 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,36 @@ +--- +name: Bug report +about: Create a report to help us improve +title: '' +labels: Bug +assignees: '' + +--- + +**Before you submit the bug** +If you're having issues with your templates or output showing up out of order, please make sure to check the [updates to output buffering](https://docs.flightphp.com/learn/migrating-to-v3#output-buffering-behavior-3-5-0) for workarounds and corrections. + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** +Steps to reproduce the behavior: +1. Here's some sample code ... +2. Here's the URL I hit ... +3. etc +4. See error + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Screenshots** +If applicable, add screenshots to help explain your problem. + +**Environment (please complete the following information):** + - OS: [e.g. Linux, Mac, Windows] + - Browser [e.g. chrome, safari] + - PHP Version [e.g. 7.4] + - Flight Version [e.g. 3.7.2] + +**Additional context** +Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..bbcbbe7 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,20 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: '' +labels: '' +assignees: '' + +--- + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context or screenshots about the feature request here. From 7fca8d9a45d5b9d529af767f772ab5c66d9b9dd3 Mon Sep 17 00:00:00 2001 From: Austin Collier Date: Mon, 8 Apr 2024 20:55:12 -0600 Subject: [PATCH 24/43] added ability to convert the response body --- flight/net/Response.php | 36 ++++++++++++++++++++++++++++++++++++ tests/ResponseTest.php | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 69 insertions(+) diff --git a/flight/net/Response.php b/flight/net/Response.php index 2971bd9..9d1c610 100644 --- a/flight/net/Response.php +++ b/flight/net/Response.php @@ -128,6 +128,13 @@ class Response */ protected bool $sent = false; + /** + * These are callbacks that can process the response body before it's sent + * + * @var array $responseBodyCallbacks + */ + protected array $responseBodyCallbacks = []; + /** * Sets the HTTP status of the response. * @@ -429,8 +436,37 @@ class Response $this->sendHeaders(); // @codeCoverageIgnore } + // Only for the v3 output buffering. + if($this->v2_output_buffering === false) { + $this->processResponseCallbacks(); + } + echo $this->body; $this->sent = true; } + + /** + * Adds a callback to process the response body before it's sent. These are processed in the order + * they are added + * + * @param callable $callback The callback to process the response body + * @return void + */ + public function addResponseBodyCallback(callable $callback): void + { + $this->responseBodyCallbacks[] = $callback; + } + + /** + * Cycles through the response body callbacks and processes them in order + * + * @return void + */ + protected function processResponseCallbacks(): void + { + foreach ($this->responseBodyCallbacks as $callback) { + $this->body = $callback($this->body); + } + } } diff --git a/tests/ResponseTest.php b/tests/ResponseTest.php index 42701d4..3226cc9 100644 --- a/tests/ResponseTest.php +++ b/tests/ResponseTest.php @@ -255,4 +255,37 @@ class ResponseTest extends TestCase $response->write('new', true); $this->assertEquals('new', $response->getBody()); } + + public function testResponseBodyCallback() + { + $response = new Response(); + $response->write('test'); + $str_rot13 = function ($body) { + return str_rot13($body); + }; + $response->addResponseBodyCallback($str_rot13); + ob_start(); + $response->send(); + $rot13_body = ob_get_clean(); + $this->assertEquals('grfg', $rot13_body); + } + + public function testResponseBodyCallbackMultiple() + { + $response = new Response(); + $response->write('test'); + $str_rot13 = function ($body) { + return str_rot13($body); + }; + $str_replace = function ($body) { + return str_replace('g', 'G', $body); + }; + $response->addResponseBodyCallback($str_rot13); + $response->addResponseBodyCallback($str_replace); + $response->addResponseBodyCallback($str_rot13); + ob_start(); + $response->send(); + $rot13_body = ob_get_clean(); + $this->assertEquals('TesT', $rot13_body); + } } From 9be596f0bf2289e7872f34976e3f4ac9b022c1b4 Mon Sep 17 00:00:00 2001 From: Austin Collier Date: Tue, 9 Apr 2024 08:20:20 -0600 Subject: [PATCH 25/43] lots of phpcs errors to fix --- flight/Engine.php | 24 +++++++++++++----------- flight/net/Request.php | 38 +++++++++++++++++++------------------- flight/net/Response.php | 3 ++- flight/net/Router.php | 2 +- phpstan.neon | 1 + tests/EngineTest.php | 3 +-- tests/server/index.php | 8 +++++--- 7 files changed, 42 insertions(+), 37 deletions(-) diff --git a/flight/Engine.php b/flight/Engine.php index 62342b6..2271b39 100644 --- a/flight/Engine.php +++ b/flight/Engine.php @@ -314,7 +314,7 @@ class Engine */ public function get(?string $key = null) { - if (null === $key) { + if ($key === null) { return $this->vars; } @@ -360,7 +360,7 @@ class Engine */ public function clear(?string $key = null): void { - if (null === $key) { + if ($key === null) { $this->vars = []; return; } @@ -570,9 +570,11 @@ class Engine public function _error(Throwable $e): void { $msg = sprintf( - '

    500 Internal Server Error

    ' . - '

    %s (%s)

    ' . - '
    %s
    ', + <<500 Internal Server Error +

    %s (%s)

    +
    %s
    + HTML, $e->getMessage(), $e->getCode(), $e->getTraceAsString() @@ -603,8 +605,8 @@ class Engine { $response = $this->response(); - if (!$response->sent()) { - if (null !== $code) { + if ($response->sent() === false) { + if ($code !== null) { $response->status($code); } @@ -729,12 +731,12 @@ class Engine { $base = $this->get('flight.base_url'); - if (null === $base) { + if ($base === null) { $base = $this->request()->base; } // Append base url to redirect url - if ('/' !== $base && false === strpos($url, '://')) { + if ($base !== '/' && strpos($url, '://') === false) { $url = $base . preg_replace('#/+#', '/', '/' . $url); } @@ -756,7 +758,7 @@ class Engine */ public function _render(string $file, ?array $data = null, ?string $key = null): void { - if (null !== $key) { + if ($key !== null) { $this->view()->set($key, $this->view()->fetch($file, $data)); return; } @@ -833,7 +835,7 @@ class Engine */ public function _etag(string $id, string $type = 'strong'): void { - $id = (('weak' === $type) ? 'W/' : '') . $id; + $id = (($type === 'weak') ? 'W/' : '') . $id; $this->response()->header('ETag', '"' . str_replace('"', '\"', $id) . '"'); diff --git a/flight/net/Request.php b/flight/net/Request.php index 569994e..fd9194b 100644 --- a/flight/net/Request.php +++ b/flight/net/Request.php @@ -151,7 +151,7 @@ class Request 'method' => self::getMethod(), 'referrer' => self::getVar('HTTP_REFERER'), 'ip' => self::getVar('REMOTE_ADDR'), - 'ajax' => 'XMLHttpRequest' === self::getVar('HTTP_X_REQUESTED_WITH'), + 'ajax' => self::getVar('HTTP_X_REQUESTED_WITH') === 'XMLHttpRequest', 'scheme' => self::getScheme(), 'user_agent' => self::getVar('HTTP_USER_AGENT'), 'type' => self::getVar('CONTENT_TYPE'), @@ -160,7 +160,7 @@ class Request 'data' => new Collection($_POST), 'cookies' => new Collection($_COOKIE), 'files' => new Collection($_FILES), - 'secure' => 'https' === self::getScheme(), + 'secure' => self::getScheme() === 'https', 'accept' => self::getVar('HTTP_ACCEPT'), 'proxy_ip' => self::getProxyIpAddress(), 'host' => self::getVar('HTTP_HOST'), @@ -188,12 +188,12 @@ class Request // This rewrites the url in case the public url and base directories match // (such as installing on a subdirectory in a web server) // @see testInitUrlSameAsBaseDirectory - if ('/' !== $this->base && '' !== $this->base && 0 === strpos($this->url, $this->base)) { + if ($this->base !== '/' && $this->base !== '' && strpos($this->url, $this->base) === 0) { $this->url = substr($this->url, \strlen($this->base)); } // Default url - if (empty($this->url)) { + if (empty($this->url) === true) { $this->url = '/'; } else { // Merge URL query parameters with $_GET @@ -203,11 +203,11 @@ class Request } // Check for JSON input - if (0 === strpos($this->type, 'application/json')) { + if (strpos($this->type, 'application/json') === 0) { $body = $this->getBody(); - if ('' !== $body) { + if ($body !== '') { $data = json_decode($body, true); - if (is_array($data)) { + if (is_array($data) === true) { $this->data->setData($data); } } @@ -225,13 +225,13 @@ class Request { $body = $this->body; - if ('' !== $body) { + if ($body !== '') { return $body; } $method = $this->method ?? self::getMethod(); - if ('POST' === $method || 'PUT' === $method || 'DELETE' === $method || 'PATCH' === $method) { + if ($method === 'POST' || $method === 'PUT' || $method === 'DELETE' || $method === 'PATCH') { $body = file_get_contents($this->stream_path); } @@ -247,9 +247,9 @@ class Request { $method = self::getVar('REQUEST_METHOD', 'GET'); - if (isset($_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE'])) { + if (isset($_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE']) === true) { $method = $_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE']; - } elseif (isset($_REQUEST['_method'])) { + } elseif (isset($_REQUEST['_method']) === true) { $method = $_REQUEST['_method']; } @@ -275,9 +275,9 @@ class Request $flags = \FILTER_FLAG_NO_PRIV_RANGE | \FILTER_FLAG_NO_RES_RANGE; foreach ($forwarded as $key) { - if (\array_key_exists($key, $_SERVER)) { + if (\array_key_exists($key, $_SERVER) === true) { sscanf($_SERVER[$key], '%[^,]', $ip); - if (false !== filter_var($ip, \FILTER_VALIDATE_IP, $flags)) { + if (filter_var($ip, \FILTER_VALIDATE_IP, $flags) !== false) { return $ip; } } @@ -322,7 +322,7 @@ class Request { $headers = []; foreach ($_SERVER as $key => $value) { - if (0 === strpos($key, 'HTTP_')) { + if (strpos($key, 'HTTP_') === 0) { // converts headers like HTTP_CUSTOM_HEADER to Custom-Header $key = str_replace(' ', '-', ucwords(str_replace('_', ' ', strtolower(substr($key, 5))))); $headers[$key] = $value; @@ -386,7 +386,7 @@ class Request $params = []; $args = parse_url($url); - if (isset($args['query'])) { + if (isset($args['query']) === true) { parse_str($args['query'], $params); } @@ -401,13 +401,13 @@ class Request public static function getScheme(): string { if ( - (isset($_SERVER['HTTPS']) && 'on' === strtolower($_SERVER['HTTPS'])) + (isset($_SERVER['HTTPS']) === true && strtolower($_SERVER['HTTPS']) === 'on') || - (isset($_SERVER['HTTP_X_FORWARDED_PROTO']) && 'https' === $_SERVER['HTTP_X_FORWARDED_PROTO']) + (isset($_SERVER['HTTP_X_FORWARDED_PROTO']) === true && $_SERVER['HTTP_X_FORWARDED_PROTO'] === 'https') || - (isset($_SERVER['HTTP_FRONT_END_HTTPS']) && 'on' === $_SERVER['HTTP_FRONT_END_HTTPS']) + (isset($_SERVER['HTTP_FRONT_END_HTTPS']) === true && $_SERVER['HTTP_FRONT_END_HTTPS'] === 'on') || - (isset($_SERVER['REQUEST_SCHEME']) && 'https' === $_SERVER['REQUEST_SCHEME']) + (isset($_SERVER['REQUEST_SCHEME']) === true && $_SERVER['REQUEST_SCHEME'] === 'https') ) { return 'https'; } diff --git a/flight/net/Response.php b/flight/net/Response.php index 9d1c610..9161f18 100644 --- a/flight/net/Response.php +++ b/flight/net/Response.php @@ -437,7 +437,7 @@ class Response } // Only for the v3 output buffering. - if($this->v2_output_buffering === false) { + if ($this->v2_output_buffering === false) { $this->processResponseCallbacks(); } @@ -451,6 +451,7 @@ class Response * they are added * * @param callable $callback The callback to process the response body + * * @return void */ public function addResponseBodyCallback(callable $callback): void diff --git a/flight/net/Router.php b/flight/net/Router.php index d494dbb..88df038 100644 --- a/flight/net/Router.php +++ b/flight/net/Router.php @@ -101,7 +101,7 @@ class Router $methods = ['*']; - if (false !== strpos($url, ' ')) { + if (strpos($url, ' ') !== false) { [$method, $url] = explode(' ', $url, 2); $url = trim($url); $methods = explode('|', $method); diff --git a/phpstan.neon b/phpstan.neon index 97e16eb..9be4ca5 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -6,6 +6,7 @@ parameters: level: 6 excludePaths: - vendor + - flight/util/ReturnTypeWillChange.php paths: - flight - index.php diff --git a/tests/EngineTest.php b/tests/EngineTest.php index 8da7b0c..6c1837e 100644 --- a/tests/EngineTest.php +++ b/tests/EngineTest.php @@ -87,8 +87,7 @@ class EngineTest extends TestCase public function testHandleException() { $engine = new Engine(); - $regex_message = preg_quote('

    500 Internal Server Error

    thrown exception message (20)

    '); - $this->expectOutputRegex('~' . $regex_message . '~'); + $this->expectOutputRegex('~\500 Internal Server Error\[\s\S]*\thrown exception message \(20\)\~'); $engine->handleException(new Exception('thrown exception message', 20)); } diff --git a/tests/server/index.php b/tests/server/index.php index 57e0aa8..d9d16bc 100644 --- a/tests/server/index.php +++ b/tests/server/index.php @@ -161,9 +161,11 @@ Flight::route('/jsonp', function () { Flight::map('error', function (Throwable $e) { echo sprintf( - '

    500 Internal Server Error

    ' . - '

    %s (%s)

    ' . - '
    %s
    ', + <<500 Internal Server Error +

    %s (%s)

    +
    %s
    + HTML, $e->getMessage(), $e->getCode(), str_replace(getenv('PWD'), '***CONFIDENTIAL***', $e->getTraceAsString()) From 3659a761061ef993de39e5687750c3abf01f2d4a Mon Sep 17 00:00:00 2001 From: Belle Aerni Date: Wed, 10 Apr 2024 17:44:13 -0700 Subject: [PATCH 26/43] Fix content length when using response body callbacks --- flight/net/Response.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/flight/net/Response.php b/flight/net/Response.php index 9161f18..d5d3afa 100644 --- a/flight/net/Response.php +++ b/flight/net/Response.php @@ -432,15 +432,15 @@ class Response } } - if (!headers_sent()) { - $this->sendHeaders(); // @codeCoverageIgnore - } - // Only for the v3 output buffering. if ($this->v2_output_buffering === false) { $this->processResponseCallbacks(); } + if (!headers_sent()) { + $this->sendHeaders(); // @codeCoverageIgnore + } + echo $this->body; $this->sent = true; From 77d313190d44c4f2bf3a246a9e6454ed4c879556 Mon Sep 17 00:00:00 2001 From: n0nag0n Date: Wed, 10 Apr 2024 22:12:05 -0600 Subject: [PATCH 27/43] added unit test, corrected other response logic --- flight/net/Response.php | 20 ++++++++++---------- phpcs.xml.dist | 1 + tests/ResponseTest.php | 16 ++++++++++++++++ 3 files changed, 27 insertions(+), 10 deletions(-) diff --git a/flight/net/Response.php b/flight/net/Response.php index d5d3afa..1798de5 100644 --- a/flight/net/Response.php +++ b/flight/net/Response.php @@ -341,6 +341,15 @@ class Response ); } + if ($this->content_length === true) { + // Send content length + $length = $this->getContentLength(); + + if ($length > 0) { + $this->setHeader('Content-Length', (string) $length); + } + } + // Send other headers foreach ($this->headers as $field => $value) { if (\is_array($value)) { @@ -352,15 +361,6 @@ class Response } } - if ($this->content_length) { - // Send content length - $length = $this->getContentLength(); - - if ($length > 0) { - $this->setRealHeader('Content-Length: ' . $length); - } - } - return $this; } @@ -437,7 +437,7 @@ class Response $this->processResponseCallbacks(); } - if (!headers_sent()) { + if (headers_sent() === false) { $this->sendHeaders(); // @codeCoverageIgnore } diff --git a/phpcs.xml.dist b/phpcs.xml.dist index bafe06d..79f3ff0 100644 --- a/phpcs.xml.dist +++ b/phpcs.xml.dist @@ -26,6 +26,7 @@ + diff --git a/tests/ResponseTest.php b/tests/ResponseTest.php index 3226cc9..443ff89 100644 --- a/tests/ResponseTest.php +++ b/tests/ResponseTest.php @@ -270,6 +270,22 @@ class ResponseTest extends TestCase $this->assertEquals('grfg', $rot13_body); } + public function testResponseBodyCallbackGzip() + { + $response = new Response(); + $response->content_length = true; + $response->write('test'); + $gzip = function ($body) { + return gzencode($body); + }; + $response->addResponseBodyCallback($gzip); + ob_start(); + $response->send(); + $gzip_body = ob_get_clean(); + $this->assertEquals('H4sIAAAAAAAAAytJLS4BAAx+f9gEAAAA', base64_encode($gzip_body)); + $this->assertEquals(strlen(gzencode('test')), strlen($gzip_body)); + } + public function testResponseBodyCallbackMultiple() { $response = new Response(); From 11de67fde9d99a3076d7474852ec6056c4ffc8a6 Mon Sep 17 00:00:00 2001 From: fadrian06 Date: Thu, 11 Apr 2024 14:25:19 -0400 Subject: [PATCH 28/43] Made stream default status code of 200 --- flight/Engine.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flight/Engine.php b/flight/Engine.php index 2271b39..0af8765 100644 --- a/flight/Engine.php +++ b/flight/Engine.php @@ -487,7 +487,7 @@ class Engine // 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']); + $response->status($route->streamed_headers['status'] ?? 200); unset($route->streamed_headers['status']); $response->header('X-Accel-Buffering', 'no'); $response->header('Connection', 'close'); From deb0d68875dac62be21843934a0c146c9a54e383 Mon Sep 17 00:00:00 2001 From: n0nag0n Date: Thu, 11 Apr 2024 23:04:28 -0600 Subject: [PATCH 29/43] added plain stream() method --- flight/Engine.php | 13 ++++++++----- flight/net/Route.php | 11 +++++++++++ tests/FlightTest.php | 24 ++++++++++++++++++++++++ tests/server/LayoutMiddleware.php | 3 ++- tests/server/index.php | 12 ++++++++++++ 5 files changed, 57 insertions(+), 6 deletions(-) diff --git a/flight/Engine.php b/flight/Engine.php index 0af8765..0d32f0a 100644 --- a/flight/Engine.php +++ b/flight/Engine.php @@ -487,13 +487,16 @@ class Engine // 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'] ?? 200); - unset($route->streamed_headers['status']); + if (count($route->streamed_headers) > 0) { + $response->status($route->streamed_headers['status'] ?? 200); + unset($route->streamed_headers['status']); + foreach ($route->streamed_headers as $header => $value) { + $response->header($header, $value); + } + } + $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; diff --git a/flight/net/Route.php b/flight/net/Route.php index 57ba2fc..4d005b9 100644 --- a/flight/net/Route.php +++ b/flight/net/Route.php @@ -238,6 +238,17 @@ class Route return $this; } + /** + * If the response should be streamed + * + * @return self + */ + public function stream(): self + { + $this->is_streamed = true; + return $this; + } + /** * This will allow the response for this route to be streamed. * diff --git a/tests/FlightTest.php b/tests/FlightTest.php index 042b6bb..d84336b 100644 --- a/tests/FlightTest.php +++ b/tests/FlightTest.php @@ -280,6 +280,30 @@ class FlightTest extends TestCase } 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'; + })->stream(); + Flight::request()->url = '/stream'; + $this->expectOutputString('stream'); + Flight::start(); + $this->assertEquals('', Flight::response()->getBody()); + $this->assertEquals([ + 'X-Accel-Buffering' => 'no', + 'Connection' => 'close' + ], Flight::response()->getHeaders()); + $this->assertEquals(200, Flight::response()->status()); + } + + public function testStreamRouteWithHeaders() { $response_mock = new class extends Response { public function setRealHeader(string $header_string, bool $replace = true, int $response_code = 0): Response diff --git a/tests/server/LayoutMiddleware.php b/tests/server/LayoutMiddleware.php index b89c4e0..d23e81c 100644 --- a/tests/server/LayoutMiddleware.php +++ b/tests/server/LayoutMiddleware.php @@ -76,7 +76,8 @@ class LayoutMiddleware
  • JSONP
  • Halt
  • Redirect
  • -
  • Stream
  • +
  • Stream Plain
  • +
  • Stream Headers
  • Overwrite Body
  • Slash in Param
  • UTF8 URL
  • diff --git a/tests/server/index.php b/tests/server/index.php index d9d16bc..a983590 100644 --- a/tests/server/index.php +++ b/tests/server/index.php @@ -122,7 +122,19 @@ Flight::group('', function () { ob_flush(); } echo "is successful!!"; + })->stream(); + + // Test 12: Redirect with status code + Flight::route('/streamWithHeaders', 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 ]); + // Test 14: Overwrite the body with a middleware Flight::route('/overwrite', function () { echo 'Route text: This route status is that it failed'; From ae47db8d8d73120bad0fc18731b549e3ab6f69b3 Mon Sep 17 00:00:00 2001 From: fadrian06 Date: Sat, 13 Apr 2024 14:10:58 -0400 Subject: [PATCH 30/43] Fixed gzip test on windows --- tests/ResponseTest.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/ResponseTest.php b/tests/ResponseTest.php index 443ff89..a163e7e 100644 --- a/tests/ResponseTest.php +++ b/tests/ResponseTest.php @@ -282,7 +282,8 @@ class ResponseTest extends TestCase ob_start(); $response->send(); $gzip_body = ob_get_clean(); - $this->assertEquals('H4sIAAAAAAAAAytJLS4BAAx+f9gEAAAA', base64_encode($gzip_body)); + $expected = PHP_OS === 'WINNT' ? 'H4sIAAAAAAAACitJLS4BAAx+f9gEAAAA' : 'H4sIAAAAAAAAAytJLS4BAAx+f9gEAAAA'; + $this->assertEquals($expected, base64_encode($gzip_body)); $this->assertEquals(strlen(gzencode('test')), strlen($gzip_body)); } From 161a35a0a284572c4e8ba6de03e013f4780b8af4 Mon Sep 17 00:00:00 2001 From: fadrian06 Date: Wed, 24 Apr 2024 19:56:01 -0400 Subject: [PATCH 31/43] Added test on ViewTest --- flight/template/View.php | 6 ++++++ tests/ViewTest.php | 14 ++++++++++++++ tests/views/myComponent.php | 1 + 3 files changed, 21 insertions(+) create mode 100644 tests/views/myComponent.php diff --git a/flight/template/View.php b/flight/template/View.php index c43a35f..9397cc6 100644 --- a/flight/template/View.php +++ b/flight/template/View.php @@ -121,6 +121,12 @@ class View \extract($this->vars); include $this->template; + + if ($data !== null) { + foreach (array_keys($data) as $variable) { + unset($this->vars[$variable]); + } + } } /** diff --git a/tests/ViewTest.php b/tests/ViewTest.php index fcb06c0..2432f15 100644 --- a/tests/ViewTest.php +++ b/tests/ViewTest.php @@ -152,4 +152,18 @@ class ViewTest extends TestCase $viewMock::normalizePath('C:/xampp/htdocs/libs/Flight\core\index.php', '°') ); } + + public function testItDoesNotKeepThePreviousStateOfOneViewComponent(): void + { + $this->expectOutputString("
    Hi
    \n
    \n"); + $this->view->render('myComponent', ['prop' => 'Hi']); + + set_error_handler(function (int $code, string $message): void { + $this->assertMatchesRegularExpression('/^Undefined variable:? \$?prop$/', $message); + }); + + $this->view->render('myComponent'); + + restore_error_handler(); + } } diff --git a/tests/views/myComponent.php b/tests/views/myComponent.php new file mode 100644 index 0000000..cf0a36f --- /dev/null +++ b/tests/views/myComponent.php @@ -0,0 +1 @@ +
    From 1b6cb088b7cbbbc2400fa5f28ddd033f7083ff34 Mon Sep 17 00:00:00 2001 From: fadrian06 Date: Wed, 24 Apr 2024 19:56:16 -0400 Subject: [PATCH 32/43] Added test on FlightTest too --- tests/FlightTest.php | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/tests/FlightTest.php b/tests/FlightTest.php index d84336b..fe5b144 100644 --- a/tests/FlightTest.php +++ b/tests/FlightTest.php @@ -354,4 +354,20 @@ class FlightTest extends TestCase $this->expectOutputString('Thisisaroutewithhtml'); } + + public function testItDoesNotKeepThePreviousStateOfOneViewComponentUsingFlightRender(): void + { + Flight::set('flight.views.path', __DIR__ . '/views'); + + $this->expectOutputString("
    Hi
    \n
    \n"); + Flight::render('myComponent', ['prop' => 'Hi']); + + set_error_handler(function (int $code, string $message): void { + $this->assertMatchesRegularExpression('/^Undefined variable:? \$?prop$/', $message); + }); + + Flight::render('myComponent'); + + restore_error_handler(); + } } From eadf4333342e445e33800f34b23b3855541f2a19 Mon Sep 17 00:00:00 2001 From: fadrian06 Date: Wed, 24 Apr 2024 20:33:12 -0400 Subject: [PATCH 33/43] Added flag to not break Flight users code --- flight/template/View.php | 4 +++- tests/FlightTest.php | 12 ++++++++++-- tests/ViewTest.php | 11 ++++++++++- 3 files changed, 23 insertions(+), 4 deletions(-) diff --git a/flight/template/View.php b/flight/template/View.php index 9397cc6..f78a968 100644 --- a/flight/template/View.php +++ b/flight/template/View.php @@ -20,6 +20,8 @@ class View /** File extension. */ public string $extension = '.php'; + public bool $preserveVars = true; + /** * View variables. * @@ -122,7 +124,7 @@ class View include $this->template; - if ($data !== null) { + if ($this->preserveVars === false && $data !== null) { foreach (array_keys($data) as $variable) { unset($this->vars[$variable]); } diff --git a/tests/FlightTest.php b/tests/FlightTest.php index fe5b144..f9f2855 100644 --- a/tests/FlightTest.php +++ b/tests/FlightTest.php @@ -22,6 +22,7 @@ class FlightTest extends TestCase $_REQUEST = []; Flight::init(); Flight::setEngine(new Engine()); + Flight::set('flight.views.path', __DIR__ . '/views'); } protected function tearDown(): void @@ -355,9 +356,9 @@ class FlightTest extends TestCase $this->expectOutputString('Thisisaroutewithhtml'); } - public function testItDoesNotKeepThePreviousStateOfOneViewComponentUsingFlightRender(): void + public function testDoesNotPreserveVarsWhenFlagIsDisabled(): void { - Flight::set('flight.views.path', __DIR__ . '/views'); + Flight::view()->preserveVars = false; $this->expectOutputString("
    Hi
    \n
    \n"); Flight::render('myComponent', ['prop' => 'Hi']); @@ -370,4 +371,11 @@ class FlightTest extends TestCase restore_error_handler(); } + + public function testKeepThePreviousStateOfOneViewComponentByDefault(): void + { + $this->expectOutputString("
    Hi
    \n
    Hi
    \n"); + Flight::render('myComponent', ['prop' => 'Hi']); + Flight::render('myComponent'); + } } diff --git a/tests/ViewTest.php b/tests/ViewTest.php index 2432f15..361bb3c 100644 --- a/tests/ViewTest.php +++ b/tests/ViewTest.php @@ -153,8 +153,10 @@ class ViewTest extends TestCase ); } - public function testItDoesNotKeepThePreviousStateOfOneViewComponent(): void + public function testDoesNotPreserveVarsWhenFlagIsDisabled(): void { + $this->view->preserveVars = false; + $this->expectOutputString("
    Hi
    \n
    \n"); $this->view->render('myComponent', ['prop' => 'Hi']); @@ -166,4 +168,11 @@ class ViewTest extends TestCase restore_error_handler(); } + + public function testKeepThePreviousStateOfOneViewComponentByDefault(): void + { + $this->expectOutputString("
    Hi
    \n
    Hi
    \n"); + $this->view->render('myComponent', ['prop' => 'Hi']); + $this->view->render('myComponent'); + } } From 276a9e69f3993da79e18caf2dd239823d27f7cd3 Mon Sep 17 00:00:00 2001 From: fadrian06 Date: Wed, 24 Apr 2024 21:03:44 -0400 Subject: [PATCH 34/43] Added more tests when component defines a default value for no passed vars --- tests/FlightTest.php | 30 +++++++++++++++++------ tests/ViewTest.php | 57 +++++++++++++++++++++++++++++++++++++------ tests/views/input.php | 7 ++++++ 3 files changed, 79 insertions(+), 15 deletions(-) create mode 100644 tests/views/input.php diff --git a/tests/FlightTest.php b/tests/FlightTest.php index f9f2855..5d196d6 100644 --- a/tests/FlightTest.php +++ b/tests/FlightTest.php @@ -356,26 +356,42 @@ class FlightTest extends TestCase $this->expectOutputString('Thisisaroutewithhtml'); } - public function testDoesNotPreserveVarsWhenFlagIsDisabled(): void + /** @dataProvider \tests\ViewTest::renderDataProvider */ + public function testDoesNotPreserveVarsWhenFlagIsDisabled( + string $output, + array $renderParams, + string $regexp + ): void { Flight::view()->preserveVars = false; - $this->expectOutputString("
    Hi
    \n
    \n"); - Flight::render('myComponent', ['prop' => 'Hi']); + $this->expectOutputString($output); + Flight::render(...$renderParams); - set_error_handler(function (int $code, string $message): void { - $this->assertMatchesRegularExpression('/^Undefined variable:? \$?prop$/', $message); + set_error_handler(function (int $code, string $message) use ($regexp): void { + $this->assertMatchesRegularExpression($regexp, $message); }); - Flight::render('myComponent'); + Flight::render($renderParams[0]); restore_error_handler(); } public function testKeepThePreviousStateOfOneViewComponentByDefault(): void { - $this->expectOutputString("
    Hi
    \n
    Hi
    \n"); + $this->expectOutputString(<<Hi
    +
    Hi
    + + + + + + html); + Flight::render('myComponent', ['prop' => 'Hi']); Flight::render('myComponent'); + Flight::render('input', ['type' => 'number']); + Flight::render('input'); } } diff --git a/tests/ViewTest.php b/tests/ViewTest.php index 361bb3c..38cabe0 100644 --- a/tests/ViewTest.php +++ b/tests/ViewTest.php @@ -153,26 +153,67 @@ class ViewTest extends TestCase ); } - public function testDoesNotPreserveVarsWhenFlagIsDisabled(): void - { + /** @dataProvider renderDataProvider */ + public function testDoesNotPreserveVarsWhenFlagIsDisabled( + string $output, + array $renderParams, + string $regexp + ): void { $this->view->preserveVars = false; - $this->expectOutputString("
    Hi
    \n
    \n"); - $this->view->render('myComponent', ['prop' => 'Hi']); + $this->expectOutputString($output); + $this->view->render(...$renderParams); - set_error_handler(function (int $code, string $message): void { - $this->assertMatchesRegularExpression('/^Undefined variable:? \$?prop$/', $message); + set_error_handler(function (int $code, string $message) use ($regexp): void { + $this->assertMatchesRegularExpression($regexp, $message); }); - $this->view->render('myComponent'); + $this->view->render($renderParams[0]); restore_error_handler(); } public function testKeepThePreviousStateOfOneViewComponentByDefault(): void { - $this->expectOutputString("
    Hi
    \n
    Hi
    \n"); + $this->expectOutputString(<<Hi +
    Hi
    + + + + + + html); + $this->view->render('myComponent', ['prop' => 'Hi']); $this->view->render('myComponent'); + $this->view->render('input', ['type' => 'number']); + $this->view->render('input'); + } + + public static function renderDataProvider(): array + { + return [ + [ + <<Hi +
    + + html, + ['myComponent', ['prop' => 'Hi']], + '/^Undefined variable:? \$?prop$/' + ], + [ + << + + + + html, + ['input', ['type' => 'number']], + '/^.*$/' + ], + ]; } } diff --git a/tests/views/input.php b/tests/views/input.php new file mode 100644 index 0000000..19e7182 --- /dev/null +++ b/tests/views/input.php @@ -0,0 +1,7 @@ + + + From 0edcd872eaf6411a9aa37fb8ab4d5f8d474f7662 Mon Sep 17 00:00:00 2001 From: vlakoff <544424+vlakoff@users.noreply.github.com> Date: Sun, 28 Apr 2024 00:39:00 +0200 Subject: [PATCH 35/43] Remove .vscode directory, and add it to .gitignore --- .gitignore | 3 ++- .vscode/settings.json | 5 ----- 2 files changed, 2 insertions(+), 6 deletions(-) delete mode 100644 .vscode/settings.json diff --git a/.gitignore b/.gitignore index 59a1f8b..6e9a244 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ -.idea +.idea/ +.vscode/ vendor/ composer.phar composer.lock diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index fcf56e7..0000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "php.suggest.basic": false, - "editor.detectIndentation": false, - "editor.insertSpaces": true -} From f8811e1d8b5c31e9303f917b2460c8b4793796c4 Mon Sep 17 00:00:00 2001 From: n0nag0n Date: Wed, 1 May 2024 21:40:42 -0600 Subject: [PATCH 36/43] allowed for middlewares to be created with container --- flight/Engine.php | 104 +++++++++++++++++------------ flight/core/Dispatcher.php | 22 ++++-- flight/net/Route.php | 4 +- phpunit.xml | 5 +- tests/EngineTest.php | 43 ++++++++++++ tests/classes/ContainerDefault.php | 5 ++ 6 files changed, 132 insertions(+), 51 deletions(-) diff --git a/flight/Engine.php b/flight/Engine.php index 0d32f0a..b603eef 100644 --- a/flight/Engine.php +++ b/flight/Engine.php @@ -382,64 +382,82 @@ class Engine * Processes each routes middleware. * * @param Route $route The route to process the middleware for. - * @param string $event_name If this is the before or after method. + * @param string $eventName If this is the before or after method. */ - protected function processMiddleware(Route $route, string $event_name): bool + protected function processMiddleware(Route $route, string $eventName): bool { - $at_least_one_middleware_failed = false; + $atLeastOneMiddlewareFailed = false; - $middlewares = $event_name === Dispatcher::FILTER_BEFORE ? $route->middleware : array_reverse($route->middleware); + // Process things normally for before, and then in reverse order for after. + $middlewares = $eventName === Dispatcher::FILTER_BEFORE + ? $route->middleware + : array_reverse($route->middleware); $params = $route->params; foreach ($middlewares as $middleware) { - $middleware_object = false; - - if ($event_name === Dispatcher::FILTER_BEFORE) { - // can be a callable or a class - $middleware_object = (is_callable($middleware) === true - ? $middleware - : (method_exists($middleware, Dispatcher::FILTER_BEFORE) === true - ? [$middleware, Dispatcher::FILTER_BEFORE] - : false - ) - ); - } 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, Dispatcher::FILTER_AFTER) === true - ) { - $middleware_object = [$middleware, Dispatcher::FILTER_AFTER]; + + // Assume that nothing is going to be executed for the middleware. + $middlewareObject = false; + + // Closure functions can only run on the before event + if ($eventName === Dispatcher::FILTER_BEFORE && is_object($middleware) === true && ($middleware instanceof Closure)) { + $middlewareObject = $middleware; + + // If the object has already been created, we can just use it if the event name exists. + } elseif (is_object($middleware) === true) { + $middlewareObject = method_exists($middleware, $eventName) === true ? [ $middleware, $eventName ] : false; + + // If the middleware is a string, we need to create the object and then call the event. + } elseif (is_string($middleware) === true && method_exists($middleware, $eventName) === true) { + $resolvedClass = null; + + // if there's a container assigned, we should use it to create the object + if ($this->dispatcher->mustUseContainer($middleware) === true) { + $resolvedClass = $this->dispatcher->resolveContainerClass($middleware, $params); + // otherwise just assume it's a plain jane class, so inject the engine + // just like in Dispatcher::invokeCallable() + } elseif (class_exists($middleware) === true) { + $resolvedClass = new $middleware($this); + } + + // If something was resolved, create an array callable that will be passed in later. + if ($resolvedClass !== null) { + $middlewareObject = [ $resolvedClass, $eventName ]; } } - if ($middleware_object === false) { + // If nothing was resolved, go to the next thing + if ($middlewareObject === false) { continue; } - $use_v3_output_buffering = + // This is the way that v3 handles output buffering (which captures output correctly) + $useV3OutputBuffering = $this->response()->v2_output_buffering === false && $route->is_streamed === false; - if ($use_v3_output_buffering === true) { + if ($useV3OutputBuffering === 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); + // Here is the array callable $middlewareObject that we created earlier. + // It looks bizarre but it's really calling [ $class, $method ]($params) + // Which loosely translates to $class->$method($params) + $middlewareResult = $middlewareObject($params); - if ($use_v3_output_buffering === true) { + if ($useV3OutputBuffering === true) { $this->response()->write(ob_get_clean()); } - if ($middleware_result === false) { - $at_least_one_middleware_failed = true; + // If you return false in your middleware, it will halt the request + // and throw a 403 forbidden error by default. + if ($middlewareResult === false) { + $atLeastOneMiddlewareFailed = true; break; } } - return $at_least_one_middleware_failed; + return $atLeastOneMiddlewareFailed; } //////////////////////// @@ -475,7 +493,7 @@ class Engine } // Route the request - $failed_middleware_check = false; + $failedMiddlewareCheck = false; while ($route = $router->route($request)) { $params = array_values($route->params); @@ -506,18 +524,18 @@ class Engine // Run any before middlewares if (count($route->middleware) > 0) { - $at_least_one_middleware_failed = $this->processMiddleware($route, 'before'); - if ($at_least_one_middleware_failed === true) { - $failed_middleware_check = true; + $atLeastOneMiddlewareFailed = $this->processMiddleware($route, 'before'); + if ($atLeastOneMiddlewareFailed === true) { + $failedMiddlewareCheck = true; break; } } - $use_v3_output_buffering = + $useV3OutputBuffering = $this->response()->v2_output_buffering === false && $route->is_streamed === false; - if ($use_v3_output_buffering === true) { + if ($useV3OutputBuffering === true) { ob_start(); } @@ -527,17 +545,17 @@ class Engine $params ); - if ($use_v3_output_buffering === true) { + if ($useV3OutputBuffering === 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($route, 'after'); + $atLeastOneMiddlewareFailed = $this->processMiddleware($route, 'after'); - if ($at_least_one_middleware_failed === true) { - $failed_middleware_check = true; + if ($atLeastOneMiddlewareFailed === true) { + $failedMiddlewareCheck = true; break; } } @@ -558,7 +576,7 @@ class Engine $response->clearBody(); } - if ($failed_middleware_check === true) { + if ($failedMiddlewareCheck === true) { $this->halt(403, 'Forbidden', empty(getenv('PHPUNIT_TEST'))); } elseif ($dispatched === false) { $this->notFound(); diff --git a/flight/core/Dispatcher.php b/flight/core/Dispatcher.php index 1d40f95..20e27b8 100644 --- a/flight/core/Dispatcher.php +++ b/flight/core/Dispatcher.php @@ -356,10 +356,7 @@ class Dispatcher [$class, $method] = $func; - $mustUseTheContainer = $this->containerHandler !== null && ( - (is_object($class) === true && strpos(get_class($class), 'flight\\') === false) - || is_string($class) - ); + $mustUseTheContainer = $this->mustUseContainer($class); if ($mustUseTheContainer === true) { $resolvedClass = $this->resolveContainerClass($class, $params); @@ -437,7 +434,7 @@ class Dispatcher * * @return ?object Class object. */ - protected function resolveContainerClass(string $class, array &$params) + public function resolveContainerClass(string $class, array &$params) { // PSR-11 if ( @@ -468,6 +465,21 @@ class Dispatcher return null; } + /** + * Checks to see if a container should be used or not. + * + * @param string|object $class the class to verify + * + * @return boolean + */ + public function mustUseContainer($class): bool + { + return $this->containerHandler !== null && ( + (is_object($class) === true && strpos(get_class($class), 'flight\\') === false) + || is_string($class) + ); + } + /** Because this could throw an exception in the middle of an output buffer, */ protected function fixOutputBuffering(): void { diff --git a/flight/net/Route.php b/flight/net/Route.php index 4d005b9..4e6e83c 100644 --- a/flight/net/Route.php +++ b/flight/net/Route.php @@ -63,7 +63,7 @@ class Route /** * The middleware to be applied to the route * - * @var array + * @var array */ public array $middleware = []; @@ -226,7 +226,7 @@ class Route /** * Sets the route middleware * - * @param array|callable $middleware + * @param array|callable|string $middleware */ public function addMiddleware($middleware): self { diff --git a/phpunit.xml b/phpunit.xml index 18134c0..cb460e5 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -11,10 +11,13 @@ stopOnFailure="true" verbose="true" colors="true"> - + flight/ + + flight/autoload.php + diff --git a/tests/EngineTest.php b/tests/EngineTest.php index 6c1837e..ee6ad76 100644 --- a/tests/EngineTest.php +++ b/tests/EngineTest.php @@ -572,6 +572,49 @@ class EngineTest extends TestCase $this->expectOutputString('OK123after123'); } + public function testMiddlewareClassStringNoContainer() + { + $middleware = new class { + public function after($params) + { + echo 'after' . $params['id']; + } + }; + $engine = new Engine(); + + $engine->route('/path1/@id', function ($id) { + echo 'OK' . $id; + }) + ->addMiddleware(get_class($middleware)); + $engine->request()->url = '/path1/123'; + $engine->start(); + $this->expectOutputString('OK123after123'); + } + + public function testMiddlewareClassStringWithContainer() + { + + $engine = new Engine(); + $dice = new \Dice\Dice(); + $dice = $dice->addRule('*', [ + 'substitutions' => [ + Engine::class => $engine + ] + ]); + $engine->registerContainerHandler(function ($class, $params) use ($dice) { + return $dice->create($class, $params); + }); + + + $engine->route('/path1/@id', function ($id) { + echo 'OK' . $id; + }) + ->addMiddleware(ContainerDefault::class); + $engine->request()->url = '/path1/123'; + $engine->start(); + $this->expectOutputString('I returned before the route was called with the following parameters: {"id":"123"}OK123'); + } + public function testMiddlewareClassAfterFailedCheck() { $middleware = new class { diff --git a/tests/classes/ContainerDefault.php b/tests/classes/ContainerDefault.php index a71ff1f..1f91aec 100644 --- a/tests/classes/ContainerDefault.php +++ b/tests/classes/ContainerDefault.php @@ -15,6 +15,11 @@ class ContainerDefault $this->app = $engine; } + public function before(array $params) + { + echo 'I returned before the route was called with the following parameters: ' . json_encode($params); + } + public function testTheContainer() { return $this->app->get('test_me_out'); From 234b3ddb0a10112ea0d9ed2b4cab58df397e89c5 Mon Sep 17 00:00:00 2001 From: n0nag0n Date: Sun, 5 May 2024 00:26:05 -0600 Subject: [PATCH 37/43] changed it so runway commands are run from any repo --- .gitignore | 1 + composer.json | 1 + flight/Engine.php | 24 +++-- flight/commands/ControllerCommand.php | 91 ++++++++++++++++ flight/commands/RouteCommand.php | 126 +++++++++++++++++++++++ index.php | 2 +- tests/commands/ControllerCommandTest.php | 79 ++++++++++++++ tests/commands/RouteCommandTest.php | 123 ++++++++++++++++++++++ 8 files changed, 438 insertions(+), 9 deletions(-) create mode 100644 flight/commands/ControllerCommand.php create mode 100644 flight/commands/RouteCommand.php create mode 100644 tests/commands/ControllerCommandTest.php create mode 100644 tests/commands/RouteCommandTest.php diff --git a/.gitignore b/.gitignore index 6e9a244..7f0b216 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,4 @@ coverage/ *.sublime* clover.xml phpcs.xml +.runway-config.json \ No newline at end of file diff --git a/composer.json b/composer.json index 9384c12..ff82cab 100644 --- a/composer.json +++ b/composer.json @@ -41,6 +41,7 @@ }, "require-dev": { "ext-pdo_sqlite": "*", + "flightphp/runway": "^0.2.0", "league/container": "^4.2", "level-2/dice": "^4.0", "phpstan/extension-installer": "^1.3", diff --git a/flight/Engine.php b/flight/Engine.php index 0d32f0a..e0b4db2 100644 --- a/flight/Engine.php +++ b/flight/Engine.php @@ -652,10 +652,12 @@ class Engine * @param string $pattern URL pattern to match * @param callable|string $callback Callback function or string class->method * @param bool $pass_route Pass the matching route object to the callback + * + * @return Route */ - public function _post(string $pattern, $callback, bool $pass_route = false, string $route_alias = ''): void + public function _post(string $pattern, $callback, bool $pass_route = false, string $route_alias = ''): Route { - $this->router()->map('POST ' . $pattern, $callback, $pass_route, $route_alias); + return $this->router()->map('POST ' . $pattern, $callback, $pass_route, $route_alias); } /** @@ -664,10 +666,12 @@ class Engine * @param string $pattern URL pattern to match * @param callable|string $callback Callback function or string class->method * @param bool $pass_route Pass the matching route object to the callback + * + * @return Route */ - public function _put(string $pattern, $callback, bool $pass_route = false, string $route_alias = ''): void + public function _put(string $pattern, $callback, bool $pass_route = false, string $route_alias = ''): Route { - $this->router()->map('PUT ' . $pattern, $callback, $pass_route, $route_alias); + return $this->router()->map('PUT ' . $pattern, $callback, $pass_route, $route_alias); } /** @@ -676,10 +680,12 @@ class Engine * @param string $pattern URL pattern to match * @param callable|string $callback Callback function or string class->method * @param bool $pass_route Pass the matching route object to the callback + * + * @return Route */ - public function _patch(string $pattern, $callback, bool $pass_route = false, string $route_alias = ''): void + public function _patch(string $pattern, $callback, bool $pass_route = false, string $route_alias = ''): Route { - $this->router()->map('PATCH ' . $pattern, $callback, $pass_route, $route_alias); + return $this->router()->map('PATCH ' . $pattern, $callback, $pass_route, $route_alias); } /** @@ -688,10 +694,12 @@ class Engine * @param string $pattern URL pattern to match * @param callable|string $callback Callback function or string class->method * @param bool $pass_route Pass the matching route object to the callback + * + * @return Route */ - public function _delete(string $pattern, $callback, bool $pass_route = false, string $route_alias = ''): void + public function _delete(string $pattern, $callback, bool $pass_route = false, string $route_alias = ''): Route { - $this->router()->map('DELETE ' . $pattern, $callback, $pass_route, $route_alias); + return $this->router()->map('DELETE ' . $pattern, $callback, $pass_route, $route_alias); } /** diff --git a/flight/commands/ControllerCommand.php b/flight/commands/ControllerCommand.php new file mode 100644 index 0000000..9706d97 --- /dev/null +++ b/flight/commands/ControllerCommand.php @@ -0,0 +1,91 @@ + $config JSON config from .runway-config.json + */ + public function __construct(array $config) + { + parent::__construct('make:controller', 'Create a controller', $config); + $this->argument('', 'The name of the controller to create (with or without the Controller suffix)'); + } + + /** + * Executes the function + * + * @return void + */ + public function execute(string $controller) + { + $io = $this->app()->io(); + if (isset($this->config['app_root']) === false) { + $io->error('app_root not set in .runway-config.json', true); + return; + } + + if (!preg_match('/Controller$/', $controller)) { + $controller .= 'Controller'; + } + + $controllerPath = getcwd() . DIRECTORY_SEPARATOR . $this->config['app_root'] . 'controllers' . DIRECTORY_SEPARATOR . $controller . '.php'; + if (file_exists($controllerPath) === true) { + $io->error($controller . ' already exists.', true); + return; + } + + if (is_dir(dirname($controllerPath)) === false) { + $io->info('Creating directory ' . dirname($controllerPath), true); + mkdir(dirname($controllerPath), 0755, true); + } + + $file = new PhpFile(); + $file->setStrictTypes(); + + $namespace = new PhpNamespace('app\\controllers'); + $namespace->addUse('flight\\Engine'); + + $class = new ClassType($controller); + $class->addProperty('app') + ->setVisibility('protected') + ->setType('flight\\Engine') + ->addComment('@var Engine'); + $method = $class->addMethod('__construct') + ->addComment('Constructor') + ->setVisibility('public') + ->setBody('$this->app = $app;'); + $method->addParameter('app') + ->setType('flight\\Engine'); + + $namespace->add($class); + $file->addNamespace($namespace); + + $this->persistClass($controller, $file); + + $io->ok('Controller successfully created at ' . $controllerPath, true); + } + + /** + * Saves the class name to a file + * + * @param string $controllerName Name of the Controller + * @param PhpFile $file Class Object from Nette\PhpGenerator + * + * @return void + */ + protected function persistClass(string $controllerName, PhpFile $file) + { + $printer = new \Nette\PhpGenerator\PsrPrinter(); + file_put_contents(getcwd() . DIRECTORY_SEPARATOR . $this->config['app_root'] . 'controllers' . DIRECTORY_SEPARATOR . $controllerName . '.php', $printer->printFile($file)); + } +} diff --git a/flight/commands/RouteCommand.php b/flight/commands/RouteCommand.php new file mode 100644 index 0000000..6ba661a --- /dev/null +++ b/flight/commands/RouteCommand.php @@ -0,0 +1,126 @@ + $config JSON config from .runway-config.json + */ + public function __construct(array $config) + { + parent::__construct('routes', 'Gets all routes for an application', $config); + + $this->option('--get', 'Only return GET requests'); + $this->option('--post', 'Only return POST requests'); + $this->option('--delete', 'Only return DELETE requests'); + $this->option('--put', 'Only return PUT requests'); + $this->option('--patch', 'Only return PATCH requests'); + } + + /** + * Executes the function + * + * @return void + */ + public function execute() + { + $io = $this->app()->io(); + + if(isset($this->config['index_root']) === false) { + $io->error('index_root not set in .runway-config.json', true); + return; + } + + $io->bold('Routes', true); + + $cwd = getcwd(); + + $index_root = $cwd . '/' . $this->config['index_root']; + + // This makes it so the framework doesn't actually execute + Flight::map('start', function () { + return; + }); + include($index_root); + $routes = Flight::router()->getRoutes(); + $arrayOfRoutes = []; + foreach ($routes as $route) { + if ($this->shouldAddRoute($route) === true) { + $middlewares = []; + if (!empty($route->middleware)) { + try { + $middlewares = array_map(function ($middleware) { + $middleware_class_name = explode("\\", get_class($middleware)); + return preg_match("/^class@anonymous/", end($middleware_class_name)) ? 'Anonymous' : end($middleware_class_name); + }, $route->middleware); + } catch (\TypeError $e) { + $middlewares[] = 'Bad Middleware'; + } finally { + if(is_string($route->middleware) === true) { + $middlewares[] = $route->middleware; + } + } + } + + $arrayOfRoutes[] = [ + 'Pattern' => $route->pattern, + 'Methods' => implode(', ', $route->methods), + 'Alias' => $route->alias ?? '', + 'Streamed' => $route->is_streamed ? 'Yes' : 'No', + 'Middleware' => !empty($middlewares) ? implode(",", $middlewares) : '-' + ]; + } + } + $io->table($arrayOfRoutes, [ + 'head' => 'boldGreen' + ]); + } + + /** + * Whether or not to add the route based on the request + * + * @param Route $route Flight Route object + * + * @return boolean + */ + public function shouldAddRoute(Route $route) + { + $boolval = false; + + $showAll = !$this->get && !$this->post && !$this->put && !$this->delete && !$this->patch; + if ($showAll === true) { + $boolval = true; + } else { + $methods = [ 'GET', 'POST', 'PUT', 'DELETE', 'PATCH' ]; + foreach ($methods as $method) { + $lowercaseMethod = strtolower($method); + if ( + $this->{$lowercaseMethod} === true && + ( + $route->methods[0] === '*' || + in_array($method, $route->methods, true) === true + ) + ) { + $boolval = true; + break; + } + } + } + return $boolval; + } +} diff --git a/index.php b/index.php index 65ea154..0db24be 100644 --- a/index.php +++ b/index.php @@ -2,7 +2,7 @@ declare(strict_types=1); -require 'flight/Flight.php'; +require_once 'flight/Flight.php'; // require 'flight/autoload.php'; Flight::route('/', function () { diff --git a/tests/commands/ControllerCommandTest.php b/tests/commands/ControllerCommandTest.php new file mode 100644 index 0000000..cee5c83 --- /dev/null +++ b/tests/commands/ControllerCommandTest.php @@ -0,0 +1,79 @@ + false); + + return $app->io(new Interactor(static::$in, static::$ou)); + } + + public function testConfigAppRootNotSet() + { + $app = $this->newApp('test', '0.0.1'); + $app->add(new ControllerCommand([])); + $app->handle(['runway', 'make:controller', 'Test']); + + $this->assertStringContainsString('app_root not set in .runway-config.json', file_get_contents(static::$ou)); + } + + public function testControllerAlreadyExists() + { + $app = $this->newApp('test', '0.0.1'); + mkdir(__DIR__.'/controllers/'); + file_put_contents(__DIR__.'/controllers/TestController.php', 'add(new ControllerCommand(['app_root' => 'tests/commands/'])); + $app->handle(['runway', 'make:controller', 'Test']); + + $this->assertStringContainsString('TestController already exists.', file_get_contents(static::$ou)); + } + + public function testCreateController() + { + $app = $this->newApp('test', '0.0.1'); + $app->add(new ControllerCommand(['app_root' => 'tests/commands/'])); + $app->handle(['runway', 'make:controller', 'Test']); + + $this->assertFileExists(__DIR__.'/controllers/TestController.php'); + } + +} diff --git a/tests/commands/RouteCommandTest.php b/tests/commands/RouteCommandTest.php new file mode 100644 index 0000000..b459734 --- /dev/null +++ b/tests/commands/RouteCommandTest.php @@ -0,0 +1,123 @@ + false); + + return $app->io(new Interactor(static::$in, static::$ou)); + } + + protected function createIndexFile() + { + $index = <<addMiddleware(function() {}); +Flight::delete('/delete', function () {}); +Flight::put('/put', function () {}); +Flight::patch('/patch', function () {})->addMiddleware('SomeMiddleware'); +Flight::router()->case_sensitive = true; + +Flight::start(); +PHP; + + file_put_contents(__DIR__.'/index.php', $index); + } + + protected function removeColors(string $str): string + { + return preg_replace('/\e\[[\d;]*m/', '', $str); + } + + public function testConfigIndexRootNotSet() + { + $app = $this->newApp('test', '0.0.1'); + $app->add(new RouteCommand([])); + $app->handle(['runway', 'routes']); + + $this->assertStringContainsString('index_root not set in .runway-config.json', file_get_contents(static::$ou)); + } + + public function testGetRoutes() + { + $app = $this->newApp('test', '0.0.1'); + $this->createIndexFile(); + $app->add(new RouteCommand(['index_root' => 'tests/commands/index.php'])); + $app->handle(['runway', 'routes']); + + $this->assertStringContainsString('Routes', file_get_contents(static::$ou)); + $this->assertStringContainsString('+---------+-----------+-------+----------+----------------+ +| Pattern | Methods | Alias | Streamed | Middleware | ++---------+-----------+-------+----------+----------------+ +| / | GET, HEAD | | No | - | +| /post | POST | | No | Closure | +| /delete | DELETE | | No | - | +| /put | PUT | | No | - | +| /patch | PATCH | | No | Bad Middleware | ++---------+-----------+-------+----------+----------------+', $this->removeColors(file_get_contents(static::$ou))); + } + + public function testGetPostRoute() { + $app = $this->newApp('test', '0.0.1'); + $this->createIndexFile(); + $app->add(new RouteCommand(['index_root' => 'tests/commands/index.php'])); + $app->handle(['runway', 'routes', '--post']); + + $this->assertStringContainsString('Routes', file_get_contents(static::$ou)); + $this->assertStringContainsString('+---------+---------+-------+----------+------------+ +| Pattern | Methods | Alias | Streamed | Middleware | ++---------+---------+-------+----------+------------+ +| /post | POST | | No | Closure | ++---------+---------+-------+----------+------------+', $this->removeColors(file_get_contents(static::$ou))); + } + +} From 508dca8cda65043ccc1d8384cee52831da4f37d7 Mon Sep 17 00:00:00 2001 From: fadrian06 Date: Thu, 9 May 2024 02:31:04 -0400 Subject: [PATCH 38/43] Simplified php version constraint #587 --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index ff82cab..bf59bbf 100644 --- a/composer.json +++ b/composer.json @@ -23,7 +23,7 @@ } ], "require": { - "php": "^7.4|^8.0|^8.1|^8.2|^8.3", + "php": ">=7.4", "ext-json": "*" }, "autoload": { From 27718cfea72da8d249f98d55ec6395cdc39636b7 Mon Sep 17 00:00:00 2001 From: Austin Collier Date: Thu, 16 May 2024 23:03:36 -0600 Subject: [PATCH 39/43] added ability to throw a method not found instead of 404 --- flight/Engine.php | 49 ++++++++------ flight/commands/RouteCommand.php | 18 ++--- flight/net/Router.php | 53 +++++++++------ tests/EngineTest.php | 43 ++++++++++++ tests/commands/ControllerCommandTest.php | 66 +++++++++--------- tests/commands/RouteCommandTest.php | 85 ++++++++++++------------ 6 files changed, 186 insertions(+), 128 deletions(-) diff --git a/flight/Engine.php b/flight/Engine.php index 47e84f7..2559810 100644 --- a/flight/Engine.php +++ b/flight/Engine.php @@ -388,15 +388,14 @@ class Engine { $atLeastOneMiddlewareFailed = false; - // Process things normally for before, and then in reverse order for after. - $middlewares = $eventName === Dispatcher::FILTER_BEFORE - ? $route->middleware - : array_reverse($route->middleware); + // Process things normally for before, and then in reverse order for after. + $middlewares = $eventName === Dispatcher::FILTER_BEFORE + ? $route->middleware + : array_reverse($route->middleware); $params = $route->params; foreach ($middlewares as $middleware) { - - // Assume that nothing is going to be executed for the middleware. + // Assume that nothing is going to be executed for the middleware. $middlewareObject = false; // Closure functions can only run on the before event @@ -426,12 +425,12 @@ class Engine } } - // If nothing was resolved, go to the next thing + // If nothing was resolved, go to the next thing if ($middlewareObject === false) { continue; } - // This is the way that v3 handles output buffering (which captures output correctly) + // This is the way that v3 handles output buffering (which captures output correctly) $useV3OutputBuffering = $this->response()->v2_output_buffering === false && $route->is_streamed === false; @@ -441,16 +440,16 @@ class Engine } // Here is the array callable $middlewareObject that we created earlier. - // It looks bizarre but it's really calling [ $class, $method ]($params) - // Which loosely translates to $class->$method($params) + // It looks bizarre but it's really calling [ $class, $method ]($params) + // Which loosely translates to $class->$method($params) $middlewareResult = $middlewareObject($params); if ($useV3OutputBuffering === true) { $this->response()->write(ob_get_clean()); } - // If you return false in your middleware, it will halt the request - // and throw a 403 forbidden error by default. + // If you return false in your middleware, it will halt the request + // and throw a 403 forbidden error by default. if ($middlewareResult === false) { $atLeastOneMiddlewareFailed = true; break; @@ -579,7 +578,13 @@ class Engine if ($failedMiddlewareCheck === true) { $this->halt(403, 'Forbidden', empty(getenv('PHPUNIT_TEST'))); } elseif ($dispatched === false) { - $this->notFound(); + // Get the previous route and check if the method failed, but the URL was good. + $lastRouteExecuted = $router->executedRoute; + if ($lastRouteExecuted !== null && $lastRouteExecuted->matchUrl($request->url) === true && $lastRouteExecuted->matchMethod($request->method) === false) { + $this->halt(405, 'Method Not Allowed', empty(getenv('PHPUNIT_TEST'))); + } else { + $this->notFound(); + } } } @@ -670,8 +675,8 @@ class Engine * @param string $pattern URL pattern to match * @param callable|string $callback Callback function or string class->method * @param bool $pass_route Pass the matching route object to the callback - * - * @return Route + * + * @return Route */ public function _post(string $pattern, $callback, bool $pass_route = false, string $route_alias = ''): Route { @@ -684,8 +689,8 @@ class Engine * @param string $pattern URL pattern to match * @param callable|string $callback Callback function or string class->method * @param bool $pass_route Pass the matching route object to the callback - * - * @return Route + * + * @return Route */ public function _put(string $pattern, $callback, bool $pass_route = false, string $route_alias = ''): Route { @@ -698,12 +703,12 @@ class Engine * @param string $pattern URL pattern to match * @param callable|string $callback Callback function or string class->method * @param bool $pass_route Pass the matching route object to the callback - * - * @return Route + * + * @return Route */ public function _patch(string $pattern, $callback, bool $pass_route = false, string $route_alias = ''): Route { - return $this->router()->map('PATCH ' . $pattern, $callback, $pass_route, $route_alias); + return $this->router()->map('PATCH ' . $pattern, $callback, $pass_route, $route_alias); } /** @@ -712,8 +717,8 @@ class Engine * @param string $pattern URL pattern to match * @param callable|string $callback Callback function or string class->method * @param bool $pass_route Pass the matching route object to the callback - * - * @return Route + * + * @return Route */ public function _delete(string $pattern, $callback, bool $pass_route = false, string $route_alias = ''): Route { diff --git a/flight/commands/RouteCommand.php b/flight/commands/RouteCommand.php index 6ba661a..a34b821 100644 --- a/flight/commands/RouteCommand.php +++ b/flight/commands/RouteCommand.php @@ -41,10 +41,10 @@ class RouteCommand extends AbstractBaseCommand { $io = $this->app()->io(); - if(isset($this->config['index_root']) === false) { - $io->error('index_root not set in .runway-config.json', true); - return; - } + if (isset($this->config['index_root']) === false) { + $io->error('index_root not set in .runway-config.json', true); + return; + } $io->bold('Routes', true); @@ -69,12 +69,12 @@ class RouteCommand extends AbstractBaseCommand return preg_match("/^class@anonymous/", end($middleware_class_name)) ? 'Anonymous' : end($middleware_class_name); }, $route->middleware); } catch (\TypeError $e) { - $middlewares[] = 'Bad Middleware'; + $middlewares[] = 'Bad Middleware'; } finally { - if(is_string($route->middleware) === true) { - $middlewares[] = $route->middleware; - } - } + if (is_string($route->middleware) === true) { + $middlewares[] = $route->middleware; + } + } } $arrayOfRoutes[] = [ diff --git a/flight/net/Router.php b/flight/net/Router.php index 88df038..a43b5ba 100644 --- a/flight/net/Router.php +++ b/flight/net/Router.php @@ -32,7 +32,7 @@ class Router /** * The current route that is has been found and executed. */ - protected ?Route $executedRoute = null; + public ?Route $executedRoute = null; /** * Pointer to current route. @@ -42,21 +42,21 @@ class Router /** * When groups are used, this is mapped against all the routes */ - protected string $group_prefix = ''; + protected string $groupPrefix = ''; /** * Group Middleware * * @var array */ - protected array $group_middlewares = []; + protected array $groupMiddlewares = []; /** * Allowed HTTP methods * * @var array */ - protected array $allowed_methods = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS']; + protected array $allowedMethods = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS']; /** * Gets mapped routes. @@ -93,7 +93,7 @@ class Router // Flight::route('', function() {}); // } // Keep the space so that it can execute the below code normally - if ($this->group_prefix !== '') { + if ($this->groupPrefix !== '') { $url = ltrim($pattern); } else { $url = trim($pattern); @@ -113,14 +113,14 @@ class Router } // And this finishes it off. - if ($this->group_prefix !== '') { - $url = rtrim($this->group_prefix . $url); + if ($this->groupPrefix !== '') { + $url = rtrim($this->groupPrefix . $url); } $route = new Route($url, $callback, $methods, $pass_route, $route_alias); // to handle group middleware - foreach ($this->group_middlewares as $gm) { + foreach ($this->groupMiddlewares as $gm) { $route->addMiddleware($gm); } @@ -197,20 +197,20 @@ class Router /** * Group together a set of routes * - * @param string $group_prefix group URL prefix (such as /api/v1) + * @param string $groupPrefix group URL prefix (such as /api/v1) * @param callable $callback The necessary calling that holds the Router class - * @param array $group_middlewares + * @param array $groupMiddlewares * The middlewares to be applied to the group. Example: `[$middleware1, $middleware2]` */ - public function group(string $group_prefix, callable $callback, array $group_middlewares = []): void + public function group(string $groupPrefix, callable $callback, array $groupMiddlewares = []): void { - $old_group_prefix = $this->group_prefix; - $old_group_middlewares = $this->group_middlewares; - $this->group_prefix .= $group_prefix; - $this->group_middlewares = array_merge($this->group_middlewares, $group_middlewares); + $oldGroupPrefix = $this->groupPrefix; + $oldGroupMiddlewares = $this->groupMiddlewares; + $this->groupPrefix .= $groupPrefix; + $this->groupMiddlewares = array_merge($this->groupMiddlewares, $groupMiddlewares); $callback($this); - $this->group_prefix = $old_group_prefix; - $this->group_middlewares = $old_group_middlewares; + $this->groupPrefix = $oldGroupPrefix; + $this->groupMiddlewares = $oldGroupMiddlewares; } /** @@ -221,9 +221,14 @@ class Router public function route(Request $request) { while ($route = $this->current()) { - if ($route->matchMethod($request->method) && $route->matchUrl($request->url, $this->case_sensitive)) { + $urlMatches = $route->matchUrl($request->url, $this->case_sensitive); + $methodMatches = $route->matchMethod($request->method); + if ($urlMatches === true && $methodMatches === true) { $this->executedRoute = $route; return $route; + // capture the route but don't execute it. We'll use this in Engine->start() to throw a 405 + } elseif ($urlMatches === true && $methodMatches === false) { + $this->executedRoute = $route; } $this->next(); } @@ -299,12 +304,20 @@ class Router return $this->routes[$this->index] ?? false; } + /** + * Gets the previous route. + */ + public function previous(): void + { + --$this->index; + } + /** * Gets the next route. */ public function next(): void { - $this->index++; + ++$this->index; } /** @@ -312,6 +325,6 @@ class Router */ public function reset(): void { - $this->index = 0; + $this->rewind(); } } diff --git a/tests/EngineTest.php b/tests/EngineTest.php index ee6ad76..3c24c88 100644 --- a/tests/EngineTest.php +++ b/tests/EngineTest.php @@ -899,4 +899,47 @@ class EngineTest extends TestCase $engine->start(); } + + public function testRouteFoundButBadMethod() { + $engine = new class extends Engine { + public function getLoader() + { + return $this->loader; + } + }; + // doing this so we can overwrite some parts of the response + $engine->getLoader()->register('response', function () { + return new class extends Response { + public function setRealHeader( + string $header_string, + bool $replace = true, + int $response_code = 0 + ): self { + return $this; + } + }; + }); + + $engine->route('POST /path1/@id', function ($id) { + echo 'OK' . $id; + }); + + $engine->route('GET /path2/@id', function ($id) { + echo 'OK' . $id; + }); + + $engine->route('PATCH /path3/@id', function ($id) { + echo 'OK' . $id; + }); + + $engine->request()->url = '/path1/123'; + $engine->request()->method = 'GET'; + + $engine->start(); + + $this->expectOutputString('Method Not Allowed'); + $this->assertEquals(405, $engine->response()->status()); + $this->assertEquals('Method Not Allowed', $engine->response()->getBody()); + } + } diff --git a/tests/commands/ControllerCommandTest.php b/tests/commands/ControllerCommandTest.php index cee5c83..82fb0c1 100644 --- a/tests/commands/ControllerCommandTest.php +++ b/tests/commands/ControllerCommandTest.php @@ -11,8 +11,7 @@ use PHPUnit\Framework\TestCase; class ControllerCommandTest extends TestCase { - - protected static $in = __DIR__ . '/input.test'; + protected static $in = __DIR__ . '/input.test'; protected static $ou = __DIR__ . '/output.test'; public function setUp(): void @@ -31,49 +30,48 @@ class ControllerCommandTest extends TestCase unlink(static::$ou); } - if (file_exists(__DIR__.'/controllers/TestController.php')) { - unlink(__DIR__.'/controllers/TestController.php'); - } + if (file_exists(__DIR__ . '/controllers/TestController.php')) { + unlink(__DIR__ . '/controllers/TestController.php'); + } - if (file_exists(__DIR__.'/controllers/')) { - rmdir(__DIR__.'/controllers/'); - } - } + if (file_exists(__DIR__ . '/controllers/')) { + rmdir(__DIR__ . '/controllers/'); + } + } - protected function newApp(string $name, string $version = '') + protected function newApp(string $name, string $version = '') { $app = new Application($name, $version ?: '0.0.1', fn () => false); return $app->io(new Interactor(static::$in, static::$ou)); } - public function testConfigAppRootNotSet() - { - $app = $this->newApp('test', '0.0.1'); - $app->add(new ControllerCommand([])); - $app->handle(['runway', 'make:controller', 'Test']); - - $this->assertStringContainsString('app_root not set in .runway-config.json', file_get_contents(static::$ou)); - } + public function testConfigAppRootNotSet() + { + $app = $this->newApp('test', '0.0.1'); + $app->add(new ControllerCommand([])); + $app->handle(['runway', 'make:controller', 'Test']); - public function testControllerAlreadyExists() - { - $app = $this->newApp('test', '0.0.1'); - mkdir(__DIR__.'/controllers/'); - file_put_contents(__DIR__.'/controllers/TestController.php', 'add(new ControllerCommand(['app_root' => 'tests/commands/'])); - $app->handle(['runway', 'make:controller', 'Test']); + $this->assertStringContainsString('app_root not set in .runway-config.json', file_get_contents(static::$ou)); + } - $this->assertStringContainsString('TestController already exists.', file_get_contents(static::$ou)); - } + public function testControllerAlreadyExists() + { + $app = $this->newApp('test', '0.0.1'); + mkdir(__DIR__ . '/controllers/'); + file_put_contents(__DIR__ . '/controllers/TestController.php', 'add(new ControllerCommand(['app_root' => 'tests/commands/'])); + $app->handle(['runway', 'make:controller', 'Test']); - public function testCreateController() - { - $app = $this->newApp('test', '0.0.1'); - $app->add(new ControllerCommand(['app_root' => 'tests/commands/'])); - $app->handle(['runway', 'make:controller', 'Test']); + $this->assertStringContainsString('TestController already exists.', file_get_contents(static::$ou)); + } - $this->assertFileExists(__DIR__.'/controllers/TestController.php'); - } + public function testCreateController() + { + $app = $this->newApp('test', '0.0.1'); + $app->add(new ControllerCommand(['app_root' => 'tests/commands/'])); + $app->handle(['runway', 'make:controller', 'Test']); + $this->assertFileExists(__DIR__ . '/controllers/TestController.php'); + } } diff --git a/tests/commands/RouteCommandTest.php b/tests/commands/RouteCommandTest.php index b459734..eae0b81 100644 --- a/tests/commands/RouteCommandTest.php +++ b/tests/commands/RouteCommandTest.php @@ -13,15 +13,14 @@ use PHPUnit\Framework\TestCase; class RouteCommandTest extends TestCase { - - protected static $in = __DIR__ . '/input.test'; + protected static $in = __DIR__ . '/input.test'; protected static $ou = __DIR__ . '/output.test'; public function setUp(): void { file_put_contents(static::$in, '', LOCK_EX); file_put_contents(static::$ou, '', LOCK_EX); - $_SERVER = []; + $_SERVER = []; $_REQUEST = []; Flight::init(); Flight::setEngine(new Engine()); @@ -37,25 +36,25 @@ class RouteCommandTest extends TestCase unlink(static::$ou); } - if (file_exists(__DIR__.'/index.php')) { - unlink(__DIR__.'/index.php'); - } + if (file_exists(__DIR__ . '/index.php')) { + unlink(__DIR__ . '/index.php'); + } - unset($_REQUEST); + unset($_REQUEST); unset($_SERVER); Flight::clear(); - } + } - protected function newApp(string $name, string $version = '') + protected function newApp(string $name, string $version = '') { $app = new Application($name, $version ?: '0.0.1', fn () => false); return $app->io(new Interactor(static::$in, static::$ou)); } - protected function createIndexFile() - { - $index = <<case_sensitive = true; Flight::start(); PHP; - file_put_contents(__DIR__.'/index.php', $index); - } + file_put_contents(__DIR__ . '/index.php', $index); + } - protected function removeColors(string $str): string - { - return preg_replace('/\e\[[\d;]*m/', '', $str); - } + protected function removeColors(string $str): string + { + return preg_replace('/\e\[[\d;]*m/', '', $str); + } - public function testConfigIndexRootNotSet() - { - $app = $this->newApp('test', '0.0.1'); - $app->add(new RouteCommand([])); - $app->handle(['runway', 'routes']); + public function testConfigIndexRootNotSet() + { + $app = $this->newApp('test', '0.0.1'); + $app->add(new RouteCommand([])); + $app->handle(['runway', 'routes']); - $this->assertStringContainsString('index_root not set in .runway-config.json', file_get_contents(static::$ou)); - } + $this->assertStringContainsString('index_root not set in .runway-config.json', file_get_contents(static::$ou)); + } - public function testGetRoutes() - { - $app = $this->newApp('test', '0.0.1'); - $this->createIndexFile(); - $app->add(new RouteCommand(['index_root' => 'tests/commands/index.php'])); - $app->handle(['runway', 'routes']); + public function testGetRoutes() + { + $app = $this->newApp('test', '0.0.1'); + $this->createIndexFile(); + $app->add(new RouteCommand(['index_root' => 'tests/commands/index.php'])); + $app->handle(['runway', 'routes']); - $this->assertStringContainsString('Routes', file_get_contents(static::$ou)); - $this->assertStringContainsString('+---------+-----------+-------+----------+----------------+ + $this->assertStringContainsString('Routes', file_get_contents(static::$ou)); + $this->assertStringContainsString('+---------+-----------+-------+----------+----------------+ | Pattern | Methods | Alias | Streamed | Middleware | +---------+-----------+-------+----------+----------------+ | / | GET, HEAD | | No | - | @@ -104,20 +103,20 @@ PHP; | /put | PUT | | No | - | | /patch | PATCH | | No | Bad Middleware | +---------+-----------+-------+----------+----------------+', $this->removeColors(file_get_contents(static::$ou))); - } + } - public function testGetPostRoute() { - $app = $this->newApp('test', '0.0.1'); - $this->createIndexFile(); - $app->add(new RouteCommand(['index_root' => 'tests/commands/index.php'])); - $app->handle(['runway', 'routes', '--post']); + public function testGetPostRoute() + { + $app = $this->newApp('test', '0.0.1'); + $this->createIndexFile(); + $app->add(new RouteCommand(['index_root' => 'tests/commands/index.php'])); + $app->handle(['runway', 'routes', '--post']); - $this->assertStringContainsString('Routes', file_get_contents(static::$ou)); - $this->assertStringContainsString('+---------+---------+-------+----------+------------+ + $this->assertStringContainsString('Routes', file_get_contents(static::$ou)); + $this->assertStringContainsString('+---------+---------+-------+----------+------------+ | Pattern | Methods | Alias | Streamed | Middleware | +---------+---------+-------+----------+------------+ | /post | POST | | No | Closure | +---------+---------+-------+----------+------------+', $this->removeColors(file_get_contents(static::$ou))); - } - + } } From 33676872ed3baef9bae1dfe6943d9099ec5b1ff2 Mon Sep 17 00:00:00 2001 From: Austin Collier Date: Wed, 22 May 2024 23:17:51 -0600 Subject: [PATCH 40/43] Maintains headers on redirect, error, and halt. Added jsonHalt --- flight/Engine.php | 41 ++++++++++++++++++++++++++++++++++++----- flight/Flight.php | 2 ++ tests/EngineTest.php | 10 ++++++++++ tests/RouterTest.php | 4 ++++ 4 files changed, 52 insertions(+), 5 deletions(-) diff --git a/flight/Engine.php b/flight/Engine.php index 2559810..a1378a6 100644 --- a/flight/Engine.php +++ b/flight/Engine.php @@ -57,6 +57,8 @@ use flight\net\Route; * @method void redirect(string $url, int $code = 303) Redirects the current request to another URL. * @method void json(mixed $data, int $code = 200, bool $encode = true, string $charset = 'utf-8', int $option = 0) * Sends a JSON response. + * @method void jsonHalt(mixed $data, int $code = 200, bool $encode = true, string $charset = 'utf-8', int $option = 0) + * Sends a JSON response and immediately halts the request. * @method void jsonp(mixed $data, string $param = 'jsonp', int $code = 200, bool $encode = true, string $charset = 'utf-8', int $option = 0) * Sends a JSONP response. * @@ -73,7 +75,7 @@ class Engine */ private const MAPPABLE_METHODS = [ 'start', 'stop', 'route', 'halt', 'error', 'notFound', - 'render', 'redirect', 'etag', 'lastModified', 'json', 'jsonp', + 'render', 'redirect', 'etag', 'lastModified', 'json', 'jsonHalt', 'jsonp', 'post', 'put', 'patch', 'delete', 'group', 'getUrl' ]; @@ -608,7 +610,7 @@ class Engine try { $this->response() - ->clear() + ->clearBody() ->status(500) ->write($msg) ->send(); @@ -735,7 +737,7 @@ class Engine public function _halt(int $code = 200, string $message = '', bool $actuallyExit = true): void { $this->response() - ->clear() + ->clearBody() ->status($code) ->write($message) ->send(); @@ -750,7 +752,7 @@ class Engine $output = '

    404 Not Found

    The page you have requested could not be found.

    '; $this->response() - ->clear() + ->clearBody() ->status(404) ->write($output) ->send(); @@ -775,7 +777,7 @@ class Engine } $this->response() - ->clear() + ->clearBody() ->status($code) ->header('Location', $url) ->send(); @@ -829,6 +831,33 @@ class Engine } } + /** + * Sends a JSON response and halts execution immediately. + * + * @param mixed $data JSON data + * @param int $code HTTP status code + * @param bool $encode Whether to perform JSON encoding + * @param string $charset Charset + * @param int $option Bitmask Json constant such as JSON_HEX_QUOT + * + * @throws Exception + */ + public function _jsonHalt( + $data, + int $code = 200, + bool $encode = true, + string $charset = 'utf-8', + int $option = 0 + ): void { + $this->json($data, $code, $encode, $charset, $option); + $jsonBody = $this->response()->getBody(); + if ($this->response()->v2_output_buffering === false) { + $this->response()->clearBody(); + $this->response()->send(); + } + $this->halt($code, $jsonBody, empty(getenv('PHPUNIT_TEST'))); + } + /** * Sends a JSONP response. * @@ -877,6 +906,7 @@ class Engine isset($_SERVER['HTTP_IF_NONE_MATCH']) && $_SERVER['HTTP_IF_NONE_MATCH'] === $id ) { + $this->response()->clear(); $this->halt(304, '', empty(getenv('PHPUNIT_TEST'))); } } @@ -894,6 +924,7 @@ class Engine isset($_SERVER['HTTP_IF_MODIFIED_SINCE']) && strtotime($_SERVER['HTTP_IF_MODIFIED_SINCE']) === $time ) { + $this->response()->clear(); $this->halt(304, '', empty(getenv('PHPUNIT_TEST'))); } } diff --git a/flight/Flight.php b/flight/Flight.php index a04ad80..207d44b 100644 --- a/flight/Flight.php +++ b/flight/Flight.php @@ -68,6 +68,8 @@ require_once __DIR__ . '/autoload.php'; * @method static void redirect(string $url, int $code = 303) Redirects to another URL. * @method static void json(mixed $data, int $code = 200, bool $encode = true, string $charset = "utf8", int $encodeOption = 0, int $encodeDepth = 512) * Sends a JSON response. + * @method void jsonHalt(mixed $data, int $code = 200, bool $encode = true, string $charset = 'utf-8', int $option = 0) + * Sends a JSON response and immediately halts the request. * @method static void jsonp(mixed $data, string $param = 'jsonp', int $code = 200, bool $encode = true, string $charset = "utf8", int $encodeOption = 0, int $encodeDepth = 512) * Sends a JSONP response. * @method static void error(Throwable $exception) Sends an HTTP 500 response. diff --git a/tests/EngineTest.php b/tests/EngineTest.php index 3c24c88..93f3ff7 100644 --- a/tests/EngineTest.php +++ b/tests/EngineTest.php @@ -370,6 +370,16 @@ class EngineTest extends TestCase $this->assertEquals(200, $engine->response()->status()); } + public function testJsonHalt() + { + $engine = new Engine(); + $this->expectOutputString('{"key1":"value1","key2":"value2"}'); + $engine->jsonHalt(['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 testJsonP() { $engine = new Engine(); diff --git a/tests/RouterTest.php b/tests/RouterTest.php index 8906168..f0ac765 100644 --- a/tests/RouterTest.php +++ b/tests/RouterTest.php @@ -631,6 +631,10 @@ class RouterTest extends TestCase $this->router->rewind(); $result = $this->router->valid(); $this->assertTrue($result); + + $this->router->previous(); + $result = $this->router->valid(); + $this->assertFalse($result); } public function testGetRootUrlByAlias() From 5549e9106ad866da72870a6fe47c3d633e168db4 Mon Sep 17 00:00:00 2001 From: Austin Collier Date: Thu, 30 May 2024 07:40:22 -0600 Subject: [PATCH 41/43] added some ui tests --- tests/server-v2/index.php | 5 +++++ tests/server/LayoutMiddleware.php | 1 + tests/server/index.php | 4 ++++ 3 files changed, 10 insertions(+) diff --git a/tests/server-v2/index.php b/tests/server-v2/index.php index 3c1951c..d0f8bcd 100644 --- a/tests/server-v2/index.php +++ b/tests/server-v2/index.php @@ -116,6 +116,10 @@ Flight::route('/jsonp', function () { echo "\n\n\n\n\n"; }); +Flight::route('/json-halt', function () { + Flight::jsonHalt(['message' => 'JSON rendered and halted successfully with no other body content!']); +}); + // Test 10: Halt Flight::route('/halt', function () { Flight::halt(400, 'Halt worked successfully'); @@ -200,6 +204,7 @@ echo '
  • Error
  • JSON
  • JSONP
  • +
  • JSON Halt
  • Halt
  • Redirect
  • '; diff --git a/tests/server/LayoutMiddleware.php b/tests/server/LayoutMiddleware.php index d23e81c..2d55f24 100644 --- a/tests/server/LayoutMiddleware.php +++ b/tests/server/LayoutMiddleware.php @@ -74,6 +74,7 @@ class LayoutMiddleware
  • Error
  • JSON
  • JSONP
  • +
  • JSON Halt
  • Halt
  • Redirect
  • Stream Plain
  • diff --git a/tests/server/index.php b/tests/server/index.php index a983590..5c86d11 100644 --- a/tests/server/index.php +++ b/tests/server/index.php @@ -171,6 +171,10 @@ Flight::route('/jsonp', function () { Flight::jsonp(['message' => 'JSONP renders successfully!'], 'jsonp'); }); +Flight::route('/json-halt', function () { + Flight::jsonHalt(['message' => 'JSON rendered and halted successfully with no other body content!']); +}); + Flight::map('error', function (Throwable $e) { echo sprintf( << Date: Sat, 1 Jun 2024 21:07:55 -0400 Subject: [PATCH 42/43] Simplified fix thanks to @vlakoff (https://github.com/vlakoff) --- flight/template/View.php | 14 ++++++-------- tests/ViewTest.php | 16 ++++++++++++++++ 2 files changed, 22 insertions(+), 8 deletions(-) diff --git a/flight/template/View.php b/flight/template/View.php index f78a968..d347a50 100644 --- a/flight/template/View.php +++ b/flight/template/View.php @@ -116,19 +116,17 @@ class View throw new \Exception("Template file not found: {$normalized_path}."); } - if (\is_array($data)) { - $this->vars = \array_merge($this->vars, $data); - } - \extract($this->vars); - include $this->template; + if (\is_array($data)) { + \extract($data); - if ($this->preserveVars === false && $data !== null) { - foreach (array_keys($data) as $variable) { - unset($this->vars[$variable]); + if ($this->preserveVars) { + $this->vars = \array_merge($this->vars, $data); } } + + include $this->template; } /** diff --git a/tests/ViewTest.php b/tests/ViewTest.php index 38cabe0..d6754c9 100644 --- a/tests/ViewTest.php +++ b/tests/ViewTest.php @@ -191,6 +191,22 @@ class ViewTest extends TestCase $this->view->render('input'); } + public function testKeepThePreviousStateOfDataSettedBySetMethod(): void + { + $this->view->preserveVars = false; + + $this->view->set('prop', 'bar'); + + $this->expectOutputString(<<qux +
    bar
    + + html); + + $this->view->render('myComponent', ['prop' => 'qux']); + $this->view->render('myComponent'); + } + public static function renderDataProvider(): array { return [ From a68fc3220418c304a69daf6b2abb4f91fb431933 Mon Sep 17 00:00:00 2001 From: fadrian06 Date: Mon, 3 Jun 2024 11:17:34 -0400 Subject: [PATCH 43/43] Unnecessary explicitness :| --- flight/template/View.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/flight/template/View.php b/flight/template/View.php index d347a50..15e4fc8 100644 --- a/flight/template/View.php +++ b/flight/template/View.php @@ -118,10 +118,10 @@ class View \extract($this->vars); - if (\is_array($data)) { + if (\is_array($data) === true) { \extract($data); - if ($this->preserveVars) { + if ($this->preserveVars === true) { $this->vars = \array_merge($this->vars, $data); } }