Skip to content

Commit cba6eb4

Browse files
committed
feat: implement route group names + resource autonaming
1 parent a68c113 commit cba6eb4

2 files changed

Lines changed: 100 additions & 12 deletions

File tree

src/Router.php

Lines changed: 49 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,11 @@ class Router
9292
*/
9393
protected static $groupRoute = '';
9494

95+
/**
96+
* Current group name prefix for named routes (e.g. "admin.")
97+
*/
98+
protected static $groupName = '';
99+
95100
/**
96101
* Default controller namespace
97102
*/
@@ -158,6 +163,7 @@ public static function mount(string $path, $handler)
158163

159164
$initialNamespace = static::$namespace;
160165
$initialGroupRoute = static::$groupRoute;
166+
$initialGroupName = static::$groupName;
161167
$initialLingoOptions = static::$lingoOptions;
162168
$initialSitemapOptions = static::$sitemapOptions;
163169
$initialGroupMiddleware = static::$routeGroupMiddleware;
@@ -172,6 +178,12 @@ public static function mount(string $path, $handler)
172178
static::$routeGroupMiddleware = $groupOptions['middleware'];
173179
}
174180

181+
if (!empty($groupOptions['name'])) {
182+
// group names cascade: group "admin" + route "users.index"
183+
// registers as "admin.users.index" — nested groups compose
184+
static::$groupName .= trim($groupOptions['name'], '.') . '.';
185+
}
186+
175187
if (isset($groupOptions['lingo.routes'])) {
176188
static::$lingoOptions['lingo.routes'] = $groupOptions['lingo.routes'];
177189
}
@@ -184,6 +196,7 @@ public static function mount(string $path, $handler)
184196

185197
static::$namespace = $initialNamespace;
186198
static::$groupRoute = $initialGroupRoute;
199+
static::$groupName = $initialGroupName;
187200
static::$lingoOptions = $initialLingoOptions;
188201
static::$sitemapOptions = $initialSitemapOptions;
189202
static::$routeGroupMiddleware = $initialGroupMiddleware;
@@ -219,6 +232,10 @@ public static function match(string $allowedMethods, string $pattern, $handler)
219232

220233
list($handler, $routeOptions) = static::mapHandler($handler);
221234

235+
if (!empty($routeOptions['name']) && static::$groupName !== '') {
236+
$routeOptions['name'] = static::$groupName . $routeOptions['name'];
237+
}
238+
222239
if (is_string($handler)) {
223240
$namespace = static::$namespace;
224241

@@ -443,6 +460,21 @@ public static function inertia(string $pattern, string $view, $data = [])
443460
* @param string $pattern The base route to use eg: /post
444461
* @param array|string $controller to handle route eg: PostController
445462
*/
463+
/**
464+
* Derive the dot-name base for a resource: '/admin/users' -> 'admin.users',
465+
* '/' inside a group -> the group's last segment
466+
*/
467+
protected static function resourceName(string $pattern): string
468+
{
469+
$base = trim($pattern, '/');
470+
471+
if ($base === '') {
472+
$base = trim(basename(static::$groupRoute), '/');
473+
}
474+
475+
return str_replace('/', '.', $base);
476+
}
477+
446478
public static function resource(string $pattern, $controller)
447479
{
448480
if (is_array($controller)) {
@@ -455,13 +487,15 @@ public static function resource(string $pattern, $controller)
455487
return static::group($pattern, $controller);
456488
}
457489

458-
static::match('GET|HEAD', $pattern, "$controller@index");
459-
static::post($pattern, "$controller@store");
460-
static::match('GET|HEAD', "$pattern/create", "$controller@create");
461-
static::match('DELETE', "$pattern/{id}", "$controller@destroy");
462-
static::match('PUT|PATCH', "$pattern/{id}", "$controller@update");
463-
static::match('GET|HEAD', "$pattern/{id}/edit", "$controller@edit");
464-
static::match('GET|HEAD', "$pattern/{id}", "$controller@show");
490+
$name = static::resourceName($pattern);
491+
492+
static::match('GET|HEAD', $pattern, ['name' => "$name.index", "$controller@index"]);
493+
static::post($pattern, ['name' => "$name.store", "$controller@store"]);
494+
static::match('GET|HEAD', "$pattern/create", ['name' => "$name.create", "$controller@create"]);
495+
static::match('DELETE', "$pattern/{id}", ['name' => "$name.destroy", "$controller@destroy"]);
496+
static::match('PUT|PATCH', "$pattern/{id}", ['name' => "$name.update", "$controller@update"]);
497+
static::match('GET|HEAD', "$pattern/{id}/edit", ['name' => "$name.edit", "$controller@edit"]);
498+
static::match('GET|HEAD', "$pattern/{id}", ['name' => "$name.show", "$controller@show"]);
465499

466500
// still keeping DELETE and PUT|PATCH so earlier versions of leaf apps don't break
467501
static::match('POST|DELETE', "$pattern/{id}/delete", "$controller@destroy");
@@ -494,11 +528,13 @@ public static function apiResource(string $pattern, $controller)
494528
return static::group($pattern, $controller);
495529
}
496530

497-
static::match('GET|HEAD', $pattern, "$controller@index");
498-
static::post($pattern, "$controller@store");
499-
static::match('GET|HEAD', "$pattern/{id}", "$controller@show");
500-
static::match('DELETE', "$pattern/{id}", "$controller@destroy");
501-
static::match('PUT|PATCH', "$pattern/{id}", "$controller@update");
531+
$name = static::resourceName($pattern);
532+
533+
static::match('GET|HEAD', $pattern, ['name' => "$name.index", "$controller@index"]);
534+
static::post($pattern, ['name' => "$name.store", "$controller@store"]);
535+
static::match('GET|HEAD', "$pattern/{id}", ['name' => "$name.show", "$controller@show"]);
536+
static::match('DELETE', "$pattern/{id}", ['name' => "$name.destroy", "$controller@destroy"]);
537+
static::match('PUT|PATCH', "$pattern/{id}", ['name' => "$name.update", "$controller@update"]);
502538

503539
// still keeping DELETE and PUT|PATCH so earlier versions of leaf apps don't break
504540
static::match('POST|DELETE', "$pattern/{id}/delete", "$controller@destroy");
@@ -1110,6 +1146,7 @@ public static function reset(): void
11101146
];
11111147
static::$sitemapOptions = [];
11121148
static::$groupRoute = '';
1149+
static::$groupName = '';
11131150
static::$namespace = '';
11141151
static::$serverBasePath = null;
11151152
static::$currentUri = null;

tests/router.test.php

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -268,3 +268,54 @@
268268
expect($fired)->toBeFalse()
269269
->and($uri)->toBe('/outside'); // path untouched when base doesn't prefix it
270270
});
271+
272+
test('group names cascade into route names', function () {
273+
// https://github.com/leafsphp/leaf/issues/279
274+
\Leaf\Router::reset();
275+
276+
app()->group('/admin', ['name' => 'admin', function () {
277+
app()->get('/dashboard', ['name' => 'dashboard', function () {}]);
278+
279+
app()->group('/reports', ['name' => 'reports', function () {
280+
app()->get('/{id}', ['name' => 'show', function () {}]);
281+
}]);
282+
}]);
283+
284+
expect(\Leaf\Router::route('admin.dashboard'))->toBe('/admin/dashboard')
285+
->and(\Leaf\Router::route('admin.reports.show', ['id' => 9]))->toBe('/admin/reports/9');
286+
});
287+
288+
test('resource routes name themselves', function () {
289+
\Leaf\Router::reset();
290+
291+
app()->resource('/users', 'UsersController');
292+
293+
expect(\Leaf\Router::route('users.index'))->toBe('/users')
294+
->and(\Leaf\Router::route('users.show', ['id' => 3]))->toBe('/users/3')
295+
->and(\Leaf\Router::route('users.edit', ['id' => 3]))->toBe('/users/3/edit');
296+
});
297+
298+
test('resource names compose with group names', function () {
299+
\Leaf\Router::reset();
300+
301+
app()->group('/admin', ['name' => 'admin', function () {
302+
app()->apiResource('/users', 'UsersController');
303+
}]);
304+
305+
expect(\Leaf\Router::route('admin.users.index'))->toBe('/admin/users')
306+
->and(\Leaf\Router::route('admin.users.update', ['id' => 5]))->toBe('/admin/users/5');
307+
});
308+
309+
test('unnamed routes in named groups stay unnamed', function () {
310+
\Leaf\Router::reset();
311+
312+
app()->group('/named', ['name' => 'named', function () {
313+
app()->get('/plain', function () {});
314+
}]);
315+
316+
$ref = new ReflectionClass(\Leaf\Router::class);
317+
$prop = $ref->getProperty('namedRoutes');
318+
$prop->setAccessible(true);
319+
320+
expect($prop->getValue())->toBe([]);
321+
});

0 commit comments

Comments
 (0)