Skip to content

Commit fb279bf

Browse files
committed
[Feature] Support Eloquent morph-one relation
1 parent eb7a6fa commit fb279bf

File tree

17 files changed

+538
-14
lines changed

17 files changed

+538
-14
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@ All notable changes to this project will be documented in this file. This projec
44

55
## Unreleased
66

7+
### Added
8+
- [#503](https://github.com/cloudcreativity/laravel-json-api/issues/503)
9+
The JSON API `hasOne` relation now also supports an Eloquent `morphOne` relation.
10+
711
### Changed
812
- Minimum Laravel version is now `5.8` (previously `5.5`).
913

docs/basics/adapters.md

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,7 @@ for Eloquent models. The relationship types available are `belongsTo`, `hasOne`,
230230
| `belongsToMany` | `hasMany` |
231231
| `hasManyThrough` | `hasManyThrough` |
232232
| `morphTo` | `belongsTo` |
233+
| `morphOne` | `hasOne` |
233234
| `morphMany` | `hasMany` |
234235
| `morphToMany` | `hasMany` |
235236
| `morphedByMany` | `morphMany` |
@@ -512,7 +513,7 @@ in user. In this case, you would only ever want resources owned by that user to
512513
from the client's perspective, any resources belonging to other users do not exist. In this case, a global
513514
scope would ensure that a `404 Not Found` is returned for resources that belong to other users.
514515

515-
> In contrast, if your API serves a mixture of resources belonging to different users, then
516+
> In contrast, if your API serves a mixture of resources belonging to different users, then
516517
`401 Unauthorized` or `403 Forbidden` responses might be more appropriate when attempting to access other
517518
users' resources. In this scenario, [Authorizers](./security.md) would be a better approach than global
518519
scopes.
@@ -610,10 +611,10 @@ class DummyClass extends AbstractResourceAdapter
610611
{
611612
// TODO: Implement persist() method.
612613
}
613-
614+
614615
/**
615616
* @inheritDoc
616-
*/
617+
*/
617618
protected function destroy($record)
618619
{
619620
// TODO: Implement destroy() method.
@@ -704,15 +705,15 @@ on our adapter:
704705
class Adapter extends AbstractAdapter
705706
{
706707
// ...
707-
708+
708709
protected function creating(Comment $comment): void
709710
{
710711
$comment->createdBy()->associate(Auth::user());
711712
}
712713
}
713714
```
714715

715-
> If your resource uses a [client-generated ID](../crud/creating.md#client-generated-ids), you
716+
> If your resource uses a [client-generated ID](../crud/creating.md#client-generated-ids), you
716717
will need to use the `creating` hook to assign the id to the model.
717718

718719
There are two additional hooks that are invoked when an adapter is deleting a resource: `deleting` and `deleted`.

src/Eloquent/HasOne.php

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ public function update($record, array $relationship, EncodingParametersInterface
4141

4242
/** If there is a current related model, we need to clear it. */
4343
if ($current) {
44-
$current->setAttribute($relation->getForeignKeyName(), null)->save();
44+
$this->clear($current, $relation);
4545
}
4646

4747
/** If there is a related model, save it. */
@@ -68,6 +68,25 @@ public function replace($record, array $relationship, EncodingParametersInterfac
6868
*/
6969
protected function acceptRelation($relation)
7070
{
71-
return $relation instanceof Relations\HasOne;
71+
if ($relation instanceof Relations\HasOne) {
72+
return true;
73+
}
74+
75+
return $relation instanceof Relations\MorphOne;
76+
}
77+
78+
/**
79+
* Clear the relation.
80+
*
81+
* @param Model $current
82+
* @param $relation
83+
*/
84+
private function clear(Model $current, $relation)
85+
{
86+
if ($relation instanceof Relations\MorphOne) {
87+
$current->setAttribute($relation->getMorphType(), null);
88+
}
89+
90+
$current->setAttribute($relation->getForeignKeyName(), null)->save();
7291
}
7392
}

tests/dummy/app/Image.php

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<?php
2+
3+
namespace DummyApp;
4+
5+
use Illuminate\Database\Eloquent\Model;
6+
use Illuminate\Database\Eloquent\Relations\MorphTo;
7+
8+
class Image extends Model
9+
{
10+
11+
/**
12+
* @return MorphTo
13+
*/
14+
public function imageable()
15+
{
16+
return $this->morphTo();
17+
}
18+
}
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
<?php
2+
/**
3+
* Copyright 2020 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 DummyApp\JsonApi\Images;
19+
20+
use CloudCreativity\LaravelJsonApi\Eloquent\AbstractAdapter;
21+
use CloudCreativity\LaravelJsonApi\Pagination\StandardStrategy;
22+
use DummyApp\Image;
23+
use Illuminate\Database\Eloquent\Builder;
24+
use Illuminate\Support\Collection;
25+
26+
class Adapter extends AbstractAdapter
27+
{
28+
29+
/**
30+
* Mapping of JSON API attribute field names to model keys.
31+
*
32+
* @var array
33+
*/
34+
protected $attributes = [];
35+
36+
/**
37+
* Mapping of JSON API filter names to model scopes.
38+
*
39+
* @var array
40+
*/
41+
protected $filterScopes = [];
42+
43+
/**
44+
* Adapter constructor.
45+
*
46+
* @param StandardStrategy $paging
47+
*/
48+
public function __construct(StandardStrategy $paging)
49+
{
50+
parent::__construct(new Image(), $paging);
51+
}
52+
53+
/**
54+
* @param Builder $query
55+
* @param Collection $filters
56+
* @return void
57+
*/
58+
protected function filter($query, Collection $filters)
59+
{
60+
$this->filterWithScopes($query, $filters);
61+
}
62+
63+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
<?php
2+
3+
namespace DummyApp\JsonApi\Images;
4+
5+
use DummyApp\Image;
6+
use Neomerx\JsonApi\Schema\SchemaProvider;
7+
8+
class Schema extends SchemaProvider
9+
{
10+
11+
/**
12+
* @var string
13+
*/
14+
protected $resourceType = 'images';
15+
16+
/**
17+
* @param Image $resource
18+
* the domain record being serialized.
19+
* @return string
20+
*/
21+
public function getId($resource)
22+
{
23+
return (string) $resource->getRouteKey();
24+
}
25+
26+
/**
27+
* @param Image $resource
28+
* the domain record being serialized.
29+
* @return array
30+
*/
31+
public function getAttributes($resource)
32+
{
33+
return [
34+
'created-at' => $resource->created_at->toAtomString(),
35+
'updated-at' => $resource->updated_at->toAtomString(),
36+
'url' => $resource->url,
37+
];
38+
}
39+
}

tests/dummy/app/JsonApi/Posts/Adapter.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
use CloudCreativity\LaravelJsonApi\Eloquent\BelongsTo;
2222
use CloudCreativity\LaravelJsonApi\Eloquent\Concerns\SoftDeletesModels;
2323
use CloudCreativity\LaravelJsonApi\Eloquent\HasMany;
24+
use CloudCreativity\LaravelJsonApi\Eloquent\HasOne;
2425
use CloudCreativity\LaravelJsonApi\Eloquent\QueriesMany;
2526
use CloudCreativity\LaravelJsonApi\Eloquent\QueriesOne;
2627
use CloudCreativity\LaravelJsonApi\Pagination\StandardStrategy;
@@ -88,6 +89,14 @@ protected function comments()
8889
return $this->hasMany();
8990
}
9091

92+
/**
93+
* @return HasOne
94+
*/
95+
protected function image()
96+
{
97+
return $this->hasOne();
98+
}
99+
91100
/**
92101
* @return HasMany
93102
*/

tests/dummy/app/JsonApi/Posts/Schema.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ class Schema extends EloquentSchema
4444
protected $relationships = [
4545
'author',
4646
'comments',
47+
'image',
4748
'tags',
4849
];
4950

tests/dummy/app/JsonApi/Posts/Validators.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ class Validators extends AbstractValidators
5454
'author',
5555
'comments',
5656
'comments.created-by',
57+
'image',
5758
'tags',
5859
];
5960

tests/dummy/app/Post.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
use Illuminate\Database\Eloquent\Model;
2222
use Illuminate\Database\Eloquent\Relations\BelongsTo;
2323
use Illuminate\Database\Eloquent\Relations\MorphMany;
24+
use Illuminate\Database\Eloquent\Relations\MorphOne;
2425
use Illuminate\Database\Eloquent\Relations\MorphToMany;
2526
use Illuminate\Database\Eloquent\SoftDeletes;
2627

@@ -63,6 +64,14 @@ public function comments()
6364
return $this->morphMany(Comment::class, 'commentable');
6465
}
6566

67+
/**
68+
* @return MorphOne
69+
*/
70+
public function image()
71+
{
72+
return $this->morphOne(Image::class, 'imageable');
73+
}
74+
6675
/**
6776
* @return MorphToMany
6877
*/

0 commit comments

Comments
 (0)