Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Group compact syntax #596

Merged
merged 7 commits into from
Oct 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 18 additions & 3 deletions flight/Engine.php
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@
* Routes a PATCH URL to a callback function.
* @method Route delete(string $pattern, callable|string $callback, bool $pass_route = false, string $alias = '')
* Routes a DELETE URL to a callback function.
* @method void resource(string $pattern, string $controllerClass, array $methods = [])
* Adds standardized RESTful routes for a controller.
* @method Router router() Gets router
* @method string getUrl(string $alias) Gets a url from an alias
*
Expand Down Expand Up @@ -77,7 +79,7 @@ class Engine
private const MAPPABLE_METHODS = [
'start', 'stop', 'route', 'halt', 'error', 'notFound',
'render', 'redirect', 'etag', 'lastModified', 'json', 'jsonHalt', 'jsonp',
'post', 'put', 'patch', 'delete', 'group', 'getUrl', 'download'
'post', 'put', 'patch', 'delete', 'group', 'getUrl', 'download', 'resource'
];

/** @var array<string, mixed> Stored variables. */
Expand Down Expand Up @@ -598,8 +600,7 @@ public function _start(): void
*/
public function _error(Throwable $e): void
{
$msg = sprintf(
<<<HTML
$msg = sprintf(<<<HTML
<h1>500 Internal Server Error</h1>
<h3>%s (%s)</h3>
<pre>%s</pre>
Expand Down Expand Up @@ -729,6 +730,20 @@ public function _delete(string $pattern, $callback, bool $pass_route = false, st
return $this->router()->map('DELETE ' . $pattern, $callback, $pass_route, $route_alias);
}

/**
* Create a resource controller customizing the methods names mapping.
*
* @param class-string $controllerClass
* @param array<string, string|array<string>> $options
*/
public function _resource(
string $pattern,
string $controllerClass,
array $options = []
): void {
$this->router()->mapResource($pattern, $controllerClass, $options);
}

/**
* Stops processing and returns a given response.
*
Expand Down
2 changes: 2 additions & 0 deletions flight/Flight.php
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@
* Routes a PATCH URL to a callback function.
* @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 void resource(string $pattern, string $controllerClass, array $methods = [])
* Adds standardized RESTful routes for a controller.
* @method static Router router() Returns Router instance.
* @method static string getUrl(string $alias, array<string, mixed> $params = []) Gets a url from an alias
*
Expand Down
65 changes: 64 additions & 1 deletion flight/net/Router.php
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ public function clear(): void
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
// This means that the route is defined in a group, but the defined route is the base
// url path. Note the '' in route()
// Ex: Flight::group('/api', function() {
// Flight::route('', function() {});
Expand Down Expand Up @@ -276,6 +276,69 @@ public function getUrlByAlias(string $alias, array $params = []): string
throw new Exception($exception_message);
}

/**
* Create a resource controller customizing the methods names mapping.
*
* @param class-string $controllerClass
* @param array<string, string|array<string>> $options
*/
public function mapResource(
string $pattern,
string $controllerClass,
array $options = []
): void {

$defaultMapping = [
'index' => 'GET ',
'create' => 'GET /create',
'store' => 'POST ',
'show' => 'GET /@id',
'edit' => 'GET /@id/edit',
'update' => 'PUT /@id',
'destroy' => 'DELETE /@id'
];

// Create a custom alias base
$aliasBase = trim(basename($pattern), '/');
if (isset($options['alias_base']) === true) {
$aliasBase = $options['alias_base'];
}

// Only use these controller methods
if (isset($options['only']) === true) {
$only = $options['only'];
$defaultMapping = array_filter($defaultMapping, function ($key) use ($only) {
return in_array($key, $only, true) === true;
}, ARRAY_FILTER_USE_KEY);

// Exclude these controller methods
} elseif (isset($options['except']) === true) {
$except = $options['except'];
$defaultMapping = array_filter($defaultMapping, function ($key) use ($except) {
return in_array($key, $except, true) === false;
}, ARRAY_FILTER_USE_KEY);
}

// Add group middleware
$middleware = [];
if (isset($options['middleware']) === true) {
$middleware = $options['middleware'];
}

$this->group(
$pattern,
function (Router $router) use ($controllerClass, $defaultMapping, $aliasBase): void {
foreach ($defaultMapping as $controllerMethod => $methodPattern) {
$router->map(
$methodPattern,
[ $controllerClass, $controllerMethod ]
)->setAlias($aliasBase . '.' . $controllerMethod);
}
},
$middleware
);
}

/**
* Rewinds the current route index.
*/
Expand Down
142 changes: 142 additions & 0 deletions tests/groupcompactsyntax/FlightRouteCompactSyntaxTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
<?php

declare(strict_types=1);

use PHPUnit\Framework\TestCase;
use tests\groupcompactsyntax\PostsController;
use tests\groupcompactsyntax\TodosController;
use tests\groupcompactsyntax\UsersController;

require_once __DIR__ . '/UsersController.php';
require_once __DIR__ . '/PostsController.php';

final class FlightRouteCompactSyntaxTest extends TestCase
{
public function setUp(): void
{
Flight::router()->clear();
}

public function testCanMapMethodsWithVerboseSyntax(): void
{
Flight::route('GET /users', [UsersController::class, 'index']);
Flight::route('DELETE /users/@id', [UsersController::class, 'destroy']);

$routes = Flight::router()->getRoutes();

$this->assertCount(2, $routes);

$this->assertSame('/users', $routes[0]->pattern);
$this->assertSame([UsersController::class, 'index'], $routes[0]->callback);
$this->assertSame('GET', $routes[0]->methods[0]);

$this->assertSame('/users/@id', $routes[1]->pattern);
$this->assertSame([UsersController::class, 'destroy'], $routes[1]->callback);
$this->assertSame('DELETE', $routes[1]->methods[0]);
}

public function testOptionsOnly(): void
{
Flight::resource('/users', UsersController::class, [
'only' => [ 'index', 'destroy' ]
]);

$routes = Flight::router()->getRoutes();

$this->assertCount(2, $routes);

$this->assertSame('/users', $routes[0]->pattern);
$this->assertSame('GET', $routes[0]->methods[0]);
$this->assertSame([UsersController::class, 'index'], $routes[0]->callback);

$this->assertSame('/users/@id', $routes[1]->pattern);
$this->assertSame('DELETE', $routes[1]->methods[0]);
$this->assertSame([UsersController::class, 'destroy'], $routes[1]->callback);
}

public function testDefaultMethods(): void
{
Flight::resource('/posts', PostsController::class);

$routes = Flight::router()->getRoutes();
$this->assertCount(7, $routes);

$this->assertSame('/posts', $routes[0]->pattern);
$this->assertSame('GET', $routes[0]->methods[0]);
$this->assertSame([PostsController::class, 'index'], $routes[0]->callback);
$this->assertSame('posts.index', $routes[0]->alias);

$this->assertSame('/posts/create', $routes[1]->pattern);
$this->assertSame('GET', $routes[1]->methods[0]);
$this->assertSame([PostsController::class, 'create'], $routes[1]->callback);
$this->assertSame('posts.create', $routes[1]->alias);

$this->assertSame('/posts', $routes[2]->pattern);
$this->assertSame('POST', $routes[2]->methods[0]);
$this->assertSame([PostsController::class, 'store'], $routes[2]->callback);
$this->assertSame('posts.store', $routes[2]->alias);

$this->assertSame('/posts/@id', $routes[3]->pattern);
$this->assertSame('GET', $routes[3]->methods[0]);
$this->assertSame([PostsController::class, 'show'], $routes[3]->callback);
$this->assertSame('posts.show', $routes[3]->alias);

$this->assertSame('/posts/@id/edit', $routes[4]->pattern);
$this->assertSame('GET', $routes[4]->methods[0]);
$this->assertSame([PostsController::class, 'edit'], $routes[4]->callback);
$this->assertSame('posts.edit', $routes[4]->alias);

$this->assertSame('/posts/@id', $routes[5]->pattern);
$this->assertSame('PUT', $routes[5]->methods[0]);
$this->assertSame([PostsController::class, 'update'], $routes[5]->callback);
$this->assertSame('posts.update', $routes[5]->alias);

$this->assertSame('/posts/@id', $routes[6]->pattern);
$this->assertSame('DELETE', $routes[6]->methods[0]);
$this->assertSame([PostsController::class, 'destroy'], $routes[6]->callback);
$this->assertSame('posts.destroy', $routes[6]->alias);
}

public function testOptionsExcept(): void
{
Flight::resource('/todos', TodosController::class, [
'except' => [ 'create', 'store', 'update', 'destroy', 'edit' ]
]);

$routes = Flight::router()->getRoutes();

$this->assertCount(2, $routes);

$this->assertSame('/todos', $routes[0]->pattern);
$this->assertSame('GET', $routes[0]->methods[0]);
$this->assertSame([TodosController::class, 'index'], $routes[0]->callback);

$this->assertSame('/todos/@id', $routes[1]->pattern);
$this->assertSame('GET', $routes[1]->methods[0]);
$this->assertSame([TodosController::class, 'show'], $routes[1]->callback);
}

public function testOptionsMiddlewareAndAliasBase(): void
{
Flight::resource('/todos', TodosController::class, [
'middleware' => [ 'auth' ],
'alias_base' => 'nothanks'
]);

$routes = Flight::router()->getRoutes();

$this->assertCount(7, $routes);

$this->assertSame('/todos', $routes[0]->pattern);
$this->assertSame('GET', $routes[0]->methods[0]);
$this->assertSame([TodosController::class, 'index'], $routes[0]->callback);
$this->assertSame('auth', $routes[0]->middleware[0]);
$this->assertSame('nothanks.index', $routes[0]->alias);

$this->assertSame('/todos/create', $routes[1]->pattern);
$this->assertSame('GET', $routes[1]->methods[0]);
$this->assertSame([TodosController::class, 'create'], $routes[1]->callback);
$this->assertSame('auth', $routes[1]->middleware[0]);
$this->assertSame('nothanks.create', $routes[1]->alias);
}
}
36 changes: 36 additions & 0 deletions tests/groupcompactsyntax/PostsController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<?php

declare(strict_types=1);

namespace tests\groupcompactsyntax;

final class PostsController
{
public function index(): void
{
}

public function show(string $id): void
{
}

public function create(): void
{
}

public function store(): void
{
}

public function edit(string $id): void
{
}

public function update(string $id): void
{
}

public function destroy(string $id): void
{
}
}
16 changes: 16 additions & 0 deletions tests/groupcompactsyntax/TodosController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?php

declare(strict_types=1);

namespace tests\groupcompactsyntax;

final class TodosController
{
public function index(): void
{
}

public function show(string $id): void
{
}
}
18 changes: 18 additions & 0 deletions tests/groupcompactsyntax/UsersController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?php

declare(strict_types=1);

namespace tests\groupcompactsyntax;

final class UsersController
{
public function index(): void
{
echo __METHOD__;
}

public function destroy(): void
{
echo __METHOD__;
}
}