Skip to content
33 changes: 33 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,39 @@ We have left no HTTP verb behind. You can use these attributes on controller met
#[Spatie\RouteAttributes\Attributes\Options('my-uri')]
```

### Resource controllers

To register a [resource controller](https://laravel.com/docs/controllers#resource-controllers), use the `Resource` attribute as shown in the example below.

You can use `only` or `except` parameters to manage your resource routes availability.

Using `Resource` attribute with `Domain`, `Prefix` and `Middleware` attributes works as well.

```php
use Spatie\RouteAttributes\Attributes\Resource;

#[Prefix('api/v1')]
#[Resource('posts', except: ['create', 'edit', 'destroy'])]
class PostController
{
public function index()
{
}

public function store(Request $request)
{
}

public function show($id)
{
}

public function update(Request $request, $id)
{
}
}
```

### Using multiple verbs

To register a route for all verbs, you can use the `Any` attribute:
Expand Down
16 changes: 16 additions & 0 deletions src/Attributes/Resource.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?php

namespace Spatie\RouteAttributes\Attributes;

use Attribute;

#[Attribute(Attribute::TARGET_CLASS)]
class Resource implements RouteAttribute
{
public function __construct(
public string $resource,
public array|string|null $except = null,
public array|string|null $only = null,
) {
}
}
41 changes: 41 additions & 0 deletions src/ClassRouteAttributes.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
use Spatie\RouteAttributes\Attributes\Domain;
use Spatie\RouteAttributes\Attributes\Middleware;
use Spatie\RouteAttributes\Attributes\Prefix;
use Spatie\RouteAttributes\Attributes\Resource;
use Spatie\RouteAttributes\Attributes\RouteAttribute;
use Spatie\RouteAttributes\Attributes\Where;

Expand Down Expand Up @@ -44,6 +45,46 @@ public function domain(): ?string
return $attribute->domain;
}

/**
* @psalm-suppress NoInterfaceProperties
*/
public function resource(): ?string
{
/** @var \Spatie\RouteAttributes\Attributes\Resource $attribute */
if (! $attribute = $this->getAttribute(Resource::class)) {
return null;
}

return $attribute->resource;
}

/**
* @psalm-suppress NoInterfaceProperties
*/
public function except(): string|array|null
{
/** @var \Spatie\RouteAttributes\Attributes\Resource $attribute */
if (! $attribute = $this->getAttribute(Resource::class)) {
return null;
}

return $attribute->except;
}


/**
* @psalm-suppress NoInterfaceProperties
*/
public function only(): string|array|null
{
/** @var \Spatie\RouteAttributes\Attributes\Resource $attribute */
if (! $attribute = $this->getAttribute(Resource::class)) {
return null;
}

return $attribute->only;
}

/**
* @psalm-suppress NoInterfaceProperties
*/
Expand Down
26 changes: 26 additions & 0 deletions src/RouteRegistrar.php
Original file line number Diff line number Diff line change
Expand Up @@ -106,11 +106,37 @@ protected function processAttributes(string $className): void

$classRouteAttributes = new ClassRouteAttributes($class);

if ($classRouteAttributes->resource()) {
$this->registerResource($class, $classRouteAttributes);
}

($prefix = $classRouteAttributes->prefix())
? $this->router->prefix($prefix)->group(fn () => $this->registerRoutes($class, $classRouteAttributes))
: $this->registerRoutes($class, $classRouteAttributes);
}

protected function registerResource(ReflectionClass $class, ClassRouteAttributes $classRouteAttributes): void
{
$this->router->group([
'domain' => $classRouteAttributes->domain(),
'prefix' => $classRouteAttributes->prefix(),
], function () use ($class, $classRouteAttributes) {
$route = $this->router->resource($classRouteAttributes->resource(), $class->getName());

if ($only = $classRouteAttributes->only()) {
$route->only($only);
}

if ($except = $classRouteAttributes->except()) {
$route->except($except);
}

if ($middleware = $classRouteAttributes->middleware()) {
$route->middleware([...$this->middleware, ...$middleware]);
}
});
}

protected function registerRoutes(ReflectionClass $class, ClassRouteAttributes $classRouteAttributes): void
{
foreach ($class->getMethods() as $method) {
Expand Down
205 changes: 205 additions & 0 deletions tests/AttributeTests/ResourceAttributeTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
<?php

namespace Spatie\RouteAttributes\Tests\AttributeTests;

use Spatie\RouteAttributes\Tests\TestCase;
use Spatie\RouteAttributes\Tests\TestClasses\Controllers\Resource\ResourceTestDomainController;
use Spatie\RouteAttributes\Tests\TestClasses\Controllers\Resource\ResourceTestExceptController;
use Spatie\RouteAttributes\Tests\TestClasses\Controllers\Resource\ResourceTestFullController;
use Spatie\RouteAttributes\Tests\TestClasses\Controllers\Resource\ResourceTestMiddlewareController;
use Spatie\RouteAttributes\Tests\TestClasses\Controllers\Resource\ResourceTestOnlyController;
use Spatie\RouteAttributes\Tests\TestClasses\Controllers\Resource\ResourceTestPrefixController;
use Spatie\RouteAttributes\Tests\TestClasses\Middleware\OtherTestMiddleware;
use Spatie\RouteAttributes\Tests\TestClasses\Middleware\TestMiddleware;

class ResourceAttributeTest extends TestCase
{
/** @test */
public function it_can_register_resource_with_prefix()
{
$this->routeRegistrar->registerClass(ResourceTestPrefixController::class);

$this
->assertRegisteredRoutesCount(2)
->assertRouteRegistered(
ResourceTestPrefixController::class,
controllerMethod: 'index',
uri: 'api/v1/my-prefix/etc/posts',
name: 'posts.index'
)
->assertRouteRegistered(
ResourceTestPrefixController::class,
controllerMethod: 'show',
uri: 'api/v1/my-prefix/etc/posts/{post}',
name: 'posts.show'
);
}

/** @test */
public function it_can_register_resource_with_middleware()
{
$this->routeRegistrar->registerClass(ResourceTestMiddlewareController::class);

$this
->assertRegisteredRoutesCount(2)
->assertRouteRegistered(
ResourceTestMiddlewareController::class,
controllerMethod: 'index',
uri: 'posts',
middleware: [TestMiddleware::class, OtherTestMiddleware::class],
name: 'posts.index',
)
->assertRouteRegistered(
ResourceTestMiddlewareController::class,
controllerMethod: 'show',
uri: 'posts/{post}',
middleware: [TestMiddleware::class, OtherTestMiddleware::class],
name: 'posts.show',
);
}

/** @test */
public function it_can_register_resource_with_domain()
{
$this->routeRegistrar->registerClass(ResourceTestDomainController::class);

$this
->assertRegisteredRoutesCount(2)
->assertRouteRegistered(
ResourceTestDomainController::class,
controllerMethod: 'index',
uri: 'posts',
name: 'posts.index',
domain: 'my-subdomain.localhost'
)
->assertRouteRegistered(
ResourceTestDomainController::class,
controllerMethod: 'show',
uri: 'posts/{post}',
name: 'posts.show',
domain: 'my-subdomain.localhost'
);
}

/** @test */
public function it_can_register_resource_with_all_methods()
{
$this->routeRegistrar->registerClass(ResourceTestFullController::class);

$this
->assertRegisteredRoutesCount(7)
->assertRouteRegistered(
ResourceTestFullController::class,
controllerMethod: 'index',
uri: 'posts',
name: 'posts.index'
)
->assertRouteRegistered(
ResourceTestFullController::class,
controllerMethod: 'create',
uri: 'posts/create',
name: 'posts.create'
)
->assertRouteRegistered(
ResourceTestFullController::class,
controllerMethod: 'store',
httpMethods: 'post',
uri: 'posts',
name: 'posts.store'
)
->assertRouteRegistered(
ResourceTestFullController::class,
controllerMethod: 'show',
uri: 'posts/{post}',
name: 'posts.show'
)
->assertRouteRegistered(
ResourceTestFullController::class,
controllerMethod: 'edit',
uri: 'posts/{post}/edit',
name: 'posts.edit'
)
->assertRouteRegistered(
ResourceTestFullController::class,
controllerMethod: 'update',
httpMethods: 'put',
uri: 'posts/{post}',
name: 'posts.update'
)
->assertRouteRegistered(
ResourceTestFullController::class,
controllerMethod: 'destroy',
httpMethods: 'delete',
uri: 'posts/{post}',
name: 'posts.destroy'
);
}

/** @test */
public function it_can_register_resource_with_only_few_methods()
{
$this->routeRegistrar->registerClass(ResourceTestOnlyController::class);

$this
->assertRegisteredRoutesCount(3)
->assertRouteRegistered(
ResourceTestOnlyController::class,
controllerMethod: 'index',
uri: 'posts',
name: 'posts.index'
)
->assertRouteRegistered(
ResourceTestOnlyController::class,
controllerMethod: 'store',
httpMethods: 'post',
uri: 'posts',
name: 'posts.store'
)
->assertRouteRegistered(
ResourceTestOnlyController::class,
controllerMethod: 'show',
uri: 'posts/{post}',
name: 'posts.show'
);
}

/** @test */
public function it_can_register_resource_without_few_methods()
{
$this->routeRegistrar->registerClass(ResourceTestExceptController::class);

$this
->assertRegisteredRoutesCount(5)
->assertRouteRegistered(
ResourceTestExceptController::class,
controllerMethod: 'index',
uri: 'posts',
name: 'posts.index'
)
->assertRouteRegistered(
ResourceTestExceptController::class,
controllerMethod: 'create',
uri: 'posts/create',
name: 'posts.create'
)
->assertRouteRegistered(
ResourceTestExceptController::class,
controllerMethod: 'store',
httpMethods: 'post',
uri: 'posts',
name: 'posts.store'
)
->assertRouteRegistered(
ResourceTestExceptController::class,
controllerMethod: 'show',
uri: 'posts/{post}',
name: 'posts.show'
)
->assertRouteRegistered(
ResourceTestExceptController::class,
controllerMethod: 'edit',
uri: 'posts/{post}/edit',
name: 'posts.edit'
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?php

namespace Spatie\RouteAttributes\Tests\TestClasses\Controllers\Resource;

use Spatie\RouteAttributes\Attributes\Domain;
use Spatie\RouteAttributes\Attributes\Resource;

#[Domain('my-subdomain.localhost')]
#[Resource('posts', only: ['index', 'show'])]
class ResourceTestDomainController
{
public function index()
{
}

public function show($id)
{
}
}
Loading