Skip to content

Commit ea7320b

Browse files
committed
fix(relations): fix bug with overriden Model relations
also refactor StoreTransformer Relations management
1 parent 4cc1501 commit ea7320b

8 files changed

+280
-83
lines changed
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
<?php
2+
3+
namespace StudioNet\GraphQL\Support\Transformer\Eloquent\Relation;
4+
5+
use Illuminate\Database\Eloquent\Relations;
6+
use Illuminate\Database\Eloquent\Model;
7+
8+
class AbstractRelationTransformer {
9+
10+
/**
11+
* Relation linked entity to hydrate.
12+
*
13+
* @var Relations\Relation
14+
*/
15+
protected $relation;
16+
17+
/**
18+
* Values used to hydrate relation.
19+
*
20+
* @var array
21+
*/
22+
protected $values;
23+
24+
/**
25+
* Entity owner of relation to update.
26+
*
27+
* @var Model
28+
*/
29+
protected $model;
30+
31+
/**
32+
* Relation's column.
33+
*
34+
* @var string
35+
*/
36+
protected $column;
37+
38+
/**
39+
* Entity to hydrate.
40+
*
41+
* @var Relations\Relation
42+
*/
43+
protected $entity;
44+
45+
/**
46+
* Constructor.
47+
*/
48+
public function __construct(Model $model, string $column, array $values) {
49+
$this->model = $model;
50+
$this->column = $column;
51+
$this->values = $values;
52+
$this->resetRelation();
53+
}
54+
55+
protected function resetRelation() {
56+
$this->relation = $this->model->{$this->column}();
57+
}
58+
59+
/**
60+
* Save values to entity linked by relation and associate it.
61+
*/
62+
public function transform() {
63+
$this->hydrate();
64+
$this->associate();
65+
}
66+
67+
/**
68+
* Store values in entity.
69+
*/
70+
protected function hydrate() {
71+
}
72+
73+
/**
74+
* Associated created/updated entity.
75+
*/
76+
protected function associate() {
77+
}
78+
79+
/**
80+
* Meant to be executed after Relation owner save.
81+
*/
82+
public function afterSAve() {
83+
}
84+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
<?php
2+
3+
namespace StudioNet\GraphQL\Support\Transformer\Eloquent\Relation;
4+
5+
use Illuminate\Database\Eloquent\Relations;
6+
7+
class BelongsToManyRelationTransformer extends AbstractRelationTransformer {
8+
9+
/**
10+
* Store values in entity.
11+
*/
12+
protected function hydrate() {
13+
if (!is_array(array_first($this->values))) {
14+
$this->values = [$this->values];
15+
}
16+
17+
$toKeep = array_map(function ($value) {
18+
return array_get($value, 'id', null);
19+
}, $this->values);
20+
21+
$this->resetRelation();
22+
23+
$this->values = array_filter($toKeep, function ($value) {
24+
return !is_null($value);
25+
});
26+
}
27+
28+
/**
29+
* Override.
30+
*/
31+
public function afterSave() {
32+
$this->relation->sync($this->values);
33+
}
34+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<?php
2+
3+
namespace StudioNet\GraphQL\Support\Transformer\Eloquent\Relation;
4+
5+
use Illuminate\Database\Eloquent\Relations;
6+
7+
class BelongsToRelationTransformer extends HasOneRelationTransformer {
8+
9+
/**
10+
* Associated created/updated entity.
11+
*/
12+
protected function associate() {
13+
$this->relation->associate($this->entity);
14+
}
15+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
<?php
2+
3+
namespace StudioNet\GraphQL\Support\Transformer\Eloquent\Relation;
4+
5+
use Illuminate\Database\Eloquent\Relations;
6+
7+
class DefaultRelationTransformer extends AbstractRelationTransformer {
8+
9+
/**
10+
* Store values in entity.
11+
*/
12+
protected function hydrate() {
13+
if (!is_array(array_first($this->values))) {
14+
$this->values = [$this->values];
15+
}
16+
17+
foreach ($this->values as $values) {
18+
$this->resetRelation();
19+
$entity = $this->relation->findOrNew(array_get($values, 'id', null));
20+
$fill = [];
21+
22+
foreach (array_keys($values) as $key) {
23+
if ($entity->isFillable($key)) {
24+
$fill[$key] = $values[$key];
25+
}
26+
}
27+
$entity->fill($fill)->save();
28+
}
29+
}
30+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<?php
2+
3+
namespace StudioNet\GraphQL\Support\Transformer\Eloquent\Relation;
4+
5+
use Illuminate\Database\Eloquent\Relations;
6+
7+
class HasOneRelationTransformer extends AbstractRelationTransformer {
8+
9+
/**
10+
* Store values in entity.
11+
*/
12+
protected function hydrate() {
13+
$this->entity = $this->relation->getRelated()->findOrNew(array_get($this->values, 'id', null));
14+
15+
if (empty($this->entity->id)) {
16+
$this->entity = $this->relation->firstOrNew([]);
17+
}
18+
$this->entity->fill($this->values)->save();
19+
}
20+
21+
/**
22+
* Associate created/updated entity.
23+
*/
24+
protected function associate() {
25+
$this->relation->save($this->entity);
26+
}
27+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
<?php
2+
3+
namespace StudioNet\GraphQL\Support\Transformer\Eloquent\Relation;
4+
5+
use Illuminate\Database\Eloquent\Relations;
6+
7+
class MorphToRelationTransformer extends AbstractRelationTransformer {
8+
9+
/**
10+
* Store values in entity.
11+
*/
12+
protected function hydrate() {
13+
$id = array_get($this->values, 'id', null);
14+
$type = array_get($this->values, '__typename', null);
15+
16+
if (is_null($type)) {
17+
throw new \Exception(
18+
"Can't update polymorphic relation without specify type"
19+
);
20+
}
21+
22+
// TODO: maybe there is a smarter way to guess type
23+
$className = '\App\\' . $type;
24+
if (!class_exists($className)) {
25+
throw new \Exception("Unknown $className type");
26+
}
27+
28+
$this->entity = $className::findOrNew($id);
29+
$this->entity->fill($this->values)->save();
30+
}
31+
32+
/**
33+
* Associate created/updated entity.
34+
*/
35+
protected function associate() {
36+
$this->relation->associate($this->entity);
37+
}
38+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
<?php
2+
3+
namespace StudioNet\GraphQL\Support\Transformer\Eloquent\Relation;
4+
5+
use Illuminate\Database\Eloquent\Model;
6+
7+
class RelationTransformerFactory {
8+
public static function getTransformer(Model $model, string $column, array $values) {
9+
$relation = $model->{$column}();
10+
$classesToTest = [];
11+
12+
// Try if transformer exists like {RelationName}RelationTransformee
13+
$classesToTest[] = (new \ReflectionClass($relation))->getShortName();
14+
15+
$eloquentNs = 'Illuminate\Database\Eloquent\Relations';
16+
// If Relation is an override of an Eloquent relation type, try to get
17+
// ancestor to find "Generic" relation.
18+
if (strpos(get_class($relation), $eloquentNs) === false) {
19+
$parents = class_parents($relation);
20+
foreach ($parents as $parent) {
21+
if (strpos($parent, $eloquentNs) === false) {
22+
continue;
23+
}
24+
$classesToTest[] = (new \ReflectionClass($parent))->getShortName();
25+
break;
26+
}
27+
}
28+
29+
foreach ($classesToTest as $classToTest) {
30+
$directClass = str_replace("Abstract", $classToTest, AbstractRelationTransformer::class);
31+
if (class_exists($directClass)) {
32+
return new $directClass($model, $column, $values);
33+
}
34+
}
35+
36+
// Fallback
37+
return new DefaultRelationTransformer($model, $column, $values);
38+
}
39+
}

src/Support/Transformer/Eloquent/StoreTransformer.php

Lines changed: 13 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
use Illuminate\Database\Eloquent\Builder;
55
use StudioNet\GraphQL\Support\Transformer\EloquentTransformer;
6+
use StudioNet\GraphQL\Support\Transformer\Eloquent\Relation\RelationTransformer;
67
use StudioNet\GraphQL\Support\Definition\Definition;
78
use StudioNet\GraphQL\Definition\Type;
89
use Illuminate\Database\Eloquent\Relations;
@@ -127,101 +128,30 @@ protected function getResolver(array $opts) {
127128

128129
$this->validate($data, $opts['rules']);
129130
$model->fill($data);
130-
$syncLater = [];
131+
$relationTransformers = [];
131132
foreach ($relationInput as $column => $values) {
132133
if (empty($values)) {
133134
// TODO: check if it's pertinent
134135
// empty values are ignored because, currently, nothing is deleted through nested update
135136
// it can be problematic because empty top level fields are emptied.
136137
continue;
137138
}
138-
139-
$relation = $model->{$column}();
140-
141-
// If we are on a hasOne or belongsTo relationship, we have to
142-
// manage the firstOrNew case
143-
//
144-
// https://laracasts.com/discuss/channels/general-discussion/hasone-create-duplicates
145-
$relationType = get_class($relation);
146-
if (in_array($relationType, [Relations\HasOne::class, Relations\BelongsTo::class])) {
147-
$dep = $relation->getRelated()->findOrNew(array_get($values, 'id', null));
148-
149-
if (empty($dep->id)) {
150-
$dep = $relation->firstOrNew([]);
151-
}
152-
$dep->fill($values)->save();
153-
154-
switch ($relationType) {
155-
case Relations\BelongsTo::class:
156-
$relation->associate($dep);
157-
break;
158-
default:
159-
$relation->save($dep);
160-
}
161-
} elseif ($relationType === Relations\MorphTo::class) {
162-
$id = array_get($values, 'id', null);
163-
$type = array_get($values, '__typename', null);
164-
165-
if (is_null($type)) {
166-
throw new \Exception(
167-
"Can't update polymorphic relation without specify type"
168-
);
169-
}
170-
171-
// TODO: maybe there is a smarter way to guess type
172-
$className = '\App\\' . $type;
173-
if (!class_exists($className)) {
174-
throw new \Exception("Unknown $className type");
175-
}
176-
177-
$dep = $className::findOrNew($id);
178-
$dep->fill($values)->save();
179-
$relation->associate($dep);
180-
} else {
181-
if (!is_array(array_first($values))) {
182-
$values = [$values];
183-
}
184-
185-
if ($relationType === Relations\BelongsToMany::class) {
186-
$toKeep = array_map(function ($value) {
187-
return array_get($value, 'id', null);
188-
}, $values);
189-
190-
$relation = $model->{$column}();
191-
// Do the sync after model save, 'coz if model doesn't exist
192-
// in DB yet, it will result with a NOT NULL error in pivot table
193-
$syncLater[] = [
194-
"relation" => $relation,
195-
"values" => array_filter($toKeep, function ($value) {
196-
return !is_null($value);
197-
})
198-
];
199-
} else {
200-
// For each relationship, find or new by id and fill with data
201-
foreach ($values as $value) {
202-
// TODO: refactor
203-
// $relation is reset because findOrNew updates it and where
204-
// clauses are stacked.
205-
$relation = $model->{$column}();
206-
$entity = $relation->findOrNew(array_get($value, 'id', null));
207-
$fill = [];
208-
209-
foreach (array_keys($value) as $key) {
210-
if ($entity->isFillable($key)) {
211-
$fill[$key] = $value[$key];
212-
}
213-
}
214-
$entity->fill($fill)->save();
215-
}
216-
}
217-
}
139+
$relationTransformer = Relation\RelationTransformerFactory::getTransformer(
140+
$model,
141+
$column,
142+
$values
143+
);
144+
$relationTransformer->transform();
145+
$relationTransformers[] = $relationTransformer;
218146
}
219147

220148
$model->save();
221149

222150
// Sync relations which need to be synced after save
223-
foreach ($syncLater as $sync) {
224-
$sync["relation"]->sync($sync["values"]);
151+
// TODO: it will be create to attach a callback to $model
152+
// to be fired at 'saved' event
153+
foreach ($relationTransformers as $relationTransformer) {
154+
$relationTransformer->afterSave();
225155
}
226156

227157
// Apply post-save callBacks

0 commit comments

Comments
 (0)