Skip to content

Commit

Permalink
Merge pull request #139 from mikeforks/main
Browse files Browse the repository at this point in the history
Add support for `withoutScopedBindings` to `ScopeBindings` attribute.
  • Loading branch information
freekmurze authored Mar 24, 2024
2 parents 9ef67de + e49120b commit bf57239
Show file tree
Hide file tree
Showing 8 changed files with 111 additions and 31 deletions.
37 changes: 25 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ return [
app_path('Http/Controllers/Web') => [
'middleware' => ['web']
],

app_path('Http/Controllers/Api') => [
'prefix' => 'api',
'middleware' => 'api'
Expand All @@ -93,10 +93,10 @@ For controllers outside the applications root namespace directories can also be
],
```

If you are using a directory structure where you co-locate multiple types of files in the same directory and want to
be more specific about which files are checked for route attributes, you can use the `patterns` and `not_patterns`
options. For example, if you are co-locating your tests with your controllers you could use the `patterns` option to only
look in controller files, or you could use `not_patterns` to configure it to not look in test files for route
If you are using a directory structure where you co-locate multiple types of files in the same directory and want to
be more specific about which files are checked for route attributes, you can use the `patterns` and `not_patterns`
options. For example, if you are co-locating your tests with your controllers you could use the `patterns` option to only
look in controller files, or you could use `not_patterns` to configure it to not look in test files for route
attributes.

```php
Expand Down Expand Up @@ -168,16 +168,16 @@ use Spatie\RouteAttributes\Attributes\Resource;

#[Prefix('api/v1')]
#[Resource(
resource: 'photos.comments',
resource: 'photos.comments',
apiResource: true,
shallow: true,
shallow: true,
parameters: ['comments' => 'comment:uuid'],
names: 'api.v1.photoComments',
except: ['destroy'],
)]
// OR #[ApiResource(resource: 'photos.comments', shallow: true, ...)]
class PhotoCommentController
{
{
public function index(Photo $photo)
{
}
Expand Down Expand Up @@ -382,15 +382,17 @@ class MyController
}
}
```
When this is parsed, it will get the value of `domains.main` from the config file and
When this is parsed, it will get the value of `domains.main` from the config file and
register the route as follows;

```php
Route::get('my-get-route', [MyController::class, 'myGetMethod'])->domain('example.com');
```

### Scoping bindings
When implicitly binding multiple Eloquent models in a single route definition, you may wish to scope the second Eloquent model such that it must be a child of the previous Eloquent model.

When implicitly binding multiple Eloquent models in a single route definition, you may wish to scope the second Eloquent model such that it must be a child of the previous Eloquent model.

By adding the `ScopeBindings` annotation, you can enable this behaviour:

````php
Expand All @@ -409,13 +411,24 @@ class MyController
````

This is akin to using the `->scopeBindings()` method on the route registrar manually:

```php
Route::get('/users/{user}/posts/{post}', function (User $user, Post $post) {
return $post;
})->scopeBindings();
```

You can also use the annotation on controllers to enable implicitly scoped bindings for all its methods.
By default, Laravel will enabled scoped bindings on a route when using a custom keyed implicit binding as a nested route parameter, such as `/users/{user}/posts/{post:slug}`.

To disable this behaviour, you can pass `false` to the attribute:

```php
#[ScopeBindings(false)]
```

This is the equivalent of calling `->withoutScopedBindings()` on the route registrar manually.

You can also use the annotation on controllers to enable implicitly scoped bindings for all its methods. For any methods where you want to override this, you can pass `false` to the attribute on those methods, just like you would normally.

### Specifying where

Expand Down Expand Up @@ -453,7 +466,7 @@ Route::post('my-post-route/{my-where}/{my-alpha-numeric}', [MyController::class,

For convenience, some commonly used regular expression patterns have helper attributes that allow you to quickly add pattern constraints to your routes.

```php
```php
#[WhereAlpha('alpha')]
#[WhereAlphaNumeric('alpha-numeric')]
#[WhereIn('in', ['value1', 'value2'])]
Expand Down
4 changes: 2 additions & 2 deletions src/ClassRouteAttributes.php
Original file line number Diff line number Diff line change
Expand Up @@ -193,11 +193,11 @@ public function middleware(): array
return $attribute->middleware;
}

public function scopeBindings(): bool
public function scopeBindings(): ?bool
{
/** @var ScopeBindings $attribute */
if (! $attribute = $this->getAttribute(ScopeBindings::class)) {
return false;
return null;
}

return $attribute->scopeBindings;
Expand Down
18 changes: 9 additions & 9 deletions src/RouteRegistrar.php
Original file line number Diff line number Diff line change
Expand Up @@ -187,15 +187,15 @@ protected function registerRoutes(ReflectionClass $class, ClassRouteAttributes $
*/
public function setScopeBindingsIfAvailable(?ReflectionAttribute $scopeBindingsAttribute, \Illuminate\Routing\Route $route, ClassRouteAttributes $classRouteAttributes): void
{
if ($scopeBindingsAttribute) {
$scopeBindingsAttributeClass = $scopeBindingsAttribute->newInstance();

if ($scopeBindingsAttributeClass->scopeBindings) {
$route->scopeBindings();
}
} elseif ($classRouteAttributes->scopeBindings()) {
$route->scopeBindings();
}
$scopeBindings = $scopeBindingsAttribute
? $scopeBindingsAttribute->newInstance()->scopeBindings
: $classRouteAttributes->scopeBindings();

match ($scopeBindings) {
true => $route->scopeBindings(),
false => $route->withoutScopedBindings(),
null => null
};
}

/**
Expand Down
47 changes: 41 additions & 6 deletions tests/AttributeTests/ScopeBindingsAttributeTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
use Spatie\RouteAttributes\Tests\TestCase;
use Spatie\RouteAttributes\Tests\TestClasses\Controllers\BindingScoping1TestController;
use Spatie\RouteAttributes\Tests\TestClasses\Controllers\BindingScoping2TestController;
use Spatie\RouteAttributes\Tests\TestClasses\Controllers\BindingScoping3TestController;

class ScopeBindingsAttributeTest extends TestCase
{
Expand All @@ -14,23 +15,32 @@ public function it_can_enable_binding_scoping_on_each_method_of_a_controller()
$this->routeRegistrar->registerClass(BindingScoping2TestController::class);

$this
->assertRegisteredRoutesCount(2)
->assertRegisteredRoutesCount(3)
->assertRouteRegistered(
BindingScoping2TestController::class,
controllerMethod: 'explicitlyEnabledScopedBinding',
uri: 'explicitly-enabled/{scoped}/{binding}',
enforcesScopedBindings: true
enforcesScopedBindings: true,
preventsScopedBindings: false
)
->assertRouteRegistered(
BindingScoping2TestController::class,
controllerMethod: 'explicitlyDisabledScopedBinding',
uri: 'explicitly-disabled/{scoped}/{binding}',
enforcesScopedBindings: false,
preventsScopedBindings: true
)
->assertRouteRegistered(
BindingScoping2TestController::class,
controllerMethod: 'implicitlyDisabledScopedBinding',
uri: 'implicitly-disabled/{scoped}/{binding}',
enforcesScopedBindings: false
enforcesScopedBindings: false,
preventsScopedBindings: false
);
}

/** @test */
public function it_can_enable_binding_scoping_on_individual_methods_of_a_controller()
public function it_can_disable_binding_scoping_on_individual_methods_of_a_controller()
{
$this->routeRegistrar->registerClass(BindingScoping1TestController::class);

Expand All @@ -40,13 +50,38 @@ public function it_can_enable_binding_scoping_on_individual_methods_of_a_control
BindingScoping1TestController::class,
controllerMethod: 'implicitlyEnabledScopedBinding',
uri: 'implicit/{scoped}/{binding}',
enforcesScopedBindings: true
enforcesScopedBindings: true,
preventsScopedBindings: false
)
->assertRouteRegistered(
BindingScoping1TestController::class,
controllerMethod: 'explicitlyDisabledScopedBinding',
uri: 'explicitly-disabled/{scoped}/{binding}',
enforcesScopedBindings: false
enforcesScopedBindings: false,
preventsScopedBindings: true
);
}

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

$this
->assertRegisteredRoutesCount(2)
->assertRouteRegistered(
BindingScoping3TestController::class,
controllerMethod: 'explicitlyDisabledByClassScopedBinding',
uri: 'explicitly-disabled-by-class/{scoped}/{binding}',
enforcesScopedBindings: false,
preventsScopedBindings: true
)
->assertRouteRegistered(
BindingScoping3TestController::class,
controllerMethod: 'explicitlyEnabledOverridingClassScopedBinding',
uri: 'explicitly-enabled-overriding-class/{scoped}/{binding}',
enforcesScopedBindings: true,
preventsScopedBindings: false
);
}
}
7 changes: 6 additions & 1 deletion tests/TestCase.php
Original file line number Diff line number Diff line change
Expand Up @@ -50,14 +50,15 @@ public function assertRouteRegistered(
?array $wheres = [],
?bool $isFallback = false,
?bool $enforcesScopedBindings = false,
?bool $preventsScopedBindings = false,
?array $defaults = []
): self {
if (! is_array($middleware)) {
$middleware = Arr::wrap($middleware);
}

$routeRegistered = collect($this->getRouteCollection()->getRoutes())
->contains(function (Route $route) use ($name, $middleware, $controllerMethod, $controller, $uri, $httpMethods, $domain, $wheres, $isFallback, $enforcesScopedBindings, $defaults) {
->contains(function (Route $route) use ($name, $middleware, $controllerMethod, $controller, $uri, $httpMethods, $domain, $wheres, $isFallback, $enforcesScopedBindings, $preventsScopedBindings, $defaults) {
foreach (Arr::wrap($httpMethods) as $httpMethod) {
if (! in_array(strtoupper($httpMethod), $route->methods)) {
return false;
Expand Down Expand Up @@ -106,6 +107,10 @@ public function assertRouteRegistered(
return false;
}

if ($route->preventsScopedBindings() !== $preventsScopedBindings) {
return false;
}

return true;
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ public function implicitlyEnabledScopedBinding()
}

#[Route('get', 'explicitly-disabled/{scoped}/{binding}')]
#[ScopeBindings(scopeBindings: false)]
#[ScopeBindings(false)]
public function explicitlyDisabledScopedBinding()
{
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,12 @@ public function explicitlyEnabledScopedBinding()
{
}

#[Route('get', 'explicitly-disabled/{scoped}/{binding}')]
#[ScopeBindings(false)]
public function explicitlyDisabledScopedBinding()
{
}

#[Route('get', 'implicitly-disabled/{scoped}/{binding}')]
public function implicitlyDisabledScopedBinding()
{
Expand Down
21 changes: 21 additions & 0 deletions tests/TestClasses/Controllers/BindingScoping3TestController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?php

namespace Spatie\RouteAttributes\Tests\TestClasses\Controllers;

use Spatie\RouteAttributes\Attributes\Route;
use Spatie\RouteAttributes\Attributes\ScopeBindings;

#[ScopeBindings(false)]
class BindingScoping3TestController
{
#[Route('get', 'explicitly-disabled-by-class/{scoped}/{binding}')]
public function explicitlyDisabledByClassScopedBinding()
{
}

#[Route('get', 'explicitly-enabled-overriding-class/{scoped}/{binding}')]
#[ScopeBindings(true)]
public function explicitlyEnabledOverridingClassScopedBinding()
{
}
}

0 comments on commit bf57239

Please sign in to comment.