Closed dev-dx closed 3 years ago
Hi, Good question. I've never explicitly tested that and I'm not sure how the Lighthouse detection mechanism works in that case. One thing to say though it that October's way of declaration of relationships works a bit differently than in pure Laravel (i.e. using an array property rather than methods). How did you define your relationship in your October model?
public function comments()
{
return $this->hasMany(Comment::class);
}
would have to be something like
class Post extends Model
{
public $hasMany = [
'comments' => 'Acme\Blog\Models\Comment'
];
}
in October.
I extend Rainlab user to have more detail profile like this
UserModel::extend( function($model) {
$model->hasOne['profile'] = ['Divertx\Profile\Models\Profile'];
$model->hasMany['schools'] = ['Divertx\Profile\Models\ProfileSchool'];
$model->hasMany['works'] = ['Divertx\Profile\Models\ProfileWork'];
$model->belongsToMany['hobbies'] = [\Divertx\Profile\Models\Hobby::class, 'table' => 'divertx_profile_profilehobbies'];
});
This looks right to me. I suspect that Lighthouse uses the type annotation BelongsTo
to infer the relationship type which wouldn't work with October's models. Could you work around this by applying the update explicitly in the resolver?
Yes, i think it is possible to using specific resolver.
But i like the way how nested mutation work. So, finally i hack the code on the vendor/nuwave/lighthouse/src/execution/MutationExecutor.php to comply with october class model
so far it work.
Great! Would you like to share the code modifications here? There may be a way to incorporate them into Headstart or even contribute them upstream.
Hi,
Here the code
`<?php
namespace Nuwave\Lighthouse\Execution;
use ReflectionClass; use ReflectionNamedType; use Illuminate\Support\Collection; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\HasOne; use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\Relations\MorphTo; use Illuminate\Database\Eloquent\Relations\MorphOne; use Illuminate\Database\Eloquent\Relations\Relation; use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\MorphMany; use Illuminate\Database\Eloquent\Relations\MorphToMany; use Illuminate\Database\Eloquent\Relations\BelongsToMany;
class MutationExecutor { /**
@return \Illuminate\Database\Eloquent\Model */ public static function executeCreate(Model $model, Collection $args, ?Relation $parentRelation = null): Model { $reflection = new ReflectionClass($model);
[$hasMany, $remaining] = self::partitionArgsByRelationType($reflection, $args, HasMany::class, $model->hasMany);
[$morphMany, $remaining] = self::partitionArgsByRelationType($reflection, $remaining, MorphMany::class, $model->morphMany);
[$hasOne, $remaining] = self::partitionArgsByRelationType($reflection, $remaining, HasOne::class, $model->hasOne);
[$morphOne, $remaining] = self::partitionArgsByRelationType($reflection, $remaining, MorphOne::class, $model->morphOne);
[$belongsToMany, $remaining] = self::partitionArgsByRelationType($reflection, $remaining, BelongsToMany::class, $model->belongsToMany);
[$morphToMany, $remaining] = self::partitionArgsByRelationType($reflection, $remaining, MorphToMany::class, $model->morphToMany);
$model = self::saveModelWithPotentialParent($model, $remaining, $parentRelation);
$createOneToMany = function (array $nestedOperations, string $relationName) use ($model): void {
/** @var \Illuminate\Database\Eloquent\Relations\HasMany|\Illuminate\Database\Eloquent\Relations\MorphMany $relation */
$relation = $model->{$relationName}();
if (isset($nestedOperations['create'])) {
self::handleMultiRelationCreate(new Collection($nestedOperations['create']), $relation);
}
};
$hasMany->each($createOneToMany);
$morphMany->each($createOneToMany);
$createOneToOne = function (array $nestedOperations, string $relationName) use ($model): void {
/** @var \Illuminate\Database\Eloquent\Relations\HasOne|\Illuminate\Database\Eloquent\Relations\MorphOne $relation */
$relation = $model->{$relationName}();
if (isset($nestedOperations['create'])) {
self::handleSingleRelationCreate(new Collection($nestedOperations['create']), $relation);
}
};
$hasOne->each($createOneToOne);
$morphOne->each($createOneToOne);
$createManyToMany = function (array $nestedOperations, string $relationName) use ($model): void {
/** @var \Illuminate\Database\Eloquent\Relations\BelongsToMany|\Illuminate\Database\Eloquent\Relations\MorphToMany $relation */
$relation = $model->{$relationName}();
if (isset($nestedOperations['sync'])) {
$relation->sync($nestedOperations['sync']);
}
if (isset($nestedOperations['create'])) {
self::handleMultiRelationCreate(new Collection($nestedOperations['create']), $relation);
}
if (isset($nestedOperations['update'])) {
(new Collection($nestedOperations['update']))->each(function ($singleValues) use ($relation): void {
self::executeUpdate(
$relation->getModel()->newInstance(),
new Collection($singleValues),
$relation
);
});
}
if (isset($nestedOperations['connect'])) {
$relation->attach($nestedOperations['connect']);
}
};
$belongsToMany->each($createManyToMany);
$morphToMany->each($createManyToMany);
return $model;
}
/**
@return \Illuminate\Database\Eloquent\Model */ protected static function saveModelWithPotentialParent(Model $model, Collection $args, ?Relation $parentRelation = null): Model { $reflection = new ReflectionClass($model);
// Extract $morphTo first, as MorphTo extends BelongsTo
[$morphTo, $remaining] = self::partitionArgsByRelationType(
$reflection,
$args,
MorphTo::class,
$model->morphTo
);
[$belongsTo, $remaining] = self::partitionArgsByRelationType(
$reflection,
$remaining,
BelongsTo::class,
$model->belongsTo
);
// Use all the remaining attributes and fill the model
$model->fill(
$remaining->all()
);
$belongsTo->each(function (array $nestedOperations, string $relationName) use ($model): void {
/** @var \Illuminate\Database\Eloquent\Relations\BelongsTo $relation */
$relation = $model->{$relationName}();
if (isset($nestedOperations['create'])) {
$belongsToModel = self::executeCreate(
$relation->getModel()->newInstance(),
new Collection($nestedOperations['create'])
);
$relation->associate($belongsToModel);
}
if (isset($nestedOperations['connect'])) {
$relation->associate($nestedOperations['connect']);
}
if (isset($nestedOperations['update'])) {
$belongsToModel = self::executeUpdate(
$relation->getModel()->newInstance(),
new Collection($nestedOperations['update'])
);
$relation->associate($belongsToModel);
}
// We proceed with disconnecting/deleting only if the given $values is truthy.
// There is no other information to be passed when issuing those operations,
// but GraphQL forces us to pass some value. It would be unintuitive for
// the end user if the given value had no effect on the execution.
if ($nestedOperations['disconnect'] ?? false) {
$relation->dissociate();
}
if ($nestedOperations['delete'] ?? false) {
$relation->delete();
}
});
$morphTo->each(function (array $nestedOperations, string $relationName) use ($model): void {
/** @var \Illuminate\Database\Eloquent\Relations\MorphTo $relation */
$relation = $model->{$relationName}();
// TODO implement create and update once we figure out how to do polymorphic input types https://github.com/nuwave/lighthouse/issues/900
if (isset($nestedOperations['connect'])) {
$connectArgs = $nestedOperations['connect'];
$morphToModel = $relation->createModelByType(
(string) $connectArgs['type']
);
$morphToModel->setAttribute(
$morphToModel->getKeyName(),
$connectArgs['id']
);
$relation->associate($morphToModel);
}
// We proceed with disconnecting/deleting only if the given $values is truthy.
// There is no other information to be passed when issuing those operations,
// but GraphQL forces us to pass some value. It would be unintuitive for
// the end user if the given value had no effect on the execution.
if ($nestedOperations['disconnect'] ?? false) {
$relation->dissociate();
}
if ($nestedOperations['delete'] ?? false) {
$relation->delete();
}
});
if ($parentRelation && ! $parentRelation instanceof BelongsToMany) {
// If we are already resolving a nested create, we might
// already have an instance of the parent relation available.
// In that case, use it to set the current model as a child.
$parentRelation->save($model);
return $model;
}
$model->save();
if ($parentRelation instanceof BelongsToMany) {
$parentRelation->syncWithoutDetaching($model);
}
return $model;
}
/**
@return void */ protected static function handleMultiRelationCreate(Collection $multiValues, Relation $relation): void { $multiValues->each(function ($singleValues) use ($relation): void { self::handleSingleRelationCreate(new Collection($singleValues), $relation); }); }
/**
@return void */ protected static function handleSingleRelationCreate(Collection $singleValues, Relation $relation): void { self::executeCreate( $relation->getModel()->newInstance(), $singleValues, $relation ); }
/**
@return \Illuminate\Database\Eloquent\Model */ public static function executeUpdate(Model $model, Collection $args, ?Relation $parentRelation = null): Model { // var_dump($args);die;
$id = $args->pull('id')
?? $args->pull(
$model->getKeyName()
);
$model = $model->newQuery()->findOrFail($id);
$reflection = new ReflectionClass($model);
[$hasMany, $remaining] = self::partitionArgsByRelationType($reflection, $args, HasMany::class, $model->hasMany);
// var_dump($hasMany);die;
[$morphMany, $remaining] = self::partitionArgsByRelationType($reflection, $remaining, MorphMany::class, $model->morphMany);
[$hasOne, $remaining] = self::partitionArgsByRelationType($reflection, $remaining, HasOne::class, $model->hasOne);
[$morphOne, $remaining] = self::partitionArgsByRelationType($reflection, $remaining, MorphOne::class, $model->morphOne);
[$belongsToMany, $remaining] = self::partitionArgsByRelationType($reflection, $remaining, BelongsToMany::class, $model->belongsToMany);
[$morphToMany, $remaining] = self::partitionArgsByRelationType($reflection, $remaining, MorphToMany::class, $model->morphToMany);
$model = self::saveModelWithPotentialParent($model, $remaining, $parentRelation);
$updateOneToMany = function (array $nestedOperations, string $relationName) use ($model): void {
/** @var \Illuminate\Database\Eloquent\Relations\HasMany|\Illuminate\Database\Eloquent\Relations\MorphMany $relation */
$relation = $model->{$relationName}();
if (isset($nestedOperations['create'])) {
self::handleMultiRelationCreate(new Collection($nestedOperations['create']), $relation);
}
if (isset($nestedOperations['update'])) {
(new Collection($nestedOperations['update']))->each(function ($singleValues) use ($relation): void {
self::executeUpdate(
$relation->getModel()->newInstance(),
new Collection($singleValues),
$relation
);
});
}
if (isset($nestedOperations['delete'])) {
$relation->getModel()::destroy($nestedOperations['delete']);
}
};
$hasMany->each($updateOneToMany);
$morphMany->each($updateOneToMany);
$updateOneToOne = function (array $nestedOperations, string $relationName) use ($model): void {
/** @var \Illuminate\Database\Eloquent\Relations\HasOne|\Illuminate\Database\Eloquent\Relations\MorphOne $relation */
$relation = $model->{$relationName}();
if (isset($nestedOperations['create'])) {
self::handleSingleRelationCreate(new Collection($nestedOperations['create']), $relation);
}
if (isset($nestedOperations['update'])) {
self::executeUpdate(
$relation->getModel()->newInstance(),
new Collection($nestedOperations['update']),
$relation
);
}
if (isset($nestedOperations['delete'])) {
$relation->getModel()::destroy($nestedOperations['delete']);
}
};
$hasOne->each($updateOneToOne);
$morphOne->each($updateOneToOne);
$updateManyToMany = function (array $nestedOperations, string $relationName) use ($model): void {
/** @var \Illuminate\Database\Eloquent\Relations\BelongsToMany|\Illuminate\Database\Eloquent\Relations\MorphToMany $relation */
$relation = $model->{$relationName}();
if (isset($nestedOperations['sync'])) {
$relation->sync($nestedOperations['sync']);
}
if (isset($nestedOperations['create'])) {
self::handleMultiRelationCreate(new Collection($nestedOperations['create']), $relation);
}
if (isset($nestedOperations['update'])) {
(new Collection($nestedOperations['update']))->each(function ($singleValues) use ($relation): void {
self::executeUpdate(
$relation->getModel()->newInstance(),
new Collection($singleValues),
$relation
);
});
}
if (isset($nestedOperations['delete'])) {
$relation->detach($nestedOperations['delete']);
$relation->getModel()::destroy($nestedOperations['delete']);
}
if (isset($nestedOperations['connect'])) {
$relation->attach($nestedOperations['connect']);
}
if (isset($nestedOperations['disconnect'])) {
$relation->detach($nestedOperations['disconnect']);
}
};
$belongsToMany->each($updateManyToMany);
$morphToMany->each($updateManyToMany);
return $model;
}
/**
@return \Illuminate\Support\Collection [relationshipArgs, remainingArgs] */ protected static function partitionArgsByRelationType(ReflectionClass $modelReflection, Collection $args, string $relationClass, array $relationArray=[]): Collection {
if ($relationArray) {
return $args->partition(
function ($value, string $key) use ($modelReflection, $relationArray): bool {
return isset($relationArray[$key])?true:false;
}
);
}
return $args->partition(
function ($value, string $key) use ($modelReflection, $relationClass): bool {
if (! $modelReflection->hasMethod($key)) {
return false;
}
$relationMethodCandidate = $modelReflection->getMethod($key);
if (! $returnType = $relationMethodCandidate->getReturnType()) {
return false;
}
if (! $returnType instanceof ReflectionNamedType) {
return false;
}
return is_a($returnType->getName(), $relationClass, true);
}
);
} } `
Hi,
I try to implement nested mutation using this plugin. But it seems the nested mutation is not executed. Only the parent Type was updated.
I notice that on the lighthouse documentation mention like this :
Return Types Required
You have to define return types on your relationship methods so that Lighthouse can detect them.
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class Post extends Model { // WORKS public function user(): BelongsTo { return $this->belongsTo(User::class); }
}
My question is, is nested mutations working using your plugin ?