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.
diff --git a/.gitignore b/.gitignore
index 9bed37c..7f0b216 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,4 +1,5 @@
-.idea
+.idea/
+.vscode/
vendor/
composer.phar
composer.lock
@@ -6,3 +7,5 @@ composer.lock
coverage/
*.sublime*
clover.xml
+phpcs.xml
+.runway-config.json
\ No newline at end of file
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
-}
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 0000000..4b1af6d
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,58 @@
+## 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 attack vectors.
+
+* **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 related to the security vuln.
+
+* **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 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!
+
+Thanks! :heart: :heart: :heart:
+
+Flight Team
diff --git a/README.md b/README.md
index 4961468..a749fdb 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)
+[![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)
![Matrix](https://img.shields.io/matrix/flight-php-framework%3Amatrix.org?server_fqdn=matrix.org&style=social&logo=matrix)
# What is Flight?
diff --git a/composer.json b/composer.json
index eeae5b4..bf9a243 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": {
@@ -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",
@@ -64,7 +65,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",
diff --git a/flight/Engine.php b/flight/Engine.php
index 62342b6..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'
];
@@ -314,7 +316,7 @@ class Engine
*/
public function get(?string $key = null)
{
- if (null === $key) {
+ if ($key === null) {
return $this->vars;
}
@@ -360,7 +362,7 @@ class Engine
*/
public function clear(?string $key = null): void
{
- if (null === $key) {
+ if ($key === null) {
$this->vars = [];
return;
}
@@ -382,64 +384,81 @@ 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 +494,7 @@ class Engine
}
// Route the request
- $failed_middleware_check = false;
+ $failedMiddlewareCheck = false;
while ($route = $router->route($request)) {
$params = array_values($route->params);
@@ -487,13 +506,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']);
- 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;
@@ -503,18 +525,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();
}
@@ -524,17 +546,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;
}
}
@@ -555,10 +577,16 @@ 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();
+ // 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();
+ }
}
}
@@ -570,9 +598,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()
@@ -580,7 +610,7 @@ class Engine
try {
$this->response()
- ->clear()
+ ->clearBody()
->status(500)
->write($msg)
->send();
@@ -603,8 +633,8 @@ class Engine
{
$response = $this->response();
- if (!$response->sent()) {
- if (null !== $code) {
+ if ($response->sent() === false) {
+ if ($code !== null) {
$response->status($code);
}
@@ -647,10 +677,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);
}
/**
@@ -659,10 +691,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);
}
/**
@@ -671,10 +705,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);
}
/**
@@ -683,10 +719,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);
}
/**
@@ -699,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();
@@ -714,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();
@@ -729,17 +767,17 @@ 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);
}
$this->response()
- ->clear()
+ ->clearBody()
->status($code)
->header('Location', $url)
->send();
@@ -756,7 +794,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;
}
@@ -793,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.
*
@@ -833,7 +898,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) . '"');
@@ -841,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')));
}
}
@@ -858,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 0a42489..207d44b 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
@@ -65,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/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..a34b821
--- /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/flight/core/Dispatcher.php b/flight/core/Dispatcher.php
index 6a167a8..20e27b8 100644
--- a/flight/core/Dispatcher.php
+++ b/flight/core/Dispatcher.php
@@ -4,10 +4,12 @@ declare(strict_types=1);
namespace flight\core;
-use Closure;
use Exception;
use flight\Engine;
use InvalidArgumentException;
+use Psr\Container\ContainerInterface;
+use ReflectionFunction;
+use Throwable;
use TypeError;
/**
@@ -23,41 +25,54 @@ 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 ?Throwable $containerException = null;
- /** @var ?Engine $engine Engine instance */
+ /** @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 = [];
/**
* 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
+ * @param ContainerInterface|(callable(string $classString, array $params): (null|object)) $containerHandler
+ * Dependency injection container.
*
- * @return void
+ * @throws InvalidArgumentException If $containerHandler is not a `callable` or instance of `Psr\Container\ContainerInterface`.
*/
public function setContainerHandler($containerHandler): void
{
- $this->containerHandler = $containerHandler;
+ $containerInterfaceNS = '\Psr\Container\ContainerInterface';
+
+ if (
+ 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
@@ -68,11 +83,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 +125,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 +153,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 +168,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 +180,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 +192,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
{
@@ -188,27 +203,38 @@ class Dispatcher
return;
}
- $this->events = [];
- $this->filters = [];
+ $this->reset();
}
/**
* 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, mixed &$output): (void|false)|callable(mixed &$output): (void|false) $callback
*
* @return $this
*/
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);
}
+ 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;
@@ -217,10 +243,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,16 +268,19 @@ 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 = [])
{
- if (is_string($callback) === true && (strpos($callback, '->') !== false || strpos($callback, '::') !== false)) {
+ if (
+ is_string($callback) === true
+ && (strpos($callback, '->') !== false || strpos($callback, '::') !== false)
+ ) {
$callback = $this->parseStringClassAndMethod($callback);
}
@@ -263,28 +292,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 = [])
@@ -295,12 +322,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 = [])
{
@@ -310,12 +337,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 = [])
@@ -323,56 +350,43 @@ class Dispatcher
// If this is a directly callable function, call it
if (is_array($func) === false) {
$this->verifyValidFunction($func);
+
return call_user_func_array($func, $params);
}
[$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->mustUseContainer($class);
+
+ if ($mustUseTheContainer === true) {
+ $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) {
+ 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);
}
/**
* 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
{
- $isInvalidFunctionName = (
- is_string($callback)
- && !function_exists($callback)
- );
-
- if ($isInvalidFunctionName) {
+ if (is_string($callback) && !function_exists($callback)) {
throw new InvalidArgumentException('Invalid callback specified.');
}
}
@@ -381,84 +395,92 @@ 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
{
- $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()?");
+ $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 !== null) {
+ $exception = $this->containerException;
- // Class is there, but no method
+ // 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.");
+ $classNamespace = get_class($class);
+ $exception = new Exception("Class found, but method '$classNamespace::$method' not found.");
}
- if ($final_exception !== null) {
+ if ($exception !== null) {
$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
+ * @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)
+ public 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)
+ is_a($this->containerHandler, '\Psr\Container\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,
+ * Checks to see if a container should be used or not.
*
- * @return void
+ * @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
{
// Cause PHPUnit has 1 level of output buffering by default
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/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 e1abaac..1798de5 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.
*
@@ -139,7 +146,7 @@ class Response
*/
public function status(?int $code = null)
{
- if (null === $code) {
+ if ($code === null) {
return $this->status;
}
@@ -279,19 +286,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']);
}
}
@@ -307,7 +317,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(
@@ -331,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)) {
@@ -342,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;
}
@@ -422,7 +432,12 @@ class Response
}
}
- if (!headers_sent()) {
+ // Only for the v3 output buffering.
+ if ($this->v2_output_buffering === false) {
+ $this->processResponseCallbacks();
+ }
+
+ if (headers_sent() === false) {
$this->sendHeaders(); // @codeCoverageIgnore
}
@@ -430,4 +445,29 @@ class Response
$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/flight/net/Route.php b/flight/net/Route.php
index 0b8b9d7..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 = [];
@@ -105,7 +105,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;
}
@@ -120,8 +120,9 @@ class Route
for ($i = 0; $i < $len; $i++) {
if ($url[$i] === '/') {
- $n++;
+ ++$n;
}
+
if ($n === $count) {
break;
}
@@ -153,24 +154,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;
}
/**
@@ -229,7 +226,7 @@ class Route
/**
* Sets the route middleware
*
- * @param array|callable $middleware
+ * @param array|callable|string $middleware
*/
public function addMiddleware($middleware): self
{
@@ -241,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/flight/net/Router.php b/flight/net/Router.php
index d494dbb..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);
@@ -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);
@@ -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/flight/template/View.php b/flight/template/View.php
index d1bc07f..15e4fc8 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.
*
@@ -88,7 +90,7 @@ class View
*/
public function clear(?string $key = null): self
{
- if (null === $key) {
+ if ($key === null) {
$this->vars = [];
} else {
unset($this->vars[$key]);
@@ -114,12 +116,16 @@ 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);
+ if (\is_array($data) === true) {
+ \extract($data);
+
+ if ($this->preserveVars === true) {
+ $this->vars = \array_merge($this->vars, $data);
+ }
+ }
+
include $this->template;
}
@@ -169,7 +175,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/index.php b/index.php
index 5a21ed6..0db24be 100644
--- a/index.php
+++ b/index.php
@@ -1,7 +1,9 @@
-
+
-
+
-
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ flight/tests/
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
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/phpunit.xml b/phpunit.xml
index 2491e84..b890bb7 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/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']);
}
}
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 ');
+ }
}
diff --git a/tests/EngineTest.php b/tests/EngineTest.php
index bdf511e..93f3ff7 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;
@@ -86,8 +87,7 @@ class EngineTest extends TestCase
public function testHandleException()
{
$engine = new Engine();
- $regex_message = preg_quote('