From 47cd98e78ec498f8e6fc6ada835f9229f980dd75 Mon Sep 17 00:00:00 2001 From: n0nag0n Date: Sat, 24 Feb 2024 08:38:25 -0700 Subject: [PATCH 01/24] added ability to overwrite the body --- flight/Engine.php | 8 ++++---- flight/net/Response.php | 20 ++++++++++++++++-- tests/FlightTest.php | 26 ++++++++++++++++++++++++ tests/ResponseTest.php | 17 ++++++++++++++++ tests/server/LayoutMiddleware.php | 1 + tests/server/OverwriteBodyMiddleware.php | 12 +++++++++++ tests/server/index.php | 5 +++++ 7 files changed, 83 insertions(+), 6 deletions(-) create mode 100644 tests/server/OverwriteBodyMiddleware.php diff --git a/flight/Engine.php b/flight/Engine.php index fb4d9e8..aaaf778 100644 --- a/flight/Engine.php +++ b/flight/Engine.php @@ -401,8 +401,8 @@ class Engine continue; } - $use_v3_output_buffering = - $this->response()->v2_output_buffering === false && + $use_v3_output_buffering = + $this->response()->v2_output_buffering === false && $route->is_streamed === false; if ($use_v3_output_buffering === true) { @@ -493,8 +493,8 @@ class Engine } } - $use_v3_output_buffering = - $this->response()->v2_output_buffering === false && + $use_v3_output_buffering = + $this->response()->v2_output_buffering === false && $route->is_streamed === false; if ($use_v3_output_buffering === true) { diff --git a/flight/net/Response.php b/flight/net/Response.php index cfc8ffd..e1abaac 100644 --- a/flight/net/Response.php +++ b/flight/net/Response.php @@ -225,16 +225,32 @@ class Response * Writes content to the response body. * * @param string $str Response content + * @param bool $overwrite Overwrite the response body * * @return $this Self reference */ - public function write(string $str): self + public function write(string $str, bool $overwrite = false): self { + if ($overwrite === true) { + $this->clearBody(); + } + $this->body .= $str; return $this; } + /** + * Clears the response body. + * + * @return $this Self reference + */ + public function clearBody(): self + { + $this->body = ''; + return $this; + } + /** * Clears the response. * @@ -244,7 +260,7 @@ class Response { $this->status = 200; $this->headers = []; - $this->body = ''; + $this->clearBody(); // This needs to clear the output buffer if it's on if ($this->v2_output_buffering === false && ob_get_length() > 0) { diff --git a/tests/FlightTest.php b/tests/FlightTest.php index a6ffa16..042b6bb 100644 --- a/tests/FlightTest.php +++ b/tests/FlightTest.php @@ -304,4 +304,30 @@ class FlightTest extends TestCase ], Flight::response()->getHeaders()); $this->assertEquals(200, Flight::response()->status()); } + + public function testOverwriteBodyWithMiddleware() + { + $middleware = new class { + public function after() + { + $response = Flight::response(); + $body = $response->getBody(); + $body = strip_tags($body); + // remove spaces for fun + $body = str_replace(' ', '', $body); + $response->write($body, true); + return $response; + } + }; + + Flight::route('/route-with-html', function () { + echo '

This is a route with html

'; + })->addMiddleware($middleware); + + Flight::request()->url = '/route-with-html'; + + Flight::start(); + + $this->expectOutputString('Thisisaroutewithhtml'); + } } diff --git a/tests/ResponseTest.php b/tests/ResponseTest.php index d7155fc..42701d4 100644 --- a/tests/ResponseTest.php +++ b/tests/ResponseTest.php @@ -238,4 +238,21 @@ class ResponseTest extends TestCase $response->send(); $this->assertTrue($response->sent()); } + + public function testClearBody() + { + $response = new Response(); + $response->write('test'); + $response->clearBody(); + $this->assertEquals('', $response->getBody()); + } + + public function testOverwriteBody() + { + $response = new Response(); + $response->write('test'); + $response->write('lots more test'); + $response->write('new', true); + $this->assertEquals('new', $response->getBody()); + } } diff --git a/tests/server/LayoutMiddleware.php b/tests/server/LayoutMiddleware.php index d5dfcde..24538f9 100644 --- a/tests/server/LayoutMiddleware.php +++ b/tests/server/LayoutMiddleware.php @@ -77,6 +77,7 @@ class LayoutMiddleware
  • Halt
  • Redirect
  • Stream
  • +
  • Overwrite Body
  • HTML; echo '
    '; diff --git a/tests/server/OverwriteBodyMiddleware.php b/tests/server/OverwriteBodyMiddleware.php new file mode 100644 index 0000000..79e1194 --- /dev/null +++ b/tests/server/OverwriteBodyMiddleware.php @@ -0,0 +1,12 @@ +write(str_replace('failed', 'successfully works!', $response->getBody()), true); + } +} diff --git a/tests/server/index.php b/tests/server/index.php index e3ea703..040cbfc 100644 --- a/tests/server/index.php +++ b/tests/server/index.php @@ -17,6 +17,7 @@ Flight::set('flight.views.extension', '.phtml'); //Flight::set('flight.v2.output_buffering', true); require_once 'LayoutMiddleware.php'; +require_once 'OverwriteBodyMiddleware.php'; Flight::group('', function () { @@ -119,6 +120,10 @@ Flight::group('', function () { } 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'; + })->addMiddleware([new OverwriteBodyMiddleware()]); }, [ new LayoutMiddleware() ]); // Test 9: JSON output (should not output any other html) From c1ba04d96e40781698dcf7ddf060a9e869d37f36 Mon Sep 17 00:00:00 2001 From: n0nag0n Date: Sun, 25 Feb 2024 14:54:08 -0700 Subject: [PATCH 02/24] added deprecated tag to stop. Halt is better --- flight/Engine.php | 1 + 1 file changed, 1 insertion(+) diff --git a/flight/Engine.php b/flight/Engine.php index aaaf778..b0ae09d 100644 --- a/flight/Engine.php +++ b/flight/Engine.php @@ -575,6 +575,7 @@ class Engine * @param ?int $code HTTP status code * * @throws Exception + * @deprecated 3.5.3 This method will be removed in v4 */ public function _stop(?int $code = null): void { From 0622fd1c2bab14e9b8f636d54364cb4365ae3e68 Mon Sep 17 00:00:00 2001 From: n0nag0n Date: Sat, 2 Mar 2024 12:06:19 -0700 Subject: [PATCH 03/24] fixed issues with @params_with_underscores and prepopulate getUrl() params. --- flight/Engine.php | 8 ++++---- flight/net/Route.php | 2 +- flight/net/Router.php | 11 +++++++++++ tests/EngineTest.php | 28 ++++++++++++++++++++++++++++ tests/server-v2/index.php | 2 +- tests/server/LayoutMiddleware.php | 2 +- 6 files changed, 46 insertions(+), 7 deletions(-) diff --git a/flight/Engine.php b/flight/Engine.php index fb4d9e8..aaaf778 100644 --- a/flight/Engine.php +++ b/flight/Engine.php @@ -401,8 +401,8 @@ class Engine continue; } - $use_v3_output_buffering = - $this->response()->v2_output_buffering === false && + $use_v3_output_buffering = + $this->response()->v2_output_buffering === false && $route->is_streamed === false; if ($use_v3_output_buffering === true) { @@ -493,8 +493,8 @@ class Engine } } - $use_v3_output_buffering = - $this->response()->v2_output_buffering === false && + $use_v3_output_buffering = + $this->response()->v2_output_buffering === false && $route->is_streamed === false; if ($use_v3_output_buffering === true) { diff --git a/flight/net/Route.php b/flight/net/Route.php index baf5ea1..22c3cf0 100644 --- a/flight/net/Route.php +++ b/flight/net/Route.php @@ -193,7 +193,7 @@ class Route */ public function hydrateUrl(array $params = []): string { - $url = preg_replace_callback("/(?:@([a-zA-Z0-9]+)(?:\:([^\/]+))?\)*)/i", function ($match) use ($params) { + $url = preg_replace_callback("/(?:@([\w]+)(?:\:([^\/]+))?\)*)/i", function ($match) use ($params) { if (isset($match[1]) && isset($params[$match[1]])) { return $params[$match[1]]; } diff --git a/flight/net/Router.php b/flight/net/Router.php index 895b337..346a791 100644 --- a/flight/net/Router.php +++ b/flight/net/Router.php @@ -29,6 +29,11 @@ class Router */ protected array $routes = []; + /** + * The current route that is has been found and executed. + */ + protected ?Route $executedRoute = null; + /** * Pointer to current route. */ @@ -213,6 +218,7 @@ class Router $url_decoded = urldecode($request->url); while ($route = $this->current()) { if ($route->matchMethod($request->method) && $route->matchUrl($url_decoded, $this->case_sensitive)) { + $this->executedRoute = $route; return $route; } $this->next(); @@ -233,6 +239,11 @@ class Router foreach ($this->routes as $route) { $potential_aliases[] = $route->alias; if ($route->matchAlias($alias)) { + // This will make it so the params that already + // exist in the url will be passed in. + if (!empty($this->executedRoute->params)) { + $params = $params + $this->executedRoute->params; + } return $route->hydrateUrl($params); } } diff --git a/tests/EngineTest.php b/tests/EngineTest.php index c7e21d2..400e193 100644 --- a/tests/EngineTest.php +++ b/tests/EngineTest.php @@ -405,6 +405,34 @@ class EngineTest extends TestCase $this->assertEquals('/path1/123', $url); } + public function testGetUrlComplex() + { + $engine = new Engine(); + $engine->route('/item/@item_param:[a-z0-9]{16}/by-status/@token:[a-z0-9]{16}', function () { + echo 'I win'; + }, false, 'path_item_1'); + $url = $engine->getUrl('path_item_1', [ 'item_param' => 1234567890123456, 'token' => 6543210987654321 ]); + $this->assertEquals('/item/1234567890123456/by-status/6543210987654321', $url); + } + + public function testGetUrlInsideRoute() + { + $engine = new Engine(); + $engine->route('/path1/@param:[0-9]{3}', function () { + echo 'I win'; + }, false, 'path1'); + $found_url = ''; + $engine->route('/path1/@param:[0-9]{3}/path2', function () use ($engine, &$found_url) { + + // this should pull the param from the first route + // since the param names are the same. + $found_url = $engine->getUrl('path1'); + }); + $engine->request()->url = '/path1/123/path2'; + $engine->start(); + $this->assertEquals('/path1/123', $found_url); + } + public function testMiddlewareCallableFunction() { $engine = new Engine(); diff --git a/tests/server-v2/index.php b/tests/server-v2/index.php index d4cc385..3c1951c 100644 --- a/tests/server-v2/index.php +++ b/tests/server-v2/index.php @@ -199,7 +199,7 @@ echo '
  • Mega group
  • Error
  • JSON
  • -
  • JSONP
  • +
  • JSONP
  • Halt
  • Redirect
  • '; diff --git a/tests/server/LayoutMiddleware.php b/tests/server/LayoutMiddleware.php index d5dfcde..36702ed 100644 --- a/tests/server/LayoutMiddleware.php +++ b/tests/server/LayoutMiddleware.php @@ -73,7 +73,7 @@ class LayoutMiddleware
  • Mega group
  • Error
  • JSON
  • -
  • JSONP
  • +
  • JSONP
  • Halt
  • Redirect
  • Stream
  • From 043debdda884119d678c25e2256c54ff975871bd Mon Sep 17 00:00:00 2001 From: n0nag0n Date: Tue, 12 Mar 2024 23:27:17 -0600 Subject: [PATCH 04/24] Update README.md --- README.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/README.md b/README.md index 7fb6ab7..4961468 100644 --- a/README.md +++ b/README.md @@ -34,6 +34,10 @@ We have our own documentation website that is built with Flight (naturally). Lea Chat with us on Matrix IRC [#flight-php-framework:matrix.org](https://matrix.to/#/#flight-php-framework:matrix.org) +# Upgrading From v2 + +If you have a current project on v2, you should be able to upgrade to v2 with no issues depending on how your project was built. If there are any issues with upgrade, they are documented in the [migrating to v3](https://docs.flightphp.com/learn/migrating-to-v3) documentation page. It is the intention of Flight to maintain longterm stability of the project and to not add rewrites with major version changes. + # Requirements > [!IMPORTANT] @@ -43,6 +47,10 @@ Chat with us on Matrix IRC [#flight-php-framework:matrix.org](https://matrix.to/ The framework also supports PHP >8. +# Roadmap + +To see the current and future roadmap for the Flight Framework, visit the [project roadmap](https://github.com/orgs/flightphp/projects/1/views/1) + # License Flight is released under the [MIT](http://docs.flightphp.com/license) license. From e6a29c747662e1f646859bfec16a2f1361371ab9 Mon Sep 17 00:00:00 2001 From: Daniel Schreiber Date: Wed, 13 Mar 2024 14:22:44 +0100 Subject: [PATCH 05/24] fix: handle encoded slashes for url parameters - fixes #552 --- flight/net/Route.php | 13 ++++++++++--- flight/net/Router.php | 3 +-- tests/RouterTest.php | 12 +++++++++++- 3 files changed, 22 insertions(+), 6 deletions(-) diff --git a/flight/net/Route.php b/flight/net/Route.php index 22c3cf0..4abf2ae 100644 --- a/flight/net/Route.php +++ b/flight/net/Route.php @@ -97,7 +97,7 @@ class Route /** * Checks if a URL matches the route pattern. Also parses named parameters in the URL. * - * @param string $url Requested URL + * @param string $url Requested URL (original format, not URL decoded) * @param bool $case_sensitive Case sensitive matching * * @return bool Match status @@ -127,11 +127,18 @@ class Route } } - $this->splat = strval(substr($url, $i + 1)); + $this->splat = urldecode(strval(substr($url, $i + 1))); } // Build the regex for matching - $regex = str_replace([')', '/*'], [')?', '(/?|/.*?)'], $this->pattern); + $pattern_utf_chars_encoded = preg_replace_callback( + '#(\\p{L}+)#u', + static function ($matches) { + return urlencode($matches[0]); + }, + $this->pattern + ); + $regex = str_replace([')', '/*'], [')?', '(/?|/.*?)'], $pattern_utf_chars_encoded); $regex = preg_replace_callback( '#@([\w]+)(:([^/\(\)]*))?#', diff --git a/flight/net/Router.php b/flight/net/Router.php index 346a791..f739a24 100644 --- a/flight/net/Router.php +++ b/flight/net/Router.php @@ -215,9 +215,8 @@ 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)) { + if ($route->matchMethod($request->method) && $route->matchUrl($request->url, $this->case_sensitive)) { $this->executedRoute = $route; return $route; } diff --git a/tests/RouterTest.php b/tests/RouterTest.php index 1b52ae1..8ed272e 100644 --- a/tests/RouterTest.php +++ b/tests/RouterTest.php @@ -198,6 +198,16 @@ class RouterTest extends TestCase $this->check('123'); } + public function testUrlParametersWithEncodedSlash() + { + $this->router->map('/redirect/@id', function ($id) { + echo $id; + }); + $this->request->url = '/redirect/before%2Fafter'; + + $this->check('before/after'); + } + // Passing URL parameters matched with regular expression public function testRegExParameters() { @@ -390,7 +400,7 @@ class RouterTest extends TestCase $this->router->map('/категория/@name:[абвгдеёжзийклмнопрстуфхцчшщъыьэюя]+', function ($name) { echo $name; }); - $this->request->url = urlencode('/категория/цветя'); + $this->request->url = '/' . urlencode('категория') . '/' . urlencode('цветя'); $this->check('цветя'); } From 765887d388aff855cf4ffad8fef2df8ef3859025 Mon Sep 17 00:00:00 2001 From: Austin Collier Date: Fri, 15 Mar 2024 08:55:41 -0600 Subject: [PATCH 06/24] some additional testing --- tests/RouterTest.php | 62 +++++++++++++++++++++++++------ tests/server/LayoutMiddleware.php | 3 ++ tests/server/index.php | 15 ++++++++ 3 files changed, 69 insertions(+), 11 deletions(-) diff --git a/tests/RouterTest.php b/tests/RouterTest.php index 8ed272e..f18eaba 100644 --- a/tests/RouterTest.php +++ b/tests/RouterTest.php @@ -208,6 +208,57 @@ class RouterTest extends TestCase $this->check('before/after'); } + public function testUrlParametersWithRealSlash() + { + $this->router->map('/redirect/@id', function ($id) { + echo $id; + }); + $this->request->url = '/redirect/before/after'; + + $this->check('404'); + } + + public function testUrlParametersWithJapanese() + { + $this->router->map('/わたしはひとです', function () { + echo 'はい'; + }); + $this->request->url = '/わたしはひとです'; + + $this->check('はい'); + } + + public function testUrlParametersWithJapaneseAndParam() + { + $this->router->map('/わたしはひとです/@name', function ($name) { + echo $name; + }); + $this->request->url = '/'.urlencode('わたしはひとです').'/'.urlencode('ええ'); + + $this->check('ええ'); + } + + // Passing URL parameters matched with regular expression for a URL containing Cyrillic letters: + public function testRegExParametersCyrillic() + { + $this->router->map('/категория/@name:[абвгдеёжзийклмнопрстуфхцчшщъыьэюя]+', function ($name) { + echo $name; + }); + $this->request->url = '/'.urlencode('категория').'/'.urlencode('цветя'); + + $this->check('цветя'); + } + + public function testRegExOnlyCyrillicUrl() + { + $this->router->map('/категория/цветя', function () { + echo 'цветя'; + }); + $this->request->url = '/категория/цветя'; + + $this->check('цветя'); + } + // Passing URL parameters matched with regular expression public function testRegExParameters() { @@ -394,17 +445,6 @@ class RouterTest extends TestCase $this->check('404'); } - // Passing URL parameters matched with regular expression for a URL containing Cyrillic letters: - public function testRegExParametersCyrillic() - { - $this->router->map('/категория/@name:[абвгдеёжзийклмнопрстуфхцчшщъыьэюя]+', function ($name) { - echo $name; - }); - $this->request->url = '/' . urlencode('категория') . '/' . urlencode('цветя'); - - $this->check('цветя'); - } - public function testGetAndClearRoutes() { $this->router->map('/path1', [$this, 'ok']); diff --git a/tests/server/LayoutMiddleware.php b/tests/server/LayoutMiddleware.php index e0dacbc..b2e59bb 100644 --- a/tests/server/LayoutMiddleware.php +++ b/tests/server/LayoutMiddleware.php @@ -78,6 +78,9 @@ class LayoutMiddleware
  • Redirect
  • Stream
  • Overwrite Body
  • +
  • Slash in Param
  • +
  • UTF8 URL
  • +
  • UTF8 URL w/ Param
  • HTML; echo '
    '; diff --git a/tests/server/index.php b/tests/server/index.php index 040cbfc..d88ac85 100644 --- a/tests/server/index.php +++ b/tests/server/index.php @@ -124,6 +124,21 @@ Flight::group('', function () { Flight::route('/overwrite', function () { echo 'Route text: This route status is that it failed'; })->addMiddleware([new OverwriteBodyMiddleware()]); + + // Test 15: UTF8 Chars in url + Flight::route('/わたしはひとです', function () { + echo 'Route text: This route status is that it succeeded はい!!!'; + }); + + // Test 16: UTF8 Chars in url with utf8 params + Flight::route('/わたしはひとです/@name', function ($name) { + echo 'Route text: This route status is that it '.($name === 'ええ' ? 'succeeded' : 'failed').' URL Param: '.$name.''; + }); + + // Test 17: Slash in param + Flight::route('/redirect/@id', function ($id) { + echo 'Route text: This route status is that it '.($id === 'before/after' ? 'succeeded' : 'failed').' URL Param: '.$id.''; + }); }, [ new LayoutMiddleware() ]); // Test 9: JSON output (should not output any other html) From 4f52e1acc0eef78fdd1f2ec673f702cf7659762f Mon Sep 17 00:00:00 2001 From: Austin Collier Date: Fri, 15 Mar 2024 08:59:07 -0600 Subject: [PATCH 07/24] phpcs fixes --- tests/RouterTest.php | 4 ++-- tests/server/index.php | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/RouterTest.php b/tests/RouterTest.php index f18eaba..5f5abcd 100644 --- a/tests/RouterTest.php +++ b/tests/RouterTest.php @@ -233,7 +233,7 @@ class RouterTest extends TestCase $this->router->map('/わたしはひとです/@name', function ($name) { echo $name; }); - $this->request->url = '/'.urlencode('わたしはひとです').'/'.urlencode('ええ'); + $this->request->url = '/' . urlencode('わたしはひとです') . '/' . urlencode('ええ'); $this->check('ええ'); } @@ -244,7 +244,7 @@ class RouterTest extends TestCase $this->router->map('/категория/@name:[абвгдеёжзийклмнопрстуфхцчшщъыьэюя]+', function ($name) { echo $name; }); - $this->request->url = '/'.urlencode('категория').'/'.urlencode('цветя'); + $this->request->url = '/' . urlencode('категория') . '/' . urlencode('цветя'); $this->check('цветя'); } diff --git a/tests/server/index.php b/tests/server/index.php index d88ac85..f00144f 100644 --- a/tests/server/index.php +++ b/tests/server/index.php @@ -132,12 +132,12 @@ Flight::group('', function () { // Test 16: UTF8 Chars in url with utf8 params Flight::route('/わたしはひとです/@name', function ($name) { - echo 'Route text: This route status is that it '.($name === 'ええ' ? 'succeeded' : 'failed').' URL Param: '.$name.''; + echo 'Route text: This route status is that it ' . ($name === 'ええ' ? 'succeeded' : 'failed') . ' URL Param: ' . $name . ''; }); // Test 17: Slash in param Flight::route('/redirect/@id', function ($id) { - echo 'Route text: This route status is that it '.($id === 'before/after' ? 'succeeded' : 'failed').' URL Param: '.$id.''; + echo 'Route text: This route status is that it ' . ($id === 'before/after' ? 'succeeded' : 'failed') . ' URL Param: ' . $id . ''; }); }, [ new LayoutMiddleware() ]); From 296c9b57d5d2a0c9d10054fb605999c2163c389f Mon Sep 17 00:00:00 2001 From: Belle Aerni Date: Fri, 15 Mar 2024 23:18:23 -0700 Subject: [PATCH 08/24] Also register HEAD for GET routes --- flight/net/Router.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/flight/net/Router.php b/flight/net/Router.php index f739a24..43c3f18 100644 --- a/flight/net/Router.php +++ b/flight/net/Router.php @@ -107,6 +107,10 @@ class Router $methods = explode('|', $method); } + if (in_array('GET', $methods) && !in_array('HEAD', $methods)) { + $methods[] = 'HEAD'; + } + // And this finishes it off. if ($this->group_prefix !== '') { $url = rtrim($this->group_prefix . $url); From 5406bbedc13dc1559896925c9c7028e0b4d5e46f Mon Sep 17 00:00:00 2001 From: Austin Collier Date: Sat, 16 Mar 2024 10:41:33 -0600 Subject: [PATCH 09/24] added test coverage and clearing body for head requests --- composer.json | 2 +- flight/Engine.php | 5 +++++ flight/net/Router.php | 7 ++++--- tests/EngineTest.php | 14 ++++++++++++++ tests/RouterTest.php | 13 +++++++++++++ 5 files changed, 37 insertions(+), 4 deletions(-) diff --git a/composer.json b/composer.json index b45d101..028f2da 100644 --- a/composer.json +++ b/composer.json @@ -56,7 +56,7 @@ }, "scripts": { "test": "phpunit", - "test-coverage": "rm clover.xml && XDEBUG_MODE=coverage vendor/bin/phpunit --coverage-html=coverage --coverage-clover=clover.xml && vendor/bin/coverage-check clover.xml 100", + "test-coverage": "rm -f clover.xml && XDEBUG_MODE=coverage vendor/bin/phpunit --coverage-html=coverage --coverage-clover=clover.xml && vendor/bin/coverage-check clover.xml 100", "test-server": "echo \"Running Test Server\" && php -S localhost:8000 -t tests/server/", "test-server-v2": "echo \"Running Test Server\" && php -S localhost:8000 -t tests/server-v2/", "test-coverage:win": "del clover.xml && phpunit --coverage-html=coverage --coverage-clover=clover.xml && coverage-check clover.xml 100", diff --git a/flight/Engine.php b/flight/Engine.php index b0ae09d..7ff98d1 100644 --- a/flight/Engine.php +++ b/flight/Engine.php @@ -533,6 +533,11 @@ class Engine $dispatched = false; } + // HEAD requests should be identical to GET requests but have no body + if($request->method === 'HEAD') { + $response->clearBody(); + } + if ($failed_middleware_check === true) { $this->halt(403, 'Forbidden', empty(getenv('PHPUNIT_TEST'))); } elseif ($dispatched === false) { diff --git a/flight/net/Router.php b/flight/net/Router.php index 43c3f18..81556cd 100644 --- a/flight/net/Router.php +++ b/flight/net/Router.php @@ -105,10 +105,11 @@ class Router [$method, $url] = explode(' ', $url, 2); $url = trim($url); $methods = explode('|', $method); - } - if (in_array('GET', $methods) && !in_array('HEAD', $methods)) { - $methods[] = 'HEAD'; + // Add head requests to get methods, should they come in as a get request + if (in_array('GET', $methods, true) === true && in_array('HEAD', $methods, true) === false) { + $methods[] = 'HEAD'; + } } // And this finishes it off. diff --git a/tests/EngineTest.php b/tests/EngineTest.php index 400e193..26df60f 100644 --- a/tests/EngineTest.php +++ b/tests/EngineTest.php @@ -263,6 +263,20 @@ class EngineTest extends TestCase $this->assertEquals('/someRoute', $routes[0]->pattern); } + public function testHeadRoute() + { + $engine = new Engine(); + $engine->route('GET /someRoute', function () { + echo 'i ran'; + }, true); + $engine->request()->method = 'HEAD'; + $engine->request()->url = '/someRoute'; + $engine->start(); + + // No body should be sent + $this->expectOutputString(''); + } + public function testHalt() { $engine = new class extends Engine { diff --git a/tests/RouterTest.php b/tests/RouterTest.php index 5f5abcd..5c186bb 100644 --- a/tests/RouterTest.php +++ b/tests/RouterTest.php @@ -81,6 +81,10 @@ class RouterTest extends TestCase $dispatched = false; } + if($this->request->method === 'HEAD') { + ob_clean(); + } + if (!$dispatched) { echo '404'; } @@ -122,6 +126,15 @@ class RouterTest extends TestCase $this->check('OK'); } + public function testHeadRouteShortcut() + { + $route = $this->router->get('/path', [$this, 'ok']); + $this->assertEquals(['GET', 'HEAD'], $route->methods); + $this->request->url = '/path'; + $this->request->method = 'HEAD'; + $this->check(''); + } + // POST route public function testPostRoute() { From 38e20242bf9aa3e2b6401e463afc6b8137432368 Mon Sep 17 00:00:00 2001 From: Austin Collier Date: Sat, 16 Mar 2024 10:42:42 -0600 Subject: [PATCH 10/24] beautify --- flight/Engine.php | 2 +- tests/RouterTest.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/flight/Engine.php b/flight/Engine.php index 7ff98d1..bb694cb 100644 --- a/flight/Engine.php +++ b/flight/Engine.php @@ -534,7 +534,7 @@ class Engine } // HEAD requests should be identical to GET requests but have no body - if($request->method === 'HEAD') { + if ($request->method === 'HEAD') { $response->clearBody(); } diff --git a/tests/RouterTest.php b/tests/RouterTest.php index 5c186bb..2314426 100644 --- a/tests/RouterTest.php +++ b/tests/RouterTest.php @@ -81,7 +81,7 @@ class RouterTest extends TestCase $dispatched = false; } - if($this->request->method === 'HEAD') { + if ($this->request->method === 'HEAD') { ob_clean(); } From 1d3b224ef6b57dd7040caca3f502bc2495b7257c Mon Sep 17 00:00:00 2001 From: Austin Collier Date: Sat, 16 Mar 2024 18:08:09 -0600 Subject: [PATCH 11/24] changed it so PdoWrapper returns collections --- flight/database/PdoWrapper.php | 27 ++++++++++++++++++--------- tests/PdoWrapperTest.php | 7 +++++++ 2 files changed, 25 insertions(+), 9 deletions(-) diff --git a/flight/database/PdoWrapper.php b/flight/database/PdoWrapper.php index 842038c..4821cb9 100644 --- a/flight/database/PdoWrapper.php +++ b/flight/database/PdoWrapper.php @@ -4,6 +4,7 @@ declare(strict_types=1); namespace flight\database; +use flight\util\Collection; use PDO; use PDOStatement; @@ -47,8 +48,9 @@ class PdoWrapper extends PDO */ public function fetchField(string $sql, array $params = []) { - $data = $this->fetchRow($sql, $params); - return reset($data); + $collection_data = $this->fetchRow($sql, $params); + $array_data = $collection_data->getData(); + return reset($array_data); } /** @@ -59,13 +61,13 @@ class PdoWrapper extends PDO * @param string $sql - Ex: "SELECT * FROM table WHERE something = ?" * @param array $params - Ex: [ $something ] * - * @return array + * @return Collection */ - public function fetchRow(string $sql, array $params = []): array + public function fetchRow(string $sql, array $params = []): Collection { $sql .= stripos($sql, 'LIMIT') === false ? ' LIMIT 1' : ''; $result = $this->fetchAll($sql, $params); - return count($result) > 0 ? $result[0] : []; + return count($result) > 0 ? $result[0] : new Collection(); } /** @@ -79,17 +81,24 @@ class PdoWrapper extends PDO * @param string $sql - Ex: "SELECT * FROM table WHERE something = ?" * @param array $params - Ex: [ $something ] * - * @return array> + * @return array */ - public function fetchAll(string $sql, array $params = []): array + public function fetchAll(string $sql, array $params = []) { $processed_sql_data = $this->processInStatementSql($sql, $params); $sql = $processed_sql_data['sql']; $params = $processed_sql_data['params']; $statement = $this->prepare($sql); $statement->execute($params); - $result = $statement->fetchAll(); - return is_array($result) ? $result : []; + $results = $statement->fetchAll(); + if (is_array($results) === true && count($results) > 0) { + foreach ($results as &$result) { + $result = new Collection($result); + } + } else { + $results = []; + } + return $results; } /** diff --git a/tests/PdoWrapperTest.php b/tests/PdoWrapperTest.php index 324c47b..0f41a92 100644 --- a/tests/PdoWrapperTest.php +++ b/tests/PdoWrapperTest.php @@ -88,6 +88,13 @@ class PdoWrapperTest extends TestCase $this->assertEquals('three', $rows[2]['name']); } + public function testFetchAllNoRows() + { + $rows = $this->pdo_wrapper->fetchAll('SELECT * FROM test WHERE 1 = 2'); + $this->assertCount(0, $rows); + $this->assertSame([], $rows); + } + public function testFetchAllWithNamedParams() { $rows = $this->pdo_wrapper->fetchAll('SELECT * FROM test WHERE name = :name', [ 'name' => 'two']); From f4c5405d1b9ee9809ff1609fe1dab1d2e4740a4b Mon Sep 17 00:00:00 2001 From: Austin Collier Date: Sun, 17 Mar 2024 08:03:55 -0600 Subject: [PATCH 12/24] little changes to requests and integration testing JSON POST requests --- flight/net/Request.php | 38 +++++++++++++++++++------------------- tests/EngineTest.php | 29 +++++++++++++++++++++++++++++ 2 files changed, 48 insertions(+), 19 deletions(-) diff --git a/flight/net/Request.php b/flight/net/Request.php index eb932c0..1922664 100644 --- a/flight/net/Request.php +++ b/flight/net/Request.php @@ -146,24 +146,24 @@ class Request // Default properties if (empty($config)) { $config = [ - 'url' => str_replace('@', '%40', self::getVar('REQUEST_URI', '/')), - 'base' => str_replace(['\\', ' '], ['/', '%20'], \dirname(self::getVar('SCRIPT_NAME'))), - 'method' => self::getMethod(), - 'referrer' => self::getVar('HTTP_REFERER'), - 'ip' => self::getVar('REMOTE_ADDR'), - 'ajax' => 'XMLHttpRequest' === self::getVar('HTTP_X_REQUESTED_WITH'), - 'scheme' => self::getScheme(), + 'url' => str_replace('@', '%40', self::getVar('REQUEST_URI', '/')), + 'base' => str_replace(['\\', ' '], ['/', '%20'], \dirname(self::getVar('SCRIPT_NAME'))), + 'method' => ($config['method'] ?? self::getMethod()), + 'referrer' => self::getVar('HTTP_REFERER'), + 'ip' => self::getVar('REMOTE_ADDR'), + 'ajax' => 'XMLHttpRequest' === self::getVar('HTTP_X_REQUESTED_WITH'), + 'scheme' => self::getScheme(), 'user_agent' => self::getVar('HTTP_USER_AGENT'), - 'type' => self::getVar('CONTENT_TYPE'), - 'length' => intval(self::getVar('CONTENT_LENGTH', 0)), - 'query' => new Collection($_GET), - 'data' => new Collection($_POST), - 'cookies' => new Collection($_COOKIE), - 'files' => new Collection($_FILES), - 'secure' => 'https' === self::getScheme(), - 'accept' => self::getVar('HTTP_ACCEPT'), - 'proxy_ip' => self::getProxyIpAddress(), - 'host' => self::getVar('HTTP_HOST'), + 'type' => self::getVar('CONTENT_TYPE'), + 'length' => intval(self::getVar('CONTENT_LENGTH', 0)), + 'query' => new Collection($_GET), + 'data' => new Collection($_POST), + 'cookies' => new Collection($_COOKIE), + 'files' => new Collection($_FILES), + 'secure' => 'https' === self::getScheme(), + 'accept' => self::getVar('HTTP_ACCEPT'), + 'proxy_ip' => self::getProxyIpAddress(), + 'host' => self::getVar('HTTP_HOST'), ]; } @@ -181,7 +181,7 @@ class Request { // Set all the defined properties foreach ($properties as $name => $value) { - $this->$name = $value; + $this->{$name} = $value; } // Get the requested URL without the base directory @@ -229,7 +229,7 @@ class Request return $body; } - $method = self::getMethod(); + $method = $this->method ?? self::getMethod(); if ('POST' === $method || 'PUT' === $method || 'DELETE' === $method || 'PATCH' === $method) { $body = file_get_contents($this->stream_path); diff --git a/tests/EngineTest.php b/tests/EngineTest.php index 400e193..0941c9f 100644 --- a/tests/EngineTest.php +++ b/tests/EngineTest.php @@ -7,7 +7,9 @@ namespace tests; use Exception; use Flight; use flight\Engine; +use flight\net\Request; use flight\net\Response; +use flight\util\Collection; use PHPUnit\Framework\TestCase; // phpcs:ignoreFile PSR2.Methods.MethodDeclaration.Underscore @@ -304,6 +306,33 @@ class EngineTest extends TestCase $this->assertEquals(301, $engine->response()->status()); } + public function testJsonRequestBody() + { + $engine = new Engine(); + $tmpfile = tmpfile(); + $stream_path = stream_get_meta_data($tmpfile)['uri']; + file_put_contents($stream_path, '{"key1":"value1","key2":"value2"}'); + + $engine->register('request', Request::class, [ + [ + 'method' => 'POST', + 'url' => '/something/fancy', + 'base' => '/vagrant/public', + 'type' => 'application/json', + 'length' => 13, + 'data' => new Collection(), + 'query' => new Collection(), + 'stream_path' => $stream_path + ] + ]); + $engine->post('/something/fancy', function () use ($engine) { + echo $engine->request()->data->key1; + echo $engine->request()->data->key2; + }); + $engine->start(); + $this->expectOutputString('value1value2'); + } + public function testJson() { $engine = new Engine(); From 42a3d84d8acbb820443fea4a971f79c93ebba84e Mon Sep 17 00:00:00 2001 From: Austin Collier Date: Sun, 17 Mar 2024 08:22:06 -0600 Subject: [PATCH 13/24] Changed Collection to allow for reset --- flight/Engine.php | 2 ++ flight/database/PdoWrapper.php | 5 ++--- flight/util/Collection.php | 28 ++++++++++++++++++++++++++++ tests/CollectionTest.php | 23 +++++++++++++++++++++++ 4 files changed, 55 insertions(+), 3 deletions(-) diff --git a/flight/Engine.php b/flight/Engine.php index b0ae09d..fdeabb2 100644 --- a/flight/Engine.php +++ b/flight/Engine.php @@ -63,6 +63,8 @@ use flight\net\Route; * # HTTP caching * @method void etag(string $id, ('strong'|'weak') $type = 'strong') Handles ETag HTTP caching. * @method void lastModified(int $time) Handles last modified HTTP caching. + * + * phpcs:disable PSR2.Methods.MethodDeclaration.Underscore */ class Engine { diff --git a/flight/database/PdoWrapper.php b/flight/database/PdoWrapper.php index 4821cb9..bdbabb8 100644 --- a/flight/database/PdoWrapper.php +++ b/flight/database/PdoWrapper.php @@ -48,9 +48,8 @@ class PdoWrapper extends PDO */ public function fetchField(string $sql, array $params = []) { - $collection_data = $this->fetchRow($sql, $params); - $array_data = $collection_data->getData(); - return reset($array_data); + $result = $this->fetchRow($sql, $params); + return reset($result); } /** diff --git a/flight/util/Collection.php b/flight/util/Collection.php index 6ffe0b5..29147d1 100644 --- a/flight/util/Collection.php +++ b/flight/util/Collection.php @@ -20,6 +20,18 @@ use JsonSerializable; */ class Collection implements ArrayAccess, Iterator, Countable, JsonSerializable { + /** + * This is to allow for reset() to work properly. + * + * WARNING! This MUST be the first variable in this class!!! + * + * PHPStan is ignoring this because we don't need actually to read the property + * + * @var mixed + * @phpstan-ignore-next-line + */ + private $first_property = null; + /** * Collection data. * @@ -35,6 +47,7 @@ class Collection implements ArrayAccess, Iterator, Countable, JsonSerializable public function __construct(array $data = []) { $this->data = $data; + $this->handleReset(); } /** @@ -55,6 +68,7 @@ class Collection implements ArrayAccess, Iterator, Countable, JsonSerializable public function __set(string $key, $value): void { $this->data[$key] = $value; + $this->handleReset(); } /** @@ -71,6 +85,7 @@ class Collection implements ArrayAccess, Iterator, Countable, JsonSerializable public function __unset(string $key): void { unset($this->data[$key]); + $this->handleReset(); } /** @@ -100,6 +115,7 @@ class Collection implements ArrayAccess, Iterator, Countable, JsonSerializable } else { $this->data[$offset] = $value; } + $this->handleReset(); } /** @@ -120,6 +136,17 @@ class Collection implements ArrayAccess, Iterator, Countable, JsonSerializable public function offsetUnset($offset): void { unset($this->data[$offset]); + $this->handleReset(); + } + + /** + * This is to allow for reset() of a Collection to work properly. + * + * @return void + */ + public function handleReset() + { + $this->first_property = reset($this->data); } /** @@ -207,6 +234,7 @@ class Collection implements ArrayAccess, Iterator, Countable, JsonSerializable public function setData(array $data): void { $this->data = $data; + $this->handleReset(); } #[\ReturnTypeWillChange] diff --git a/tests/CollectionTest.php b/tests/CollectionTest.php index b6eac46..4f742fe 100644 --- a/tests/CollectionTest.php +++ b/tests/CollectionTest.php @@ -99,4 +99,27 @@ class CollectionTest extends TestCase $this->collection->clear(); $this->assertEquals(0, $this->collection->count()); } + + public function testResetByProperty() + { + $this->collection->a = 11; + $this->collection->b = 22; + $result = reset($this->collection); + $this->assertEquals(11, $result); + } + + public function testResetBySetData() + { + $this->collection->setData(['a' => 11, 'b' => 22]); + $result = reset($this->collection); + $this->assertEquals(11, $result); + } + + public function testResetByArraySet() + { + $this->collection['a'] = 11; + $this->collection['b'] = 22; + $result = reset($this->collection); + $this->assertEquals(11, $result); + } } From 9786820474ac191704045378d7c60695f932e6bf Mon Sep 17 00:00:00 2001 From: Austin Collier Date: Sun, 17 Mar 2024 08:23:41 -0600 Subject: [PATCH 14/24] Misunderstanding with $config --- flight/net/Request.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flight/net/Request.php b/flight/net/Request.php index 1922664..569994e 100644 --- a/flight/net/Request.php +++ b/flight/net/Request.php @@ -148,7 +148,7 @@ class Request $config = [ 'url' => str_replace('@', '%40', self::getVar('REQUEST_URI', '/')), 'base' => str_replace(['\\', ' '], ['/', '%20'], \dirname(self::getVar('SCRIPT_NAME'))), - 'method' => ($config['method'] ?? self::getMethod()), + 'method' => self::getMethod(), 'referrer' => self::getVar('HTTP_REFERER'), 'ip' => self::getVar('REMOTE_ADDR'), 'ajax' => 'XMLHttpRequest' === self::getVar('HTTP_X_REQUESTED_WITH'), From 6d41115e9af83a0ec9b827b14b2cbab4d1f68093 Mon Sep 17 00:00:00 2001 From: n0nag0n Date: Mon, 18 Mar 2024 23:14:24 -0600 Subject: [PATCH 15/24] initial commit for containerization --- composer.json | 6 +- flight/Engine.php | 42 ++++++---- flight/Flight.php | 40 ++------- flight/core/Dispatcher.php | 158 ++++++++++++++++++++++++++++++++---- flight/net/Route.php | 4 +- flight/net/Router.php | 24 +++--- tests/DispatcherTest.php | 16 ++-- tests/EngineTest.php | 29 +++++++ tests/classes/Container.php | 23 ++++++ 9 files changed, 251 insertions(+), 91 deletions(-) create mode 100644 tests/classes/Container.php diff --git a/composer.json b/composer.json index 028f2da..2be3e4b 100644 --- a/composer.json +++ b/composer.json @@ -41,9 +41,11 @@ }, "require-dev": { "ext-pdo_sqlite": "*", - "phpunit/phpunit": "^9.5", - "phpstan/phpstan": "^1.10", + "league/container": "^4.2", + "level-2/dice": "^4.0", "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" }, diff --git a/flight/Engine.php b/flight/Engine.php index effb0af..b22456e 100644 --- a/flight/Engine.php +++ b/flight/Engine.php @@ -30,17 +30,17 @@ use flight\net\Route; * @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 = '') + * @method Route route(string $pattern, callable|string $callback, bool $pass_route = false, string $alias = '') * Routes a URL to a callback function with all applicable methods * @method void group(string $pattern, callable $callback, array $group_middlewares = []) * Groups a set of routes together under a common prefix. - * @method Route post(string $pattern, callable $callback, bool $pass_route = false, string $alias = '') + * @method Route post(string $pattern, callable|string $callback, bool $pass_route = false, string $alias = '') * Routes a POST URL to a callback function. - * @method Route put(string $pattern, callable $callback, bool $pass_route = false, string $alias = '') + * @method Route put(string $pattern, callable|string $callback, bool $pass_route = false, string $alias = '') * Routes a PUT URL to a callback function. - * @method Route patch(string $pattern, callable $callback, bool $pass_route = false, string $alias = '') + * @method Route patch(string $pattern, callable|string $callback, bool $pass_route = false, string $alias = '') * Routes a PATCH URL to a callback function. - * @method Route delete(string $pattern, callable $callback, bool $pass_route = false, string $alias = '') + * @method Route delete(string $pattern, callable|string $callback, bool $pass_route = false, string $alias = '') * Routes a DELETE URL to a callback function. * @method Router router() Gets router * @method string getUrl(string $alias) Gets a url from an alias @@ -217,6 +217,18 @@ class Engine $this->error($e); } + /** + * Registers the container handler + * + * @param callable $callback Callback function that sets the container and how it will inject classes + * + * @return void + */ + public function registerContainerHandler($callback): void + { + $this->dispatcher->setContainerHandler($callback); + } + /** * Maps a callback to a framework method. * @@ -605,11 +617,11 @@ class Engine * Routes a URL to a callback function. * * @param string $pattern URL pattern to match - * @param callable $callback Callback function + * @param callable|string $callback Callback function * @param bool $pass_route Pass the matching route object to the callback * @param string $alias The alias for the route */ - public function _route(string $pattern, callable $callback, bool $pass_route = false, string $alias = ''): Route + public function _route(string $pattern, $callback, bool $pass_route = false, string $alias = ''): Route { return $this->router()->map($pattern, $callback, $pass_route, $alias); } @@ -630,10 +642,10 @@ class Engine * Routes a URL to a callback function. * * @param string $pattern URL pattern to match - * @param callable $callback Callback function + * @param callable|string $callback Callback function or string class->method * @param bool $pass_route Pass the matching route object to the callback */ - public function _post(string $pattern, callable $callback, bool $pass_route = false, string $route_alias = ''): void + public function _post(string $pattern, $callback, bool $pass_route = false, string $route_alias = ''): void { $this->router()->map('POST ' . $pattern, $callback, $pass_route, $route_alias); } @@ -642,10 +654,10 @@ class Engine * Routes a URL to a callback function. * * @param string $pattern URL pattern to match - * @param callable $callback Callback function + * @param callable|string $callback Callback function or string class->method * @param bool $pass_route Pass the matching route object to the callback */ - public function _put(string $pattern, callable $callback, bool $pass_route = false, string $route_alias = ''): void + public function _put(string $pattern, $callback, bool $pass_route = false, string $route_alias = ''): void { $this->router()->map('PUT ' . $pattern, $callback, $pass_route, $route_alias); } @@ -654,10 +666,10 @@ class Engine * Routes a URL to a callback function. * * @param string $pattern URL pattern to match - * @param callable $callback Callback function + * @param callable|string $callback Callback function or string class->method * @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, $callback, bool $pass_route = false, string $route_alias = ''): void { $this->router()->map('PATCH ' . $pattern, $callback, $pass_route, $route_alias); } @@ -666,10 +678,10 @@ class Engine * Routes a URL to a callback function. * * @param string $pattern URL pattern to match - * @param callable $callback Callback function + * @param callable|string $callback Callback function or string class->method * @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, $callback, bool $pass_route = false, string $route_alias = ''): void { $this->router()->map('DELETE ' . $pattern, $callback, $pass_route, $route_alias); } diff --git a/flight/Flight.php b/flight/Flight.php index 6e29781..9a3042c 100644 --- a/flight/Flight.php +++ b/flight/Flight.php @@ -26,17 +26,17 @@ require_once __DIR__ . '/autoload.php'; * Stop the framework with an optional status code and message. * * # Routing - * @method static Route route(string $pattern, callable $callback, bool $pass_route = false, string $alias = '') + * @method static Route route(string $pattern, callable|string $callback, bool $pass_route = false, string $alias = '') * Maps a URL pattern to a callback with all applicable methods. * @method static void group(string $pattern, callable $callback, callable[] $group_middlewares = []) * Groups a set of routes together under a common prefix. - * @method static Route post(string $pattern, callable $callback, bool $pass_route = false, string $alias = '') + * @method static Route post(string $pattern, callable|string $callback, bool $pass_route = false, string $alias = '') * Routes a POST URL to a callback function. - * @method static Route put(string $pattern, callable $callback, bool $pass_route = false, string $alias = '') + * @method static Route put(string $pattern, callable|string $callback, bool $pass_route = false, string $alias = '') * Routes a PUT URL to a callback function. - * @method static Route patch(string $pattern, callable $callback, bool $pass_route = false, string $alias = '') + * @method static Route patch(string $pattern, callable|string $callback, bool $pass_route = false, string $alias = '') * Routes a PATCH URL to a callback function. - * @method static Route delete(string $pattern, callable $callback, bool $pass_route = false, string $alias = '') + * @method static Route delete(string $pattern, callable|string $callback, bool $pass_route = false, string $alias = '') * Routes a DELETE URL to a callback function. * @method static Router router() Returns Router instance. * @method static string getUrl(string $alias, array $params = []) Gets a url from an alias @@ -101,34 +101,6 @@ class Flight { } - /** - * Registers a class to a framework method. - * - * # Usage example: - * ``` - * Flight::register('user', User::class); - * - * Flight::user(); # <- Return a User instance - * ``` - * - * @param string $name Static method name - * @param class-string $class Fully Qualified Class Name - * @param array $params Class constructor params - * @param ?Closure(T $instance): void $callback Perform actions with the instance - * - * @template T of object - */ - public static function register($name, $class, $params = [], $callback = null): void - { - static::__callStatic('register', [$name, $class, $params, $callback]); - } - - /** Unregisters a class. */ - public static function unregister(string $methodName): void - { - static::__callStatic('unregister', [$methodName]); - } - /** * Handles calls to static methods. * @@ -140,7 +112,7 @@ class Flight */ public static function __callStatic(string $name, array $params) { - return Dispatcher::invokeMethod([self::app(), $name], $params); + return self::app()->{$name}(...$params); } /** @return Engine Application instance */ diff --git a/flight/core/Dispatcher.php b/flight/core/Dispatcher.php index 6af0d66..67a92f9 100644 --- a/flight/core/Dispatcher.php +++ b/flight/core/Dispatcher.php @@ -35,6 +35,25 @@ class Dispatcher */ protected array $filters = []; + /** + * This is a container for the dependency injection. + * + * @var callable|object|null + */ + protected $container_handler = null; + + /** + * Sets the dependency injection container handler. + * + * @param callable|object $container_handler Dependency injection container + * + * @return void + */ + public function setContainerHandler($container_handler): void + { + $this->container_handler = $container_handler; + } + /** * Dispatches an event. * @@ -80,10 +99,10 @@ 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 $requestedMethod(...$params); + return $this->execute($requestedMethod, $params); } /** @@ -194,7 +213,7 @@ class Dispatcher * * @throws Exception If an event throws an `Exception` or if `$filters` contains an invalid filter. */ - public static function filter(array $filters, array &$params, &$output): void + public function filter(array $filters, array &$params, &$output): void { foreach ($filters as $key => $callback) { if (!is_callable($callback)) { @@ -219,22 +238,39 @@ class Dispatcher * @return mixed Function results * @throws Exception If `$callback` also throws an `Exception`. */ - public static function execute($callback, array &$params = []) + public function execute($callback, array &$params = []) { - $isInvalidFunctionName = ( - is_string($callback) - && !function_exists($callback) - ); + if (is_string($callback) === true && (strpos($callback, '->') !== false || strpos($callback, '::') !== false)) { + $callback = $this->parseStringClassAndMethod($callback); + } - if ($isInvalidFunctionName) { - throw new InvalidArgumentException('Invalid callback specified.'); + $this->handleInvalidCallbackType($callback); + + if (is_array($callback) === true) { + return $this->invokeMethod($callback, $params); } - if (is_array($callback)) { - return self::invokeMethod($callback, $params); + return $this->callFunction($callback, $params); + } + + /** + * Parses a string into a class and method. + * + * @param string $classAndMethod Class and method + * + * @return array{class-string|object, 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]); } - return self::callFunction($callback, $params); + $class = $class_parts[0]; + $method = $class_parts[1]; + + return [ $class, $method ]; } /** @@ -244,10 +280,11 @@ class Dispatcher * @param array &$params Function parameters * * @return mixed Function results + * @deprecated 3.7.0 Use invokeCallable instead */ - public static function callFunction(callable $func, array &$params = []) + public function callFunction(callable $func, array &$params = []) { - return call_user_func_array($func, $params); + return $this->invokeCallable($func, $params); } /** @@ -257,12 +294,48 @@ class Dispatcher * @param array &$params Class method parameters * * @return mixed Function results - * @throws TypeError For unexistent class name. + * @throws TypeError For nonexistent class name. + * @deprecated 3.7.0 Use invokeCallable instead + */ + public function invokeMethod(array $func, array &$params = []) + { + return $this->invokeCallable($func, $params); + } + + /** + * Invokes a callable (anonymous function or Class->method). + * + * @param array{class-string|object, string}|Callable $func Class method + * @param array &$params Class method parameters + * + * @return mixed Function results + * @throws TypeError For nonexistent class name. + * @throws InvalidArgumentException If the constructor requires parameters */ - public static function invokeMethod(array $func, array &$params = []) + public function invokeCallable($func, array &$params = []) { + // If this is a directly callable function, call it + if (is_array($func) === false) { + return call_user_func_array($func, $params); + } + [$class, $method] = $func; + // Only execute this if it's not a Flight class + if ( + $this->container_handler !== null && + ( + ( + is_object($class) === true && + strpos(get_class($class), 'flight\\') === false + ) || + is_string($class) === true + ) + ) { + $container_handler = $this->container_handler; + $class = $this->resolveContainerClass($container_handler, $class, $params); + } + if (is_string($class) && class_exists($class)) { $constructor = (new ReflectionClass($class))->getConstructor(); $constructorParamsNumber = 0; @@ -284,7 +357,56 @@ class Dispatcher $class = new $class(); } - 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 + * + * @throws InvalidArgumentException If `$callback` is an invalid type + */ + protected function handleInvalidCallbackType($callback): void + { + $isInvalidFunctionName = ( + is_string($callback) + && !function_exists($callback) + ); + + if ($isInvalidFunctionName) { + throw new InvalidArgumentException('Invalid callback specified.'); + } + } + + /** + * 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) + { + $class_object = null; + + // PSR-11 + if ( + is_object($container_handler) === true && + method_exists($container_handler, 'has') === true && + $container_handler->has($class) + ) { + $class_object = call_user_func([$container_handler, 'get'], $class); + + // Just a callable where you configure the behavior (Dice, PHP-DI, etc.) + } elseif (is_callable($container_handler) === true) { + $class_object = call_user_func($container_handler, $class, $params); + } + + return $class_object; } /** diff --git a/flight/net/Route.php b/flight/net/Route.php index 4abf2ae..0b8b9d7 100644 --- a/flight/net/Route.php +++ b/flight/net/Route.php @@ -81,11 +81,11 @@ class Route * Constructor. * * @param string $pattern URL pattern - * @param callable $callback Callback function + * @param callable|string $callback Callback function * @param array $methods HTTP methods * @param bool $pass Pass self in callback parameters */ - public function __construct(string $pattern, callable $callback, array $methods, bool $pass, string $alias = '') + public function __construct(string $pattern, $callback, array $methods, bool $pass, string $alias = '') { $this->pattern = $pattern; $this->callback = $callback; diff --git a/flight/net/Router.php b/flight/net/Router.php index 81556cd..d494dbb 100644 --- a/flight/net/Router.php +++ b/flight/net/Router.php @@ -80,11 +80,11 @@ class Router * Maps a URL pattern to a callback function. * * @param string $pattern URL pattern to match. - * @param callable $callback Callback function. + * @param callable|string $callback Callback function or string class->method * @param bool $pass_route Pass the matching route object to the callback. * @param string $route_alias Alias for the route. */ - public function map(string $pattern, callable $callback, bool $pass_route = false, string $route_alias = ''): Route + public function map(string $pattern, $callback, bool $pass_route = false, string $route_alias = ''): Route { // This means that the route ies defined in a group, but the defined route is the base @@ -133,11 +133,11 @@ class Router * Creates a GET based route * * @param string $pattern URL pattern to match - * @param callable $callback Callback function + * @param callable|string $callback Callback function or string class->method * @param bool $pass_route Pass the matching route object to the callback * @param string $alias Alias for the route */ - public function get(string $pattern, callable $callback, bool $pass_route = false, string $alias = ''): Route + public function get(string $pattern, $callback, bool $pass_route = false, string $alias = ''): Route { return $this->map('GET ' . $pattern, $callback, $pass_route, $alias); } @@ -146,11 +146,11 @@ class Router * Creates a POST based route * * @param string $pattern URL pattern to match - * @param callable $callback Callback function + * @param callable|string $callback Callback function or string class->method * @param bool $pass_route Pass the matching route object to the callback * @param string $alias Alias for the route */ - public function post(string $pattern, callable $callback, bool $pass_route = false, string $alias = ''): Route + public function post(string $pattern, $callback, bool $pass_route = false, string $alias = ''): Route { return $this->map('POST ' . $pattern, $callback, $pass_route, $alias); } @@ -159,11 +159,11 @@ class Router * Creates a PUT based route * * @param string $pattern URL pattern to match - * @param callable $callback Callback function + * @param callable|string $callback Callback function or string class->method * @param bool $pass_route Pass the matching route object to the callback * @param string $alias Alias for the route */ - public function put(string $pattern, callable $callback, bool $pass_route = false, string $alias = ''): Route + public function put(string $pattern, $callback, bool $pass_route = false, string $alias = ''): Route { return $this->map('PUT ' . $pattern, $callback, $pass_route, $alias); } @@ -172,11 +172,11 @@ class Router * Creates a PATCH based route * * @param string $pattern URL pattern to match - * @param callable $callback Callback function + * @param callable|string $callback Callback function or string class->method * @param bool $pass_route Pass the matching route object to the callback * @param string $alias Alias for the route */ - public function patch(string $pattern, callable $callback, bool $pass_route = false, string $alias = ''): Route + public function patch(string $pattern, $callback, bool $pass_route = false, string $alias = ''): Route { return $this->map('PATCH ' . $pattern, $callback, $pass_route, $alias); } @@ -185,11 +185,11 @@ class Router * Creates a DELETE based route * * @param string $pattern URL pattern to match - * @param callable $callback Callback function + * @param callable|string $callback Callback function or string class->method * @param bool $pass_route Pass the matching route object to the callback * @param string $alias Alias for the route */ - public function delete(string $pattern, callable $callback, bool $pass_route = false, string $alias = ''): Route + public function delete(string $pattern, $callback, bool $pass_route = false, string $alias = ''): Route { return $this->map('DELETE ' . $pattern, $callback, $pass_route, $alias); } diff --git a/tests/DispatcherTest.php b/tests/DispatcherTest.php index 826c190..2fd0d90 100644 --- a/tests/DispatcherTest.php +++ b/tests/DispatcherTest.php @@ -107,11 +107,11 @@ class DispatcherTest extends TestCase }); $this->dispatcher - ->hook('hello', $this->dispatcher::FILTER_BEFORE, function (array &$params): void { + ->hook('hello', Dispatcher::FILTER_BEFORE, function (array &$params): void { // Manipulate the parameter $params[0] = 'Fred'; }) - ->hook('hello', $this->dispatcher::FILTER_AFTER, function (array &$params, string &$output): void { + ->hook('hello', Dispatcher::FILTER_AFTER, function (array &$params, string &$output): void { // Manipulate the output $output .= ' Have a nice day!'; }); @@ -125,7 +125,7 @@ class DispatcherTest extends TestCase { $this->expectException(TypeError::class); - Dispatcher::execute(['NonExistentClass', 'nonExistentMethod']); + $this->dispatcher->execute(['NonExistentClass', 'nonExistentMethod']); } public function testInvalidCallableString(): void @@ -133,7 +133,7 @@ class DispatcherTest extends TestCase $this->expectException(InvalidArgumentException::class); $this->expectExceptionMessage('Invalid callback specified.'); - Dispatcher::execute('inexistentGlobalFunction'); + $this->dispatcher->execute('inexistentGlobalFunction'); } public function testInvalidCallbackBecauseConstructorParameters(): void @@ -147,13 +147,13 @@ class DispatcherTest extends TestCase $this->expectExceptionMessage($exceptionMessage); static $params = []; - Dispatcher::invokeMethod([$class, $method], $params); + $this->dispatcher->invokeMethod([$class, $method], $params); } // It will be useful for executing instance Controller methods statically public function testCanExecuteAnNonStaticMethodStatically(): void { - $this->assertSame('hello', Dispatcher::execute([Hello::class, 'sayHi'])); + $this->assertSame('hello', $this->dispatcher->execute([Hello::class, 'sayHi'])); } public function testItThrowsAnExceptionWhenRunAnUnregistedEventName(): void @@ -237,7 +237,7 @@ class DispatcherTest extends TestCase $validCallable = function (): void { }; - Dispatcher::filter([$validCallable, $invalidCallable], $params, $output); + $this->dispatcher->filter([$validCallable, $invalidCallable], $params, $output); } public function testCallFunction6Params(): void @@ -247,7 +247,7 @@ class DispatcherTest extends TestCase }; $params = ['param1', 'param2', 'param3', 'param4', 'param5', 'param6']; - $result = Dispatcher::callFunction($func, $params); + $result = $this->dispatcher->callFunction($func, $params); $this->assertSame('helloparam1param2param3param4param5param6', $result); } diff --git a/tests/EngineTest.php b/tests/EngineTest.php index a977e3a..157bbb6 100644 --- a/tests/EngineTest.php +++ b/tests/EngineTest.php @@ -11,6 +11,7 @@ use flight\net\Request; use flight\net\Response; use flight\util\Collection; use PHPUnit\Framework\TestCase; +use tests\classes\Container; // phpcs:ignoreFile PSR2.Methods.MethodDeclaration.Underscore class EngineTest extends TestCase @@ -676,4 +677,32 @@ class EngineTest extends TestCase $engine->start(); $this->expectOutputString('before456before123OKafter123456after123'); } + + public function testContainerDice() { + $engine = new Engine(); + $dice = new \Dice\Dice(); + $engine->registerContainerHandler(function ($class, $params) use ($dice) { + return $dice->create($class, $params); + }); + + $engine->route('/container', Container::class.'->testTheContainer'); + $engine->request()->url = '/container'; + $engine->start(); + + $this->expectOutputString('yay! I injected a collection, and it has 1 items'); + } + + public function testContainerPsr11() { + $engine = new Engine(); + $container = new \League\Container\Container(); + $container->add(Container::class)->addArgument(Collection::class); + $container->add(Collection::class); + $engine->registerContainerHandler($container); + + $engine->route('/container', Container::class.'->testTheContainer'); + $engine->request()->url = '/container'; + $engine->start(); + + $this->expectOutputString('yay! I injected a collection, and it has 1 items'); + } } diff --git a/tests/classes/Container.php b/tests/classes/Container.php new file mode 100644 index 0000000..ffb6fe1 --- /dev/null +++ b/tests/classes/Container.php @@ -0,0 +1,23 @@ +collection = $collection; + } + + public function testTheContainer() + { + $this->collection->whatever = 'yay!'; + echo 'yay! I injected a collection, and it has ' . $this->collection->count() . ' items'; + } +} From f610adfc145f20177af6c95196e46837784f13af Mon Sep 17 00:00:00 2001 From: Austin Collier Date: Tue, 19 Mar 2024 09:03:41 -0600 Subject: [PATCH 16/24] lots more testing around containers --- flight/core/Dispatcher.php | 29 +++++++++++++++-- tests/DispatcherTest.php | 5 +-- tests/EngineTest.php | 64 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 94 insertions(+), 4 deletions(-) diff --git a/flight/core/Dispatcher.php b/flight/core/Dispatcher.php index 67a92f9..41d9d39 100644 --- a/flight/core/Dispatcher.php +++ b/flight/core/Dispatcher.php @@ -333,7 +333,10 @@ class Dispatcher ) ) { $container_handler = $this->container_handler; - $class = $this->resolveContainerClass($container_handler, $class, $params); + $resolved_class = $this->resolveContainerClass($container_handler, $class, $params); + if($resolved_class !== null) { + $class = $resolved_class; + } } if (is_string($class) && class_exists($class)) { @@ -357,6 +360,21 @@ class Dispatcher $class = new $class(); } + // Final check to make sure it's actually a class and a method, or throw an error + if (is_object($class) === false) { + if(ob_get_level() > 1) { + ob_end_clean(); + } + throw new Exception("Class '$class' not found. Is it being correctly autoloaded with Flight::path()?"); + } + + if (method_exists($class, $method) === false) { + if(ob_get_level() > 1) { + ob_end_clean(); + } + throw new Exception("Method '".get_class($class)."::$method' not found."); + } + return call_user_func_array([ $class, $method ], $params); } @@ -403,7 +421,14 @@ class Dispatcher // Just a callable where you configure the behavior (Dice, PHP-DI, etc.) } elseif (is_callable($container_handler) === true) { - $class_object = call_user_func($container_handler, $class, $params); + + // 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 $class_object; diff --git a/tests/DispatcherTest.php b/tests/DispatcherTest.php index 2fd0d90..7cf2c24 100644 --- a/tests/DispatcherTest.php +++ b/tests/DispatcherTest.php @@ -123,7 +123,8 @@ class DispatcherTest extends TestCase public function testInvalidCallback(): void { - $this->expectException(TypeError::class); + $this->expectException(Exception::class); + $this->expectExceptionMessage("Class 'NonExistentClass' not found. Is it being correctly autoloaded with Flight::path()?"); $this->dispatcher->execute(['NonExistentClass', 'nonExistentMethod']); } @@ -133,7 +134,7 @@ class DispatcherTest extends TestCase $this->expectException(InvalidArgumentException::class); $this->expectExceptionMessage('Invalid callback specified.'); - $this->dispatcher->execute('inexistentGlobalFunction'); + $this->dispatcher->execute('nonexistentGlobalFunction'); } public function testInvalidCallbackBecauseConstructorParameters(): void diff --git a/tests/EngineTest.php b/tests/EngineTest.php index 157bbb6..dc5947b 100644 --- a/tests/EngineTest.php +++ b/tests/EngineTest.php @@ -692,6 +692,38 @@ class EngineTest extends TestCase $this->expectOutputString('yay! I injected a collection, and it has 1 items'); } + public function testContainerDiceBadClass() { + $engine = new Engine(); + $dice = new \Dice\Dice(); + $engine->registerContainerHandler(function ($class, $params) use ($dice) { + return $dice->create($class, $params); + }); + + $engine->route('/container', 'BadClass->testTheContainer'); + $engine->request()->url = '/container'; + + $this->expectException(Exception::class); + $this->expectExceptionMessage("Class 'BadClass' not found. Is it being correctly autoloaded with Flight::path()?"); + + $engine->start(); + } + + public function testContainerDiceBadMethod() { + $engine = new Engine(); + $dice = new \Dice\Dice(); + $engine->registerContainerHandler(function ($class, $params) use ($dice) { + return $dice->create($class, $params); + }); + + $engine->route('/container', Container::class.'->badMethod'); + $engine->request()->url = '/container'; + + $this->expectException(Exception::class); + $this->expectExceptionMessage("Method 'tests\classes\Container::badMethod' not found."); + + $engine->start(); + } + public function testContainerPsr11() { $engine = new Engine(); $container = new \League\Container\Container(); @@ -705,4 +737,36 @@ class EngineTest extends TestCase $this->expectOutputString('yay! I injected a collection, and it has 1 items'); } + + public function testContainerPsr11ClassNotFound() { + $engine = new Engine(); + $container = new \League\Container\Container(); + $container->add(Container::class)->addArgument(Collection::class); + $container->add(Collection::class); + $engine->registerContainerHandler($container); + + $engine->route('/container', 'BadClass->testTheContainer'); + $engine->request()->url = '/container'; + + $this->expectException(Exception::class); + $this->expectExceptionMessage("Class 'BadClass' not found. Is it being correctly autoloaded with Flight::path()?"); + + $engine->start(); + } + + public function testContainerPsr11MethodNotFound() { + $engine = new Engine(); + $container = new \League\Container\Container(); + $container->add(Container::class)->addArgument(Collection::class); + $container->add(Collection::class); + $engine->registerContainerHandler($container); + + $engine->route('/container', Container::class.'->badMethod'); + $engine->request()->url = '/container'; + + $this->expectException(Exception::class); + $this->expectExceptionMessage("Method 'tests\classes\Container::badMethod' not found."); + + $engine->start(); + } } From 3f887e305f5c0bd52ff96ca16fe325327f878423 Mon Sep 17 00:00:00 2001 From: Austin Collier Date: Tue, 19 Mar 2024 22:09:46 -0600 Subject: [PATCH 17/24] Added more tests. Captured callable exception thrown --- flight/Engine.php | 6 ++--- flight/core/Dispatcher.php | 47 +++++++++++++++++++++++--------------- tests/DispatcherTest.php | 8 +++---- tests/EngineTest.php | 9 ++++++-- 4 files changed, 43 insertions(+), 27 deletions(-) diff --git a/flight/Engine.php b/flight/Engine.php index b22456e..7f45daa 100644 --- a/flight/Engine.php +++ b/flight/Engine.php @@ -220,13 +220,13 @@ class Engine /** * Registers the container handler * - * @param callable $callback Callback function that sets the container and how it will inject classes + * @param callable|object $containerHandler Callback function or PSR-11 Container object that sets the container and how it will inject classes * * @return void */ - public function registerContainerHandler($callback): void + public function registerContainerHandler($containerHandler): void { - $this->dispatcher->setContainerHandler($callback); + $this->dispatcher->setContainerHandler($containerHandler); } /** diff --git a/flight/core/Dispatcher.php b/flight/core/Dispatcher.php index 41d9d39..00a521b 100644 --- a/flight/core/Dispatcher.php +++ b/flight/core/Dispatcher.php @@ -25,6 +25,9 @@ class Dispatcher public const FILTER_AFTER = 'after'; private const FILTER_TYPES = [self::FILTER_BEFORE, self::FILTER_AFTER]; + /** @var mixed $container_exception Exception message if thrown by setting the container as a callable method */ + public static $container_exception = null; + /** @var array Mapped events. */ protected array $events = []; @@ -40,18 +43,18 @@ class Dispatcher * * @var callable|object|null */ - protected $container_handler = null; + protected $containerHandler = null; /** * Sets the dependency injection container handler. * - * @param callable|object $container_handler Dependency injection container + * @param callable|object $containerHandler Dependency injection container * * @return void */ - public function setContainerHandler($container_handler): void + public function setContainerHandler($containerHandler): void { - $this->container_handler = $container_handler; + $this->containerHandler = $containerHandler; } /** @@ -246,11 +249,7 @@ class Dispatcher $this->handleInvalidCallbackType($callback); - if (is_array($callback) === true) { - return $this->invokeMethod($callback, $params); - } - - return $this->callFunction($callback, $params); + return $this->invokeCallable($callback, $params); } /** @@ -311,6 +310,7 @@ class Dispatcher * @return mixed Function results * @throws TypeError For nonexistent class name. * @throws InvalidArgumentException If the constructor requires parameters + * @version 3.7.0 */ public function invokeCallable($func, array &$params = []) { @@ -321,9 +321,9 @@ class Dispatcher [$class, $method] = $func; - // Only execute this if it's not a Flight class + // Only execute the container handler if it's not a Flight class if ( - $this->container_handler !== null && + $this->containerHandler !== null && ( ( is_object($class) === true && @@ -332,13 +332,15 @@ class Dispatcher is_string($class) === true ) ) { - $container_handler = $this->container_handler; - $resolved_class = $this->resolveContainerClass($container_handler, $class, $params); - if($resolved_class !== null) { - $class = $resolved_class; + $containerHandler = $this->containerHandler; + $resolvedClass = $this->resolveContainerClass($containerHandler, $class, $params); + if($resolvedClass !== null) { + $class = $resolvedClass; } } + // If there's no container handler set, you can use [ 'className', 'methodName' ] + // to call a method dynamically. Nothing is injected into the class though. if (is_string($class) && class_exists($class)) { $constructor = (new ReflectionClass($class))->getConstructor(); $constructorParamsNumber = 0; @@ -362,17 +364,20 @@ class Dispatcher // Final check to make sure it's actually a class and a method, or throw an error if (is_object($class) === false) { - if(ob_get_level() > 1) { + // Cause PHPUnit has 1 level of output buffering by default + if(ob_get_level() > (getenv('PHPUNIT_TEST') ? 1 : 0)) { ob_end_clean(); } throw new Exception("Class '$class' not found. Is it being correctly autoloaded with Flight::path()?"); } + // Class is there, but no method if (method_exists($class, $method) === false) { - if(ob_get_level() > 1) { + // Cause PHPUnit has 1 level of output buffering by default + if(ob_get_level() > (getenv('PHPUNIT_TEST') ? 1 : 0)) { ob_end_clean(); } - throw new Exception("Method '".get_class($class)."::$method' not found."); + throw new Exception("Class found, but method '".get_class($class)."::$method' not found."); } return call_user_func_array([ $class, $method ], $params); @@ -428,6 +433,12 @@ class Dispatcher } catch (Exception $e) { // could not resolve a class for some reason $class_object = null; + + // 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. + self::$container_exception = $e; } } diff --git a/tests/DispatcherTest.php b/tests/DispatcherTest.php index 7cf2c24..9e3289e 100644 --- a/tests/DispatcherTest.php +++ b/tests/DispatcherTest.php @@ -192,7 +192,7 @@ class DispatcherTest extends TestCase { set_error_handler(function (int $errno, string $errstr): void { $this->assertSame(E_USER_NOTICE, $errno); - $this->assertSame("Event 'myMethod' has been overriden!", $errstr); + $this->assertSame("Event 'myMethod' has been overridden!", $errstr); }); $this->dispatcher->set('myMethod', function (): string { @@ -200,10 +200,10 @@ class DispatcherTest extends TestCase }); $this->dispatcher->set('myMethod', function (): string { - return 'Overriden'; + return 'Overridden'; }); - $this->assertSame('Overriden', $this->dispatcher->run('myMethod')); + $this->assertSame('Overridden', $this->dispatcher->run('myMethod')); restore_error_handler(); } @@ -219,7 +219,7 @@ class DispatcherTest extends TestCase return 'Original'; }) ->hook('myMethod', 'invalid', function (array &$params, &$output): void { - $output = 'Overriden'; + $output = 'Overridden'; }); $this->assertSame('Original', $this->dispatcher->run('myMethod')); diff --git a/tests/EngineTest.php b/tests/EngineTest.php index dc5947b..8ce6dfc 100644 --- a/tests/EngineTest.php +++ b/tests/EngineTest.php @@ -6,6 +6,7 @@ namespace tests; use Exception; use Flight; +use flight\core\Dispatcher; use flight\Engine; use flight\net\Request; use flight\net\Response; @@ -706,6 +707,8 @@ class EngineTest extends TestCase $this->expectExceptionMessage("Class 'BadClass' not found. Is it being correctly autoloaded with Flight::path()?"); $engine->start(); + + $this->assertEquals('Class BadClass not found', Dispatcher::$container_exception->getMessage()); } public function testContainerDiceBadMethod() { @@ -719,9 +722,11 @@ class EngineTest extends TestCase $engine->request()->url = '/container'; $this->expectException(Exception::class); - $this->expectExceptionMessage("Method 'tests\classes\Container::badMethod' not found."); + $this->expectExceptionMessage("Class found, but method 'tests\classes\Container::badMethod' not found."); $engine->start(); + + $this->assertNull(Dispatcher::$container_exception); } public function testContainerPsr11() { @@ -765,7 +770,7 @@ class EngineTest extends TestCase $engine->request()->url = '/container'; $this->expectException(Exception::class); - $this->expectExceptionMessage("Method 'tests\classes\Container::badMethod' not found."); + $this->expectExceptionMessage("Class found, but method 'tests\classes\Container::badMethod' not found."); $engine->start(); } From f752073f7d1fa6a5b80b474899e840bc6a9ff037 Mon Sep 17 00:00:00 2001 From: Austin Collier Date: Tue, 19 Mar 2024 23:52:39 -0600 Subject: [PATCH 18/24] more testing and catching things. --- flight/core/Dispatcher.php | 57 ++++++++++++++++-------------------- tests/DispatcherTest.php | 23 ++++++--------- tests/EngineTest.php | 58 +++++++++++++++++++++++++++++++++++-- tests/MapTest.php | 2 ++ tests/classes/Container.php | 11 ++++++- 5 files changed, 101 insertions(+), 50 deletions(-) diff --git a/flight/core/Dispatcher.php b/flight/core/Dispatcher.php index 00a521b..e36644c 100644 --- a/flight/core/Dispatcher.php +++ b/flight/core/Dispatcher.php @@ -339,44 +339,24 @@ class Dispatcher } } - // If there's no container handler set, you can use [ 'className', 'methodName' ] - // to call a method dynamically. Nothing is injected into the class though. - if (is_string($class) && class_exists($class)) { - $constructor = (new ReflectionClass($class))->getConstructor(); - $constructorParamsNumber = 0; - - if ($constructor !== null) { - $constructorParamsNumber = count($constructor->getParameters()); - } - - if ($constructorParamsNumber > 0) { - $exceptionMessage = "Method '$class::$method' cannot be called statically. "; - $exceptionMessage .= sprintf( - "$class::__construct require $constructorParamsNumber parameter%s", - $constructorParamsNumber > 1 ? 's' : '' - ); - - throw new InvalidArgumentException($exceptionMessage, E_ERROR); - } - - $class = new $class(); - } - // Final check to make sure it's actually a class and a method, or throw an error - if (is_object($class) === false) { - // Cause PHPUnit has 1 level of output buffering by default - if(ob_get_level() > (getenv('PHPUNIT_TEST') ? 1 : 0)) { - ob_end_clean(); - } + if (is_object($class) === false && class_exists($class) === false) { + $this->fixOutputBuffering(); throw 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 + if (isset($resolvedClass) === false && self::$container_exception !== null) { + $this->fixOutputBuffering(); + throw self::$container_exception; + } + + // If we made it this far, we can forget the container exception and move on... + self::$container_exception = null; + // Class is there, but no method if (method_exists($class, $method) === false) { - // Cause PHPUnit has 1 level of output buffering by default - if(ob_get_level() > (getenv('PHPUNIT_TEST') ? 1 : 0)) { - ob_end_clean(); - } + $this->fixOutputBuffering(); throw new Exception("Class found, but method '".get_class($class)."::$method' not found."); } @@ -445,6 +425,19 @@ class Dispatcher return $class_object; } + /** + * Because this could throw an exception in the middle of an output buffer, + * + * @return void + */ + protected function fixOutputBuffering(): void + { + // Cause PHPUnit has 1 level of output buffering by default + if(ob_get_level() > (getenv('PHPUNIT_TEST') ? 1 : 0)) { + ob_end_clean(); + } + } + /** * Resets the object to the initial state. * diff --git a/tests/DispatcherTest.php b/tests/DispatcherTest.php index 9e3289e..5fb902c 100644 --- a/tests/DispatcherTest.php +++ b/tests/DispatcherTest.php @@ -21,6 +21,7 @@ class DispatcherTest extends TestCase protected function setUp(): void { $this->dispatcher = new Dispatcher(); + Dispatcher::$container_exception = null; } public function testClosureMapping(): void @@ -137,20 +138,6 @@ class DispatcherTest extends TestCase $this->dispatcher->execute('nonexistentGlobalFunction'); } - public function testInvalidCallbackBecauseConstructorParameters(): void - { - $class = TesterClass::class; - $method = 'instanceMethod'; - $exceptionMessage = "Method '$class::$method' cannot be called statically. "; - $exceptionMessage .= "$class::__construct require 6 parameters"; - - $this->expectException(InvalidArgumentException::class); - $this->expectExceptionMessage($exceptionMessage); - - static $params = []; - $this->dispatcher->invokeMethod([$class, $method], $params); - } - // It will be useful for executing instance Controller methods statically public function testCanExecuteAnNonStaticMethodStatically(): void { @@ -252,4 +239,12 @@ class DispatcherTest extends TestCase $this->assertSame('helloparam1param2param3param4param5param6', $result); } + + public function testInvokeMethod(): void + { + $class = new TesterClass('param1', 'param2', 'param3', 'param4', 'param5', 'param6'); + $result = $this->dispatcher->invokeMethod([ $class, 'instanceMethod' ]); + + $this->assertSame('param1', $class->param2); + } } diff --git a/tests/EngineTest.php b/tests/EngineTest.php index 8ce6dfc..ba8739b 100644 --- a/tests/EngineTest.php +++ b/tests/EngineTest.php @@ -5,12 +5,13 @@ declare(strict_types=1); namespace tests; use Exception; -use Flight; use flight\core\Dispatcher; +use flight\database\PdoWrapper; use flight\Engine; use flight\net\Request; use flight\net\Response; use flight\util\Collection; +use PDOException; use PHPUnit\Framework\TestCase; use tests\classes\Container; @@ -20,6 +21,7 @@ class EngineTest extends TestCase public function setUp(): void { $_SERVER = []; + Dispatcher::$container_exception = null; } public function tearDown(): void @@ -682,6 +684,12 @@ class EngineTest extends TestCase public function testContainerDice() { $engine = new Engine(); $dice = new \Dice\Dice(); + $dice = $dice->addRules([ + PdoWrapper::class => [ + 'shared' => true, + 'constructParams' => [ 'sqlite::memory:' ] + ] + ]); $engine->registerContainerHandler(function ($class, $params) use ($dice) { return $dice->create($class, $params); }); @@ -693,6 +701,42 @@ class EngineTest extends TestCase $this->expectOutputString('yay! I injected a collection, and it has 1 items'); } + public function testContainerDicePdoWrapperTest() { + $engine = new Engine(); + $dice = new \Dice\Dice(); + $dice = $dice->addRules([ + PdoWrapper::class => [ + 'shared' => true, + 'constructParams' => [ 'sqlite::memory:' ] + ] + ]); + $engine->registerContainerHandler(function ($class, $params) use ($dice) { + return $dice->create($class, $params); + }); + + $engine->route('/container', Container::class.'->testThePdoWrapper'); + $engine->request()->url = '/container'; + $engine->start(); + + $this->expectOutputString('Yay! I injected a PdoWrapper, and it returned the number 5 from the database!'); + } + + public function testContainerDicePdoWrapperTestBadParams() { + $engine = new Engine(); + $dice = new \Dice\Dice(); + $engine->registerContainerHandler(function ($class, $params) use ($dice) { + return $dice->create($class, $params); + }); + + $engine->route('/container', Container::class.'->testThePdoWrapper'); + $engine->request()->url = '/container'; + + $this->expectException(PDOException::class); + $this->expectExceptionMessage("invalid data source name"); + + $engine->start(); + } + public function testContainerDiceBadClass() { $engine = new Engine(); $dice = new \Dice\Dice(); @@ -714,6 +758,12 @@ class EngineTest extends TestCase public function testContainerDiceBadMethod() { $engine = new Engine(); $dice = new \Dice\Dice(); + $dice = $dice->addRules([ + PdoWrapper::class => [ + 'shared' => true, + 'constructParams' => [ 'sqlite::memory:' ] + ] + ]); $engine->registerContainerHandler(function ($class, $params) use ($dice) { return $dice->create($class, $params); }); @@ -732,8 +782,9 @@ class EngineTest extends TestCase public function testContainerPsr11() { $engine = new Engine(); $container = new \League\Container\Container(); - $container->add(Container::class)->addArgument(Collection::class); + $container->add(Container::class)->addArgument(Collection::class)->addArgument(PdoWrapper::class); $container->add(Collection::class); + $container->add(PdoWrapper::class)->addArgument('sqlite::memory:'); $engine->registerContainerHandler($container); $engine->route('/container', Container::class.'->testTheContainer'); @@ -762,8 +813,9 @@ class EngineTest extends TestCase public function testContainerPsr11MethodNotFound() { $engine = new Engine(); $container = new \League\Container\Container(); - $container->add(Container::class)->addArgument(Collection::class); + $container->add(Container::class)->addArgument(Collection::class)->addArgument(PdoWrapper::class); $container->add(Collection::class); + $container->add(PdoWrapper::class)->addArgument('sqlite::memory:'); $engine->registerContainerHandler($container); $engine->route('/container', Container::class.'->badMethod'); diff --git a/tests/MapTest.php b/tests/MapTest.php index 445e8eb..79c39dc 100644 --- a/tests/MapTest.php +++ b/tests/MapTest.php @@ -5,6 +5,7 @@ declare(strict_types=1); namespace tests; use Exception; +use flight\core\Dispatcher; use flight\Engine; use tests\classes\Hello; use PHPUnit\Framework\TestCase; @@ -15,6 +16,7 @@ class MapTest extends TestCase protected function setUp(): void { + Dispatcher::$container_exception = null; $this->app = new Engine(); } diff --git a/tests/classes/Container.php b/tests/classes/Container.php index ffb6fe1..7e67518 100644 --- a/tests/classes/Container.php +++ b/tests/classes/Container.php @@ -4,15 +4,18 @@ declare(strict_types=1); namespace tests\classes; +use flight\database\PdoWrapper; use flight\util\Collection; class Container { protected Collection $collection; + protected PdoWrapper $pdoWrapper; - public function __construct(Collection $collection) + public function __construct(Collection $collection, PdoWrapper $pdoWrapper) { $this->collection = $collection; + $this->pdoWrapper = $pdoWrapper; } public function testTheContainer() @@ -20,4 +23,10 @@ class Container $this->collection->whatever = 'yay!'; echo 'yay! I injected a collection, and it has ' . $this->collection->count() . ' items'; } + + public function testThePdoWrapper() + { + $value = (int) $this->pdoWrapper->fetchField('SELECT 5'); + echo 'Yay! I injected a PdoWrapper, and it returned the number ' . $value . ' from the database!'; + } } From dbf05ebf4044b8b778b7073e00f0da2b9ef953ff Mon Sep 17 00:00:00 2001 From: Austin Collier Date: Wed, 20 Mar 2024 08:42:56 -0600 Subject: [PATCH 19/24] Lots more tests. Now will dynamically create object with engine instance. --- flight/Engine.php | 3 ++ flight/core/Dispatcher.php | 27 +++++++++----- tests/DispatcherTest.php | 57 +++++++++++++++++++++++++++++- tests/EngineTest.php | 5 --- tests/MapTest.php | 1 - tests/classes/ContainerDefault.php | 25 +++++++++++++ 6 files changed, 103 insertions(+), 15 deletions(-) create mode 100644 tests/classes/ContainerDefault.php diff --git a/flight/Engine.php b/flight/Engine.php index 7f45daa..62342b6 100644 --- a/flight/Engine.php +++ b/flight/Engine.php @@ -138,6 +138,9 @@ class Engine $this->dispatcher->reset(); } + // Add this class to Dispatcher + $this->dispatcher->setEngine($this); + // Register default components $this->loader->register('request', Request::class); $this->loader->register('response', Response::class); diff --git a/flight/core/Dispatcher.php b/flight/core/Dispatcher.php index e36644c..672cb82 100644 --- a/flight/core/Dispatcher.php +++ b/flight/core/Dispatcher.php @@ -6,6 +6,7 @@ namespace flight\core; use Closure; use Exception; +use flight\Engine; use InvalidArgumentException; use ReflectionClass; use TypeError; @@ -26,7 +27,10 @@ class Dispatcher private const FILTER_TYPES = [self::FILTER_BEFORE, self::FILTER_AFTER]; /** @var mixed $container_exception Exception message if thrown by setting the container as a callable method */ - public static $container_exception = null; + protected $container_exception = null; + + /** @var ?Engine $engine Engine instance */ + protected ?Engine $engine = null; /** @var array Mapped events. */ protected array $events = []; @@ -57,6 +61,11 @@ class Dispatcher $this->containerHandler = $containerHandler; } + public function setEngine(Engine $engine): void + { + $this->engine = $engine; + } + /** * Dispatches an event. * @@ -346,20 +355,22 @@ class Dispatcher } // If this tried to resolve a class in a container and failed somehow, throw the exception - if (isset($resolvedClass) === false && self::$container_exception !== null) { + if (isset($resolvedClass) === false && $this->container_exception !== null) { $this->fixOutputBuffering(); - throw self::$container_exception; + throw $this->container_exception; } - // If we made it this far, we can forget the container exception and move on... - self::$container_exception = null; - // Class is there, but no method - if (method_exists($class, $method) === false) { + if (is_object($class) === true && method_exists($class, $method) === false) { $this->fixOutputBuffering(); throw new Exception("Class found, but method '".get_class($class)."::$method' not found."); } + // Class is a string, and method exists, create the object by hand and inject only the Engine + if (is_string($class) === true) { + $class = new $class($this->engine); + } + return call_user_func_array([ $class, $method ], $params); } @@ -418,7 +429,7 @@ class Dispatcher // and store it somewhere. If we just let it throw itself, it // doesn't properly close the output buffers and can cause other // issues. - self::$container_exception = $e; + $this->container_exception = $e; } } diff --git a/tests/DispatcherTest.php b/tests/DispatcherTest.php index 5fb902c..a75f18f 100644 --- a/tests/DispatcherTest.php +++ b/tests/DispatcherTest.php @@ -4,13 +4,16 @@ declare(strict_types=1); namespace tests; +use ArgumentCountError; use Closure; use Exception; use flight\core\Dispatcher; +use flight\Engine; use InvalidArgumentException; use PharIo\Manifest\InvalidEmailException; use tests\classes\Hello; use PHPUnit\Framework\TestCase; +use tests\classes\ContainerDefault; use tests\classes\TesterClass; use TypeError; @@ -21,7 +24,6 @@ class DispatcherTest extends TestCase protected function setUp(): void { $this->dispatcher = new Dispatcher(); - Dispatcher::$container_exception = null; } public function testClosureMapping(): void @@ -247,4 +249,57 @@ class DispatcherTest extends TestCase $this->assertSame('param1', $class->param2); } + + public function testExecuteStringClassBadConstructParams(): void + { + $this->expectException(ArgumentCountError::class); + $this->expectExceptionMessageMatches('#Too few arguments to function tests\\\\classes\\\\TesterClass::__construct\(\), 1 passed .+ and exactly 6 expected#'); + $this->dispatcher->execute(TesterClass::class . '->instanceMethod'); + } + + public function testExecuteStringClassNoConstruct(): void + { + $result = $this->dispatcher->execute(Hello::class . '->sayHi'); + $this->assertSame('hello', $result); + } + + public function testExecuteStringClassNoConstructDoubleColon(): void + { + $result = $this->dispatcher->execute(Hello::class . '::sayHi'); + $this->assertSame('hello', $result); + } + + public function testExecuteStringClassNoConstructArraySyntax(): void + { + $result = $this->dispatcher->execute([ Hello::class, 'sayHi' ]); + $this->assertSame('hello', $result); + } + + public function testExecuteStringClassDefaultContainer(): void + { + $this->dispatcher->setEngine(new Engine); + $result = $this->dispatcher->execute(ContainerDefault::class . '->testTheContainer'); + $this->assertSame('You got it boss!', $result); + } + + public function testExecuteStringClassDefaultContainerDoubleColon(): void + { + $this->dispatcher->setEngine(new Engine); + $result = $this->dispatcher->execute(ContainerDefault::class . '::testTheContainer'); + $this->assertSame('You got it boss!', $result); + } + + public function testExecuteStringClassDefaultContainerArraySyntax(): void + { + $this->dispatcher->setEngine(new Engine); + $result = $this->dispatcher->execute([ ContainerDefault::class, 'testTheContainer' ]); + $this->assertSame('You got it boss!', $result); + } + + public function testExecuteStringClassDefaultContainerButForgotInjectingEngine(): void + { + $this->expectException(TypeError::class); + $this->expectExceptionMessageMatches('#Argument 1 passed to tests\\\\classes\\\\ContainerDefault::__construct\(\) must be an instance of flight\\\\Engine, null given#'); + $result = $this->dispatcher->execute([ ContainerDefault::class, 'testTheContainer' ]); + } } diff --git a/tests/EngineTest.php b/tests/EngineTest.php index ba8739b..64c8911 100644 --- a/tests/EngineTest.php +++ b/tests/EngineTest.php @@ -21,7 +21,6 @@ class EngineTest extends TestCase public function setUp(): void { $_SERVER = []; - Dispatcher::$container_exception = null; } public function tearDown(): void @@ -751,8 +750,6 @@ class EngineTest extends TestCase $this->expectExceptionMessage("Class 'BadClass' not found. Is it being correctly autoloaded with Flight::path()?"); $engine->start(); - - $this->assertEquals('Class BadClass not found', Dispatcher::$container_exception->getMessage()); } public function testContainerDiceBadMethod() { @@ -775,8 +772,6 @@ class EngineTest extends TestCase $this->expectExceptionMessage("Class found, but method 'tests\classes\Container::badMethod' not found."); $engine->start(); - - $this->assertNull(Dispatcher::$container_exception); } public function testContainerPsr11() { diff --git a/tests/MapTest.php b/tests/MapTest.php index 79c39dc..bf276fb 100644 --- a/tests/MapTest.php +++ b/tests/MapTest.php @@ -16,7 +16,6 @@ class MapTest extends TestCase protected function setUp(): void { - Dispatcher::$container_exception = null; $this->app = new Engine(); } diff --git a/tests/classes/ContainerDefault.php b/tests/classes/ContainerDefault.php new file mode 100644 index 0000000..28abf4a --- /dev/null +++ b/tests/classes/ContainerDefault.php @@ -0,0 +1,25 @@ +set('test_me_out', 'You got it boss!'); + $this->app = $engine; + } + + public function testTheContainer() + { + return $this->app->get('test_me_out'); + } + +} From 1b6c5b942bc28975ff64de008b32852d4b929c98 Mon Sep 17 00:00:00 2001 From: Austin Collier Date: Wed, 20 Mar 2024 22:51:55 -0600 Subject: [PATCH 20/24] refactored some errors. UI Tests and cleanup. --- flight/Flight.php | 9 ++-- flight/core/Dispatcher.php | 77 +++++++++++++++++++----------- tests/DispatcherTest.php | 6 +-- tests/EngineTest.php | 1 - tests/MapTest.php | 1 - tests/classes/Container.php | 2 +- tests/classes/ContainerDefault.php | 5 +- tests/server/LayoutMiddleware.php | 2 + tests/server/index.php | 26 ++++++++++ 9 files changed, 89 insertions(+), 40 deletions(-) diff --git a/flight/Flight.php b/flight/Flight.php index 9a3042c..0a42489 100644 --- a/flight/Flight.php +++ b/flight/Flight.php @@ -19,11 +19,12 @@ require_once __DIR__ . '/autoload.php'; * @copyright Copyright (c) 2011, Mike Cao * * # Core methods - * @method static void start() Starts the framework. - * @method static void path(string $path) Adds a path for autoloading classes. - * @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) + * @method static void start() Starts the framework. + * @method static void path(string $path) Adds a path for autoloading classes. + * @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 registerContainerHandler(callable|object $containerHandler) Registers a container handler. * * # Routing * @method static Route route(string $pattern, callable|string $callback, bool $pass_route = false, string $alias = '') diff --git a/flight/core/Dispatcher.php b/flight/core/Dispatcher.php index 672cb82..2841dd7 100644 --- a/flight/core/Dispatcher.php +++ b/flight/core/Dispatcher.php @@ -26,8 +26,8 @@ class Dispatcher public const FILTER_AFTER = 'after'; private const FILTER_TYPES = [self::FILTER_BEFORE, self::FILTER_AFTER]; - /** @var mixed $container_exception Exception message if thrown by setting the container as a callable method */ - protected $container_exception = null; + /** @var mixed $containerException Exception message if thrown by setting the container as a callable method */ + protected $containerException = null; /** @var ?Engine $engine Engine instance */ protected ?Engine $engine = null; @@ -256,8 +256,6 @@ class Dispatcher $callback = $this->parseStringClassAndMethod($callback); } - $this->handleInvalidCallbackType($callback); - return $this->invokeCallable($callback, $params); } @@ -325,10 +323,12 @@ 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 ( @@ -343,28 +343,12 @@ class Dispatcher ) { $containerHandler = $this->containerHandler; $resolvedClass = $this->resolveContainerClass($containerHandler, $class, $params); - if($resolvedClass !== null) { + if ($resolvedClass !== null) { $class = $resolvedClass; } } - // 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) { - $this->fixOutputBuffering(); - throw 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 - if (isset($resolvedClass) === false && $this->container_exception !== null) { - $this->fixOutputBuffering(); - throw $this->container_exception; - } - - // Class is there, but no method - if (is_object($class) === true && method_exists($class, $method) === false) { - $this->fixOutputBuffering(); - throw new Exception("Class found, but method '".get_class($class)."::$method' not found."); - } + $this->verifyValidClassCallable($class, $method, $resolvedClass); // Class is a string, and method exists, create the object by hand and inject only the Engine if (is_string($class) === true) { @@ -382,7 +366,7 @@ class Dispatcher * * @throws InvalidArgumentException If `$callback` is an invalid type */ - protected function handleInvalidCallbackType($callback): void + protected function verifyValidFunction($callback): void { $isInvalidFunctionName = ( is_string($callback) @@ -394,6 +378,41 @@ class Dispatcher } } + + /** + * Verifies if the provided class and method are valid callable. + * + * @param 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; + + // 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 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; + + // 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."); + } + + if ($final_exception !== null) { + $this->fixOutputBuffering(); + throw $final_exception; + } + } + /** * Resolves the container class. * @@ -417,7 +436,6 @@ class Dispatcher // 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 try { $class_object = call_user_func($container_handler, $class, $params); @@ -425,11 +443,12 @@ class Dispatcher // could not resolve a class for some reason $class_object = null; - // 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 + // 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->container_exception = $e; + // This is thrown in the verifyValidClassCallable method + $this->containerException = $e; } } @@ -444,7 +463,7 @@ class Dispatcher protected function fixOutputBuffering(): void { // Cause PHPUnit has 1 level of output buffering by default - if(ob_get_level() > (getenv('PHPUNIT_TEST') ? 1 : 0)) { + if (ob_get_level() > (getenv('PHPUNIT_TEST') ? 1 : 0)) { ob_end_clean(); } } diff --git a/tests/DispatcherTest.php b/tests/DispatcherTest.php index a75f18f..f2f3107 100644 --- a/tests/DispatcherTest.php +++ b/tests/DispatcherTest.php @@ -277,21 +277,21 @@ class DispatcherTest extends TestCase public function testExecuteStringClassDefaultContainer(): void { - $this->dispatcher->setEngine(new Engine); + $this->dispatcher->setEngine(new Engine()); $result = $this->dispatcher->execute(ContainerDefault::class . '->testTheContainer'); $this->assertSame('You got it boss!', $result); } public function testExecuteStringClassDefaultContainerDoubleColon(): void { - $this->dispatcher->setEngine(new Engine); + $this->dispatcher->setEngine(new Engine()); $result = $this->dispatcher->execute(ContainerDefault::class . '::testTheContainer'); $this->assertSame('You got it boss!', $result); } public function testExecuteStringClassDefaultContainerArraySyntax(): void { - $this->dispatcher->setEngine(new Engine); + $this->dispatcher->setEngine(new Engine()); $result = $this->dispatcher->execute([ ContainerDefault::class, 'testTheContainer' ]); $this->assertSame('You got it boss!', $result); } diff --git a/tests/EngineTest.php b/tests/EngineTest.php index 64c8911..b79e344 100644 --- a/tests/EngineTest.php +++ b/tests/EngineTest.php @@ -5,7 +5,6 @@ declare(strict_types=1); namespace tests; use Exception; -use flight\core\Dispatcher; use flight\database\PdoWrapper; use flight\Engine; use flight\net\Request; diff --git a/tests/MapTest.php b/tests/MapTest.php index bf276fb..445e8eb 100644 --- a/tests/MapTest.php +++ b/tests/MapTest.php @@ -5,7 +5,6 @@ declare(strict_types=1); namespace tests; use Exception; -use flight\core\Dispatcher; use flight\Engine; use tests\classes\Hello; use PHPUnit\Framework\TestCase; diff --git a/tests/classes/Container.php b/tests/classes/Container.php index 7e67518..5b9d216 100644 --- a/tests/classes/Container.php +++ b/tests/classes/Container.php @@ -26,7 +26,7 @@ class Container public function testThePdoWrapper() { - $value = (int) $this->pdoWrapper->fetchField('SELECT 5'); + $value = intval($this->pdoWrapper->fetchField('SELECT 5')); echo 'Yay! I injected a PdoWrapper, and it returned the number ' . $value . ' from the database!'; } } diff --git a/tests/classes/ContainerDefault.php b/tests/classes/ContainerDefault.php index 28abf4a..183d287 100644 --- a/tests/classes/ContainerDefault.php +++ b/tests/classes/ContainerDefault.php @@ -8,7 +8,6 @@ use flight\Engine; class ContainerDefault { - protected Engine $app; public function __construct(Engine $engine) @@ -22,4 +21,8 @@ class ContainerDefault return $this->app->get('test_me_out'); } + public function testUi() + { + echo 'Route text: The container successfully injected a value into the engine! Engine class: ' . get_class($this->app) . ' test_me_out Value: ' . $this->app->get('test_me_out') . ''; + } } diff --git a/tests/server/LayoutMiddleware.php b/tests/server/LayoutMiddleware.php index b2e59bb..500cbd7 100644 --- a/tests/server/LayoutMiddleware.php +++ b/tests/server/LayoutMiddleware.php @@ -81,6 +81,8 @@ class LayoutMiddleware
  • Slash in Param
  • UTF8 URL
  • UTF8 URL w/ Param
  • +
  • Dice Container
  • +
  • No Container Registered
  • HTML; echo '
    '; diff --git a/tests/server/index.php b/tests/server/index.php index f00144f..8417759 100644 --- a/tests/server/index.php +++ b/tests/server/index.php @@ -2,6 +2,10 @@ declare(strict_types=1); +use flight\database\PdoWrapper; +use tests\classes\Container; +use tests\classes\ContainerDefault; + /* * This is the test file where we can open up a quick test server and make * sure that the UI is really working the way we would expect it to. @@ -139,6 +143,9 @@ Flight::group('', function () { Flight::route('/redirect/@id', function ($id) { echo 'Route text: This route status is that it ' . ($id === 'before/after' ? 'succeeded' : 'failed') . ' URL Param: ' . $id . ''; }); + + Flight::route('/no-container', ContainerDefault::class . '->testUi'); + Flight::route('/dice', Container::class . '->testThePdoWrapper'); }, [ new LayoutMiddleware() ]); // Test 9: JSON output (should not output any other html) @@ -167,4 +174,23 @@ Flight::map('notFound', function () { echo "Go back"; }); +Flight::map('start', function () { + + if (Flight::request()->url === '/dice') { + $dice = new \Dice\Dice(); + $dice = $dice->addRules([ + PdoWrapper::class => [ + 'shared' => true, + 'constructParams' => [ 'sqlite::memory:' ] + ] + ]); + Flight::registerContainerHandler(function ($class, $params) use ($dice) { + return $dice->create($class, $params); + }); + } + + // Default start behavior now + Flight::_start(); +}); + Flight::start(); From 54403bede8b5041f81788e592059156160de94ad Mon Sep 17 00:00:00 2001 From: Austin Collier Date: Thu, 21 Mar 2024 08:45:02 -0600 Subject: [PATCH 21/24] One last test to make sure Dice maintains the same instance --- flight/core/Dispatcher.php | 1 - tests/DispatcherTest.php | 12 +++++++++--- tests/EngineTest.php | 21 +++++++++++++++++++++ tests/classes/ContainerDefault.php | 6 +++++- tests/server/index.php | 1 + 5 files changed, 36 insertions(+), 5 deletions(-) diff --git a/flight/core/Dispatcher.php b/flight/core/Dispatcher.php index 2841dd7..6a167a8 100644 --- a/flight/core/Dispatcher.php +++ b/flight/core/Dispatcher.php @@ -8,7 +8,6 @@ use Closure; use Exception; use flight\Engine; use InvalidArgumentException; -use ReflectionClass; use TypeError; /** diff --git a/tests/DispatcherTest.php b/tests/DispatcherTest.php index f2f3107..d4e1ca1 100644 --- a/tests/DispatcherTest.php +++ b/tests/DispatcherTest.php @@ -277,21 +277,27 @@ class DispatcherTest extends TestCase public function testExecuteStringClassDefaultContainer(): void { - $this->dispatcher->setEngine(new Engine()); + $engine = new Engine(); + $engine->set('test_me_out', 'You got it boss!'); + $this->dispatcher->setEngine($engine); $result = $this->dispatcher->execute(ContainerDefault::class . '->testTheContainer'); $this->assertSame('You got it boss!', $result); } public function testExecuteStringClassDefaultContainerDoubleColon(): void { - $this->dispatcher->setEngine(new Engine()); + $engine = new Engine(); + $engine->set('test_me_out', 'You got it boss!'); + $this->dispatcher->setEngine($engine); $result = $this->dispatcher->execute(ContainerDefault::class . '::testTheContainer'); $this->assertSame('You got it boss!', $result); } public function testExecuteStringClassDefaultContainerArraySyntax(): void { - $this->dispatcher->setEngine(new Engine()); + $engine = new Engine(); + $engine->set('test_me_out', 'You got it boss!'); + $this->dispatcher->setEngine($engine); $result = $this->dispatcher->execute([ ContainerDefault::class, 'testTheContainer' ]); $this->assertSame('You got it boss!', $result); } diff --git a/tests/EngineTest.php b/tests/EngineTest.php index b79e344..a8d58ef 100644 --- a/tests/EngineTest.php +++ b/tests/EngineTest.php @@ -13,6 +13,7 @@ use flight\util\Collection; use PDOException; use PHPUnit\Framework\TestCase; use tests\classes\Container; +use tests\classes\ContainerDefault; // phpcs:ignoreFile PSR2.Methods.MethodDeclaration.Underscore class EngineTest extends TestCase @@ -719,6 +720,26 @@ class EngineTest extends TestCase $this->expectOutputString('Yay! I injected a PdoWrapper, and it returned the number 5 from the database!'); } + public function testContainerDiceFlightEngine() { + $engine = new Engine(); + $engine->set('test_me_out', 'You got it boss!'); + $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('/container', ContainerDefault::class.'->echoTheContainer'); + $engine->request()->url = '/container'; + $engine->start(); + + $this->expectOutputString('You got it boss!'); + } + public function testContainerDicePdoWrapperTestBadParams() { $engine = new Engine(); $dice = new \Dice\Dice(); diff --git a/tests/classes/ContainerDefault.php b/tests/classes/ContainerDefault.php index 183d287..a71ff1f 100644 --- a/tests/classes/ContainerDefault.php +++ b/tests/classes/ContainerDefault.php @@ -12,7 +12,6 @@ class ContainerDefault public function __construct(Engine $engine) { - $engine->set('test_me_out', 'You got it boss!'); $this->app = $engine; } @@ -21,6 +20,11 @@ class ContainerDefault return $this->app->get('test_me_out'); } + public function echoTheContainer() + { + echo $this->app->get('test_me_out'); + } + public function testUi() { echo 'Route text: The container successfully injected a value into the engine! Engine class: ' . get_class($this->app) . ' test_me_out Value: ' . $this->app->get('test_me_out') . ''; diff --git a/tests/server/index.php b/tests/server/index.php index 8417759..769dc0c 100644 --- a/tests/server/index.php +++ b/tests/server/index.php @@ -144,6 +144,7 @@ Flight::group('', function () { echo 'Route text: This route status is that it ' . ($id === 'before/after' ? 'succeeded' : 'failed') . ' URL Param: ' . $id . ''; }); + Flight::set('test_me_out', 'You got it boss!'); // used in /no-container route Flight::route('/no-container', ContainerDefault::class . '->testUi'); Flight::route('/dice', Container::class . '->testThePdoWrapper'); }, [ new LayoutMiddleware() ]); From 79ffb61f94139a4cc5f51568b6a0c1dfbd6ee191 Mon Sep 17 00:00:00 2001 From: n0nag0n Date: Sat, 23 Mar 2024 09:49:19 -0600 Subject: [PATCH 22/24] phpunit fixes for deprecated notices. Also pdowrapper reset removal --- flight/database/PdoWrapper.php | 3 ++- flight/util/Collection.php | 28 ---------------------------- phpunit.xml | 1 + tests/CollectionTest.php | 23 ----------------------- tests/DispatcherTest.php | 2 +- tests/EngineTest.php | 5 +++-- 6 files changed, 7 insertions(+), 55 deletions(-) diff --git a/flight/database/PdoWrapper.php b/flight/database/PdoWrapper.php index bdbabb8..297121a 100644 --- a/flight/database/PdoWrapper.php +++ b/flight/database/PdoWrapper.php @@ -49,7 +49,8 @@ class PdoWrapper extends PDO public function fetchField(string $sql, array $params = []) { $result = $this->fetchRow($sql, $params); - return reset($result); + $data = $result->getData(); + return reset($data); } /** diff --git a/flight/util/Collection.php b/flight/util/Collection.php index 29147d1..6ffe0b5 100644 --- a/flight/util/Collection.php +++ b/flight/util/Collection.php @@ -20,18 +20,6 @@ use JsonSerializable; */ class Collection implements ArrayAccess, Iterator, Countable, JsonSerializable { - /** - * This is to allow for reset() to work properly. - * - * WARNING! This MUST be the first variable in this class!!! - * - * PHPStan is ignoring this because we don't need actually to read the property - * - * @var mixed - * @phpstan-ignore-next-line - */ - private $first_property = null; - /** * Collection data. * @@ -47,7 +35,6 @@ class Collection implements ArrayAccess, Iterator, Countable, JsonSerializable public function __construct(array $data = []) { $this->data = $data; - $this->handleReset(); } /** @@ -68,7 +55,6 @@ class Collection implements ArrayAccess, Iterator, Countable, JsonSerializable public function __set(string $key, $value): void { $this->data[$key] = $value; - $this->handleReset(); } /** @@ -85,7 +71,6 @@ class Collection implements ArrayAccess, Iterator, Countable, JsonSerializable public function __unset(string $key): void { unset($this->data[$key]); - $this->handleReset(); } /** @@ -115,7 +100,6 @@ class Collection implements ArrayAccess, Iterator, Countable, JsonSerializable } else { $this->data[$offset] = $value; } - $this->handleReset(); } /** @@ -136,17 +120,6 @@ class Collection implements ArrayAccess, Iterator, Countable, JsonSerializable public function offsetUnset($offset): void { unset($this->data[$offset]); - $this->handleReset(); - } - - /** - * This is to allow for reset() of a Collection to work properly. - * - * @return void - */ - public function handleReset() - { - $this->first_property = reset($this->data); } /** @@ -234,7 +207,6 @@ class Collection implements ArrayAccess, Iterator, Countable, JsonSerializable public function setData(array $data): void { $this->data = $data; - $this->handleReset(); } #[\ReturnTypeWillChange] diff --git a/phpunit.xml b/phpunit.xml index c97f669..18134c0 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -23,6 +23,7 @@ + diff --git a/tests/CollectionTest.php b/tests/CollectionTest.php index 4f742fe..b6eac46 100644 --- a/tests/CollectionTest.php +++ b/tests/CollectionTest.php @@ -99,27 +99,4 @@ class CollectionTest extends TestCase $this->collection->clear(); $this->assertEquals(0, $this->collection->count()); } - - public function testResetByProperty() - { - $this->collection->a = 11; - $this->collection->b = 22; - $result = reset($this->collection); - $this->assertEquals(11, $result); - } - - public function testResetBySetData() - { - $this->collection->setData(['a' => 11, 'b' => 22]); - $result = reset($this->collection); - $this->assertEquals(11, $result); - } - - public function testResetByArraySet() - { - $this->collection['a'] = 11; - $this->collection['b'] = 22; - $result = reset($this->collection); - $this->assertEquals(11, $result); - } } diff --git a/tests/DispatcherTest.php b/tests/DispatcherTest.php index d4e1ca1..a755666 100644 --- a/tests/DispatcherTest.php +++ b/tests/DispatcherTest.php @@ -305,7 +305,7 @@ class DispatcherTest extends TestCase public function testExecuteStringClassDefaultContainerButForgotInjectingEngine(): void { $this->expectException(TypeError::class); - $this->expectExceptionMessageMatches('#Argument 1 passed to tests\\\\classes\\\\ContainerDefault::__construct\(\) must be an instance of flight\\\\Engine, null given#'); + $this->expectExceptionMessageMatches('#tests\\\\classes\\\\ContainerDefault::__construct\(\).+flight\\\\Engine, null given#'); $result = $this->dispatcher->execute([ ContainerDefault::class, 'testTheContainer' ]); } } diff --git a/tests/EngineTest.php b/tests/EngineTest.php index a8d58ef..bdf511e 100644 --- a/tests/EngineTest.php +++ b/tests/EngineTest.php @@ -4,6 +4,7 @@ declare(strict_types=1); namespace tests; +use ErrorException; use Exception; use flight\database\PdoWrapper; use flight\Engine; @@ -750,8 +751,8 @@ class EngineTest extends TestCase $engine->route('/container', Container::class.'->testThePdoWrapper'); $engine->request()->url = '/container'; - $this->expectException(PDOException::class); - $this->expectExceptionMessage("invalid data source name"); + $this->expectException(ErrorException::class); + $this->expectExceptionMessageMatches("/Passing null to parameter/"); $engine->start(); } From 4b4f07ea56e59e38481a36cec1df2ff5fcd35a1e Mon Sep 17 00:00:00 2001 From: n0nag0n Date: Sat, 23 Mar 2024 12:49:09 -0600 Subject: [PATCH 23/24] 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 24/24] 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)