Skip to content

Commit d8c2ad9

Browse files
committed
✨ automatic gating
1 parent d88d8dc commit d8c2ad9

35 files changed

+1254
-14
lines changed

README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ Laravel Rest Api is an elegant way to expose your app through an API, it takes f
99
PHP 7.3+ and Laravel 8.0+
1010

1111
# BETA
12-
Please note that this package is under beta and is not recommended to use for production environment for now. End of beta should be by the end of 2023.
12+
Please note that this package is under beta and is not recommended to use for production environment for now. End of beta should be by October 2023.
1313

1414
## Documentation, Installation, and Usage Instructions
1515

@@ -91,6 +91,6 @@ TODO
9191

9292
- Custom directives (Filters / sorting)
9393
- Actions / Metrics
94-
- Automatic Gates (with config customisation)
9594
- Automatic documentation with extension possible
96-
- Add the possibility to disable authorization
95+
- Add the possibility to disable authorization
96+
- Refactor the response class

config/rest.php

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<?php
2+
3+
return [
4+
5+
/*
6+
|--------------------------------------------------------------------------
7+
| Rest Automatic Gates
8+
|--------------------------------------------------------------------------
9+
|
10+
| The following configuration option contains gates customisation. You might
11+
| want to adapt this feature to your needs.
12+
|
13+
*/
14+
15+
'automatic_gates' => [
16+
'enabled' => true,
17+
'key' => 'gates',
18+
// Here you can customize the keys for each gate
19+
'names' => [
20+
'authorized_to_view' => 'authorized_to_view',
21+
'authorized_to_create' => 'authorized_to_create',
22+
'authorized_to_update' => 'authorized_to_update',
23+
'authorized_to_delete' => 'authorized_to_delete',
24+
'authorized_to_restore' => 'authorized_to_restore',
25+
'authorized_to_force_delete' => 'authorized_to_force_delete',
26+
]
27+
],
28+
];
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<?php
2+
3+
namespace Lomkit\Rest\Concerns\Resource;
4+
5+
trait DisableAutomaticGates
6+
{
7+
public function isAutomaticGatingEnabled() : bool {
8+
return false;
9+
}
10+
}

src/Http/Resource.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,4 +65,8 @@ public function defaultOrderBy(RestRequest $request) {
6565
'id' => 'desc'
6666
];
6767
}
68+
69+
public function isAutomaticGatingEnabled() : bool {
70+
return config('rest.automatic_gates.enabled');
71+
}
6872
}

src/Http/Response.php

Lines changed: 47 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,10 @@
55
use Illuminate\Contracts\Support\Responsable;
66
use Illuminate\Database\Eloquent\Model;
77
use Illuminate\Database\Eloquent\Relations\Pivot;
8+
use Illuminate\Http\Resources\Json\PaginatedResourceResponse;
89
use Illuminate\Pagination\LengthAwarePaginator;
910
use Illuminate\Support\Collection;
11+
use Illuminate\Support\Facades\Gate;
1012
use Illuminate\Support\Str;
1113
use Lomkit\Rest\Http\Requests\RestRequest;
1214
use Lomkit\Rest\Relations\Relation;
@@ -28,6 +30,16 @@ public function resource(Resource $resource) {
2830
});
2931
}
3032

33+
protected function buildGatesForModel(Model $model) {
34+
return [
35+
config('rest.automatic_gates.names.authorized_to_view') => Gate::allows('view', $model),
36+
config('rest.automatic_gates.names.authorized_to_update') => Gate::allows('update', $model),
37+
config('rest.automatic_gates.names.authorized_to_delete') => Gate::allows('delete', $model),
38+
config('rest.automatic_gates.names.authorized_to_restore') => Gate::allows('restore', $model),
39+
config('rest.automatic_gates.names.authorized_to_force_delete') => Gate::allows('forceDelete', $model),
40+
];
41+
}
42+
3143
public function modelToResponse(Model $model, Resource $resource, array $requestArray, Relation $relation = null) {
3244
return array_merge(
3345
// toArray to take advantage of Laravel's logic
@@ -45,6 +57,12 @@ public function modelToResponse(Model $model, Resource $resource, array $request
4557
->toArray()
4658
)
4759
)
60+
->when($resource->isAutomaticGatingEnabled(), function ($attributes) use ($model) {
61+
return $attributes->put(
62+
config('rest.automatic_gates.key'),
63+
$this->buildGatesForModel($model)
64+
);
65+
})
4866
->toArray(),
4967
collect($model->getRelations())
5068
->mapWithKeys(function ($modelRelation, $relationName) use ($requestArray, $relation, $resource) {
@@ -94,19 +112,38 @@ public function modelToResponse(Model $model, Resource $resource, array $request
94112

95113
public function toResponse($request) {
96114
if ($this->responsable instanceof LengthAwarePaginator) {
97-
return $this->responsable->through(function ($model) use ($request) {
115+
$restLengthAwarePaginator = new \Lomkit\Rest\Pagination\LengthAwarePaginator(
116+
$this->responsable->items(),
117+
$this->responsable->total(),
118+
$this->responsable->perPage(),
119+
$this->responsable->currentPage(),
120+
$this->responsable->getOptions(),
121+
$this->resource->isAutomaticGatingEnabled() ? [
122+
config('rest.automatic_gates.key') => [
123+
config('rest.automatic_gates.names.authorized_to_create') => Gate::allows('create', $this->responsable->first()::class),
124+
]
125+
] : []
126+
);
127+
128+
$restLengthAwarePaginator->through(function ($model) use ($request) {
98129
return $this->map($model, $this->modelToResponse($model, $this->resource, $request->input()));
99130
});
131+
132+
return $restLengthAwarePaginator;
133+
100134
} elseif ($this->responsable instanceof Collection) {
101-
return [
102-
'data' => $this->responsable->map(function ($model) use ($request) {
103-
return $this->map($model, $this->modelToResponse($model, $this->resource, $request->input()));
104-
})
105-
];
135+
$data = $this->responsable->map(function ($model) use ($request) {
136+
return $this->map($model, $this->modelToResponse($model, $this->resource, $request->input()));
137+
});
106138
}
107139

108140
return [
109-
'data' => $this->map($this->responsable, $this->modelToResponse($this->responsable, $this->resource, $request->input()))
141+
'data' => $data ?? $this->map($this->responsable, $this->modelToResponse($this->responsable, $this->resource, $request->input())),
142+
'meta' => [
143+
config('rest.automatic_gates.key') => [
144+
config('rest.automatic_gates.names.authorized_to_create') => Gate::allows('create', $this->responsable->first()::class),
145+
]
146+
]
110147
];
111148
}
112149

@@ -121,4 +158,7 @@ public function toResponse($request) {
121158
protected function map(\Illuminate\Database\Eloquent\Model $model, array $responseModel) : array {
122159
return $responseModel;
123160
}
161+
162+
// @TODO: this class needs a refactor because it has grown a lot since the beginning also with the "lengthAwarePaginator"
163+
// @TODO: recursive response for those gates ?
124164
}
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
<?php
2+
3+
namespace Lomkit\Rest\Pagination;
4+
5+
use Illuminate\Support\Collection;
6+
use \Illuminate\Pagination\LengthAwarePaginator as BaseLengthAwarePaginator;
7+
8+
class LengthAwarePaginator extends BaseLengthAwarePaginator
9+
{
10+
/**
11+
* The meta returned by the api.
12+
*
13+
* @var array
14+
*/
15+
protected array $meta;
16+
17+
/**
18+
* Create a new paginator instance.
19+
*
20+
* @param mixed $items
21+
* @param int $total
22+
* @param int $perPage
23+
* @param int|null $currentPage
24+
* @param array $options (path, query, fragment, pageName)
25+
* @return void
26+
*/
27+
public function __construct($items, $total, $perPage, $currentPage = null, array $options = [], $meta = [])
28+
{
29+
parent::__construct($items, $total, $perPage, $currentPage, $options);
30+
31+
$this->meta = $meta;
32+
}
33+
34+
/**
35+
* Get the meta of items being paginated.
36+
*
37+
* @return array
38+
*/
39+
public function meta()
40+
{
41+
return $this->meta;
42+
}
43+
44+
/**
45+
* Get the instance as an array.
46+
*
47+
* @return array
48+
*/
49+
public function toArray()
50+
{
51+
return [
52+
'current_page' => $this->currentPage(),
53+
'data' => $this->items->toArray(),
54+
'from' => $this->firstItem(),
55+
'last_page' => $this->lastPage(),
56+
'per_page' => $this->perPage(),
57+
'to' => $this->lastItem(),
58+
'total' => $this->total(),
59+
'meta' => $this->meta()
60+
];
61+
}
62+
}

src/RestServiceProvider.php

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,10 @@ class RestServiceProvider extends ServiceProvider{
2121
*/
2222
public function register()
2323
{
24+
$this->mergeConfigFrom(
25+
__DIR__.'/../config/rest.php', 'rest'
26+
);
27+
2428
$this->registerServices();
2529
}
2630

@@ -32,6 +36,7 @@ public function register()
3236
public function boot()
3337
{
3438
$this->registerCommands();
39+
$this->registerPublishing();
3540
}
3641

3742
/**
@@ -53,6 +58,20 @@ protected function registerCommands()
5358
}
5459
}
5560

61+
/**
62+
* Register the package's publishable resources.
63+
*
64+
* @return void
65+
*/
66+
private function registerPublishing()
67+
{
68+
if ($this->app->runningInConsole()) {
69+
$this->publishes([
70+
__DIR__.'/../config/rest.php' => config_path('rest.php'),
71+
], 'rest-config');
72+
}
73+
}
74+
5675
/**
5776
* Register Rest's services in the container.
5877
*

0 commit comments

Comments
 (0)