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

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

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

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

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

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

Loading…
Cancel
Save