10000 [Feature] Add Eloquent adapter scopes (#363) · BrianLangevin/laravel-json-api@f39dfc1 · GitHub
[go: up one dir, main page]

Skip to content

Commit f39dfc1

Browse files
[Feature] Add Eloquent adapter scopes (cloudcreativity#363)
Allows global scopes to be set on an Eloquent adapter.
1 parent 2910bb0 commit f39dfc1

File tree

4 files changed

+240
-3
lines changed

4 files changed

+240
-3
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ All notable changes to this project will be documented in this file. This projec
88
- [#360](https://github.com/cloudcreativity/laravel-json-api/issues/360)
99
Allow soft delete attribute path to use dot notation.
1010
- Added `domain` method to API fluent routing methods.
11+
- [#337](https://github.com/cloudcreativity/laravel-json-api/issues/337)
12+
Can now apply global scopes to JSON API resources via [adapter scopes.](./docs/basics/adapters.md#scopes)
1113

1214
### Fixed
1315
- [#347](https://github.com/cloudcreativity/laravel-json-api/issues/347)

docs/basics/adapters.md

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -495,6 +495,50 @@ modify this default behaviour and expose soft-deleting capabilities to the clien
495495

496496
See the [Soft Deleting](../features/soft-deletes.md) for information on implementing this.
497497

498+
### Scopes
499+
500+
Eloquent adapters allow you to apply scopes to your API resources, using Laravel's
501+
[global scopes](https://laravel.com/docs/eloquent#global-scopes) feature.
502+
503+
When a scope is applied to an Eloquent adapter, any routes that return that API resource in the response
504+
content will have the scope applied. An example use would be if you only want a user to access their own `posts`
505+
resources - you would add a scope to the `posts` adapter.
506+
507+
Scopes can be added to an Eloquent adapter as either scope classes or as closure scopes. To use the former,
508+
write a class that implements Laravel's `Illuminate\Database\Eloquent\Scope` interface. The class can then
509+
be added to your adapter using constructor dependency injection and the `addScopes` method:
510+
511+
```php
512+
namespace App\JsonApi\Posts;
513+
514+
use CloudCreativity\LaravelJsonApi\Eloquent\AbstractAdapter;
515+
516+
class Adapter extends AbstractAdapter
517+
{
518+
519+
public function __construct(\App\Scopes\UserScope $scope)
520+
{
521+
parent::__construct(new \App\Post());
522+
$this->addScopes($scope);
523+
}
524+
525+
// ...
526+
}
527+
```
528+
529+
> Using a class scope allows you to reuse that scope across multiple adapters.
530+
531+
If you prefer to use a closure for your scope, these can be added to an Eloquent adapter using the
532+
`addClosureScope` method. For example, in our `AppServiceProvider::register()` method:
533+
534+
```php
535+
$this->app->afterResolving(\App\JsonApi\Posts\Adapter::class, function ($adapter) {
536+
$adapter->addClosureScope(function ($query) {
537+
$query->where('author_id', \Auth::id());
538+
});
539+
});
540+
```
541+
498542
## Custom Adapters
499543

500544
Custom adapters can be used for any domain record that is not an Eloquent model. Adapters will work with this

src/Eloquent/AbstractAdapter.php

Lines changed: 65 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
use Illuminate\Database\Eloquent\Builder;
2929
use Illuminate\Database\Eloquent\Model;
3030
use Illuminate\Database\Eloquent\Relations;
31+
use Illuminate\Database\Eloquent\Scope;
3132
use Illuminate\Support\Collection;
3233
use Neomerx\JsonApi\Contracts\Encoder\Parameters\EncodingParametersInterface;
3334
use Neomerx\JsonApi\Encoder\Parameters\EncodingParameters;
@@ -86,6 +87,11 @@ abstract class AbstractAdapter extends AbstractResourceAdapter
8687
*/
8788
protected $defaultPagination = null;
8889

90+
/**
91+
* @var array
92+
*/
93+
private $scopes;
94+
8995
/**
9096
* Apply the supplied filters to the builder instance.
9197
*
@@ -105,6 +111,7 @@ public function __construct(Model $model, PagingStrategyInterface $paging = null
105111
{
106112
$this->model = $model;
107113
$this->paging = $paging;
114+
$this->scopes = [];
108115
}
109116

110117
/**
@@ -130,8 +137,12 @@ public function query(EncodingParametersInterface $parameters)
130137
*/
131138
public function queryToMany($relation, EncodingParametersInterface $parameters)
132139
{
140+
$this->applyScopes(
141+
$query = $relation->newQuery()
142+
);
143+
133144
return $this->queryAllOrOne(
134-
$relation->newQuery(),
145+
$query,
135146
$this->getQueryParameters($parameters)
136147
);
137148
}
@@ -148,8 +159,12 @@ public function queryToMany($relation, EncodingParametersInterface $parameters)
148159
*/
149160
public function queryToOne($relation, EncodingParametersInterface $parameters)
150161
{
162+
$this->applyScopes(
163+
$query = $relation->newQuery()
164+
);
165+
151166
return $this->queryOne(
152-
$relation->newQuery(),
167+
$query,
153168
$this->getQueryParameters($parameters)
154169
);
155170
}
@@ -210,6 +225,49 @@ public function findMany(array $resourceIds)
210225
return $this->findManyQuery($resourceIds)->get()->all();
211226
}
212227

228+
/**
229+
* Add scopes.
230+
*
231+
* @param Scope ...$scopes
232+
* @return $this
233+
*/
234+
public function addScopes(Scope ...$scopes): self
235+
{
236+
foreach ($scopes as $scope) {
237+
$this->scopes[get_class($scope)] = $scope;
238+
}
239+
240+
return $this;
241+
}
242+
243+
/**
244+
* Add a global scope using a closure.
245+
*
246+
* @param \Closure $scope
247+
* @param string|null $identifier
248+
* @return $this
249+
*/
250+
public function addClosureScope(\Closure $scope, string $identifier = null): self
251+
{
252+
$identifier = $identifier ?: spl_object_hash($scope);
253+
254+
$this->scopes[$identifier] = $scope;
255+
256+
return $this;
257+
}
258+
259+
/**
260+
* @param Builder $query
261+
* @return void
262+
*/
263+
protected function applyScopes($query): void
264+
{
265+
/** @var Scope $scope */
266+
foreach ($this->scopes as $identifier => $scope) {
267+
$query->withGlobalScope($identifier, $scope);
268+
}
269+
}
270+
213271
/**
214272
* Get a new query builder.
215273
*
@@ -220,7 +278,11 @@ public function findMany(array $resourceIds)
220278
*/
221279
protected function newQuery()
222280
{
223-
return $this->model->newQuery();
281+
$this->applyScopes(
282+
$builder = $this->model->newQuery()
283+
);
284+
285+
return $builder;
224286
}
225287

226288
/**
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
<?php
2+
/**
3+
* Copyright 2019 Cloud Creativity Limited
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
namespace CloudCreativity\LaravelJsonApi\Tests\Integration\Eloquent;
19+
20+
use CloudCreativity\LaravelJsonApi\Tests\Integration\TestCase;
21+
use DummyApp\Country;
22+
use DummyApp\JsonApi\Posts\Adapter;
23+
use DummyApp\Post;
24+
use DummyApp\User;
25+
26+
class ScopesTest extends TestCase
27+
{
28+
29+
/**
30+
* @var string
31+
*/
32+
protected $resourceType = 'posts';
33+
34+
/**
35+
* @var User
36+
*/
37+
private $user;
38+
39+
/**
40+
* @return void
41+
*/
42+
protected function setUp(): void
43+
{
44+
parent::setUp();
45+
46+
$this->user = factory(User::class)->create();
47+
48+
$this->app->afterResolving(Adapter::class, function (Adapter $adapter) {
49+
$adapter->addClosureScope(function ($query) {
50+
$query->where('author_id', $this->user->getKey());
51+
});
52+
});
53+
}
54+
55+
public function testListAll(): void
56+
{
57+
$expected = factory(Post::class, 2)->create(['author_id' => $this->user->getKey()]);
58+
factory(Post::class, 3)->create();
59+
60+
$this->getJsonApi('/api/v1/posts')->assertFetchedMany($expected);
61+
}
62+
63+
public function testRead(): void
64+
{
65+
$post = factory(Post::class)->create(['author_id' => $this->user->getKey()]);
66+
67+
$this->getJsonApi(url('/api/v1/posts', $post))->assertFetchedOne([
68+
'type' => 'posts',
69+
'id' => (string) $post->getRouteKey(),
70+
]);
71+
}
72+
73+
public function testRead404(): void
74+
{
75+
$post = factory(Post::class)->create();
76+
77+
$this->getJsonApi(url('/api/v1/posts', $post))->assertStatus(404);
78+
}
79+
80+
public function testReadToOne(): void
81+
{
82+
$this->markTestIncomplete('@todo');
83+
}
84+
85+
public function testReadToOneRelationship(): void
86+
{
87+
$this->markTestIncomplete('@todo');
88+
}
89+
90+
public function testReadToMany(): void
91+
{
92+
$country = factory(Country::class)->create();
93+
94+
$this->user->country()->associate($country);
95+
$this->user->save();
96+
97+
$expected = factory(Post::class, 2)->create(['author_id< EDF9 /span>' => $this->user->getKey()]);
98+
99+
factory(Post::class)->create([
100+
'author_id' => factory(User::class)->create([
101+
'country_id' => $country->getKey(),
102+
]),
103+
]);
104+
105+
$url = url('/api/v1/countries', [$country, 'posts']);
106+
107+
$this->getJsonApi($url)->assertFetchedMany($expected);
108+
}
109+
110+
public function testReadToManyRelationship(): void
111+
{
112+
$country = factory(Country::class)->create();
113+
114+
$this->user->country()->associate($country);
115+
$this->user->save();
116+
117+
$expected = factory(Post::class, 2)->create(['author_id' => $this->user->getKey()]);
118+
119+
factory(Post::class)->create([
120+
'author_id' => factory(User::class)->create([
121+
'country_id' => $country->getKey(),
122+
]),
123+
]);
124+
125+
$url = url('/api/v1/countries', [$country, 'relationships', 'posts']);
126+
127+
$this->getJsonApi($url)->assertFetchedToMany($expected);
128+
}
129+
}

0 commit comments

Comments
 (0)
0