new Deps
This commit is contained in:
@@ -0,0 +1,209 @@
|
||||
<?php
|
||||
|
||||
namespace Nuwave\Lighthouse\Execution\Arguments;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Database\Eloquent\Relations\HasOne;
|
||||
use Illuminate\Database\Eloquent\Relations\MorphMany;
|
||||
use Illuminate\Database\Eloquent\Relations\MorphOne;
|
||||
use Illuminate\Database\Eloquent\Relations\MorphToMany;
|
||||
use Nuwave\Lighthouse\Exceptions\DefinitionException;
|
||||
use Nuwave\Lighthouse\Support\Contracts\ArgResolver;
|
||||
use Nuwave\Lighthouse\Support\Utils;
|
||||
use ReflectionClass;
|
||||
use ReflectionNamedType;
|
||||
|
||||
class ArgPartitioner
|
||||
{
|
||||
/**
|
||||
* Partition the arguments into nested and regular.
|
||||
*
|
||||
* @param \Nuwave\Lighthouse\Execution\Arguments\ArgumentSet $argumentSet
|
||||
* @return array<\Nuwave\Lighthouse\Execution\Arguments\ArgumentSet>
|
||||
*/
|
||||
public static function nestedArgResolvers(ArgumentSet $argumentSet, $root): array
|
||||
{
|
||||
$model = $root instanceof Model
|
||||
? new \ReflectionClass($root)
|
||||
: null;
|
||||
|
||||
foreach ($argumentSet->arguments as $name => $argument) {
|
||||
static::attachNestedArgResolver($name, $argument, $model);
|
||||
}
|
||||
|
||||
return static::partition(
|
||||
$argumentSet,
|
||||
static function (string $name, Argument $argument): bool {
|
||||
return $argument->resolver !== null;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract all the arguments that correspond to a relation of a certain type on the model.
|
||||
*
|
||||
* For example, if the args input looks like this:
|
||||
*
|
||||
* [
|
||||
* 'name' => 'Ralf',
|
||||
* 'comments' =>
|
||||
* ['foo' => 'Bar'],
|
||||
* ]
|
||||
*
|
||||
* and the model has a method "comments" that returns a HasMany relationship,
|
||||
* the result will be:
|
||||
* [
|
||||
* [
|
||||
* 'comments' =>
|
||||
* ['foo' => 'Bar'],
|
||||
* ],
|
||||
* [
|
||||
* 'name' => 'Ralf',
|
||||
* ]
|
||||
* ]
|
||||
*
|
||||
* @param \Nuwave\Lighthouse\Execution\Arguments\ArgumentSet $argumentSet
|
||||
* @return array{0: \Nuwave\Lighthouse\Execution\Arguments\ArgumentSet, 1: \Nuwave\Lighthouse\Execution\Arguments\ArgumentSet}
|
||||
*/
|
||||
public static function relationMethods(
|
||||
ArgumentSet $argumentSet,
|
||||
Model $model,
|
||||
string $relationClass
|
||||
): array {
|
||||
$modelReflection = new ReflectionClass($model);
|
||||
|
||||
[$relations, $remaining] = static::partition(
|
||||
$argumentSet,
|
||||
static function (string $name) use ($modelReflection, $relationClass): bool {
|
||||
return static::methodReturnsRelation($modelReflection, $name, $relationClass);
|
||||
}
|
||||
);
|
||||
|
||||
$nonNullRelations = new ArgumentSet();
|
||||
$nonNullRelations->arguments = array_filter(
|
||||
$relations->arguments,
|
||||
static function (Argument $argument): bool {
|
||||
return null !== $argument->value;
|
||||
}
|
||||
);
|
||||
|
||||
return [$nonNullRelations, $remaining];
|
||||
}
|
||||
|
||||
/**
|
||||
* Attach a nested argument resolver to an argument.
|
||||
*
|
||||
* @param \Nuwave\Lighthouse\Execution\Arguments\Argument $argument
|
||||
*/
|
||||
protected static function attachNestedArgResolver(string $name, Argument &$argument, ?ReflectionClass $model): void
|
||||
{
|
||||
$resolverDirective = $argument->directives->first(
|
||||
Utils::instanceofMatcher(ArgResolver::class)
|
||||
);
|
||||
|
||||
if ($resolverDirective) {
|
||||
$argument->resolver = $resolverDirective;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (isset($model)) {
|
||||
$isRelation = static function (string $relationClass) use ($model, $name): bool {
|
||||
return static::methodReturnsRelation($model, $name, $relationClass);
|
||||
};
|
||||
|
||||
if (
|
||||
$isRelation(HasOne::class)
|
||||
|| $isRelation(MorphOne::class)
|
||||
) {
|
||||
$argument->resolver = new ResolveNested(new NestedOneToOne($name));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
$isRelation(HasMany::class)
|
||||
|| $isRelation(MorphMany::class)
|
||||
) {
|
||||
$argument->resolver = new ResolveNested(new NestedOneToMany($name));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
$isRelation(BelongsToMany::class)
|
||||
|| $isRelation(MorphToMany::class)
|
||||
) {
|
||||
$argument->resolver = new ResolveNested(new NestedManyToMany($name));
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Partition arguments based on a predicate.
|
||||
*
|
||||
* The predicate will be called for each argument within the ArgumentSet
|
||||
* with the following parameters:
|
||||
* 1. The name of the argument
|
||||
* 2. The argument itself
|
||||
*
|
||||
* Returns an array of two new ArgumentSet instances:
|
||||
* - the first one contains all arguments for which the predicate matched
|
||||
* - the second one contains all arguments for which the predicate did not match
|
||||
*
|
||||
* @param \Nuwave\Lighthouse\Execution\Arguments\ArgumentSet $argumentSet
|
||||
* @return array{0: \Nuwave\Lighthouse\Execution\Arguments\ArgumentSet, 1: \Nuwave\Lighthouse\Execution\Arguments\ArgumentSet}
|
||||
*/
|
||||
public static function partition(ArgumentSet $argumentSet, \Closure $predicate): array
|
||||
{
|
||||
$matched = new ArgumentSet();
|
||||
$notMatched = new ArgumentSet();
|
||||
|
||||
foreach ($argumentSet->arguments as $name => $argument) {
|
||||
if ($predicate($name, $argument)) {
|
||||
$matched->arguments[$name] = $argument;
|
||||
} else {
|
||||
$notMatched->arguments[$name] = $argument;
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
$matched,
|
||||
$notMatched,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Does a method on the model return a relation of the given class?
|
||||
*/
|
||||
public static function methodReturnsRelation(
|
||||
ReflectionClass $modelReflection,
|
||||
string $name,
|
||||
string $relationClass
|
||||
): bool {
|
||||
if (! $modelReflection->hasMethod($name)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$relationMethodCandidate = $modelReflection->getMethod($name);
|
||||
|
||||
$returnType = $relationMethodCandidate->getReturnType();
|
||||
if ($returnType === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (! $returnType instanceof ReflectionNamedType) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (! class_exists($returnType->getName())) {
|
||||
throw new DefinitionException('Class '.$returnType->getName().' does not exist, did you forget to import the Eloquent relation class?');
|
||||
}
|
||||
|
||||
return is_a($returnType->getName(), $relationClass, true);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
<?php
|
||||
|
||||
namespace Nuwave\Lighthouse\Execution\Arguments;
|
||||
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
class Argument
|
||||
{
|
||||
/**
|
||||
* The value given by the client.
|
||||
*
|
||||
* @var \Nuwave\Lighthouse\Execution\Arguments\ArgumentSet|array<\Nuwave\Lighthouse\Execution\Arguments\ArgumentSet>|mixed|array<mixed>
|
||||
*/
|
||||
public $value;
|
||||
|
||||
/**
|
||||
* The type of the argument.
|
||||
*
|
||||
* @var \Nuwave\Lighthouse\Execution\Arguments\ListType|\Nuwave\Lighthouse\Execution\Arguments\NamedType|null
|
||||
*/
|
||||
public $type;
|
||||
|
||||
/**
|
||||
* A list of directives associated with that argument.
|
||||
*
|
||||
* @var \Illuminate\Support\Collection<\Nuwave\Lighthouse\Support\Contracts\Directive>
|
||||
*/
|
||||
public $directives;
|
||||
|
||||
/**
|
||||
* An argument may have a resolver that handles it's given value.
|
||||
*
|
||||
* @var \Nuwave\Lighthouse\Support\Contracts\ArgResolver|null
|
||||
*/
|
||||
public $resolver;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->directives = new Collection();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the plain PHP value of this argument.
|
||||
*
|
||||
* @return mixed The plain PHP value.
|
||||
*/
|
||||
public function toPlain()
|
||||
{
|
||||
return static::toPlainRecursive($this->value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert the given value to plain PHP values recursively.
|
||||
*
|
||||
* @param \Nuwave\Lighthouse\Execution\Arguments\ArgumentSet|array<\Nuwave\Lighthouse\Execution\Arguments\ArgumentSet>|mixed|array<mixed> $value
|
||||
* @return mixed|array<mixed>
|
||||
*/
|
||||
protected static function toPlainRecursive($value)
|
||||
{
|
||||
if ($value instanceof ArgumentSet) {
|
||||
return $value->toArray();
|
||||
}
|
||||
|
||||
if (is_array($value)) {
|
||||
return array_map([static::class, 'toPlainRecursive'], $value);
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,275 @@
|
||||
<?php
|
||||
|
||||
namespace Nuwave\Lighthouse\Execution\Arguments;
|
||||
|
||||
use Closure;
|
||||
use Nuwave\Lighthouse\Schema\Directives\RenameDirective;
|
||||
use Nuwave\Lighthouse\Schema\Directives\SpreadDirective;
|
||||
use Nuwave\Lighthouse\Scout\ScoutEnhancer;
|
||||
use Nuwave\Lighthouse\Support\Contracts\ArgBuilderDirective;
|
||||
use Nuwave\Lighthouse\Support\Contracts\FieldBuilderDirective;
|
||||
use Nuwave\Lighthouse\Support\Utils;
|
||||
|
||||
class ArgumentSet
|
||||
{
|
||||
/**
|
||||
* An associative array from argument names to arguments.
|
||||
*
|
||||
* @var array<string, \Nuwave\Lighthouse\Execution\Arguments\Argument>
|
||||
*/
|
||||
public $arguments = [];
|
||||
|
||||
/**
|
||||
* An associative array of arguments that were not given.
|
||||
*
|
||||
* @var array<string, \Nuwave\Lighthouse\Execution\Arguments\Argument>
|
||||
*/
|
||||
public $undefined = [];
|
||||
|
||||
/**
|
||||
* A list of directives.
|
||||
*
|
||||
* This may be coming from
|
||||
* - the field the arguments are a part of
|
||||
* - the parent argument when in a tree of nested inputs.
|
||||
*
|
||||
* @var \Illuminate\Support\Collection<\Nuwave\Lighthouse\Support\Contracts\Directive>
|
||||
*/
|
||||
public $directives;
|
||||
|
||||
/**
|
||||
* Get a plain array representation of this ArgumentSet.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function toArray(): array
|
||||
{
|
||||
$plainArguments = [];
|
||||
|
||||
foreach ($this->arguments as $name => $argument) {
|
||||
$plainArguments[$name] = $argument->toPlain();
|
||||
}
|
||||
|
||||
return $plainArguments;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the ArgumentSet has a non-null value with the given key.
|
||||
*/
|
||||
public function has(string $key): bool
|
||||
{
|
||||
$argument = $this->arguments[$key] ?? null;
|
||||
|
||||
if ($argument === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $argument->value !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply the @spread directive and return a new, modified instance.
|
||||
*
|
||||
* @noRector \Rector\DeadCode\Rector\ClassMethod\RemoveDeadRecursiveClassMethodRector
|
||||
*/
|
||||
public function spread(): self
|
||||
{
|
||||
$argumentSet = new self();
|
||||
$argumentSet->directives = $this->directives;
|
||||
|
||||
foreach ($this->arguments as $name => $argument) {
|
||||
$value = $argument->value;
|
||||
|
||||
// In this case, we do not care about argument sets nested within
|
||||
// lists, spreading only makes sense for single nested inputs.
|
||||
if ($value instanceof self) {
|
||||
// Recurse down first, as that resolves the more deeply nested spreads first
|
||||
$value = $value->spread();
|
||||
|
||||
if ($argument->directives->contains(
|
||||
Utils::instanceofMatcher(SpreadDirective::class)
|
||||
)) {
|
||||
$argumentSet->arguments += $value->arguments;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
$argumentSet->arguments[$name] = $argument;
|
||||
}
|
||||
|
||||
return $argumentSet;
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply the @rename directive and return a new, modified instance.
|
||||
*
|
||||
* @noRector \Rector\DeadCode\Rector\ClassMethod\RemoveDeadRecursiveClassMethodRector
|
||||
*/
|
||||
public function rename(): self
|
||||
{
|
||||
$argumentSet = new self();
|
||||
$argumentSet->directives = $this->directives;
|
||||
|
||||
foreach ($this->arguments as $name => $argument) {
|
||||
// Recursively apply the renaming to nested inputs.
|
||||
// We look for further ArgumentSet instances, they
|
||||
// might be contained within an array.
|
||||
$argument->value = Utils::applyEach(
|
||||
function ($value) {
|
||||
if ($value instanceof self) {
|
||||
return $value->rename();
|
||||
}
|
||||
|
||||
return $value;
|
||||
},
|
||||
$argument->value
|
||||
);
|
||||
|
||||
/** @var \Nuwave\Lighthouse\Schema\Directives\RenameDirective|null $renameDirective */
|
||||
$renameDirective = $argument->directives->first(function ($directive) {
|
||||
return $directive instanceof RenameDirective;
|
||||
});
|
||||
|
||||
if ($renameDirective !== null) {
|
||||
$argumentSet->arguments[$renameDirective->attributeArgValue()] = $argument;
|
||||
} else {
|
||||
$argumentSet->arguments[$name] = $argument;
|
||||
}
|
||||
}
|
||||
|
||||
return $argumentSet;
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply ArgBuilderDirectives and scopes to the builder.
|
||||
*
|
||||
* @param \Illuminate\Database\Query\Builder|\Illuminate\Database\Eloquent\Builder $builder
|
||||
* @param array<string> $scopes
|
||||
* @param \Closure $directiveFilter
|
||||
*
|
||||
* @return \Illuminate\Database\Query\Builder|\Illuminate\Database\Eloquent\Builder|\Laravel\Scout\Builder
|
||||
*/
|
||||
public function enhanceBuilder(object $builder, array $scopes, Closure $directiveFilter = null): object
|
||||
{
|
||||
$scoutEnhancer = new ScoutEnhancer($this, $builder);
|
||||
if ($scoutEnhancer->hasSearchArguments()) {
|
||||
return $scoutEnhancer->enhanceBuilder();
|
||||
}
|
||||
|
||||
self::applyArgBuilderDirectives($this, $builder, $directiveFilter);
|
||||
self::applyFieldBuilderDirectives($this, $builder);
|
||||
|
||||
foreach ($scopes as $scope) {
|
||||
$builder->{$scope}($this->toArray());
|
||||
}
|
||||
|
||||
return $builder;
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively apply the ArgBuilderDirectives onto the builder.
|
||||
*
|
||||
* TODO get rid of the reference passing in here. The issue is that @search makes a new builder instance,
|
||||
* but we must special case that in some way anyhow, as only eq filters can be added on top of search.
|
||||
*
|
||||
* @param \Nuwave\Lighthouse\Execution\Arguments\ArgumentSet $argumentSet
|
||||
* @param \Illuminate\Database\Query\Builder|\Illuminate\Database\Eloquent\Builder $builder
|
||||
* @param (\Closure(\Nuwave\Lighthouse\Support\Contracts\ArgBuilderDirective): bool)|null $directiveFilter
|
||||
*/
|
||||
protected static function applyArgBuilderDirectives(self $argumentSet, object &$builder, Closure $directiveFilter = null): void
|
||||
{
|
||||
foreach ($argumentSet->arguments as $argument) {
|
||||
$value = $argument->toPlain();
|
||||
|
||||
// TODO switch to instanceof when we require bensampo/laravel-enum
|
||||
// Unbox Enum values to ensure their underlying value is used for queries
|
||||
if (is_a($value, '\BenSampo\Enum\Enum')) {
|
||||
$value = $value->value;
|
||||
}
|
||||
|
||||
$filteredDirectives = $argument
|
||||
->directives
|
||||
->filter(Utils::instanceofMatcher(ArgBuilderDirective::class));
|
||||
|
||||
if (null !== $directiveFilter) {
|
||||
$filteredDirectives = $filteredDirectives->filter($directiveFilter);
|
||||
}
|
||||
|
||||
$filteredDirectives->each(static function (ArgBuilderDirective $argBuilderDirective) use (&$builder, $value): void {
|
||||
$builder = $argBuilderDirective->handleBuilder($builder, $value);
|
||||
});
|
||||
|
||||
Utils::applyEach(
|
||||
static function ($value) use (&$builder, $directiveFilter) {
|
||||
if ($value instanceof self) {
|
||||
self::applyArgBuilderDirectives($value, $builder, $directiveFilter);
|
||||
}
|
||||
},
|
||||
$argument->value
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply the FieldBuilderDirectives onto the builder.
|
||||
*
|
||||
* TODO get rid of the reference passing in here. The issue is that @search makes a new builder instance,
|
||||
* but we must special case that in some way anyhow, as only eq filters can be added on top of search.
|
||||
*
|
||||
* @param \Nuwave\Lighthouse\Execution\Arguments\ArgumentSet $argumentSet
|
||||
* @param \Illuminate\Database\Query\Builder|\Illuminate\Database\Eloquent\Builder $builder
|
||||
*/
|
||||
protected static function applyFieldBuilderDirectives(self $argumentSet, object &$builder): void
|
||||
{
|
||||
$argumentSet->directives
|
||||
->filter(Utils::instanceofMatcher(FieldBuilderDirective::class))
|
||||
->each(static function (FieldBuilderDirective $fieldBuilderDirective) use (&$builder): void {
|
||||
$builder = $fieldBuilderDirective->handleFieldBuilder($builder);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a value at the dot-separated path.
|
||||
*
|
||||
* Works just like @see \Illuminate\Support\Arr::add().
|
||||
*
|
||||
* @param mixed $value Any value to inject.
|
||||
* @return $this
|
||||
*/
|
||||
public function addValue(string $path, $value): self
|
||||
{
|
||||
$argumentSet = $this;
|
||||
$keys = explode('.', $path);
|
||||
|
||||
while (count($keys) > 1) {
|
||||
$key = array_shift($keys);
|
||||
|
||||
// If the key doesn't exist at this depth, we will just create an empty ArgumentSet
|
||||
// to hold the next value, allowing us to create the ArgumentSet to hold a final
|
||||
// value at the correct depth. Then we'll keep digging into the ArgumentSet.
|
||||
if (! isset($argumentSet->arguments[$key])) {
|
||||
$argument = new Argument();
|
||||
$argument->value = new self();
|
||||
$argumentSet->arguments[$key] = $argument;
|
||||
}
|
||||
|
||||
$argumentSet = $argumentSet->arguments[$key]->value;
|
||||
}
|
||||
|
||||
$argument = new Argument();
|
||||
$argument->value = $value;
|
||||
$argumentSet->arguments[array_shift($keys)] = $argument;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* The contained arguments, including all that were not passed.
|
||||
*
|
||||
* @return array<string, \Nuwave\Lighthouse\Execution\Arguments\Argument>
|
||||
*/
|
||||
public function argumentsWithUndefined(): array
|
||||
{
|
||||
return array_merge($this->arguments, $this->undefined);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,179 @@
|
||||
<?php
|
||||
|
||||
namespace Nuwave\Lighthouse\Execution\Arguments;
|
||||
|
||||
use GraphQL\Language\AST\FieldDefinitionNode;
|
||||
use GraphQL\Language\AST\InputObjectTypeDefinitionNode;
|
||||
use GraphQL\Language\AST\InputValueDefinitionNode;
|
||||
use GraphQL\Language\AST\Node;
|
||||
use GraphQL\Type\Definition\ResolveInfo;
|
||||
use InvalidArgumentException;
|
||||
use Nuwave\Lighthouse\Schema\AST\ASTBuilder;
|
||||
use Nuwave\Lighthouse\Schema\DirectiveLocator;
|
||||
|
||||
class ArgumentSetFactory
|
||||
{
|
||||
/**
|
||||
* @var \Nuwave\Lighthouse\Schema\AST\DocumentAST
|
||||
*/
|
||||
protected $documentAST;
|
||||
|
||||
/**
|
||||
* @var \Nuwave\Lighthouse\Execution\Arguments\ArgumentTypeNodeConverter
|
||||
*/
|
||||
protected $argumentTypeNodeConverter;
|
||||
|
||||
/**
|
||||
* @var \Nuwave\Lighthouse\Schema\DirectiveLocator
|
||||
*/
|
||||
protected $directiveLocator;
|
||||
|
||||
public function __construct(
|
||||
ASTBuilder $astBuilder,
|
||||
ArgumentTypeNodeConverter $argumentTypeNodeConverter,
|
||||
DirectiveLocator $directiveLocator
|
||||
) {
|
||||
$this->documentAST = $astBuilder->documentAST();
|
||||
$this->argumentTypeNodeConverter = $argumentTypeNodeConverter;
|
||||
$this->directiveLocator = $directiveLocator;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrap client-given args with type information.
|
||||
*
|
||||
* @param array<mixed> $args
|
||||
* @return \Nuwave\Lighthouse\Execution\Arguments\ArgumentSet
|
||||
*/
|
||||
public function fromResolveInfo(array $args, ResolveInfo $resolveInfo): ArgumentSet
|
||||
{
|
||||
/**
|
||||
* TODO handle programmatic types without an AST gracefully.
|
||||
*
|
||||
* @var \GraphQL\Language\AST\FieldDefinitionNode $definition
|
||||
*/
|
||||
$definition = $resolveInfo->fieldDefinition->astNode;
|
||||
|
||||
return $this->wrapArgs($definition, $args);
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrap client-given args with type information.
|
||||
*
|
||||
* @param \GraphQL\Language\AST\FieldDefinitionNode|\GraphQL\Language\AST\InputObjectTypeDefinitionNode $definition
|
||||
* @param array<mixed> $args
|
||||
* @return \Nuwave\Lighthouse\Execution\Arguments\ArgumentSet
|
||||
*/
|
||||
public function wrapArgs(Node $definition, array $args): ArgumentSet
|
||||
{
|
||||
$argumentSet = new ArgumentSet();
|
||||
$argumentSet->directives = $this->directiveLocator->associated($definition);
|
||||
|
||||
if ($definition instanceof FieldDefinitionNode) {
|
||||
$argDefinitions = $definition->arguments;
|
||||
} elseif ($definition instanceof InputObjectTypeDefinitionNode) {
|
||||
$argDefinitions = $definition->fields;
|
||||
} else {
|
||||
throw new InvalidArgumentException('Got unexpected node of type '.get_class($definition));
|
||||
}
|
||||
|
||||
$argumentDefinitionMap = $this->makeDefinitionMap($argDefinitions);
|
||||
|
||||
foreach ($argumentDefinitionMap as $name => $definition) {
|
||||
if (array_key_exists($name, $args)) {
|
||||
$argumentSet->arguments[$name] = $this->wrapInArgument($args[$name], $definition);
|
||||
} else {
|
||||
$argumentSet->undefined[$name] = $this->wrapInArgument(null, $definition);
|
||||
}
|
||||
}
|
||||
|
||||
return $argumentSet;
|
||||
}
|
||||
|
||||
/**
|
||||
* Make a map with the name as keys.
|
||||
*
|
||||
* @param iterable<\GraphQL\Language\AST\InputValueDefinitionNode> $argumentDefinitions
|
||||
* @return array<string, \GraphQL\Language\AST\InputValueDefinitionNode>
|
||||
*/
|
||||
protected function makeDefinitionMap($argumentDefinitions): array
|
||||
{
|
||||
$argumentDefinitionMap = [];
|
||||
|
||||
foreach ($argumentDefinitions as $definition) {
|
||||
$argumentDefinitionMap[$definition->name->value] = $definition;
|
||||
}
|
||||
|
||||
return $argumentDefinitionMap;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrap a single client-given argument with type information.
|
||||
*
|
||||
* @param mixed $value The client given value.
|
||||
* @return \Nuwave\Lighthouse\Execution\Arguments\Argument
|
||||
*/
|
||||
protected function wrapInArgument($value, InputValueDefinitionNode $definition): Argument
|
||||
{
|
||||
$type = $this->argumentTypeNodeConverter->convert($definition->type);
|
||||
|
||||
$argument = new Argument();
|
||||
$argument->directives = $this->directiveLocator->associated($definition);
|
||||
$argument->type = $type;
|
||||
$argument->value = $this->wrapWithType($value, $type);
|
||||
|
||||
return $argument;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrap a client-given value with information from a type.
|
||||
*
|
||||
* @param mixed|array<mixed> $valueOrValues
|
||||
* @param \Nuwave\Lighthouse\Execution\Arguments\ListType|\Nuwave\Lighthouse\Execution\Arguments\NamedType $type
|
||||
* @return array|mixed|\Nuwave\Lighthouse\Execution\Arguments\ArgumentSet
|
||||
*/
|
||||
protected function wrapWithType($valueOrValues, $type)
|
||||
{
|
||||
// No need to recurse down further if the value is null
|
||||
if ($valueOrValues === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// We have to do this conversion as we are resolving a client query
|
||||
// because the incoming arguments put a bound on recursion depth
|
||||
if ($type instanceof ListType) {
|
||||
$typeInList = $type->type;
|
||||
|
||||
$values = [];
|
||||
foreach ($valueOrValues as $singleValue) {
|
||||
$values [] = $this->wrapWithType($singleValue, $typeInList);
|
||||
}
|
||||
|
||||
return $values;
|
||||
}
|
||||
|
||||
return $this->wrapWithNamedType($valueOrValues, $type);
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrap a client-given value with information from a named type.
|
||||
*
|
||||
* @param mixed $value The client given value.
|
||||
* @param \Nuwave\Lighthouse\Execution\Arguments\NamedType $namedType
|
||||
* @return \Nuwave\Lighthouse\Execution\Arguments\ArgumentSet|mixed
|
||||
*/
|
||||
protected function wrapWithNamedType($value, NamedType $namedType)
|
||||
{
|
||||
// This might be null if the type is
|
||||
// - created outside of the schema string
|
||||
// - one of the built in types
|
||||
$typeDef = $this->documentAST->types[$namedType->name] ?? null;
|
||||
|
||||
// We recurse down only if the type is an Input
|
||||
if ($typeDef instanceof InputObjectTypeDefinitionNode) {
|
||||
return $this->wrapArgs($typeDef, $value);
|
||||
}
|
||||
|
||||
// Otherwise, we just return the value as is and are done with that subtree
|
||||
return $value;
|
||||
}
|
||||
}
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
namespace Nuwave\Lighthouse\Execution\Arguments;
|
||||
|
||||
use Nuwave\Lighthouse\Schema\AST\TypeNodeConverter;
|
||||
|
||||
class ArgumentTypeNodeConverter extends TypeNodeConverter
|
||||
{
|
||||
/**
|
||||
* @param \Nuwave\Lighthouse\Execution\Arguments\ListType|\Nuwave\Lighthouse\Execution\Arguments\NamedType $type
|
||||
* @return \Nuwave\Lighthouse\Execution\Arguments\ListType|\Nuwave\Lighthouse\Execution\Arguments\NamedType
|
||||
*/
|
||||
protected function nonNull($type): object
|
||||
{
|
||||
$type->nonNull = true;
|
||||
|
||||
return $type;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \Nuwave\Lighthouse\Execution\Arguments\NamedType $type
|
||||
* @return \Nuwave\Lighthouse\Execution\Arguments\ListType
|
||||
*/
|
||||
protected function listOf($type): object
|
||||
{
|
||||
return new ListType($type);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \Nuwave\Lighthouse\Execution\Arguments\NamedType
|
||||
*/
|
||||
protected function namedType(string $nodeName): NamedType
|
||||
{
|
||||
return new NamedType($nodeName);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
namespace Nuwave\Lighthouse\Execution\Arguments;
|
||||
|
||||
class ListType
|
||||
{
|
||||
/**
|
||||
* The type contained within the list.
|
||||
*
|
||||
* @var \Nuwave\Lighthouse\Execution\Arguments\NamedType|\Nuwave\Lighthouse\Execution\Arguments\ListType
|
||||
*/
|
||||
public $type;
|
||||
|
||||
/**
|
||||
* Is the list itself defined to be non-nullable?
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public $nonNull = false;
|
||||
|
||||
/**
|
||||
* @param \Nuwave\Lighthouse\Execution\Arguments\NamedType|\Nuwave\Lighthouse\Execution\Arguments\ListType $type
|
||||
*/
|
||||
public function __construct($type)
|
||||
{
|
||||
$this->type = $type;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
namespace Nuwave\Lighthouse\Execution\Arguments;
|
||||
|
||||
class NamedType
|
||||
{
|
||||
/**
|
||||
* The name of the type as defined in the schema.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $name;
|
||||
|
||||
/**
|
||||
* Is this type defined to be non-nullable?
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public $nonNull = false;
|
||||
|
||||
public function __construct(string $name)
|
||||
{
|
||||
$this->name = $name;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
<?php
|
||||
|
||||
namespace Nuwave\Lighthouse\Execution\Arguments;
|
||||
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Nuwave\Lighthouse\Support\Contracts\ArgResolver;
|
||||
|
||||
class NestedBelongsTo implements ArgResolver
|
||||
{
|
||||
/**
|
||||
* @var \Illuminate\Database\Eloquent\Relations\BelongsTo
|
||||
*/
|
||||
protected $relation;
|
||||
|
||||
public function __construct(BelongsTo $relation)
|
||||
{
|
||||
$this->relation = $relation;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \Illuminate\Database\Eloquent\Model $parent
|
||||
* @param \Nuwave\Lighthouse\Execution\Arguments\ArgumentSet $args
|
||||
*/
|
||||
public function __invoke($parent, $args): void
|
||||
{
|
||||
if ($args->has('create')) {
|
||||
$saveModel = new ResolveNested(new SaveModel($this->relation));
|
||||
|
||||
$related = $saveModel(
|
||||
// @phpstan-ignore-next-line Unrecognized mixin
|
||||
$this->relation->make(),
|
||||
$args->arguments['create']->value
|
||||
);
|
||||
$this->relation->associate($related);
|
||||
}
|
||||
|
||||
if ($args->has('connect')) {
|
||||
$this->relation->associate($args->arguments['connect']->value);
|
||||
}
|
||||
|
||||
if ($args->has('update')) {
|
||||
$updateModel = new ResolveNested(new UpdateModel(new SaveModel($this->relation)));
|
||||
|
||||
$related = $updateModel(
|
||||
// @phpstan-ignore-next-line Unrecognized mixin
|
||||
$this->relation->make(),
|
||||
$args->arguments['update']->value
|
||||
);
|
||||
$this->relation->associate($related);
|
||||
}
|
||||
|
||||
if ($args->has('upsert')) {
|
||||
$upsertModel = new ResolveNested(new UpsertModel(new SaveModel($this->relation)));
|
||||
|
||||
$related = $upsertModel(
|
||||
// @phpstan-ignore-next-line Unrecognized mixin
|
||||
$this->relation->make(),
|
||||
$args->arguments['upsert']->value
|
||||
);
|
||||
$this->relation->associate($related);
|
||||
}
|
||||
|
||||
self::disconnectOrDelete($this->relation, $args);
|
||||
}
|
||||
|
||||
public static function disconnectOrDelete(BelongsTo $relation, ArgumentSet $args): void
|
||||
{
|
||||
// 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 (
|
||||
$args->has('disconnect')
|
||||
&& $args->arguments['disconnect']->value
|
||||
) {
|
||||
$relation->dissociate();
|
||||
}
|
||||
|
||||
if (
|
||||
$args->has('delete')
|
||||
&& $args->arguments['delete']->value
|
||||
) {
|
||||
$relation->dissociate();
|
||||
// @phpstan-ignore-next-line Unrecognized mixin
|
||||
$relation->delete();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
<?php
|
||||
|
||||
namespace Nuwave\Lighthouse\Execution\Arguments;
|
||||
|
||||
use Illuminate\Support\Arr;
|
||||
use Nuwave\Lighthouse\Support\Contracts\ArgResolver;
|
||||
|
||||
class NestedManyToMany implements ArgResolver
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $relationName;
|
||||
|
||||
public function __construct(string $relationName)
|
||||
{
|
||||
$this->relationName = $relationName;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \Illuminate\Database\Eloquent\Model $parent
|
||||
* @param \Nuwave\Lighthouse\Execution\Arguments\ArgumentSet $args
|
||||
*/
|
||||
public function __invoke($parent, $args): void
|
||||
{
|
||||
/** @var \Illuminate\Database\Eloquent\Relations\BelongsToMany|\Illuminate\Database\Eloquent\Relations\MorphToMany $relation */
|
||||
$relation = $parent->{$this->relationName}();
|
||||
|
||||
if ($args->has('sync')) {
|
||||
$relation->sync(
|
||||
$this->generateRelationArray($args->arguments['sync'])
|
||||
);
|
||||
}
|
||||
|
||||
if ($args->has('syncWithoutDetaching')) {
|
||||
$relation->syncWithoutDetaching(
|
||||
$this->generateRelationArray($args->arguments['syncWithoutDetaching'])
|
||||
);
|
||||
}
|
||||
|
||||
NestedOneToMany::createUpdateUpsert($args, $relation);
|
||||
|
||||
if ($args->has('delete')) {
|
||||
$ids = $args->arguments['delete']->toPlain();
|
||||
|
||||
$relation->detach($ids);
|
||||
$relation->getRelated()::destroy($ids);
|
||||
}
|
||||
|
||||
if ($args->has('connect')) {
|
||||
$relation->attach(
|
||||
$this->generateRelationArray($args->arguments['connect'])
|
||||
);
|
||||
}
|
||||
|
||||
if ($args->has('disconnect')) {
|
||||
$relation->detach(
|
||||
$args->arguments['disconnect']->toPlain()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate an array for passing into sync, syncWithoutDetaching or connect method.
|
||||
*
|
||||
* Those functions natively have the capability of passing additional
|
||||
* data to store in the pivot table. That array expects passing the id's
|
||||
* as keys, so we transform the passed arguments to match that.
|
||||
*
|
||||
* @param \Nuwave\Lighthouse\Execution\Arguments\Argument $args
|
||||
* @return array<mixed>
|
||||
*/
|
||||
protected function generateRelationArray(Argument $args): array
|
||||
{
|
||||
$values = $args->toPlain();
|
||||
|
||||
if (empty($values)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Since GraphQL inputs are monomorphic, we can just look at the first
|
||||
// given value and can deduce the value of all given args.
|
||||
$exemplaryValue = $values[0];
|
||||
|
||||
// We assume that the values contain pivot information
|
||||
if (is_array($exemplaryValue)) {
|
||||
$relationArray = [];
|
||||
foreach ($values as $value) {
|
||||
$id = Arr::pull($value, 'id');
|
||||
$relationArray[$id] = $value;
|
||||
}
|
||||
|
||||
return $relationArray;
|
||||
}
|
||||
|
||||
// The default case is simply a flat array of IDs which we don't have to transform
|
||||
return $values;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
namespace Nuwave\Lighthouse\Execution\Arguments;
|
||||
|
||||
use Illuminate\Database\Eloquent\Relations\MorphTo;
|
||||
use Nuwave\Lighthouse\Support\Contracts\ArgResolver;
|
||||
|
||||
class NestedMorphTo implements ArgResolver
|
||||
{
|
||||
/**
|
||||
* @var \Illuminate\Database\Eloquent\Relations\MorphTo
|
||||
*/
|
||||
protected $relation;
|
||||
|
||||
public function __construct(MorphTo $relation)
|
||||
{
|
||||
$this->relation = $relation;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \Illuminate\Database\Eloquent\Model $parent
|
||||
* @param \Nuwave\Lighthouse\Execution\Arguments\ArgumentSet $args
|
||||
*/
|
||||
public function __invoke($parent, $args): void
|
||||
{
|
||||
// TODO implement create and update once we figure out how to do polymorphic input types https://github.com/nuwave/lighthouse/issues/900
|
||||
|
||||
if ($args->has('connect')) {
|
||||
$connectArgs = $args->arguments['connect']->value;
|
||||
|
||||
$morphToModel = $this->relation->createModelByType(
|
||||
(string) $connectArgs->arguments['type']->value
|
||||
);
|
||||
$morphToModel->setAttribute(
|
||||
$morphToModel->getKeyName(),
|
||||
$connectArgs->arguments['id']->value
|
||||
);
|
||||
|
||||
$this->relation->associate($morphToModel);
|
||||
}
|
||||
|
||||
NestedBelongsTo::disconnectOrDelete($this->relation, $args);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
<?php
|
||||
|
||||
namespace Nuwave\Lighthouse\Execution\Arguments;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Database\Eloquent\Relations\HasOneOrMany;
|
||||
use Illuminate\Database\Eloquent\Relations\Relation;
|
||||
use Nuwave\Lighthouse\Support\Contracts\ArgResolver;
|
||||
|
||||
class NestedOneToMany implements ArgResolver
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $relationName;
|
||||
|
||||
public function __construct(string $relationName)
|
||||
{
|
||||
$this->relationName = $relationName;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \Illuminate\Database\Eloquent\Model $parent
|
||||
* @param \Nuwave\Lighthouse\Execution\Arguments\ArgumentSet $args
|
||||
*/
|
||||
public function __invoke($parent, $args): void
|
||||
{
|
||||
/** @var \Illuminate\Database\Eloquent\Relations\HasMany|\Illuminate\Database\Eloquent\Relations\MorphMany $relation */
|
||||
$relation = $parent->{$this->relationName}();
|
||||
|
||||
static::createUpdateUpsert($args, $relation);
|
||||
static::connectDisconnect($args, $relation);
|
||||
|
||||
if ($args->has('delete')) {
|
||||
$relation->getRelated()::destroy(
|
||||
$args->arguments['delete']->toPlain()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \Nuwave\Lighthouse\Execution\Arguments\ArgumentSet $args
|
||||
*/
|
||||
public static function createUpdateUpsert(ArgumentSet $args, Relation $relation): void
|
||||
{
|
||||
if ($args->has('create')) {
|
||||
$saveModel = new ResolveNested(new SaveModel($relation));
|
||||
|
||||
foreach ($args->arguments['create']->value as $childArgs) {
|
||||
// @phpstan-ignore-next-line Relation&Builder mixin not recognized
|
||||
$saveModel($relation->make(), $childArgs);
|
||||
}
|
||||
}
|
||||
|
||||
if ($args->has('update')) {
|
||||
$updateModel = new ResolveNested(new UpdateModel(new SaveModel($relation)));
|
||||
|
||||
foreach ($args->arguments['update']->value as $childArgs) {
|
||||
// @phpstan-ignore-next-line Relation&Builder mixin not recognized
|
||||
$updateModel($relation->make(), $childArgs);
|
||||
}
|
||||
}
|
||||
|
||||
if ($args->has('upsert')) {
|
||||
$upsertModel = new ResolveNested(new UpsertModel(new SaveModel($relation)));
|
||||
|
||||
foreach ($args->arguments['upsert']->value as $childArgs) {
|
||||
// @phpstan-ignore-next-line Relation&Builder mixin not recognized
|
||||
$upsertModel($relation->make(), $childArgs);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static function connectDisconnect(ArgumentSet $args, HasOneOrMany $relation): void
|
||||
{
|
||||
if ($args->has('connect')) {
|
||||
// @phpstan-ignore-next-line Relation&Builder mixin not recognized
|
||||
$children = $relation
|
||||
->make()
|
||||
->whereIn(
|
||||
self::getLocalKeyName($relation),
|
||||
$args->arguments['connect']->value
|
||||
)
|
||||
->get();
|
||||
|
||||
// @phpstan-ignore-next-line Relation&Builder mixin not recognized
|
||||
$relation->saveMany($children);
|
||||
}
|
||||
|
||||
if ($args->has('disconnect')) {
|
||||
// @phpstan-ignore-next-line Relation&Builder mixin not recognized
|
||||
$relation
|
||||
->make()
|
||||
->whereIn(
|
||||
self::getLocalKeyName($relation),
|
||||
$args->arguments['disconnect']->value
|
||||
)
|
||||
->update([$relation->getForeignKeyName() => null]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* TODO remove this horrible hack when we no longer support Laravel 5.6.
|
||||
*/
|
||||
protected static function getLocalKeyName(HasOneOrMany $relation): string
|
||||
{
|
||||
$getLocalKeyName = Closure::bind(
|
||||
function () {
|
||||
/** @psalm-suppress InvalidScope */
|
||||
// @phpstan-ignore-next-line This is a dirty hack
|
||||
return $this->localKey;
|
||||
},
|
||||
$relation,
|
||||
get_class($relation)
|
||||
);
|
||||
|
||||
return $getLocalKeyName();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
<?php
|
||||
|
||||
namespace Nuwave\Lighthouse\Execution\Arguments;
|
||||
|
||||
use Nuwave\Lighthouse\Support\Contracts\ArgResolver;
|
||||
|
||||
class NestedOneToOne implements ArgResolver
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $relationName;
|
||||
|
||||
public function __construct(string $relationName)
|
||||
{
|
||||
$this->relationName = $relationName;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \Illuminate\Database\Eloquent\Model $parent
|
||||
* @param \Nuwave\Lighthouse\Execution\Arguments\ArgumentSet $args
|
||||
*/
|
||||
public function __invoke($parent, $args): void
|
||||
{
|
||||
/** @var \Illuminate\Database\Eloquent\Relations\HasOne|\Illuminate\Database\Eloquent\Relations\MorphOne $relation */
|
||||
$relation = $parent->{$this->relationName}();
|
||||
|
||||
if ($args->has('create')) {
|
||||
$saveModel = new ResolveNested(new SaveModel($relation));
|
||||
|
||||
$saveModel($relation->make(), $args->arguments['create']->value);
|
||||
}
|
||||
|
||||
if ($args->has('update')) {
|
||||
$updateModel = new ResolveNested(new UpdateModel(new SaveModel($relation)));
|
||||
|
||||
$updateModel($relation->make(), $args->arguments['update']->value);
|
||||
}
|
||||
|
||||
if ($args->has('upsert')) {
|
||||
$upsertModel = new ResolveNested(new UpsertModel(new SaveModel($relation)));
|
||||
|
||||
$upsertModel($relation->make(), $args->arguments['upsert']->value);
|
||||
}
|
||||
|
||||
if ($args->has('delete')) {
|
||||
$relation->getRelated()::destroy(
|
||||
$args->arguments['delete']->toPlain()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
namespace Nuwave\Lighthouse\Execution\Arguments;
|
||||
|
||||
use Nuwave\Lighthouse\Support\Contracts\ArgResolver;
|
||||
|
||||
class ResolveNested implements ArgResolver
|
||||
{
|
||||
/**
|
||||
* @var callable|\Nuwave\Lighthouse\Support\Contracts\ArgResolver|null
|
||||
*/
|
||||
protected $previous;
|
||||
|
||||
/**
|
||||
* @var callable
|
||||
*/
|
||||
protected $argPartitioner;
|
||||
|
||||
/**
|
||||
* @param callable|\Nuwave\Lighthouse\Support\Contracts\ArgResolver|null $previous
|
||||
*/
|
||||
public function __construct(callable $previous = null, callable $argPartitioner = null)
|
||||
{
|
||||
$this->previous = $previous;
|
||||
$this->argPartitioner = $argPartitioner ?? [ArgPartitioner::class, 'nestedArgResolvers'];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \Nuwave\Lighthouse\Execution\Arguments\ArgumentSet $args
|
||||
*/
|
||||
public function __invoke($root, $args)
|
||||
{
|
||||
[$nestedArgs, $regularArgs] = ($this->argPartitioner)($args, $root);
|
||||
|
||||
if ($this->previous) {
|
||||
$root = ($this->previous)($root, $regularArgs);
|
||||
}
|
||||
|
||||
/** @var \Nuwave\Lighthouse\Execution\Arguments\Argument $nested */
|
||||
foreach ($nestedArgs->arguments as $nested) {
|
||||
// @phpstan-ignore-next-line we know the resolver is there because we partitioned for it
|
||||
($nested->resolver)($root, $nested->value);
|
||||
}
|
||||
|
||||
return $root;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
<?php
|
||||
|
||||
namespace Nuwave\Lighthouse\Execution\Arguments;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||
use Illuminate\Database\Eloquent\Relations\HasOneOrMany;
|
||||
use Illuminate\Database\Eloquent\Relations\MorphTo;
|
||||
use Illuminate\Database\Eloquent\Relations\Relation;
|
||||
use Nuwave\Lighthouse\Support\Contracts\ArgResolver;
|
||||
|
||||
class SaveModel implements ArgResolver
|
||||
{
|
||||
/**
|
||||
* @var \Illuminate\Database\Eloquent\Relations\Relation|null
|
||||
*/
|
||||
protected $parentRelation;
|
||||
|
||||
public function __construct(?Relation $parentRelation = null)
|
||||
{
|
||||
$this->parentRelation = $parentRelation;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \Illuminate\Database\Eloquent\Model $model
|
||||
* @param \Nuwave\Lighthouse\Execution\Arguments\ArgumentSet $args
|
||||
*/
|
||||
public function __invoke($model, $args): Model
|
||||
{
|
||||
// Extract $morphTo first, as MorphTo extends BelongsTo
|
||||
[$morphTo, $remaining] = ArgPartitioner::relationMethods(
|
||||
$args,
|
||||
$model,
|
||||
MorphTo::class
|
||||
);
|
||||
|
||||
[$belongsTo, $remaining] = ArgPartitioner::relationMethods(
|
||||
$remaining,
|
||||
$model,
|
||||
BelongsTo::class
|
||||
);
|
||||
|
||||
$argsToFill = $remaining->toArray();
|
||||
|
||||
// Use all the remaining attributes and fill the model
|
||||
if (config('lighthouse.force_fill')) {
|
||||
$model->forceFill($argsToFill);
|
||||
} else {
|
||||
$model->fill($argsToFill);
|
||||
}
|
||||
|
||||
foreach ($belongsTo->arguments as $relationName => $nestedOperations) {
|
||||
/** @var \Illuminate\Database\Eloquent\Relations\BelongsTo $belongsTo */
|
||||
$belongsTo = $model->{$relationName}();
|
||||
$belongsToResolver = new ResolveNested(new NestedBelongsTo($belongsTo));
|
||||
$belongsToResolver($model, $nestedOperations->value);
|
||||
}
|
||||
|
||||
foreach ($morphTo->arguments as $relationName => $nestedOperations) {
|
||||
/** @var \Illuminate\Database\Eloquent\Relations\MorphTo $morphTo */
|
||||
$morphTo = $model->{$relationName}();
|
||||
$morphToResolver = new ResolveNested(new NestedMorphTo($morphTo));
|
||||
$morphToResolver($model, $nestedOperations->value);
|
||||
}
|
||||
|
||||
if ($this->parentRelation instanceof HasOneOrMany) {
|
||||
// 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.
|
||||
$this->parentRelation->save($model);
|
||||
|
||||
return $model;
|
||||
}
|
||||
|
||||
$model->save();
|
||||
|
||||
if ($this->parentRelation instanceof BelongsTo) {
|
||||
$parentModel = $this->parentRelation->associate($model);
|
||||
|
||||
// If the parent Model does not exist (still to be saved),
|
||||
// a save could break any pending belongsTo relations that still
|
||||
// needs to be created and associated with the parent model
|
||||
if ($parentModel->exists) {
|
||||
$parentModel->save();
|
||||
}
|
||||
}
|
||||
|
||||
if ($this->parentRelation instanceof BelongsToMany) {
|
||||
$this->parentRelation->syncWithoutDetaching($model);
|
||||
}
|
||||
|
||||
return $model;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
namespace Nuwave\Lighthouse\Execution\Arguments;
|
||||
|
||||
use GraphQL\Error\Error;
|
||||
use Illuminate\Support\Arr;
|
||||
use Nuwave\Lighthouse\Support\Contracts\ArgResolver;
|
||||
|
||||
class UpdateModel implements ArgResolver
|
||||
{
|
||||
const MISSING_PRIMARY_KEY_FOR_UPDATE = 'Missing primary key for update.';
|
||||
|
||||
/**
|
||||
* @var callable|\Nuwave\Lighthouse\Support\Contracts\ArgResolver
|
||||
*/
|
||||
protected $previous;
|
||||
|
||||
/**
|
||||
* @param callable|\Nuwave\Lighthouse\Support\Contracts\ArgResolver $previous
|
||||
*/
|
||||
public function __construct(callable $previous)
|
||||
{
|
||||
$this->previous = $previous;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \Illuminate\Database\Eloquent\Model $model
|
||||
* @param \Nuwave\Lighthouse\Execution\Arguments\ArgumentSet $args
|
||||
*/
|
||||
public function __invoke($model, $args)
|
||||
{
|
||||
/** @var \Nuwave\Lighthouse\Execution\Arguments\Argument|null $id */
|
||||
$id = Arr::pull($args->arguments, 'id')
|
||||
?? Arr::pull($args->arguments, $model->getKeyName())
|
||||
?? null;
|
||||
|
||||
if ($id === null) {
|
||||
throw new Error(self::MISSING_PRIMARY_KEY_FOR_UPDATE);
|
||||
}
|
||||
|
||||
$model = $model->newQuery()->findOrFail($id->value);
|
||||
|
||||
return ($this->previous)($model, $args);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
namespace Nuwave\Lighthouse\Execution\Arguments;
|
||||
|
||||
use Nuwave\Lighthouse\Support\Contracts\ArgResolver;
|
||||
|
||||
class UpsertModel implements ArgResolver
|
||||
{
|
||||
/**
|
||||
* @var callable|\Nuwave\Lighthouse\Support\Contracts\ArgResolver
|
||||
*/
|
||||
protected $previous;
|
||||
|
||||
/**
|
||||
* @param callable|\Nuwave\Lighthouse\Support\Contracts\ArgResolver $previous
|
||||
*/
|
||||
public function __construct(callable $previous)
|
||||
{
|
||||
$this->previous = $previous;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \Illuminate\Database\Eloquent\Model $model
|
||||
* @param \Nuwave\Lighthouse\Execution\Arguments\ArgumentSet $args
|
||||
*/
|
||||
public function __invoke($model, $args)
|
||||
{
|
||||
// TODO consider Laravel native ->upsert(), available from 8.10
|
||||
$id = $args->arguments['id']
|
||||
?? $args->arguments[$model->getKeyName()]
|
||||
?? null;
|
||||
|
||||
if ($id !== null) {
|
||||
$existingModel = $model
|
||||
->newQuery()
|
||||
->find($id->value);
|
||||
|
||||
if ($existingModel !== null) {
|
||||
$model = $existingModel;
|
||||
}
|
||||
}
|
||||
|
||||
return ($this->previous)($model, $args);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user