Merge pull request #591 from flightphp/method-not-found

Added ability to throw a method not found instead of 404
pull/594/head
n0nag0n 8 months ago committed by GitHub
commit 10165ebda3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -388,15 +388,14 @@ class Engine
{ {
$atLeastOneMiddlewareFailed = false; $atLeastOneMiddlewareFailed = false;
// Process things normally for before, and then in reverse order for after. // Process things normally for before, and then in reverse order for after.
$middlewares = $eventName === Dispatcher::FILTER_BEFORE $middlewares = $eventName === Dispatcher::FILTER_BEFORE
? $route->middleware ? $route->middleware
: array_reverse($route->middleware); : array_reverse($route->middleware);
$params = $route->params; $params = $route->params;
foreach ($middlewares as $middleware) { foreach ($middlewares as $middleware) {
// Assume that nothing is going to be executed for the middleware.
// Assume that nothing is going to be executed for the middleware.
$middlewareObject = false; $middlewareObject = false;
// Closure functions can only run on the before event // Closure functions can only run on the before event
@ -426,12 +425,12 @@ class Engine
} }
} }
// If nothing was resolved, go to the next thing // If nothing was resolved, go to the next thing
if ($middlewareObject === false) { if ($middlewareObject === false) {
continue; continue;
} }
// This is the way that v3 handles output buffering (which captures output correctly) // This is the way that v3 handles output buffering (which captures output correctly)
$useV3OutputBuffering = $useV3OutputBuffering =
$this->response()->v2_output_buffering === false && $this->response()->v2_output_buffering === false &&
$route->is_streamed === false; $route->is_streamed === false;
@ -441,16 +440,16 @@ class Engine
} }
// Here is the array callable $middlewareObject that we created earlier. // Here is the array callable $middlewareObject that we created earlier.
// It looks bizarre but it's really calling [ $class, $method ]($params) // It looks bizarre but it's really calling [ $class, $method ]($params)
// Which loosely translates to $class->$method($params) // Which loosely translates to $class->$method($params)
$middlewareResult = $middlewareObject($params); $middlewareResult = $middlewareObject($params);
if ($useV3OutputBuffering === true) { if ($useV3OutputBuffering === true) {
$this->response()->write(ob_get_clean()); $this->response()->write(ob_get_clean());
} }
// If you return false in your middleware, it will halt the request // If you return false in your middleware, it will halt the request
// and throw a 403 forbidden error by default. // and throw a 403 forbidden error by default.
if ($middlewareResult === false) { if ($middlewareResult === false) {
$atLeastOneMiddlewareFailed = true; $atLeastOneMiddlewareFailed = true;
break; break;
@ -579,7 +578,13 @@ class Engine
if ($failedMiddlewareCheck === true) { if ($failedMiddlewareCheck === true) {
$this->halt(403, 'Forbidden', empty(getenv('PHPUNIT_TEST'))); $this->halt(403, 'Forbidden', empty(getenv('PHPUNIT_TEST')));
} elseif ($dispatched === false) { } elseif ($dispatched === false) {
$this->notFound(); // Get the previous route and check if the method failed, but the URL was good.
$lastRouteExecuted = $router->executedRoute;
if ($lastRouteExecuted !== null && $lastRouteExecuted->matchUrl($request->url) === true && $lastRouteExecuted->matchMethod($request->method) === false) {
$this->halt(405, 'Method Not Allowed', empty(getenv('PHPUNIT_TEST')));
} else {
$this->notFound();
}
} }
} }
@ -670,8 +675,8 @@ class Engine
* @param string $pattern URL pattern to match * @param string $pattern URL pattern to match
* @param callable|string $callback Callback function or string class->method * @param callable|string $callback Callback function or string class->method
* @param bool $pass_route Pass the matching route object to the callback * @param bool $pass_route Pass the matching route object to the callback
* *
* @return Route * @return Route
*/ */
public function _post(string $pattern, $callback, bool $pass_route = false, string $route_alias = ''): Route public function _post(string $pattern, $callback, bool $pass_route = false, string $route_alias = ''): Route
{ {
@ -684,8 +689,8 @@ class Engine
* @param string $pattern URL pattern to match * @param string $pattern URL pattern to match
* @param callable|string $callback Callback function or string class->method * @param callable|string $callback Callback function or string class->method
* @param bool $pass_route Pass the matching route object to the callback * @param bool $pass_route Pass the matching route object to the callback
* *
* @return Route * @return Route
*/ */
public function _put(string $pattern, $callback, bool $pass_route = false, string $route_alias = ''): Route public function _put(string $pattern, $callback, bool $pass_route = false, string $route_alias = ''): Route
{ {
@ -698,12 +703,12 @@ class Engine
* @param string $pattern URL pattern to match * @param string $pattern URL pattern to match
* @param callable|string $callback Callback function or string class->method * @param callable|string $callback Callback function or string class->method
* @param bool $pass_route Pass the matching route object to the callback * @param bool $pass_route Pass the matching route object to the callback
* *
* @return Route * @return Route
*/ */
public function _patch(string $pattern, $callback, bool $pass_route = false, string $route_alias = ''): Route public function _patch(string $pattern, $callback, bool $pass_route = false, string $route_alias = ''): Route
{ {
return $this->router()->map('PATCH ' . $pattern, $callback, $pass_route, $route_alias); return $this->router()->map('PATCH ' . $pattern, $callback, $pass_route, $route_alias);
} }
/** /**
@ -712,8 +717,8 @@ class Engine
* @param string $pattern URL pattern to match * @param string $pattern URL pattern to match
* @param callable|string $callback Callback function or string class->method * @param callable|string $callback Callback function or string class->method
* @param bool $pass_route Pass the matching route object to the callback * @param bool $pass_route Pass the matching route object to the callback
* *
* @return Route * @return Route
*/ */
public function _delete(string $pattern, $callback, bool $pass_route = false, string $route_alias = ''): Route public function _delete(string $pattern, $callback, bool $pass_route = false, string $route_alias = ''): Route
{ {

@ -41,10 +41,10 @@ class RouteCommand extends AbstractBaseCommand
{ {
$io = $this->app()->io(); $io = $this->app()->io();
if(isset($this->config['index_root']) === false) { if (isset($this->config['index_root']) === false) {
$io->error('index_root not set in .runway-config.json', true); $io->error('index_root not set in .runway-config.json', true);
return; return;
} }
$io->bold('Routes', true); $io->bold('Routes', true);
@ -69,12 +69,12 @@ class RouteCommand extends AbstractBaseCommand
return preg_match("/^class@anonymous/", end($middleware_class_name)) ? 'Anonymous' : end($middleware_class_name); return preg_match("/^class@anonymous/", end($middleware_class_name)) ? 'Anonymous' : end($middleware_class_name);
}, $route->middleware); }, $route->middleware);
} catch (\TypeError $e) { } catch (\TypeError $e) {
$middlewares[] = 'Bad Middleware'; $middlewares[] = 'Bad Middleware';
} finally { } finally {
if(is_string($route->middleware) === true) { if (is_string($route->middleware) === true) {
$middlewares[] = $route->middleware; $middlewares[] = $route->middleware;
} }
} }
} }
$arrayOfRoutes[] = [ $arrayOfRoutes[] = [

@ -32,7 +32,7 @@ class Router
/** /**
* The current route that is has been found and executed. * The current route that is has been found and executed.
*/ */
protected ?Route $executedRoute = null; public ?Route $executedRoute = null;
/** /**
* Pointer to current route. * Pointer to current route.
@ -42,21 +42,21 @@ class Router
/** /**
* When groups are used, this is mapped against all the routes * When groups are used, this is mapped against all the routes
*/ */
protected string $group_prefix = ''; protected string $groupPrefix = '';
/** /**
* Group Middleware * Group Middleware
* *
* @var array<int,mixed> * @var array<int,mixed>
*/ */
protected array $group_middlewares = []; protected array $groupMiddlewares = [];
/** /**
* Allowed HTTP methods * Allowed HTTP methods
* *
* @var array<int, string> * @var array<int, string>
*/ */
protected array $allowed_methods = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS']; protected array $allowedMethods = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS'];
/** /**
* Gets mapped routes. * Gets mapped routes.
@ -93,7 +93,7 @@ class Router
// Flight::route('', function() {}); // Flight::route('', function() {});
// } // }
// Keep the space so that it can execute the below code normally // Keep the space so that it can execute the below code normally
if ($this->group_prefix !== '') { if ($this->groupPrefix !== '') {
$url = ltrim($pattern); $url = ltrim($pattern);
} else { } else {
$url = trim($pattern); $url = trim($pattern);
@ -113,14 +113,14 @@ class Router
} }
// And this finishes it off. // And this finishes it off.
if ($this->group_prefix !== '') { if ($this->groupPrefix !== '') {
$url = rtrim($this->group_prefix . $url); $url = rtrim($this->groupPrefix . $url);
} }
$route = new Route($url, $callback, $methods, $pass_route, $route_alias); $route = new Route($url, $callback, $methods, $pass_route, $route_alias);
// to handle group middleware // to handle group middleware
foreach ($this->group_middlewares as $gm) { foreach ($this->groupMiddlewares as $gm) {
$route->addMiddleware($gm); $route->addMiddleware($gm);
} }
@ -197,20 +197,20 @@ class Router
/** /**
* Group together a set of routes * Group together a set of routes
* *
* @param string $group_prefix group URL prefix (such as /api/v1) * @param string $groupPrefix group URL prefix (such as /api/v1)
* @param callable $callback The necessary calling that holds the Router class * @param callable $callback The necessary calling that holds the Router class
* @param array<int, callable|object> $group_middlewares * @param array<int, callable|object> $groupMiddlewares
* The middlewares to be applied to the group. Example: `[$middleware1, $middleware2]` * The middlewares to be applied to the group. Example: `[$middleware1, $middleware2]`
*/ */
public function group(string $group_prefix, callable $callback, array $group_middlewares = []): void public function group(string $groupPrefix, callable $callback, array $groupMiddlewares = []): void
{ {
$old_group_prefix = $this->group_prefix; $oldGroupPrefix = $this->groupPrefix;
$old_group_middlewares = $this->group_middlewares; $oldGroupMiddlewares = $this->groupMiddlewares;
$this->group_prefix .= $group_prefix; $this->groupPrefix .= $groupPrefix;
$this->group_middlewares = array_merge($this->group_middlewares, $group_middlewares); $this->groupMiddlewares = array_merge($this->groupMiddlewares, $groupMiddlewares);
$callback($this); $callback($this);
$this->group_prefix = $old_group_prefix; $this->groupPrefix = $oldGroupPrefix;
$this->group_middlewares = $old_group_middlewares; $this->groupMiddlewares = $oldGroupMiddlewares;
} }
/** /**
@ -221,9 +221,14 @@ class Router
public function route(Request $request) public function route(Request $request)
{ {
while ($route = $this->current()) { while ($route = $this->current()) {
if ($route->matchMethod($request->method) && $route->matchUrl($request->url, $this->case_sensitive)) { $urlMatches = $route->matchUrl($request->url, $this->case_sensitive);
$methodMatches = $route->matchMethod($request->method);
if ($urlMatches === true && $methodMatches === true) {
$this->executedRoute = $route; $this->executedRoute = $route;
return $route; return $route;
// capture the route but don't execute it. We'll use this in Engine->start() to throw a 405
} elseif ($urlMatches === true && $methodMatches === false) {
$this->executedRoute = $route;
} }
$this->next(); $this->next();
} }
@ -299,12 +304,20 @@ class Router
return $this->routes[$this->index] ?? false; return $this->routes[$this->index] ?? false;
} }
/**
* Gets the previous route.
*/
public function previous(): void
{
--$this->index;
}
/** /**
* Gets the next route. * Gets the next route.
*/ */
public function next(): void public function next(): void
{ {
$this->index++; ++$this->index;
} }
/** /**
@ -312,6 +325,6 @@ class Router
*/ */
public function reset(): void public function reset(): void
{ {
$this->index = 0; $this->rewind();
} }
} }

@ -899,4 +899,47 @@ class EngineTest extends TestCase
$engine->start(); $engine->start();
} }
public function testRouteFoundButBadMethod() {
$engine = new class extends Engine {
public function getLoader()
{
return $this->loader;
}
};
// doing this so we can overwrite some parts of the response
$engine->getLoader()->register('response', function () {
return new class extends Response {
public function setRealHeader(
string $header_string,
bool $replace = true,
int $response_code = 0
): self {
return $this;
}
};
});
$engine->route('POST /path1/@id', function ($id) {
echo 'OK' . $id;
});
$engine->route('GET /path2/@id', function ($id) {
echo 'OK' . $id;
});
$engine->route('PATCH /path3/@id', function ($id) {
echo 'OK' . $id;
});
$engine->request()->url = '/path1/123';
$engine->request()->method = 'GET';
$engine->start();
$this->expectOutputString('Method Not Allowed');
$this->assertEquals(405, $engine->response()->status());
$this->assertEquals('Method Not Allowed', $engine->response()->getBody());
}
} }

@ -11,8 +11,7 @@ use PHPUnit\Framework\TestCase;
class ControllerCommandTest extends TestCase class ControllerCommandTest extends TestCase
{ {
protected static $in = __DIR__ . '/input.test';
protected static $in = __DIR__ . '/input.test';
protected static $ou = __DIR__ . '/output.test'; protected static $ou = __DIR__ . '/output.test';
public function setUp(): void public function setUp(): void
@ -31,49 +30,48 @@ class ControllerCommandTest extends TestCase
unlink(static::$ou); unlink(static::$ou);
} }
if (file_exists(__DIR__.'/controllers/TestController.php')) { if (file_exists(__DIR__ . '/controllers/TestController.php')) {
unlink(__DIR__.'/controllers/TestController.php'); unlink(__DIR__ . '/controllers/TestController.php');
} }
if (file_exists(__DIR__.'/controllers/')) { if (file_exists(__DIR__ . '/controllers/')) {
rmdir(__DIR__.'/controllers/'); rmdir(__DIR__ . '/controllers/');
} }
} }
protected function newApp(string $name, string $version = '') protected function newApp(string $name, string $version = '')
{ {
$app = new Application($name, $version ?: '0.0.1', fn () => false); $app = new Application($name, $version ?: '0.0.1', fn () => false);
return $app->io(new Interactor(static::$in, static::$ou)); return $app->io(new Interactor(static::$in, static::$ou));
} }
public function testConfigAppRootNotSet() public function testConfigAppRootNotSet()
{ {
$app = $this->newApp('test', '0.0.1'); $app = $this->newApp('test', '0.0.1');
$app->add(new ControllerCommand([])); $app->add(new ControllerCommand([]));
$app->handle(['runway', 'make:controller', 'Test']); $app->handle(['runway', 'make:controller', 'Test']);
$this->assertStringContainsString('app_root not set in .runway-config.json', file_get_contents(static::$ou));
}
public function testControllerAlreadyExists() $this->assertStringContainsString('app_root not set in .runway-config.json', file_get_contents(static::$ou));
{ }
$app = $this->newApp('test', '0.0.1');
mkdir(__DIR__.'/controllers/');
file_put_contents(__DIR__.'/controllers/TestController.php', '<?php class TestController {}');
$app->add(new ControllerCommand(['app_root' => 'tests/commands/']));
$app->handle(['runway', 'make:controller', 'Test']);
$this->assertStringContainsString('TestController already exists.', file_get_contents(static::$ou)); public function testControllerAlreadyExists()
} {
$app = $this->newApp('test', '0.0.1');
mkdir(__DIR__ . '/controllers/');
file_put_contents(__DIR__ . '/controllers/TestController.php', '<?php class TestController {}');
$app->add(new ControllerCommand(['app_root' => 'tests/commands/']));
$app->handle(['runway', 'make:controller', 'Test']);
public function testCreateController() $this->assertStringContainsString('TestController already exists.', file_get_contents(static::$ou));
{ }
$app = $this->newApp('test', '0.0.1');
$app->add(new ControllerCommand(['app_root' => 'tests/commands/']));
$app->handle(['runway', 'make:controller', 'Test']);
$this->assertFileExists(__DIR__.'/controllers/TestController.php'); public function testCreateController()
} {
$app = $this->newApp('test', '0.0.1');
$app->add(new ControllerCommand(['app_root' => 'tests/commands/']));
$app->handle(['runway', 'make:controller', 'Test']);
$this->assertFileExists(__DIR__ . '/controllers/TestController.php');
}
} }

@ -13,15 +13,14 @@ use PHPUnit\Framework\TestCase;
class RouteCommandTest extends TestCase class RouteCommandTest extends TestCase
{ {
protected static $in = __DIR__ . '/input.test';
protected static $in = __DIR__ . '/input.test';
protected static $ou = __DIR__ . '/output.test'; protected static $ou = __DIR__ . '/output.test';
public function setUp(): void public function setUp(): void
{ {
file_put_contents(static::$in, '', LOCK_EX); file_put_contents(static::$in, '', LOCK_EX);
file_put_contents(static::$ou, '', LOCK_EX); file_put_contents(static::$ou, '', LOCK_EX);
$_SERVER = []; $_SERVER = [];
$_REQUEST = []; $_REQUEST = [];
Flight::init(); Flight::init();
Flight::setEngine(new Engine()); Flight::setEngine(new Engine());
@ -37,25 +36,25 @@ class RouteCommandTest extends TestCase
unlink(static::$ou); unlink(static::$ou);
} }
if (file_exists(__DIR__.'/index.php')) { if (file_exists(__DIR__ . '/index.php')) {
unlink(__DIR__.'/index.php'); unlink(__DIR__ . '/index.php');
} }
unset($_REQUEST); unset($_REQUEST);
unset($_SERVER); unset($_SERVER);
Flight::clear(); Flight::clear();
} }
protected function newApp(string $name, string $version = '') protected function newApp(string $name, string $version = '')
{ {
$app = new Application($name, $version ?: '0.0.1', fn () => false); $app = new Application($name, $version ?: '0.0.1', fn () => false);
return $app->io(new Interactor(static::$in, static::$ou)); return $app->io(new Interactor(static::$in, static::$ou));
} }
protected function createIndexFile() protected function createIndexFile()
{ {
$index = <<<PHP $index = <<<PHP
<?php <?php
require __DIR__ . '/../../vendor/autoload.php'; require __DIR__ . '/../../vendor/autoload.php';
@ -70,32 +69,32 @@ Flight::router()->case_sensitive = true;
Flight::start(); Flight::start();
PHP; PHP;
file_put_contents(__DIR__.'/index.php', $index); file_put_contents(__DIR__ . '/index.php', $index);
} }
protected function removeColors(string $str): string protected function removeColors(string $str): string
{ {
return preg_replace('/\e\[[\d;]*m/', '', $str); return preg_replace('/\e\[[\d;]*m/', '', $str);
} }
public function testConfigIndexRootNotSet() public function testConfigIndexRootNotSet()
{ {
$app = $this->newApp('test', '0.0.1'); $app = $this->newApp('test', '0.0.1');
$app->add(new RouteCommand([])); $app->add(new RouteCommand([]));
$app->handle(['runway', 'routes']); $app->handle(['runway', 'routes']);
$this->assertStringContainsString('index_root not set in .runway-config.json', file_get_contents(static::$ou)); $this->assertStringContainsString('index_root not set in .runway-config.json', file_get_contents(static::$ou));
} }
public function testGetRoutes() public function testGetRoutes()
{ {
$app = $this->newApp('test', '0.0.1'); $app = $this->newApp('test', '0.0.1');
$this->createIndexFile(); $this->createIndexFile();
$app->add(new RouteCommand(['index_root' => 'tests/commands/index.php'])); $app->add(new RouteCommand(['index_root' => 'tests/commands/index.php']));
$app->handle(['runway', 'routes']); $app->handle(['runway', 'routes']);
$this->assertStringContainsString('Routes', file_get_contents(static::$ou)); $this->assertStringContainsString('Routes', file_get_contents(static::$ou));
$this->assertStringContainsString('+---------+-----------+-------+----------+----------------+ $this->assertStringContainsString('+---------+-----------+-------+----------+----------------+
| Pattern | Methods | Alias | Streamed | Middleware | | Pattern | Methods | Alias | Streamed | Middleware |
+---------+-----------+-------+----------+----------------+ +---------+-----------+-------+----------+----------------+
| / | GET, HEAD | | No | - | | / | GET, HEAD | | No | - |
@ -104,20 +103,20 @@ PHP;
| /put | PUT | | No | - | | /put | PUT | | No | - |
| /patch | PATCH | | No | Bad Middleware | | /patch | PATCH | | No | Bad Middleware |
+---------+-----------+-------+----------+----------------+', $this->removeColors(file_get_contents(static::$ou))); +---------+-----------+-------+----------+----------------+', $this->removeColors(file_get_contents(static::$ou)));
} }
public function testGetPostRoute() { public function testGetPostRoute()
$app = $this->newApp('test', '0.0.1'); {
$this->createIndexFile(); $app = $this->newApp('test', '0.0.1');
$app->add(new RouteCommand(['index_root' => 'tests/commands/index.php'])); $this->createIndexFile();
$app->handle(['runway', 'routes', '--post']); $app->add(new RouteCommand(['index_root' => 'tests/commands/index.php']));
$app->handle(['runway', 'routes', '--post']);
$this->assertStringContainsString('Routes', file_get_contents(static::$ou)); $this->assertStringContainsString('Routes', file_get_contents(static::$ou));
$this->assertStringContainsString('+---------+---------+-------+----------+------------+ $this->assertStringContainsString('+---------+---------+-------+----------+------------+
| Pattern | Methods | Alias | Streamed | Middleware | | Pattern | Methods | Alias | Streamed | Middleware |
+---------+---------+-------+----------+------------+ +---------+---------+-------+----------+------------+
| /post | POST | | No | Closure | | /post | POST | | No | Closure |
+---------+---------+-------+----------+------------+', $this->removeColors(file_get_contents(static::$ou))); +---------+---------+-------+----------+------------+', $this->removeColors(file_get_contents(static::$ou)));
} }
} }

Loading…
Cancel
Save