8000 Merge branch 'release/1.1.0' · GIANTCRAB/laravel-json-api@44b58c8 · GitHub
[go: up one dir, main page]

Skip to content

Commit 44b58c8

Browse files
committed
Merge branch 'release/1.1.0'
2 parents a5370b7 + 9134d5a commit 44b58c8

File tree

15 files changed

+378
-26
lines changed

15 files changed

+378
-26
lines changed

CHANGELOG.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,26 @@
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.1.0] - 2019-04-12
6+
7+
### Added
8+
- [#315](https://github.com/cloudcreativity/laravel-json-api/issues/315)
9+
Allow developers to use the exact JSON API field name as the relationship method name on their
10+
adapters, plus for default conversion of names in include paths. Although we recommend following
11+
the PSR1 standard of using camel case for method names, this does allow a developer to use snake
12+
case field names with snake case method names.
13+
- Exception handlers can now use the `parseJsonApiException()` helper method if they need to
14+
convert JSON API exceptions to HTTP exceptions. Refer to the [installation instructions](./docs/installation.md)
15+
for an example of how to do this on an exception handler.
16+
17+
### Fixed
18+
- [#329](https://github.com/cloudcreativity/laravel-json-api/issues/329)
19+
Render JSON API error responses when a codec has matched, but the client has not explicitly
20+
asked for JSON API response (e.g. asked for `Accept: */*`).
21+
- [#313](https://github.com/cloudcreativity/laravel-json-api/issues/313)
22+
Ensure that the standard paging strategy uses the resource identifier so that pages have a
23+
[deterministic sort order](https://tighten.co/blog/a-cautionary-tale-of-nondeterministic-laravel-pagination).
24+
525
## [1.0.1] - 2019-03-12
626

7< 8000 code>27
### Fixed

docs/fetching/pagination.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -327,16 +327,16 @@ means the most recently created model is the first in the list, and the oldest i
327327
column is not unique (there could be multiple rows created at the same time), it uses the resource id
328328
column as a secondary sort order, as the resource id must always be unique.
329329

330-
To change the column that is used for the list order use the `withColumn` method. If you prefer your
331-
list to be in ascending order, use the `withAscending` method. For example:
330+
To change the column that is used for the list order use the `withQualifiedColumn` method. If you prefer
331+
your list to be in ascending order, use the `withAscending` method. For example:
332332

333333
```php
334-
$strategy->withColumn('published_at')->withAscending();
334+
$strategy->withQualfiedColumn('posts.published_at')->withAscending();
335335
```
336336

337337
> The Eloquent adapter will always set the secondary column for sort order to the same column that is
338338
being used for the resource ID. If you are using the cursor strategy in a custom adapter, you will
339-
need to set the unique column using the `withIdentifierColumn` method. Note that whatever you set the
339+
need to set the unique column using the `withQualifiedKeyName` method. Note that whatever you set the
340340
column to, this will mean the client needs to provide the value of that column for the `after` and
341341
`before` page parameters - so really it should always match whatever you are using for the resource ID.
342342

docs/installation.md

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ add support for JSON API error rendering to your application's exception handler
7171
To do this, simply add the `CloudCreativity\LaravelJsonApi\Exceptions\HandlesErrors` trait to your handler and
7272
modify your `render()` method as follows:
7373

74-
``` php
74+
```php
7575
namespace App\Exceptions;
7676

7777
use CloudCreativity\LaravelJsonApi\Exceptions\HandlesErrors;
@@ -91,13 +91,22 @@ class Handler extends ExceptionHandler
9191

9292
// ...
9393

94-
public function render($request, Exception $e)
95-
{
96-
if ($this->isJsonApi($request, $e)) {
97-
return $this->renderJsonApi($request, $e);
94+
public function render($request, Exception $e)
95+
{
96+
if ($this->isJsonApi($request, $e)) {
97+
return $this->renderJsonApi($request, $e);
98+
}
99+
100+
// do standard exception rendering here...
101+
}
102+
103+
protected function prepareException(Exception $e)
104+
{
105+
if ($e instanceof JsonApiException) {
106+
return $this->prepareJsonApiException($e);
98107
}
99108

100-
// do standard exception rendering here...
101-
}
109+
return parent::prepareException($e);
110+
}
102111
}
103112
```

src/Adapter/AbstractResourceAdapter.php

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -197,11 +197,32 @@ protected function isFillableRelation($field, $record)
197197
}
198198

199199
/**
200-
* @param $field
200+
* Get the method name on this adapter for the supplied JSON API field.
201+
*
202+
* By default we expect the developer to be following the PSR1 standard,
203+
* so the method name on the adapter should use camel case.
204+
*
205+
* However, some developers may prefer to use the actual JSON API field
206+
* name. E.g. they could use `user_history` as the JSON API field name
207+
* and the method name.
208+
*
209+
* Therefore we return the field name if it exactly exists on the adapter,
210+
* otherwise we camelize it.
211+
*
212+
* A developer can use completely different logic by overloading this
213+
* method.
214+
*
215+
* @param string $field
216+
* the JSON API field name.
201217
* @return string|null
218+
* the adapter's method name, or null if none is implemented.
202219
*/
203220
protected function methodForRelation($field)
204221
{
222+
if (method_exists($this, $field)) {
223+
return $field;
224+
}
225+
205226
$method = Str::camelize($field);
206227

207228
return method_exists($this, $method) ? $method : null;

src/Eloquent/AbstractAdapter.php

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@
2525
use CloudCreativity\LaravelJsonApi\Contracts\Pagination\PagingStrategyInterface;
2626
use CloudCreativity\LaravelJsonApi\Document\ResourceObject;
2727
use CloudCreativity\LaravelJsonApi\Exceptions\RuntimeException;
28-
use CloudCreativity\LaravelJsonApi\Pagination\CursorStrategy;
2928
use Illuminate\Database\Eloquent\Builder;
3029
use Illuminate\Database\Eloquent\Model;
3130
use Illuminate\Database\Eloquent\Relations;
@@ -431,9 +430,14 @@ protected function paginate($query, EncodingParametersInterface $parameters)
431430
throw new RuntimeException('Paging is not supported on adapter: ' . get_class($this));
432431
}
433432

434-
/** If using the cursor strategy, we need to set the key name for the cursor. */
435-
if ($this->paging instanceof CursorStrategy) {
436-
$this->paging->withIdentifierColumn($this->getKeyName());
433+
/**
434+
* Set the key name on the strategy, so it knows what column is being used
435+
* for the resource's ID.
436+
*
437+
* @todo 2.0 add `withQualifiedKeyName` to the paging strategy interface.
438+
*/
439+
if (method_exists($this->paging, 'withQualifiedKeyName')) {
440+
$this->paging->withQualifiedKeyName($this->getQualifiedKeyName());
437441
}
438442

439443
return $this->paging->paginate($query, $parameters);
@@ -622,7 +626,7 @@ private function guessRelation()
622626
{
623627
list($one, $two, $caller) = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 3);
624628

625-
return $caller['function'];
629+
return $this->modelRelationForField($caller['function']);
626630
}
627631

628632
}

src/Eloquent/Concerns/IncludesModels.php

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,13 @@ trait IncludesModels
7979
*/
8080
protected $includePaths = [];
8181

82+
/**
83+
* Whether Eloquent relations are camel cased.
84+
*
85+
* @var bool
86+
*/
87+
protected $camelCaseRelations = true;
88+
8289
/**
8390
* Add eager loading to the query.
8491
*
@@ -145,7 +152,38 @@ protected function convertIncludePath($path)
145152
}
146153

147154
return collect(explode('.', $path))->map(function ($segment) {
148-
return Str::camelize($segment);
155+
return $this->modelRelationForField($segment);
149156
})->implode('.');
150157
}
158+
159+
/**
160+
* Convert a JSON API field name to an Eloquent model relation name.
161+
*
162+
* According to the PSR1 spec, method names on classes MUST be camel case.
163+
* However, there seem to be some Laravel developers who snake case
164+
* relationship methods on their models, so that the method name matches
165+
* the snake case format of attributes (column values).
166+
*
167+
* The `$camelCaseRelations` property controls the behaviour of this
168+
* conversion:
169+
*
170+
* - If `true`, a field name of `user-history` or `user_history` will
171+
* expect the Eloquent model relation method to be `userHistory`.
172+
* - If `false`, a field name of `user-history` or `user_history` will
173+
* expect the Eloquent model relation method to be `user_history`. I.e.
174+
* if PSR1 is not being followed, the best guess is that method names
175+
* are snake case.
176+
*
177+
* If the developer has different conversion logic, they should overload
178+
* this method and implement it themselves.
179+
*
180+
* @param string $field
181+
* the JSON API field name.
182+
* @return string
183+
* the expected relation name on the Eloquent model.
184+
*/
185+
protected function modelRelationForField($field)
186+
{
187+
return $this->camelCaseRelations ? Str::camelize($field) : Str::underscore($field);
188+
}
151189
}

src/Exceptions/HandlesErrors.php

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,15 @@
1818

1919
namespace CloudCreativity\LaravelJsonApi\Exceptions;
2020

21+
use CloudCreativity\LaravelJsonApi\Routing\Route;
22+
use CloudCreativity\LaravelJsonApi\Services\JsonApiService;
2123
use CloudCreativity\LaravelJsonApi\Utils\Helpers;
2224
use Exception;
2325
use Illuminate\Http\Request;
2426
use Illuminate\Http\Response;
27+
use Neomerx\JsonApi\Contracts\Document\ErrorInterface;
28+
use Neomerx\JsonApi\Exceptions\JsonApiException;
29+
use Symfony\Component\HttpKernel\Exception\HttpException;
2530

2631
/**
2732
* Trait HandlesErrors
@@ -44,10 +49,19 @@ trait HandlesErrors
4449
*/
4550
public function isJsonApi($request, Exception $e)
4651
{
47-
return Helpers::wantsJsonApi($request);
52+
if (Helpers::wantsJsonApi($request)) {
53+
return true;
54+
}
55+
56+
/** @var Route $route */
57+
$route = app(JsonApiService::class)->currentRoute();
58+
59+
return $route->hasCodec() && $route->getCodec()->willEncode();
4860
}
4961

5062
/**
63+
* Render an exception as a JSON API error response.
64+
*
5165
* @param Request $request
5266
* @param Exception $e
5367
* @return Response
@@ -57,4 +71,19 @@ public function renderJsonApi($request, Exception $e)
5771
return json_api()->response()->exception($e);
5872
}
5973

74+
/**
75+
* Prepare JSON API exception for non-JSON API rendering.
76+
*
77+
* @param JsonApiException $ex
78+
* @return HttpException
79+
*/
80+
protected function prepareJsonApiException(JsonApiException $ex)
81+
{
82+
$error = collect($ex->getErrors())->map(function (ErrorInterface $err) {
83+
return $err->getDetail() ?: $err->getTitle();
84+
})->filter()->first();
85+
86+
return new HttpException($ex->getHttpCode(), $error, $ex);
87+
}
88+
6089
}

src/Pagination/CursorStrategy.php

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,27 @@ public function withAscending()
161161
*
162162
* @param $column
163163
* @return $this
164+
* @todo 2.0 pass qualified columns to the cursor builder.
165+
*/
166+
public function withQualifiedColumn($column)
167+
{
168+
$parts = explode('.', $column);
169+
170+
if (!isset($parts[1])) {
171+
throw new \InvalidArgumentException('Expecting a valid qualified column name.');
172+
}
173+
174+
$this->withColumn($parts[1]);
175+
176+
return $this;
177+
}
178+
179+
/**
180+
* Set the cursor column.
181+
*
182+
* @param $column
183+
* @return $this
184+
* @deprecated 2.0 use `withQualifiedColumn` instead.
164185
*/
165186
public function withColumn($column)
166187
{
@@ -169,11 +190,32 @@ public function withColumn($column)
169190
return $this;
170191
}
171192

193+
/**
194+
* Set the column name for the resource's ID.
195+
*
196+
* @param string $keyName
197+
* @return $this
198+
* @todo 2.0 pass qualified key name to the cursor builder.
199+
*/
200+
public function withQualifiedKeyName($keyName)
201+
{
202+
$parts = explode('.', $keyName);
203+
204+
if (!isset($parts[1])) {
205+
throw new \InvalidArgumentException('Expecting a valid qualified column name.');
206+
}
207+
208+
$this->withIdentifierColumn($parts[1]);
209+
210+
return $this;
211+
}
212+
172213
/**
173214
* Set the column for the before/after identifiers.
174215
*
175216
* @param string|null $column
176217
* @return $this
218+
* @deprecated 2.0 use `withQualifiedKeyName` instead.
177219
*/
178220
public function withIdentifierColumn($column)
179221
{
@@ -183,6 +225,8 @@ public function withIdentifierColumn($column)
183225
}
184226

185227
/**
228+
* Set the select columns for the query.
229+
*
186230
* @param $cols
187231
* @return $this
188232
*/

0 commit comments

Comments
 (0)
0