8000 Merge branch 'release/1.2.0' · Naoray/laravel-json-api@ef5786d · GitHub
[go: up one dir, main page]

Skip to content

Commit ef5786d

Browse files
committed
Merge branch 'release/1.2.0'
2 parents 7eca388 + 8b6f304 commit ef5786d

30 files changed

+676
-104
lines changed

CHANGELOG.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,28 @@
22
All notable changes to this project will be documented in this file. This project adheres to
33
[Semantic Versioning](http://semver.org/) and [this changelog format](http://keepachangelog.com/).
44

5+
## [1.2.0] - 2019-06-20
6+
7+
### Added
8+
- [#360](https://github.com/cloudcreativity/laravel-json-api/issues/360)
9+
Allow soft delete attribute path to use dot notation.
10+
- 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)
13+
14+
### Fixed
15+
- [#370](https://github.com/cloudcreativity/laravel-json-api/pull/370)
16+
Fix wrong validation error title when creating a custom validator.
17+
- [#369](https://github.com/cloudcreativity/laravel-json-api/issues/369)
18+
Fix using an alternative decoding type for update (`PATCH`) requests.
19+
- [#362](https://github.com/cloudcreativity/laravel-json-api/issues/362)
20+
Fix fatal error in route class caused by polymorphic entity types.
21+
- [#358](https://github.com/cloudcreativity/laravel-json-api/issues/358)
22+
Queue listener could trigger a `ModelNotFoundException` when deserializing a job that had deleted a
23+
model during its `handle()` method.
24+
- [#347](https://github.com/cloudcreativity/laravel-json-api/issues/347)
25+
Update `zend-diactoros` dependency.
26+
527
## [1.1.0] - 2019-04-12
628

729
### Added

composer.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@
3535
"neomerx/json-api": "^1.0.3",
3636
"ramsey/uuid": "^3.0",
3737
"symfony/psr-http-message-bridge": "^1.0",
38-
"zendframework/zend-diactoros": "^1.3"
38+
"zendframework/zend-diactoros": "^1.0|^2.0"
3939
},
4040
"require-dev": {
4141
"ext-sqlite3": "*",

docs/basics/adapters.md

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -495,6 +495,58 @@ 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. When a scope is applied
502+
to an Eloquent adapter, any routes that return that API resource in the response content will have
503+
the scope applied.
504+
505+
> An example use case for this would be if your API only contains resources related to the current signed
506+
in user. In this case, you would only ever want resources owned by that user to appear in the API. I.e.
507+
from the client's perspective, any resources belonging to other users do not exist. In this case, a global
508+
scope would ensure that a `404 Not Found` is returned for resources that belong to other users.
509+
510+
> In contrast, if your API serves a mixture of resources belonging to different users, then
511+
`401 Unauthorized` or `403 Forbidden` responses might be more appropriate when attempting to access other
512+
users' resources. In this scenario, [Authorizers](./security.md) would be a better approach than global
513+
scopes.
514+
515+
Scopes can be added to an Eloquent adapter as either scope classes or as closure scopes. To use the former,
516+
write a class that implements Laravel's `Illuminate\Database\Eloquent\Scope` interface. The class can then
517+
be added to your adapter using constructor dependency injection and the `addScopes` method:
518+
519+
```php
520+
namespace App\JsonApi\Posts;
521+
522+
use CloudCreativity\LaravelJsonApi\Eloquent\AbstractAdapter;
523+
524+
class Adapter extends AbstractAdapter
525+
{
526+
527+
public function __construct(\App\Scopes\UserScope $scope)
528+
{
529+
parent::__construct(new \App\Post());
530+
$this->addScopes($scope);
531+
}
532+
533+
// ...
534+
}
535+
```
536+
537+
> Using a class scope allows you to reuse that scope across multiple adapters.
538+
539+
If you prefer to use a closure for your scope, these can be added to an Eloquent adapter using the
540+
`addClosureScope` method. For example, in our `AppServiceProvider::register()` method:
541+
542+
```php
543+
$this->app->afterResolving(\App\JsonApi\Posts\Adapter::class, function ($adapter) {
544+
$adapter->addClosureScope(function ($query) {
545+
$query->where('author_id', \Auth::id());
546+
});
547+
});
548+
```
549+
498550
## Custom Adapters
499551

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

docs/features/async.md

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,43 @@ class ProcessPodcast implements ShouldQueue
217217
}
218218
```
219219

220+
## Manually Marking Client Jobs as Complete
221+
222+
This package will, in most cases, automatically mark the stored representation of the job as complete.
223+
We do this by listening the Laravel's queue events.
224+
225+
There is one scenario where we cannot do this: if your job deletes a model during its `handle` method.
226+
This is because we cannot deserialize the job in our listener without causing a `ModelNotFoundException`.
227+
228+
In these scenarios, you will need to manually mark the stored representation of the job as complete.
229+
Use the `didComplete()` method, which accepts one argument: a boolean indicating success (will be
230+
`true` if not provided).
231+
232+
For example:
233+
234+
```php
235+
namespace App\Jobs;
236+
237+
use CloudCreativity\LaravelJsonApi\Queue\ClientDispatchable;
238+
use Illuminate\Contracts\Queue\ShouldQueue;
239+
240+
class RemovePodcast implements ShouldQueue
241+
{
242+
243+
use ClientDispatchable;
244+
245+
// ...
246+
247+
public function handle()
248+
{
249+
// ...logic to remove a podcast.
250+
251+
$this->podcast->delete();
252+
$this->didComplete();
253+
}
254+
}
255+
```
256+
220257
## Routing
221258

222259
The final step of setup is to enable asynchronous process routes on a resource. These

docs/features/media-types.md

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -504,6 +504,8 @@ the request as its first argument. For a create request, the argument will be `n
504504
resource. E.g. `GET /api/v1/posts` or when the resource is in a relationship such as
505505
`GET /api/v1/users/1/posts`.
506506

507+
#### Encoding Example
508+
507509
For example, say we wanted to support returning an avatar's image via our API we would need to
508510
support the media type of the stored avatar. Our avatar content negotiator may look like this:
509511

@@ -534,7 +536,7 @@ class ContentNegotiator extends BaseContentNegotiator
534536
```
535537

536538
In this example, `encodingMediaTypes()` returns the list of the encodings supported by our API. The
537-
`when` method adds an encoding to the list if the first argument is true - in this case, if the
539+
`when` method adds an encoding to the list if the first argument is `true` - in this case, if the
538540
request method is `GET`.
539541

540542
> The `EncodingList` class also has an `unless` method, along with other helper methods.
@@ -594,6 +596,9 @@ class ContentNegotiator extends BaseContentNegotiator
594596
}
595597
```
596598

599+
> The `MultipartDecoder` is a decoder you will need to write with your own logic. There's an
600+
example of what a decoder might look like below.
601+
597602
The media types listed on your content negotiator are **added** to the list of media types that
598603
your API supports. They will be used for every controller action that the content negotiator
599604
is used for.
@@ -609,12 +614,35 @@ be `null`.
609614
relationship object. E.g. `POST /api/v1/posts/1/tags`. This method receives the domain record for
610615
the request as its first arguments, and the relationship field name as its second argument.
611616

617+
#### Decoding Example
618+
612619
For example, if we wanted to support uploading an Avatar image to create an `avatars` resource,
613-
our content negotiator could be:
620+
we would need to write a decoder that handled files:
614621

615622
```php
623+
namespace App\JsonApi;
616624

617-
namespace DummyApp\JsonApi\Avatars;
625+
use CloudCreativity\LaravelJsonApi\Contracts\Decoder\DecoderInterface;
626+
627+
class MultipartDecoder implements DecoderInterface
628+
{
629+
630+
/**
631+
* @inheritdoc
632+
*/
633+
public function decode($request): array
634+
{
635+
// return whatever array data you expect from the request.
636+
// in this example we are expecting a file, so we will return all files.
637+
return $request->allFiles();
638+
}
639+
}
640+
```
641+
642+
Then we would need to use this decoder in our `avatars` content negotiator:
643+
644+
```php
645+
namespace App\JsonApi\Avatars;
618646

619647
use App\Avatar;
620648
use App\JsonApi\MultipartDecoder;

docs/features/soft-deletes.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,8 @@ class Adapter extends AbstractAdapter
172172
}
173173
```
174174

175+
> You can use dot notation for the `$softDeleteField` value, if you are using a nested attribute field.
176+
175177
The client can now send the following request to soft-delete a resource:
176178

177179
```http

src/Contracts/Resolver/ResolverInterface.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ public function isType($type);
3737
* Get the domain record type for the supplied JSON API resource type.
3838
*
3939
* @param string $resourceType
40-
* @return string|null
40+
* @return string|string[]|null
4141
*/
4242
public function getType($resourceType);
4343

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
/**

src/Eloquent/Concerns/SoftDeletesModels.php

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
use CloudCreativity\LaravelJsonApi\Utils\Str;
2222
use Illuminate\Database\Eloquent\Model;
2323
use Illuminate\Database\Schema\Builder;
24+
use Illuminate\Support\Arr;
2425
use Illuminate\Support\Collection;
2526
use Neomerx\JsonApi\Contracts\Encoder\Parameters\EncodingParametersInterface;
2627

@@ -61,13 +62,14 @@ protected function findQuery($resourceId)
6162
protected function fillAttributes($record, Collection $attributes)
6263
{
6364
$field = $this->getSoftDeleteField($record);
65+
$attributesArr = $attributes->toArray();
6466

65-
if ($attributes->has($field)) {
66-
$this->fillSoftDelete($record, $field, $attributes->get($field));
67+
if (Arr::has($attributesArr, $field)) {
68+
$this->fillSoftDelete($record, $field, Arr::get($attributesArr, $field));
6769
}
6870

6971
$record->fill(
70-
$this->deserializeAttributes($attributes->forget($field), $record)
72+
$this->deserializeAttributes(Arr::except($attributesArr, $field), $record)
7173
);
7274
}
7375

src/Facades/JsonApi.php

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,17 +20,17 @@
2020

2121
use CloudCreativity\LaravelJsonApi\Routing\ApiRegistration;
2222
use CloudCreativity\LaravelJsonApi\Routing\Route;
23-
use Illuminate\Support\Facades\Facade as BaseFacade;
23+
use Illuminate\Support\Facades\Facade;
2424

2525
/**
26-
* Class Facade
26+
* Class JsonApi
2727
*
2828
* @package CloudCreativity\LaravelJsonApi
2929
* @method static ApiRegistration register(string $apiName, array|\Closure $options = [], \Closure|null $callback = null)
3030
* @method static string defaultApi(string|null $apiName)
3131
* @method static Route currentRoute()
3232
*/
33-
class JsonApi extends BaseFacade
33+
class JsonApi extends Facade
3434
{
3535

3636
/**

0 commit comments

Comments
 (0)
0