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);
|
||||
}
|
||||
}
|
||||
@@ -1,98 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Nuwave\Lighthouse\Execution;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
abstract class BaseRequest implements GraphQLRequest
|
||||
{
|
||||
/**
|
||||
* The current batch index.
|
||||
*
|
||||
* Is null if we are not resolving a batched query.
|
||||
*
|
||||
* @var int|null
|
||||
*/
|
||||
protected $batchIndex;
|
||||
|
||||
/**
|
||||
* Get the contents of a field by key.
|
||||
*
|
||||
* This is expected to take batched requests into consideration.
|
||||
*
|
||||
* @param string $key
|
||||
* @return array|string|null
|
||||
*/
|
||||
abstract protected function fieldValue(string $key);
|
||||
|
||||
/**
|
||||
* Are there more batched queries to process?
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
abstract protected function hasMoreBatches(): bool;
|
||||
|
||||
/**
|
||||
* Construct this from a HTTP request.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @return void
|
||||
*/
|
||||
abstract public function __construct(Request $request);
|
||||
|
||||
/**
|
||||
* Get the contained GraphQL query string.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function query(): string
|
||||
{
|
||||
return $this->fieldValue('query');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the operationName of the current request.
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function operationName(): ?string
|
||||
{
|
||||
return $this->fieldValue('operationName');
|
||||
}
|
||||
|
||||
/**
|
||||
* Is the current query a batched query?
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isBatched(): bool
|
||||
{
|
||||
return ! is_null($this->batchIndex);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the index of the current batch.
|
||||
*
|
||||
* Returns null if we are not resolving a batched query.
|
||||
*
|
||||
* @return int|null
|
||||
*/
|
||||
public function batchIndex(): ?int
|
||||
{
|
||||
return $this->batchIndex;
|
||||
}
|
||||
|
||||
/**
|
||||
* Advance the batch index and indicate if there are more batches to process.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function advanceBatchIndex(): bool
|
||||
{
|
||||
if ($result = $this->hasMoreBatches()) {
|
||||
$this->batchIndex++;
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
<?php
|
||||
|
||||
namespace Nuwave\Lighthouse\Execution\BatchLoader;
|
||||
|
||||
abstract class BatchLoaderRegistry
|
||||
{
|
||||
/**
|
||||
* Active BatchLoader instances.
|
||||
*
|
||||
* @var array<string, object>
|
||||
*/
|
||||
protected static $instances = [];
|
||||
|
||||
/**
|
||||
* Return an instance of a BatchLoader for a specific field.
|
||||
*
|
||||
* @param array<int|string> $pathToField Path to the GraphQL field from the root, is used as a key for BatchLoader instances
|
||||
* @param callable(): object $makeInstance Function to instantiate the instance once
|
||||
* @return object The result of calling makeInstance
|
||||
*
|
||||
* @throws \Exception
|
||||
*/
|
||||
public static function instance(array $pathToField, callable $makeInstance): object
|
||||
{
|
||||
// The path to the field serves as the unique key for the instance
|
||||
$instanceKey = static::instanceKey($pathToField);
|
||||
|
||||
if (isset(self::$instances[$instanceKey])) {
|
||||
return self::$instances[$instanceKey];
|
||||
}
|
||||
|
||||
return self::$instances[$instanceKey] = $makeInstance();
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove all stored BatchLoaders.
|
||||
*
|
||||
* This is called after Lighthouse has resolved a query, so multiple
|
||||
* queries can be handled in a single request/session.
|
||||
*/
|
||||
public static function forgetInstances(): void
|
||||
{
|
||||
self::$instances = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a unique key for the instance, using the path in the query.
|
||||
*
|
||||
* @param array<int|string> $path
|
||||
*/
|
||||
protected static function instanceKey(array $path): string
|
||||
{
|
||||
$significantPathSegments = array_filter(
|
||||
$path,
|
||||
static function ($segment): bool {
|
||||
// Ignore numeric path entries, as those signify a list of fields.
|
||||
// Combining the queries for those is the very purpose of the
|
||||
// batch loader, so they must not be included.
|
||||
return ! is_numeric($segment);
|
||||
}
|
||||
);
|
||||
|
||||
// Using . as the separator would combine relations in nested fields with
|
||||
// higher up relations using dot notation, matching the field path.
|
||||
// We might optimize this in the future to enable batching them anyways,
|
||||
// but employ this solution for now, as it preserves correctness.
|
||||
return implode('|', $significantPathSegments);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
<?php
|
||||
|
||||
namespace Nuwave\Lighthouse\Execution\BatchLoader;
|
||||
|
||||
use GraphQL\Deferred;
|
||||
use Illuminate\Database\Eloquent\Collection as EloquentCollection;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Nuwave\Lighthouse\Execution\ModelsLoader\ModelsLoader;
|
||||
use Nuwave\Lighthouse\Execution\Utils\ModelKey;
|
||||
|
||||
class RelationBatchLoader
|
||||
{
|
||||
/**
|
||||
* @var \Nuwave\Lighthouse\Execution\ModelsLoader\ModelsLoader
|
||||
*/
|
||||
protected $relationLoader;
|
||||
|
||||
/**
|
||||
* A map from unique keys to parent model instances.
|
||||
*
|
||||
* @var array<string, \Illuminate\Database\Eloquent\Model>
|
||||
*/
|
||||
protected $parents = [];
|
||||
|
||||
/**
|
||||
* Marks when the actual batch loading happened.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
protected $hasResolved = false;
|
||||
|
||||
public function __construct(ModelsLoader $relationLoader)
|
||||
{
|
||||
$this->relationLoader = $relationLoader;
|
||||
}
|
||||
|
||||
/**
|
||||
* Schedule loading a relation off of a concrete model.
|
||||
*
|
||||
* This returns effectively a promise that will resolve to
|
||||
* the result of loading the relation.
|
||||
*
|
||||
* As a side-effect, the model will then hold the relation.
|
||||
*/
|
||||
public function load(Model $model): Deferred
|
||||
{
|
||||
$modelKey = ModelKey::build($model);
|
||||
$this->parents[$modelKey] = $model;
|
||||
|
||||
return new Deferred(function () use ($modelKey) {
|
||||
if (! $this->hasResolved) {
|
||||
$this->resolve();
|
||||
}
|
||||
|
||||
// When we are deep inside a nested query, we can come across the
|
||||
// same model in two different paths, so this might be another
|
||||
// model instance then $model.
|
||||
$parent = $this->parents[$modelKey];
|
||||
|
||||
return $this->relationLoader->extract($parent);
|
||||
});
|
||||
}
|
||||
|
||||
public function resolve(): void
|
||||
{
|
||||
$parentModels = new EloquentCollection($this->parents);
|
||||
|
||||
// Monomorphize the models to simplify eager loading relations onto them
|
||||
$parentsGroupedByClass = $parentModels->groupBy(
|
||||
/**
|
||||
* @return class-string<\Illuminate\Database\Eloquent\Model>
|
||||
*/
|
||||
static function (Model $model): string {
|
||||
return get_class($model);
|
||||
},
|
||||
true
|
||||
);
|
||||
|
||||
foreach ($parentsGroupedByClass as $parentsOfSameClass) {
|
||||
$this->relationLoader->load($parentsOfSameClass);
|
||||
}
|
||||
|
||||
$this->hasResolved = true;
|
||||
}
|
||||
}
|
||||
@@ -1,79 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Nuwave\Lighthouse\Execution;
|
||||
|
||||
use Illuminate\Support\Arr;
|
||||
use Nuwave\Lighthouse\Support\Contracts\ArgBuilderDirective;
|
||||
|
||||
class Builder
|
||||
{
|
||||
/**
|
||||
* A map from argument names to associated query builder directives.
|
||||
*
|
||||
* @var ArgBuilderDirective[]
|
||||
*/
|
||||
protected $builderDirectives = [];
|
||||
|
||||
/**
|
||||
* Scopes to be applied to the query builder.
|
||||
*
|
||||
* @var string[]
|
||||
*/
|
||||
protected $scopes = [];
|
||||
|
||||
/**
|
||||
* Apply the bound QueryBuilderDirectives to the given builder.
|
||||
*
|
||||
* @param \Illuminate\Database\Query\Builder|\Illuminate\Database\Eloquent\Builder $builder
|
||||
* @param mixed[] $args
|
||||
* @return \Illuminate\Database\Query\Builder|\Illuminate\Database\Eloquent\Builder
|
||||
*/
|
||||
public function apply($builder, array $args)
|
||||
{
|
||||
foreach ($args as $key => $value) {
|
||||
// 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;
|
||||
}
|
||||
|
||||
/** @var \Nuwave\Lighthouse\Support\Contracts\ArgBuilderDirective $builderDirective */
|
||||
if ($builderDirective = Arr::get($this->builderDirectives, $key)) {
|
||||
$builder = $builderDirective->handleBuilder($builder, $value);
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($this->scopes as $scope) {
|
||||
call_user_func([$builder, $scope], $args);
|
||||
}
|
||||
|
||||
return $builder;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add scopes that are then called upon the query with the field arguments.
|
||||
*
|
||||
* @param string[] $scopes
|
||||
* @return $this
|
||||
*/
|
||||
public function addScopes(array $scopes): self
|
||||
{
|
||||
$this->scopes = array_merge($this->scopes, $scopes);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a query builder directive keyed by the argument name.
|
||||
*
|
||||
* @param string $argumentName
|
||||
* @param \Nuwave\Lighthouse\Support\Contracts\ArgBuilderDirective $argBuilderDirective
|
||||
* @return $this
|
||||
*/
|
||||
public function addBuilderDirective(string $argumentName, ArgBuilderDirective $argBuilderDirective): self
|
||||
{
|
||||
$this->builderDirectives[$argumentName] = $argBuilderDirective;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
@@ -9,12 +9,6 @@ use Nuwave\Lighthouse\Support\Contracts\GraphQLContext;
|
||||
|
||||
class ContextFactory implements CreatesContext
|
||||
{
|
||||
/**
|
||||
* Generate GraphQL context.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @return \Nuwave\Lighthouse\Support\Contracts\GraphQLContext
|
||||
*/
|
||||
public function generate(Request $request): GraphQLContext
|
||||
{
|
||||
return new Context($request);
|
||||
|
||||
@@ -2,29 +2,32 @@
|
||||
|
||||
namespace Nuwave\Lighthouse\Execution\DataLoader;
|
||||
|
||||
use Exception;
|
||||
use GraphQL\Deferred;
|
||||
use Illuminate\Support\Collection;
|
||||
use Nuwave\Lighthouse\Execution\GraphQLRequest;
|
||||
use Nuwave\Lighthouse\Support\Traits\HandlesCompositeKey;
|
||||
|
||||
/**
|
||||
* @deprecated implement your own batch loader instead.
|
||||
* @see \Nuwave\Lighthouse\Execution\BatchLoader\BatchLoaderRegistry to resolve instances.
|
||||
*/
|
||||
abstract class BatchLoader
|
||||
{
|
||||
use HandlesCompositeKey;
|
||||
/**
|
||||
* Active BatchLoader instances.
|
||||
*
|
||||
* @var array<string, static>
|
||||
*/
|
||||
protected static $instances = [];
|
||||
|
||||
/**
|
||||
* Keys to resolve.
|
||||
* Map from keys to metainfo for resolving.
|
||||
*
|
||||
* @var array
|
||||
* @var array<mixed, array<mixed>>
|
||||
*/
|
||||
protected $keys = [];
|
||||
|
||||
/**
|
||||
* Map of loaded results.
|
||||
* Map from keys to resolved values.
|
||||
*
|
||||
* [key => resolvedValue]
|
||||
*
|
||||
* @var mixed[]
|
||||
* @var array<mixed, mixed>
|
||||
*/
|
||||
protected $results = [];
|
||||
|
||||
@@ -39,8 +42,8 @@ abstract class BatchLoader
|
||||
* Return an instance of a BatchLoader for a specific field.
|
||||
*
|
||||
* @param string $loaderClass The class name of the concrete BatchLoader to instantiate
|
||||
* @param mixed[] $pathToField Path to the GraphQL field from the root, is used as a key for BatchLoader instances
|
||||
* @param mixed[] $constructorArgs Those arguments are passed to the constructor of the new BatchLoader instance
|
||||
* @param array<int|string> $pathToField Path to the GraphQL field from the root, is used as a key for BatchLoader instances
|
||||
* @param array<mixed> $constructorArgs Those arguments are passed to the constructor of the new BatchLoader instance
|
||||
* @return static
|
||||
*
|
||||
* @throws \Exception
|
||||
@@ -50,60 +53,52 @@ abstract class BatchLoader
|
||||
// The path to the field serves as the unique key for the instance
|
||||
$instanceName = static::instanceKey($pathToField);
|
||||
|
||||
// If we are resolving a batched query, we need to assign each
|
||||
// query a uniquely indexed instance
|
||||
/** @var \Nuwave\Lighthouse\Execution\GraphQLRequest $graphQLRequest */
|
||||
$graphQLRequest = app(GraphQLRequest::class);
|
||||
if ($graphQLRequest->isBatched()) {
|
||||
$currentBatchIndex = $graphQLRequest->batchIndex();
|
||||
$instanceName = "batch_{$currentBatchIndex}_{$instanceName}";
|
||||
if (isset(self::$instances[$instanceName])) {
|
||||
return self::$instances[$instanceName];
|
||||
}
|
||||
|
||||
// Only register a new instance if it is not already bound
|
||||
$instance = app()->bound($instanceName)
|
||||
? app($instanceName)
|
||||
: app()->instance(
|
||||
$instanceName,
|
||||
app()->makeWith($loaderClass, $constructorArgs)
|
||||
);
|
||||
|
||||
if (! $instance instanceof self) {
|
||||
throw new Exception(
|
||||
"The given class '$loaderClass' must resolve to an instance of Nuwave\Lighthouse\Execution\DataLoader\BatchLoader"
|
||||
);
|
||||
}
|
||||
|
||||
return $instance;
|
||||
return self::$instances[$instanceName] = app()->makeWith($loaderClass, $constructorArgs);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a unique key for the instance, using the path in the query.
|
||||
*
|
||||
* @param mixed[] $path
|
||||
* @return string
|
||||
* @param array<int|string> $path
|
||||
*/
|
||||
public static function instanceKey(array $path): string
|
||||
{
|
||||
return (new Collection($path))
|
||||
->filter(function ($path): bool {
|
||||
// Ignore numeric path entries, as those signify an array of fields.
|
||||
$significantPathSegments = array_filter(
|
||||
$path,
|
||||
function ($path): bool {
|
||||
// Ignore numeric path entries, as those signify a list of fields.
|
||||
// Combining the queries for those is the very purpose of the
|
||||
// batch loader, so they must not be included.
|
||||
return ! is_numeric($path);
|
||||
})
|
||||
->implode('_');
|
||||
}
|
||||
);
|
||||
$pathIgnoringLists = implode('.', $significantPathSegments);
|
||||
|
||||
return 'nuwave/lighthouse/batchloader/'.$pathIgnoringLists;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load object by key.
|
||||
* Remove all stored BatchLoaders.
|
||||
*
|
||||
* @param mixed $key
|
||||
* @param mixed[] $metaInfo
|
||||
* @return \GraphQL\Deferred
|
||||
* This is called after Lighthouse has resolved a query, so multiple
|
||||
* queries can be handled in a single request/session.
|
||||
*/
|
||||
public function load($key, array $metaInfo = []): Deferred
|
||||
public static function forgetInstances(): void
|
||||
{
|
||||
self::$instances = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Schedule a result to be loaded.
|
||||
*
|
||||
* @param array<mixed> $metaInfo
|
||||
*/
|
||||
public function load(string $key, array $metaInfo = []): Deferred
|
||||
{
|
||||
$key = $this->buildKey($key);
|
||||
$this->keys[$key] = $metaInfo;
|
||||
|
||||
return new Deferred(function () use ($key) {
|
||||
@@ -116,12 +111,29 @@ abstract class BatchLoader
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Schedule multiple results to be loaded.
|
||||
*
|
||||
* @param array<mixed> $keys
|
||||
* @param array<mixed> $metaInfo
|
||||
* @return array<\GraphQL\Deferred>
|
||||
*/
|
||||
public function loadMany(array $keys, array $metaInfo = []): array
|
||||
{
|
||||
return array_map(
|
||||
function ($key) use ($metaInfo): Deferred {
|
||||
return $this->load($key, $metaInfo);
|
||||
},
|
||||
$keys
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the keys.
|
||||
*
|
||||
* The result has to be an associative array: [key => result]
|
||||
* The result has to be a map from keys to resolved values.
|
||||
*
|
||||
* @return mixed[]
|
||||
* @return array<mixed, mixed>
|
||||
*/
|
||||
abstract public function resolve(): array;
|
||||
}
|
||||
|
||||
@@ -1,410 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Nuwave\Lighthouse\Execution\DataLoader;
|
||||
|
||||
use Closure;
|
||||
use ReflectionClass;
|
||||
use ReflectionMethod;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Pagination\LengthAwarePaginator;
|
||||
use Illuminate\Database\Eloquent\Relations\Relation;
|
||||
use Nuwave\Lighthouse\Support\Traits\HandlesCompositeKey;
|
||||
use Illuminate\Database\Eloquent\Builder as EloquentBuilder;
|
||||
use Illuminate\Database\Eloquent\Collection as EloquentCollection;
|
||||
|
||||
class ModelRelationFetcher
|
||||
{
|
||||
use HandlesCompositeKey;
|
||||
|
||||
/**
|
||||
* The parent models that relations should be loaded for.
|
||||
*
|
||||
* @var \Illuminate\Database\Eloquent\Collection
|
||||
*/
|
||||
protected $models;
|
||||
|
||||
/**
|
||||
* The relations to be loaded. Same format as the `with` method in Eloquent builder.
|
||||
*
|
||||
* @var mixed[]
|
||||
*/
|
||||
protected $relations;
|
||||
|
||||
/**
|
||||
* @param mixed $models The parent models that relations should be loaded for
|
||||
* @param mixed[] $relations The relations to be loaded. Same format as the `with` method in Eloquent builder.
|
||||
* @return void
|
||||
*/
|
||||
public function __construct($models, array $relations)
|
||||
{
|
||||
$this->setModels($models);
|
||||
$this->setRelations($relations);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the relations to be loaded.
|
||||
*
|
||||
* @param array $relations
|
||||
* @return $this
|
||||
*/
|
||||
public function setRelations(array $relations): self
|
||||
{
|
||||
// Parse and set the relations.
|
||||
$this->relations = $this->newModelQuery()
|
||||
->with($relations)
|
||||
->getEagerLoads();
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a fresh instance of a query builder for the underlying model.
|
||||
*
|
||||
* @return \Illuminate\Database\Eloquent\Builder
|
||||
*/
|
||||
protected function newModelQuery(): EloquentBuilder
|
||||
{
|
||||
return $this->models()
|
||||
->first()
|
||||
->newModelQuery();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all the underlying models.
|
||||
*
|
||||
* @return \Illuminate\Database\Eloquent\Collection<\Illuminate\Database\Eloquent\Model>
|
||||
*/
|
||||
public function models(): EloquentCollection
|
||||
{
|
||||
return $this->models;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set one or more Model instances as an EloquentCollection.
|
||||
*
|
||||
* @param mixed $models
|
||||
* @return $this
|
||||
*/
|
||||
protected function setModels($models): self
|
||||
{
|
||||
$this->models = $models instanceof EloquentCollection
|
||||
? $models
|
||||
: new EloquentCollection($models);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load all the relations of all the models.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function loadRelations(): self
|
||||
{
|
||||
$this->models->load($this->relations);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load all relations for the model, but constrain the query to the current page.
|
||||
*
|
||||
* @param int $perPage
|
||||
* @param int $page
|
||||
* @return $this
|
||||
*/
|
||||
public function loadRelationsForPage(int $perPage, int $page = 1): self
|
||||
{
|
||||
foreach ($this->relations as $name => $constraints) {
|
||||
$this->loadRelationForPage($perPage, $page, $name, $constraints);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load only one page of relations of all the models.
|
||||
*
|
||||
* The relation will be converted to a `Paginator` instance.
|
||||
*
|
||||
* @param int $first
|
||||
* @param int $page
|
||||
* @param string $relationName
|
||||
* @param \Closure $relationConstraints
|
||||
* @return $this
|
||||
*/
|
||||
public function loadRelationForPage(int $first, int $page, string $relationName, Closure $relationConstraints): self
|
||||
{
|
||||
// Load the count of relations of models, this will be the `total` argument of `Paginator`.
|
||||
// Be aware that this will reload all the models entirely with the count of their relations,
|
||||
// which will bring extra DB queries, always prefer querying without pagination if possible.
|
||||
$this->reloadModelsWithRelationCount();
|
||||
|
||||
$relations = $this
|
||||
->buildRelationsFromModels($relationName, $relationConstraints)
|
||||
->map(
|
||||
function (Relation $relation) use ($first, $page) {
|
||||
return $relation->forPage($page, $first);
|
||||
}
|
||||
);
|
||||
|
||||
/** @var \Illuminate\Database\Eloquent\Collection $relationModels */
|
||||
$relationModels = $this
|
||||
->unionAllRelationQueries($relations)
|
||||
->get();
|
||||
|
||||
$this->hydratePivotRelation($relationName, $relationModels);
|
||||
|
||||
$this->loadDefaultWith($relationModels);
|
||||
|
||||
$this->associateRelationModels($relationName, $relationModels);
|
||||
|
||||
$this->convertRelationToPaginator($first, $page, $relationName);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reload the models to get the `{relation}_count` attributes of models set.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function reloadModelsWithRelationCount(): self
|
||||
{
|
||||
/** @var \Illuminate\Database\Eloquent\Builder $query */
|
||||
$query = $this->models()
|
||||
->first()
|
||||
->newQuery()
|
||||
->withCount($this->relations);
|
||||
|
||||
$ids = $this->getModelIds();
|
||||
|
||||
$reloadedModels = $query
|
||||
->whereKey($ids)
|
||||
->get()
|
||||
->filter(function (Model $model) use ($ids): bool {
|
||||
return in_array(
|
||||
$model->getKey(),
|
||||
$ids,
|
||||
true
|
||||
);
|
||||
});
|
||||
|
||||
return $this->setModels($reloadedModels);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract the primary keys from the underlying models.
|
||||
*
|
||||
* @return mixed[]
|
||||
*/
|
||||
protected function getModelIds(): array
|
||||
{
|
||||
return $this->models
|
||||
->map(function (Model $model) {
|
||||
return $model->getKey();
|
||||
})
|
||||
->all();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get queries to fetch relationships.
|
||||
*
|
||||
* @param string $relationName
|
||||
* @param \Closure $relationConstraints
|
||||
* @return \Illuminate\Support\Collection<\Illuminate\Database\Eloquent\Relations\Relation>
|
||||
*/
|
||||
protected function buildRelationsFromModels(string $relationName, Closure $relationConstraints): Collection
|
||||
{
|
||||
return $this->models->toBase()->map(
|
||||
function (Model $model) use ($relationName, $relationConstraints): Relation {
|
||||
$relation = $this->getRelationInstance($relationName);
|
||||
|
||||
$relation->addEagerConstraints([$model]);
|
||||
|
||||
// Call the constraints
|
||||
$relationConstraints($relation, $model);
|
||||
|
||||
if (method_exists($relation, 'shouldSelect')) {
|
||||
$shouldSelect = new ReflectionMethod(get_class($relation), 'shouldSelect');
|
||||
$shouldSelect->setAccessible(true);
|
||||
$select = $shouldSelect->invoke($relation, ['*']);
|
||||
|
||||
$relation->addSelect($select);
|
||||
} elseif (method_exists($relation, 'getSelectColumns')) {
|
||||
$getSelectColumns = new ReflectionMethod(get_class($relation), 'getSelectColumns');
|
||||
$getSelectColumns->setAccessible(true);
|
||||
$select = $getSelectColumns->invoke($relation, ['*']);
|
||||
|
||||
$relation->addSelect($select);
|
||||
}
|
||||
|
||||
$relation->initRelation([$model], $relationName);
|
||||
|
||||
return $relation;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Load default eager loads.
|
||||
*
|
||||
* @param \Illuminate\Database\Eloquent\Collection<\Illuminate\Database\Eloquent\Model> $collection
|
||||
* @return $this
|
||||
*/
|
||||
protected function loadDefaultWith(EloquentCollection $collection): self
|
||||
{
|
||||
if ($collection->isEmpty()) {
|
||||
return $this;
|
||||
}
|
||||
|
||||
$model = $collection->first();
|
||||
$reflection = new ReflectionClass($model);
|
||||
$withProperty = $reflection->getProperty('with');
|
||||
$withProperty->setAccessible(true);
|
||||
|
||||
$with = array_filter((array) $withProperty->getValue($model), function ($relation) use ($model): bool {
|
||||
return ! $model->relationLoaded($relation);
|
||||
});
|
||||
|
||||
if (! empty($with)) {
|
||||
$collection->load($with);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* This is the name that Eloquent gives to the attribute that contains the count.
|
||||
*
|
||||
* @see \Illuminate\Database\Eloquent\Concerns\QueriesRelationships->withCount()
|
||||
*
|
||||
* @param string $relationName
|
||||
* @return string
|
||||
*/
|
||||
public function getRelationCountName(string $relationName): string
|
||||
{
|
||||
return Str::snake("{$relationName}_count");
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an associative array of relations, keyed by the models primary key.
|
||||
*
|
||||
* @param string $relationName
|
||||
* @return mixed[]
|
||||
*/
|
||||
public function getRelationDictionary(string $relationName): array
|
||||
{
|
||||
return $this->models
|
||||
->mapWithKeys(
|
||||
function (Model $model) use ($relationName): array {
|
||||
return [$this->buildKey($model->getKey()) => $model->getRelation($relationName)];
|
||||
}
|
||||
)->all();
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge all the relation queries into a single query with UNION ALL.
|
||||
*
|
||||
* @param \Illuminate\Support\Collection $relations
|
||||
* @return \Illuminate\Database\Eloquent\Builder
|
||||
*/
|
||||
protected function unionAllRelationQueries(Collection $relations): EloquentBuilder
|
||||
{
|
||||
return $relations
|
||||
->reduce(
|
||||
function (EloquentBuilder $builder, Relation $relation) {
|
||||
return $builder->unionAll(
|
||||
$relation->getQuery()
|
||||
);
|
||||
},
|
||||
// Use the first query as the initial starting point
|
||||
$relations->shift()->getQuery()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $first
|
||||
* @param int $page
|
||||
* @param string $relationName
|
||||
* @return $this
|
||||
*/
|
||||
protected function convertRelationToPaginator(int $first, int $page, string $relationName): self
|
||||
{
|
||||
$this->models->each(function (Model $model) use ($page, $first, $relationName): void {
|
||||
$total = $model->getAttribute(
|
||||
$this->getRelationCountName($relationName)
|
||||
);
|
||||
|
||||
$paginator = app()->makeWith(
|
||||
LengthAwarePaginator::class,
|
||||
[
|
||||
'items' => $model->getRelation($relationName),
|
||||
'total' => $total,
|
||||
'perPage' => $first,
|
||||
'currentPage' => $page,
|
||||
'options' => [],
|
||||
]
|
||||
);
|
||||
|
||||
$model->setRelation($relationName, $paginator);
|
||||
});
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Associate the collection of all fetched relationModels back with their parents.
|
||||
*
|
||||
* @param string $relationName
|
||||
* @param \Illuminate\Database\Eloquent\Collection $relationModels
|
||||
* @return $this
|
||||
*/
|
||||
protected function associateRelationModels(string $relationName, EloquentCollection $relationModels): self
|
||||
{
|
||||
$relation = $this->getRelationInstance($relationName);
|
||||
|
||||
$relation->match(
|
||||
$this->models->all(),
|
||||
$relationModels,
|
||||
$relationName
|
||||
);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure the pivot relation is hydrated too, if it exists.
|
||||
*
|
||||
* @param string $relationName
|
||||
* @param \Illuminate\Database\Eloquent\Collection<\Illuminate\Database\Eloquent\Model> $relationModels
|
||||
* @return $this
|
||||
*/
|
||||
protected function hydratePivotRelation(string $relationName, EloquentCollection $relationModels): self
|
||||
{
|
||||
$relation = $this->getRelationInstance($relationName);
|
||||
|
||||
if ($relationModels->isNotEmpty() && method_exists($relation, 'hydratePivotRelation')) {
|
||||
$hydrationMethod = new ReflectionMethod(get_class($relation), 'hydratePivotRelation');
|
||||
$hydrationMethod->setAccessible(true);
|
||||
$hydrationMethod->invoke($relation, $relationModels->all());
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Use the underlying model to instantiate a relation by name.
|
||||
*
|
||||
* @param string $relationName
|
||||
* @return \Illuminate\Database\Eloquent\Relations\Relation
|
||||
*/
|
||||
protected function getRelationInstance(string $relationName): Relation
|
||||
{
|
||||
return $this
|
||||
->newModelQuery()
|
||||
->getRelation($relationName);
|
||||
}
|
||||
}
|
||||
@@ -1,122 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Nuwave\Lighthouse\Execution\DataLoader;
|
||||
|
||||
use Illuminate\Support\Collection;
|
||||
use GraphQL\Type\Definition\ResolveInfo;
|
||||
|
||||
class RelationBatchLoader extends BatchLoader
|
||||
{
|
||||
/**
|
||||
* The name of the Eloquent relation to load.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $relationName;
|
||||
|
||||
/**
|
||||
* The arguments that were passed to the field.
|
||||
*
|
||||
* @var mixed[]
|
||||
*/
|
||||
protected $args;
|
||||
|
||||
/**
|
||||
* Names of the scopes that have to be called for the query.
|
||||
*
|
||||
* @var string[]
|
||||
*/
|
||||
protected $scopes;
|
||||
|
||||
/**
|
||||
* The ResolveInfo of the currently executing field.
|
||||
*
|
||||
* @var \GraphQL\Type\Definition\ResolveInfo
|
||||
*/
|
||||
protected $resolveInfo;
|
||||
|
||||
/**
|
||||
* Present when using pagination, the amount of rows to be fetched.
|
||||
*
|
||||
* @var int|null
|
||||
*/
|
||||
protected $first;
|
||||
|
||||
/**
|
||||
* Present when using pagination, the page to be fetched.
|
||||
*
|
||||
* @var int|null
|
||||
*/
|
||||
protected $page;
|
||||
|
||||
/**
|
||||
* @param string $relationName
|
||||
* @param mixed[] $args
|
||||
* @param string[] $scopes
|
||||
* @param \GraphQL\Type\Definition\ResolveInfo $resolveInfo
|
||||
* @param int|null $first
|
||||
* @param int|null $page
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(
|
||||
string $relationName,
|
||||
array $args,
|
||||
array $scopes,
|
||||
ResolveInfo $resolveInfo,
|
||||
?int $first = null,
|
||||
?int $page = null
|
||||
) {
|
||||
$this->relationName = $relationName;
|
||||
$this->args = $args;
|
||||
$this->scopes = $scopes;
|
||||
$this->resolveInfo = $resolveInfo;
|
||||
$this->first = $first;
|
||||
$this->page = $page;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the keys.
|
||||
*
|
||||
* @return mixed[]
|
||||
*/
|
||||
public function resolve(): array
|
||||
{
|
||||
$modelRelationFetcher = $this->getRelationFetcher();
|
||||
|
||||
if ($this->first !== null) {
|
||||
$modelRelationFetcher->loadRelationsForPage($this->first, $this->page);
|
||||
} else {
|
||||
$modelRelationFetcher->loadRelations();
|
||||
}
|
||||
|
||||
return $modelRelationFetcher->getRelationDictionary($this->relationName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct a new instance of a relation fetcher.
|
||||
*
|
||||
* @return \Nuwave\Lighthouse\Execution\DataLoader\ModelRelationFetcher
|
||||
*/
|
||||
protected function getRelationFetcher(): ModelRelationFetcher
|
||||
{
|
||||
return new ModelRelationFetcher(
|
||||
$this->getParentModels(),
|
||||
[$this->relationName => function ($query) {
|
||||
return $this->resolveInfo
|
||||
->builder
|
||||
->addScopes($this->scopes)
|
||||
->apply($query, $this->args);
|
||||
}]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the parents from the keys that are present on the BatchLoader.
|
||||
*
|
||||
* @return \Illuminate\Support\Collection<\Illuminate\Database\Eloquent\Model>
|
||||
*/
|
||||
protected function getParentModels(): Collection
|
||||
{
|
||||
return (new Collection($this->keys))->pluck('parent');
|
||||
}
|
||||
}
|
||||
@@ -1,159 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Nuwave\Lighthouse\Execution;
|
||||
|
||||
use Closure;
|
||||
use Nuwave\Lighthouse\Exceptions\GenericException;
|
||||
|
||||
class ErrorBuffer
|
||||
{
|
||||
/**
|
||||
* The gathered error messages.
|
||||
*
|
||||
* @var string[]
|
||||
*/
|
||||
protected $errors = [];
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $errorType;
|
||||
|
||||
/**
|
||||
* @var \Closure
|
||||
*/
|
||||
protected $exceptionResolver;
|
||||
|
||||
/**
|
||||
* ErrorBuffer constructor.
|
||||
*
|
||||
* @param string $errorType
|
||||
* @param \Closure|null $exceptionResolver
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(string $errorType = 'generic', ?Closure $exceptionResolver = null)
|
||||
{
|
||||
$this->errorType = $errorType;
|
||||
$this->exceptionResolver = $exceptionResolver ?? $this->defaultExceptionResolver();
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct a default exception resolver.
|
||||
*
|
||||
* @return \Closure
|
||||
*/
|
||||
protected function defaultExceptionResolver(): Closure
|
||||
{
|
||||
return function (string $errorMessage): GenericException {
|
||||
return (new GenericException($errorMessage))
|
||||
->setExtensions([$this->errorType => $this->errors])
|
||||
->setCategory($this->errorType);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the Exception resolver.
|
||||
*
|
||||
* @param \Closure $exceptionResolver
|
||||
* @return $this
|
||||
*/
|
||||
public function setExceptionResolver(Closure $exceptionResolver): self
|
||||
{
|
||||
$this->exceptionResolver = $exceptionResolver;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the exception by calling the exception handler with the given args.
|
||||
*
|
||||
* @param mixed ...$args
|
||||
* @return mixed
|
||||
*/
|
||||
protected function resolveException(...$args)
|
||||
{
|
||||
return ($this->exceptionResolver)(...$args);
|
||||
}
|
||||
|
||||
/**
|
||||
* Push an error message into the buffer.
|
||||
*
|
||||
* @param string $errorMessage
|
||||
* @param string|null $key
|
||||
* @return $this
|
||||
*/
|
||||
public function push(string $errorMessage, ?string $key = null): self
|
||||
{
|
||||
if ($key === null) {
|
||||
$this->errors[] = $errorMessage;
|
||||
} else {
|
||||
$this->errors[$key][] = $errorMessage;
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Flush the errors.
|
||||
*
|
||||
* @param string $errorMessage
|
||||
* @return void
|
||||
*
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function flush(string $errorMessage): void
|
||||
{
|
||||
if (! $this->hasErrors()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$exception = $this->resolveException($errorMessage, $this);
|
||||
|
||||
$this->clearErrors();
|
||||
|
||||
throw $exception;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset the errors to an empty array.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function clearErrors(): void
|
||||
{
|
||||
$this->errors = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the error type.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function errorType(): string
|
||||
{
|
||||
return $this->errorType;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the error type.
|
||||
*
|
||||
* @param string $errorType
|
||||
* @return $this
|
||||
*/
|
||||
public function setErrorType(string $errorType): self
|
||||
{
|
||||
$this->errorType = $errorType;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Have we encountered any errors yet?
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function hasErrors(): bool
|
||||
{
|
||||
return count($this->errors) > 0;
|
||||
}
|
||||
}
|
||||
+9
-7
@@ -5,17 +5,19 @@ namespace Nuwave\Lighthouse\Execution;
|
||||
use Closure;
|
||||
use GraphQL\Error\Error;
|
||||
|
||||
/**
|
||||
* Instantiated through the container, once per query.
|
||||
*/
|
||||
interface ErrorHandler
|
||||
{
|
||||
/**
|
||||
* This function receives all GraphQL errors and may alter them or do something else with them.
|
||||
* Called with each GraphQL error, allows doing anything with them.
|
||||
*
|
||||
* Always call $next($error) to keep the Pipeline going. Multiple such Handlers may be registered
|
||||
* as an array in the config.
|
||||
* Multiple such Handlers may be registered as an array in the config.
|
||||
* Always call $next($error) to keep the Pipeline going.
|
||||
* Returning null discards the error.
|
||||
*
|
||||
* @param \GraphQL\Error\Error $error
|
||||
* @param \Closure $next
|
||||
* @return array
|
||||
* @return array<string, mixed>|null
|
||||
*/
|
||||
public static function handle(Error $error, Closure $next): array;
|
||||
public function __invoke(?Error $error, Closure $next): ?array;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
namespace Nuwave\Lighthouse\Execution;
|
||||
|
||||
use GraphQL\Error\Error;
|
||||
use Throwable;
|
||||
|
||||
class ErrorPool
|
||||
{
|
||||
/**
|
||||
* The buffered errors.
|
||||
*
|
||||
* @var array<int, \Throwable>
|
||||
*/
|
||||
protected $throwables = [];
|
||||
|
||||
/**
|
||||
* Stores an error that will be added to the result.
|
||||
*/
|
||||
public function record(Throwable $throwable): void
|
||||
{
|
||||
$this->throwables [] = $throwable;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<\GraphQL\Error\Error>
|
||||
*/
|
||||
public function errors(): array
|
||||
{
|
||||
return array_map(
|
||||
function (Throwable $throwable): Error {
|
||||
if ($throwable instanceof Error) {
|
||||
return $throwable;
|
||||
}
|
||||
|
||||
return new Error(
|
||||
$throwable->getMessage(),
|
||||
null,
|
||||
null,
|
||||
[],
|
||||
null,
|
||||
$throwable
|
||||
);
|
||||
},
|
||||
$this->throwables
|
||||
);
|
||||
}
|
||||
|
||||
public function clear(): void
|
||||
{
|
||||
$this->throwables = [];
|
||||
}
|
||||
}
|
||||
@@ -6,32 +6,32 @@ use Closure;
|
||||
use GraphQL\Error\Error;
|
||||
use Nuwave\Lighthouse\Exceptions\RendersErrorsExtensions;
|
||||
|
||||
/**
|
||||
* Handle Exceptions that implement Nuwave\Lighthouse\Exceptions\RendersErrorsExtensions
|
||||
* and add extra content from them to the 'extensions' key of the Error that is rendered
|
||||
* to the User.
|
||||
*/
|
||||
class ExtensionErrorHandler implements ErrorHandler
|
||||
{
|
||||
/**
|
||||
* Handle Exceptions that implement Nuwave\Lighthouse\Exceptions\RendersErrorsExtensions
|
||||
* and add extra content from them to the 'extensions' key of the Error that is rendered
|
||||
* to the User.
|
||||
*
|
||||
* @param \GraphQL\Error\Error $error
|
||||
* @param \Closure $next
|
||||
* @return array
|
||||
*/
|
||||
public static function handle(Error $error, Closure $next): array
|
||||
public function __invoke(?Error $error, Closure $next): ?array
|
||||
{
|
||||
$underlyingException = $error->getPrevious();
|
||||
if ($error === null) {
|
||||
return $next(null);
|
||||
}
|
||||
|
||||
$underlyingException = $error->getPrevious();
|
||||
if ($underlyingException instanceof RendersErrorsExtensions) {
|
||||
// Reconstruct the error, passing in the extensions of the underlying exception
|
||||
$error = new Error(
|
||||
$error->message,
|
||||
$error->nodes,
|
||||
return $next(new Error(
|
||||
$error->getMessage(),
|
||||
// @phpstan-ignore-next-line graphql-php and phpstan disagree with themselves
|
||||
$error->getNodes(),
|
||||
$error->getSource(),
|
||||
$error->getPositions(),
|
||||
$error->getPath(),
|
||||
$underlyingException,
|
||||
$underlyingException->extensionsContent()
|
||||
);
|
||||
));
|
||||
}
|
||||
|
||||
return $next($error);
|
||||
|
||||
@@ -3,8 +3,7 @@
|
||||
namespace Nuwave\Lighthouse\Execution;
|
||||
|
||||
/**
|
||||
* May be returned from listeners of the event:.
|
||||
* @see \Nuwave\Lighthouse\Events\BuildExtensionsResponse
|
||||
* May be returned from listeners of @see \Nuwave\Lighthouse\Events\BuildExtensionsResponse.
|
||||
*/
|
||||
class ExtensionsResponse
|
||||
{
|
||||
@@ -16,18 +15,12 @@ class ExtensionsResponse
|
||||
protected $key;
|
||||
|
||||
/**
|
||||
* JSON-encodable content of the extension.
|
||||
*
|
||||
* @var mixed
|
||||
* @var mixed JSON-encodable content of the extension.
|
||||
*/
|
||||
protected $content;
|
||||
|
||||
/**
|
||||
* ExtensionsResponse constructor.
|
||||
*
|
||||
* @param string $key
|
||||
* @param mixed $content
|
||||
* @return void
|
||||
* @param mixed $content JSON-encodable content
|
||||
*/
|
||||
public function __construct(string $key, $content)
|
||||
{
|
||||
@@ -37,8 +30,6 @@ class ExtensionsResponse
|
||||
|
||||
/**
|
||||
* Return the key of the extension.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function key(): string
|
||||
{
|
||||
@@ -46,9 +37,7 @@ class ExtensionsResponse
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the JSON-encodable content of the extension.
|
||||
*
|
||||
* @return mixed
|
||||
* @return mixed JSON-encodable content of the extension.
|
||||
*/
|
||||
public function content()
|
||||
{
|
||||
|
||||
@@ -1,50 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Nuwave\Lighthouse\Execution;
|
||||
|
||||
interface GraphQLRequest
|
||||
{
|
||||
/**
|
||||
* Get the contained GraphQL query string.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function query(): string;
|
||||
|
||||
/**
|
||||
* Get the given variables for the query.
|
||||
*
|
||||
* @return mixed[]
|
||||
*/
|
||||
public function variables(): array;
|
||||
|
||||
/**
|
||||
* Get the operationName of the current request.
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function operationName(): ?string;
|
||||
|
||||
/**
|
||||
* Is the current query a batched query?
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isBatched(): bool;
|
||||
|
||||
/**
|
||||
* Advance the batch index and indicate if there are more batches to process.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function advanceBatchIndex(): bool;
|
||||
|
||||
/**
|
||||
* Get the index of the current batch.
|
||||
*
|
||||
* Returns null if we are not resolving a batched query.
|
||||
*
|
||||
* @return int|null
|
||||
*/
|
||||
public function batchIndex(): ?int;
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Nuwave\Lighthouse\Execution;
|
||||
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Validation\Validator;
|
||||
use GraphQL\Type\Definition\ResolveInfo;
|
||||
use Nuwave\Lighthouse\Support\Contracts\GraphQLContext;
|
||||
|
||||
class GraphQLValidator extends Validator
|
||||
{
|
||||
/**
|
||||
* Get the root object that was passed to the field that is being validated.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function getRoot()
|
||||
{
|
||||
return Arr::get($this->customAttributes, 'root');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the context that was passed to the field that is being validated.
|
||||
*
|
||||
* @return \Nuwave\Lighthouse\Support\Contracts\GraphQLContext
|
||||
*/
|
||||
public function getContext(): GraphQLContext
|
||||
{
|
||||
return Arr::get($this->customAttributes, 'context');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the resolve info that was passed to the field that is being validated.
|
||||
*
|
||||
* @return \GraphQL\Type\Definition\ResolveInfo
|
||||
*/
|
||||
public function getResolveInfo(): ResolveInfo
|
||||
{
|
||||
return Arr::get($this->customAttributes, 'resolveInfo');
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the dot separated path of the field that is being validated.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getFieldPath(): string
|
||||
{
|
||||
return implode(
|
||||
'.',
|
||||
$this->getResolveInfo()->path
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,75 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Nuwave\Lighthouse\Execution;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class LighthouseRequest extends BaseRequest
|
||||
{
|
||||
/**
|
||||
* The incoming HTTP request.
|
||||
*
|
||||
* @var \Illuminate\Http\Request
|
||||
*/
|
||||
protected $request;
|
||||
|
||||
/**
|
||||
* LighthouseRequest constructor.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(Request $request)
|
||||
{
|
||||
$this->request = $request;
|
||||
|
||||
// If the request has neither a query, nor an operationName,
|
||||
// we assume we are resolving a batched query.
|
||||
if (! $request->hasAny('query', 'operationName')) {
|
||||
$this->batchIndex = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the given variables for the query.
|
||||
*
|
||||
* @return mixed[]
|
||||
*/
|
||||
public function variables(): array
|
||||
{
|
||||
$variables = $this->fieldValue('variables');
|
||||
|
||||
// In case we are resolving a GET request, variables
|
||||
// are sent as a JSON encoded string
|
||||
if (is_string($variables)) {
|
||||
return json_decode($variables, true) ?? [];
|
||||
}
|
||||
|
||||
// If this is a POST request, Laravel already decoded the input for us
|
||||
return $variables ?? [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Are there more batched queries to process?
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
protected function hasMoreBatches(): bool
|
||||
{
|
||||
return count($this->request->input()) - 1 > $this->batchIndex;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the contents of a field by key.
|
||||
*
|
||||
* This is expected to take batched requests into consideration.
|
||||
*
|
||||
* @param string $key
|
||||
* @return array|string|null
|
||||
*/
|
||||
protected function fieldValue(string $key)
|
||||
{
|
||||
return $this->request->input($key)
|
||||
?? $this->request->input("{$this->batchIndex}.{$key}");
|
||||
}
|
||||
}
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
<?php
|
||||
|
||||
namespace Nuwave\Lighthouse\Execution\ModelsLoader;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Database\Eloquent\Collection as EloquentCollection;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class AggregateModelsLoader implements ModelsLoader
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $relation;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $column;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $function;
|
||||
|
||||
/**
|
||||
* @var \Closure
|
||||
*/
|
||||
protected $decorateBuilder;
|
||||
|
||||
public function __construct(string $relation, string $column, string $function, Closure $decorateBuilder)
|
||||
{
|
||||
$this->relation = $relation;
|
||||
$this->column = $column;
|
||||
$this->function = $function;
|
||||
$this->decorateBuilder = $decorateBuilder;
|
||||
}
|
||||
|
||||
public function load(EloquentCollection $parents): void
|
||||
{
|
||||
// @phpstan-ignore-next-line Only present in Laravel 8+
|
||||
$parents->loadAggregate([$this->relation => $this->decorateBuilder], $this->column, $this->function);
|
||||
}
|
||||
|
||||
public function extract(Model $model)
|
||||
{
|
||||
/**
|
||||
* This is the name that Eloquent gives to the attribute that contains the aggregate.
|
||||
*
|
||||
* @see \Illuminate\Database\Eloquent\Concerns\QueriesRelationships::withAggregate()
|
||||
*/
|
||||
$attribute = Str::snake(
|
||||
\Safe\preg_replace('/[^[:alnum:][:space:]_]/u', '', "$this->relation $this->function $this->column")
|
||||
);
|
||||
|
||||
return $model->getAttribute($attribute);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
<?php
|
||||
|
||||
namespace Nuwave\Lighthouse\Execution\ModelsLoader;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Database\Eloquent\Collection as EloquentCollection;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class CountModelsLoader implements ModelsLoader
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $relation;
|
||||
|
||||
/**
|
||||
* @var \Closure
|
||||
*/
|
||||
protected $decorateBuilder;
|
||||
|
||||
public function __construct(string $relation, Closure $decorateBuilder)
|
||||
{
|
||||
$this->relation = $relation;
|
||||
$this->decorateBuilder = $decorateBuilder;
|
||||
}
|
||||
|
||||
public function load(EloquentCollection $parents): void
|
||||
{
|
||||
self::loadCount($parents, [$this->relation => $this->decorateBuilder]);
|
||||
}
|
||||
|
||||
public function extract(Model $model): int
|
||||
{
|
||||
return self::extractCount($model, $this->relation);
|
||||
}
|
||||
|
||||
public static function extractCount(Model $model, string $relationName): int
|
||||
{
|
||||
/**
|
||||
* This is the name that Eloquent gives to the attribute that contains the count.
|
||||
*
|
||||
* @see \Illuminate\Database\Eloquent\Concerns\QueriesRelationships::withCount()
|
||||
*/
|
||||
$countAttributeName = Str::snake("${relationName}_count");
|
||||
|
||||
/**
|
||||
* We just assert this is an int and let PHP run into a type error if not.
|
||||
*
|
||||
* @var int $count
|
||||
*/
|
||||
$count = $model->getAttribute($countAttributeName);
|
||||
|
||||
return $count;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reload the models to get the `{relation}_count` attributes of models set.
|
||||
*
|
||||
* @deprecated Laravel 5.7 has native ->loadCount() on EloquentCollection
|
||||
* @see \Illuminate\Database\Eloquent\Collection::loadCount()
|
||||
*
|
||||
* @param array<string, \Closure> $relations
|
||||
*/
|
||||
public static function loadCount(EloquentCollection $parents, array $relations): void
|
||||
{
|
||||
if ($parents->isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$models = $parents->first()->newModelQuery()
|
||||
->whereKey($parents->modelKeys())
|
||||
->select($parents->first()->getKeyName())
|
||||
->withCount($relations)
|
||||
->get()
|
||||
->keyBy($parents->first()->getKeyName());
|
||||
|
||||
$attributes = Arr::except(
|
||||
array_keys($models->first()->getAttributes()),
|
||||
$models->first()->getKeyName()
|
||||
);
|
||||
|
||||
foreach ($parents as $model) {
|
||||
$extraAttributes = Arr::only($models->get($model->getKey())->getAttributes(), $attributes);
|
||||
|
||||
$model->forceFill($extraAttributes);
|
||||
|
||||
foreach ($attributes as $attribute) {
|
||||
$model->syncOriginalAttribute($attribute);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
namespace Nuwave\Lighthouse\Execution\ModelsLoader;
|
||||
|
||||
use Illuminate\Database\Eloquent\Collection as EloquentCollection;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
interface ModelsLoader
|
||||
{
|
||||
/**
|
||||
* Load the result onto the given parent models.
|
||||
*/
|
||||
public function load(EloquentCollection $parents): void;
|
||||
|
||||
/**
|
||||
* Extract the result of loading from the given model.
|
||||
*
|
||||
* @return mixed Whatever was loaded
|
||||
*/
|
||||
public function extract(Model $model);
|
||||
}
|
||||
+208
@@ -0,0 +1,208 @@
|
||||
<?php
|
||||
|
||||
namespace Nuwave\Lighthouse\Execution\ModelsLoader;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Database\Eloquent\Builder as EloquentBuilder;
|
||||
use Illuminate\Database\Eloquent\Collection as EloquentCollection;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||
use Illuminate\Database\Eloquent\Relations\HasManyThrough;
|
||||
use Illuminate\Database\Eloquent\Relations\Relation;
|
||||
use Illuminate\Pagination\LengthAwarePaginator;
|
||||
use Nuwave\Lighthouse\Pagination\PaginationArgs;
|
||||
use ReflectionClass;
|
||||
use ReflectionMethod;
|
||||
|
||||
class PaginatedModelsLoader implements ModelsLoader
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $relation;
|
||||
|
||||
/**
|
||||
* @var \Closure
|
||||
*/
|
||||
protected $decorateBuilder;
|
||||
|
||||
/**
|
||||
* @var \Nuwave\Lighthouse\Pagination\PaginationArgs
|
||||
*/
|
||||
protected $paginationArgs;
|
||||
|
||||
public function __construct(string $relation, Closure $decorateBuilder, PaginationArgs $paginationArgs)
|
||||
{
|
||||
$this->relation = $relation;
|
||||
$this->decorateBuilder = $decorateBuilder;
|
||||
$this->paginationArgs = $paginationArgs;
|
||||
}
|
||||
|
||||
public function load(EloquentCollection $parents): void
|
||||
{
|
||||
CountModelsLoader::loadCount($parents, [$this->relation => $this->decorateBuilder]);
|
||||
|
||||
$relatedModels = $this->loadRelatedModels($parents);
|
||||
|
||||
$this->hydratePivotRelation($parents, $relatedModels);
|
||||
$this->loadDefaultWith($relatedModels);
|
||||
$this->associateRelationModels($parents, $relatedModels);
|
||||
$this->convertRelationToPaginator($parents);
|
||||
}
|
||||
|
||||
public function extract(Model $model)
|
||||
{
|
||||
return $model->getRelation($this->relation);
|
||||
}
|
||||
|
||||
protected function loadRelatedModels(EloquentCollection $parents): EloquentCollection
|
||||
{
|
||||
$relations = $parents
|
||||
->toBase()
|
||||
->map(function (Model $model) use ($parents): Relation {
|
||||
$relation = $this->relationInstance($parents);
|
||||
|
||||
$relation->addEagerConstraints([$model]);
|
||||
|
||||
($this->decorateBuilder)($relation, $model);
|
||||
|
||||
if ($relation instanceof BelongsToMany || $relation instanceof HasManyThrough) {
|
||||
$shouldSelect = new ReflectionMethod(get_class($relation), 'shouldSelect');
|
||||
$shouldSelect->setAccessible(true);
|
||||
$select = $shouldSelect->invoke($relation, ['*']);
|
||||
|
||||
// @phpstan-ignore-next-line Builder mixin is not understood
|
||||
$relation->addSelect($select);
|
||||
}
|
||||
|
||||
$relation->initRelation([$model], $this->relation);
|
||||
|
||||
// @phpstan-ignore-next-line Builder mixin is not understood
|
||||
return $relation->forPage($this->paginationArgs->page, $this->paginationArgs->first);
|
||||
});
|
||||
|
||||
// Merge all the relation queries into a single query with UNION ALL.
|
||||
|
||||
/**
|
||||
* Use the first query as the initial starting point.
|
||||
*
|
||||
* We can assume this to be non-null because only non-empty lists of parents
|
||||
* are passed into this loader.
|
||||
*
|
||||
* @var \Illuminate\Database\Eloquent\Relations\Relation $firstRelation
|
||||
*/
|
||||
$firstRelation = $relations->shift();
|
||||
|
||||
// We have to make sure to use ->getQuery() in order to respect
|
||||
// model scopes, such as soft deletes
|
||||
$mergedRelationQuery = $relations->reduce(
|
||||
static function (EloquentBuilder $builder, Relation $relation): EloquentBuilder {
|
||||
return $builder->unionAll(
|
||||
// @phpstan-ignore-next-line Laravel can deal with an EloquentBuilder just fine
|
||||
$relation->getQuery()
|
||||
);
|
||||
},
|
||||
$firstRelation->getQuery()
|
||||
);
|
||||
|
||||
return $mergedRelationQuery->get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Use the underlying model to instantiate a relation by name.
|
||||
*/
|
||||
protected function relationInstance(EloquentCollection $parents): Relation
|
||||
{
|
||||
return $this
|
||||
->newModelQuery($parents)
|
||||
->getRelation($this->relation);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a fresh instance of a query builder for the underlying model.
|
||||
*/
|
||||
protected function newModelQuery(EloquentCollection $parents): EloquentBuilder
|
||||
{
|
||||
/** @var \Illuminate\Database\Eloquent\Model $anyModelInstance */
|
||||
$anyModelInstance = $parents->first();
|
||||
|
||||
/** @var \Illuminate\Database\Eloquent\Builder $newModelQuery */
|
||||
$newModelQuery = $anyModelInstance->newModelQuery();
|
||||
|
||||
return $newModelQuery;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure the pivot relation is hydrated too, if it exists.
|
||||
*
|
||||
* @param \Illuminate\Database\Eloquent\Collection<\Illuminate\Database\Eloquent\Model> $relatedModels
|
||||
*/
|
||||
protected function hydratePivotRelation(EloquentCollection $parents, EloquentCollection $relatedModels): void
|
||||
{
|
||||
$relation = $this->relationInstance($parents);
|
||||
|
||||
if ($relatedModels->isNotEmpty() && method_exists($relation, 'hydratePivotRelation')) {
|
||||
$hydrationMethod = new ReflectionMethod(get_class($relation), 'hydratePivotRelation');
|
||||
$hydrationMethod->setAccessible(true);
|
||||
$hydrationMethod->invoke($relation, $relatedModels->all());
|
||||
}
|
||||
}
|
||||
|
||||
protected function loadDefaultWith(EloquentCollection $collection): void
|
||||
{
|
||||
/** @var \Illuminate\Database\Eloquent\Model|null $model */
|
||||
$model = $collection->first();
|
||||
if ($model === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
$reflection = new ReflectionClass($model);
|
||||
$withProperty = $reflection->getProperty('with');
|
||||
$withProperty->setAccessible(true);
|
||||
|
||||
$unloadedWiths = array_filter(
|
||||
(array) $withProperty->getValue($model),
|
||||
static function (string $relation) use ($model): bool {
|
||||
return ! $model->relationLoaded($relation);
|
||||
}
|
||||
);
|
||||
|
||||
if (count($unloadedWiths) > 0) {
|
||||
$collection->load($unloadedWiths);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Associate the collection of all fetched relationModels back with their parents.
|
||||
*/
|
||||
protected function associateRelationModels(EloquentCollection $parents, EloquentCollection $relatedModels): void
|
||||
{
|
||||
$this
|
||||
->relationInstance($parents)
|
||||
->match(
|
||||
$parents->all(),
|
||||
$relatedModels,
|
||||
$this->relation
|
||||
);
|
||||
}
|
||||
|
||||
protected function convertRelationToPaginator(EloquentCollection $parents): void
|
||||
{
|
||||
foreach ($parents as $model) {
|
||||
$total = CountModelsLoader::extractCount($model, $this->relation);
|
||||
|
||||
$paginator = app()->makeWith(
|
||||
LengthAwarePaginator::class,
|
||||
[
|
||||
'items' => $model->getRelation($this->relation),
|
||||
'total' => $total,
|
||||
'perPage' => $this->paginationArgs->first,
|
||||
'currentPage' => $this->paginationArgs->page,
|
||||
'options' => [],
|
||||
]
|
||||
);
|
||||
|
||||
$model->setRelation($this->relation, $paginator);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
namespace Nuwave\Lighthouse\Execution\ModelsLoader;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Database\Eloquent\Collection as EloquentCollection;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class SimpleModelsLoader implements ModelsLoader
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $relation;
|
||||
|
||||
/**
|
||||
* @var \Closure
|
||||
*/
|
||||
protected $decorateBuilder;
|
||||
|
||||
public function __construct(string $relation, Closure $decorateBuilder)
|
||||
{
|
||||
$this->relation = $relation;
|
||||
$this->decorateBuilder = $decorateBuilder;
|
||||
}
|
||||
|
||||
public function load(EloquentCollection $parents): void
|
||||
{
|
||||
$parents->load([$this->relation => $this->decorateBuilder]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract the relation that was loaded.
|
||||
*
|
||||
* @return mixed The model's relation.
|
||||
*/
|
||||
public function extract(Model $model)
|
||||
{
|
||||
// Dot notation may be used to eager load nested relations
|
||||
$parts = explode('.', $this->relation);
|
||||
|
||||
// We just return the first level of relations for now.
|
||||
// They hold the nested relations in case they are needed.
|
||||
$firstRelation = $parts[0];
|
||||
|
||||
return $model->getRelation($firstRelation);
|
||||
}
|
||||
}
|
||||
@@ -1,93 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Nuwave\Lighthouse\Execution;
|
||||
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Http\Request;
|
||||
use GraphQL\Error\InvariantViolation;
|
||||
|
||||
class MultipartFormRequest extends BaseRequest
|
||||
{
|
||||
/**
|
||||
* One or more operations, consisting of query, variables and operationName.
|
||||
*
|
||||
* https://github.com/jaydenseric/graphql-multipart-request-spec#single-file
|
||||
*
|
||||
* @var mixed[]
|
||||
*/
|
||||
protected $operations;
|
||||
|
||||
/**
|
||||
* MultipartFormRequest constructor.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(Request $request)
|
||||
{
|
||||
if (! $request->has('map')) {
|
||||
throw new InvariantViolation(
|
||||
'Could not find a valid map, be sure to conform to GraphQL multipart request specification: https://github.com/jaydenseric/graphql-multipart-request-spec'
|
||||
);
|
||||
}
|
||||
|
||||
$this->operations = json_decode(
|
||||
$request->input('operations'),
|
||||
true
|
||||
);
|
||||
|
||||
// If operations is 0-indexed, we assume we are resolving a batched query
|
||||
if (isset($this->operations[0])) {
|
||||
$this->batchIndex = 0;
|
||||
}
|
||||
|
||||
$map = json_decode($request->input('map'), true);
|
||||
|
||||
/**
|
||||
* @var string
|
||||
* @var array $operationsPaths
|
||||
*/
|
||||
foreach ($map as $fileKey => $operationsPaths) {
|
||||
$file = $request->file($fileKey);
|
||||
|
||||
/** @var string $operationsPath */
|
||||
foreach ($operationsPaths as $operationsPath) {
|
||||
Arr::set($this->operations, $operationsPath, $file);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the given variables for the query.
|
||||
*
|
||||
* @return mixed[]
|
||||
*/
|
||||
public function variables(): array
|
||||
{
|
||||
return $this->fieldValue('variables') ?? [];
|
||||
}
|
||||
|
||||
/**
|
||||
* If we are dealing with a batched request, this gets the
|
||||
* contents of the currently resolving batch index.
|
||||
*
|
||||
* @param string $key
|
||||
* @return array|string|null
|
||||
*/
|
||||
protected function fieldValue(string $key)
|
||||
{
|
||||
return $this->isBatched()
|
||||
? Arr::get($this->operations, $this->batchIndex.'.'.$key)
|
||||
: $this->operations[$key] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Are there more batched queries to process?
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
protected function hasMoreBatches(): bool
|
||||
{
|
||||
return count($this->operations) - 1 > $this->batchIndex;
|
||||
}
|
||||
}
|
||||
@@ -1,426 +0,0 @@
|
||||
<?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
|
||||
{
|
||||
/**
|
||||
* Execute a create mutation.
|
||||
*
|
||||
* @param \Illuminate\Database\Eloquent\Model $model
|
||||
* An empty instance of the model that should be created
|
||||
* @param \Illuminate\Support\Collection $args
|
||||
* The corresponding slice of the input arguments for creating this model
|
||||
* @param \Illuminate\Database\Eloquent\Relations\Relation|null $parentRelation
|
||||
* If we are in a nested create, we can use this to associate the new model to its parent
|
||||
* @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);
|
||||
|
||||
[$morphMany, $remaining] = self::partitionArgsByRelationType($reflection, $remaining, MorphMany::class);
|
||||
|
||||
[$hasOne, $remaining] = self::partitionArgsByRelationType($reflection, $remaining, HasOne::class);
|
||||
|
||||
[$morphOne, $remaining] = self::partitionArgsByRelationType($reflection, $remaining, MorphOne::class);
|
||||
|
||||
[$belongsToMany, $remaining] = self::partitionArgsByRelationType($reflection, $remaining, BelongsToMany::class);
|
||||
|
||||
[$morphToMany, $remaining] = self::partitionArgsByRelationType($reflection, $remaining, MorphToMany::class);
|
||||
|
||||
$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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Save a model that maybe has a parent.
|
||||
*
|
||||
* @param \Illuminate\Database\Eloquent\Model $model
|
||||
* @param \Illuminate\Support\Collection $args
|
||||
* @param \Illuminate\Database\Eloquent\Relations\Relation|null $parentRelation
|
||||
* @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
|
||||
);
|
||||
|
||||
[$belongsTo, $remaining] = self::partitionArgsByRelationType(
|
||||
$reflection,
|
||||
$remaining,
|
||||
BelongsTo::class
|
||||
);
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the creation with multiple relations.
|
||||
*
|
||||
* @param \Illuminate\Support\Collection $multiValues
|
||||
* @param \Illuminate\Database\Eloquent\Relations\Relation $relation
|
||||
* @return void
|
||||
*/
|
||||
protected static function handleMultiRelationCreate(Collection $multiValues, Relation $relation): void
|
||||
{
|
||||
$multiValues->each(function ($singleValues) use ($relation): void {
|
||||
self::handleSingleRelationCreate(new Collection($singleValues), $relation);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the creation with a single relation.
|
||||
*
|
||||
* @param \Illuminate\Support\Collection $singleValues
|
||||
* @param \Illuminate\Database\Eloquent\Relations\Relation $relation
|
||||
* @return void
|
||||
*/
|
||||
protected static function handleSingleRelationCreate(Collection $singleValues, Relation $relation): void
|
||||
{
|
||||
self::executeCreate(
|
||||
$relation->getModel()->newInstance(),
|
||||
$singleValues,
|
||||
$relation
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute an update mutation.
|
||||
*
|
||||
* @param \Illuminate\Database\Eloquent\Model $model
|
||||
* An empty instance of the model that should be updated
|
||||
* @param \Illuminate\Support\Collection $args
|
||||
* The corresponding slice of the input arguments for updating this model
|
||||
* @param \Illuminate\Database\Eloquent\Relations\Relation|null $parentRelation
|
||||
* If we are in a nested update, we can use this to associate the new model to its parent
|
||||
* @return \Illuminate\Database\Eloquent\Model
|
||||
*/
|
||||
public static function executeUpdate(Model $model, Collection $args, ?Relation $parentRelation = null): Model
|
||||
{
|
||||
$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);
|
||||
|
||||
[$morphMany, $remaining] = self::partitionArgsByRelationType($reflection, $remaining, MorphMany::class);
|
||||
|
||||
[$hasOne, $remaining] = self::partitionArgsByRelationType($reflection, $remaining, HasOne::class);
|
||||
|
||||
[$morphOne, $remaining] = self::partitionArgsByRelationType($reflection, $remaining, MorphOne::class);
|
||||
|
||||
[$belongsToMany, $remaining] = self::partitionArgsByRelationType($reflection, $remaining, BelongsToMany::class);
|
||||
|
||||
[$morphToMany, $remaining] = self::partitionArgsByRelationType($reflection, $remaining, MorphToMany::class);
|
||||
|
||||
$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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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:
|
||||
*
|
||||
* [
|
||||
* 'comments' =>
|
||||
* ['foo' => 'Bar'],
|
||||
* 'name' => 'Ralf',
|
||||
* ]
|
||||
*
|
||||
* and the model has a method "comments" that returns a HasMany relationship,
|
||||
* the result will be:
|
||||
* [
|
||||
* [
|
||||
* 'comments' =>
|
||||
* ['foo' => 'Bar'],
|
||||
* ],
|
||||
* [
|
||||
* 'name' => 'Ralf',
|
||||
* ]
|
||||
* ]
|
||||
*
|
||||
* @param \ReflectionClass $modelReflection
|
||||
* @param \Illuminate\Support\Collection $args
|
||||
* @param string $relationClass
|
||||
* @return \Illuminate\Support\Collection [relationshipArgs, remainingArgs]
|
||||
*/
|
||||
protected static function partitionArgsByRelationType(ReflectionClass $modelReflection, Collection $args, string $relationClass): Collection
|
||||
{
|
||||
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);
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,76 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Nuwave\Lighthouse\Execution;
|
||||
|
||||
use GraphQL\Language\Parser;
|
||||
use GraphQL\Language\AST\Node;
|
||||
use Illuminate\Support\Collection;
|
||||
use GraphQL\Language\AST\DocumentNode;
|
||||
use GraphQL\Language\AST\FragmentDefinitionNode;
|
||||
use GraphQL\Language\AST\OperationDefinitionNode;
|
||||
|
||||
class QueryAST
|
||||
{
|
||||
/**
|
||||
* The definitions contained in the AST of an incoming query.
|
||||
*
|
||||
* @var \Illuminate\Support\Collection
|
||||
*/
|
||||
protected $definitions;
|
||||
|
||||
/**
|
||||
* @param \GraphQL\Language\AST\DocumentNode $documentNode
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(DocumentNode $documentNode)
|
||||
{
|
||||
$this->definitions = new Collection($documentNode->definitions);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new instance from a query string.
|
||||
*
|
||||
* @param string $query
|
||||
* @return static
|
||||
*/
|
||||
public static function fromSource(string $query): self
|
||||
{
|
||||
return new static(
|
||||
Parser::parse($query)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all operation definitions.
|
||||
*
|
||||
* @return \Illuminate\Support\Collection<\GraphQL\Language\AST\OperationDefinitionNode>
|
||||
*/
|
||||
public function operationDefinitions(): Collection
|
||||
{
|
||||
return $this->definitionsByType(OperationDefinitionNode::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all fragment definitions.
|
||||
*
|
||||
* @return \Illuminate\Support\Collection<\GraphQL\Language\AST\FragmentDefinitionNode>
|
||||
*/
|
||||
public function fragmentDefinitions(): Collection
|
||||
{
|
||||
return $this->definitionsByType(FragmentDefinitionNode::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all definitions of a given type.
|
||||
*
|
||||
* @param string $typeClassName
|
||||
* @return \Illuminate\Support\Collection
|
||||
*/
|
||||
protected function definitionsByType(string $typeClassName): Collection
|
||||
{
|
||||
return $this->definitions
|
||||
->filter(function (Node $node) use ($typeClassName): bool {
|
||||
return $node instanceof $typeClassName;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
namespace Nuwave\Lighthouse\Execution;
|
||||
|
||||
use Closure;
|
||||
use GraphQL\Error\Error;
|
||||
use Illuminate\Contracts\Debug\ExceptionHandler;
|
||||
|
||||
/**
|
||||
* Report errors through the default exception handler configured in Laravel.
|
||||
*/
|
||||
class ReportingErrorHandler implements ErrorHandler
|
||||
{
|
||||
/**
|
||||
* @var \Illuminate\Contracts\Debug\ExceptionHandler
|
||||
*/
|
||||
protected $exceptionHandler;
|
||||
|
||||
public function __construct(ExceptionHandler $exceptionHandler)
|
||||
{
|
||||
$this->exceptionHandler = $exceptionHandler;
|
||||
}
|
||||
|
||||
public function __invoke(?Error $error, Closure $next): ?array
|
||||
{
|
||||
if ($error === null) {
|
||||
return $next(null);
|
||||
}
|
||||
|
||||
// Client-safe errors are assumed to be something that a client can handle
|
||||
// or is expected to happen, e.g. wrong syntax, authentication or validation
|
||||
if ($error->isClientSafe()) {
|
||||
return $next($error);
|
||||
}
|
||||
|
||||
$previous = $error->getPrevious();
|
||||
if ($previous !== null) {
|
||||
// @phpstan-ignore-next-line Laravel versions prior to 7 are limited to accepting \Exception
|
||||
$this->exceptionHandler->report($previous);
|
||||
}
|
||||
|
||||
return $next($error);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace Nuwave\Lighthouse\Execution;
|
||||
|
||||
use GraphQL\Deferred;
|
||||
|
||||
class Resolved
|
||||
{
|
||||
/**
|
||||
* Apply the transform function to a result or chain it onto a Deferred.
|
||||
*
|
||||
* @param \GraphQL\Deferred|mixed $resolved The result of calling a resolver
|
||||
* @param callable(mixed $result): mixed $handle A function that takes that result and transforms it
|
||||
* @return \GraphQL\Deferred|mixed The transformed result or enhanced Deferred
|
||||
*/
|
||||
public static function handle($resolved, callable $handle)
|
||||
{
|
||||
if ($resolved instanceof Deferred) {
|
||||
return $resolved->then($handle);
|
||||
}
|
||||
|
||||
return $handle($resolved);
|
||||
}
|
||||
}
|
||||
@@ -2,17 +2,11 @@
|
||||
|
||||
namespace Nuwave\Lighthouse\Execution;
|
||||
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Nuwave\Lighthouse\Support\Contracts\CreatesResponse;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class SingleResponse implements CreatesResponse
|
||||
{
|
||||
/**
|
||||
* Create a HTTP response from the final result.
|
||||
*
|
||||
* @param mixed[] $result
|
||||
* @return \Symfony\Component\HttpFoundation\Response
|
||||
*/
|
||||
public function createResponse(array $result): Response
|
||||
{
|
||||
return response($result);
|
||||
|
||||
@@ -1,69 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Nuwave\Lighthouse\Execution\Utils;
|
||||
|
||||
use Nuwave\Lighthouse\Support\Contracts\GlobalId as GlobalIdContract;
|
||||
|
||||
/**
|
||||
* The default encoding of global IDs in Lighthouse.
|
||||
*
|
||||
* The way that IDs are generated basically works like this:
|
||||
*
|
||||
* 1. Take the name of a type, e.g. "User" and an ID, e.g. 123
|
||||
* 2. Glue them together, separated by a colon, e.g. "User:123"
|
||||
* 3. base64_encode the result
|
||||
*
|
||||
* This can then be reversed to uniquely identify an entity in our
|
||||
* schema, just by looking at a single ID.
|
||||
*/
|
||||
class GlobalId implements GlobalIdContract
|
||||
{
|
||||
/**
|
||||
* Glue together a type and an id to create a global id.
|
||||
*
|
||||
* @param string $type
|
||||
* @param string|int $id
|
||||
* @return string
|
||||
*/
|
||||
public function encode(string $type, $id): string
|
||||
{
|
||||
return base64_encode($type.':'.$id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Split a global id into the type and the id it contains.
|
||||
*
|
||||
* @param string $globalID
|
||||
* @return array Contains [$type, $id], e.g. ['User', '123']
|
||||
*/
|
||||
public function decode(string $globalID): array
|
||||
{
|
||||
return explode(':', base64_decode($globalID));
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode the Global ID and get just the ID.
|
||||
*
|
||||
* @param string $globalID
|
||||
* @return string
|
||||
*/
|
||||
public function decodeID(string $globalID): string
|
||||
{
|
||||
[$type, $id] = self::decode($globalID);
|
||||
|
||||
return $id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode the Global ID and get just the type.
|
||||
*
|
||||
* @param string $globalID
|
||||
* @return string
|
||||
*/
|
||||
public function decodeType(string $globalID): string
|
||||
{
|
||||
[$type, $id] = self::decode($globalID);
|
||||
|
||||
return $type;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace Nuwave\Lighthouse\Execution\Utils;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
/**
|
||||
* Create a model key that concatenates the models fully-qualified class
|
||||
* name and key or composite key.
|
||||
*/
|
||||
class ModelKey
|
||||
{
|
||||
public static function build(Model $model): string
|
||||
{
|
||||
return implode(
|
||||
':',
|
||||
array_merge(
|
||||
[get_class($model)],
|
||||
// Might be one or more keys
|
||||
(array) ($model->getKey())
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
+16
-24
@@ -2,32 +2,27 @@
|
||||
|
||||
namespace Nuwave\Lighthouse\Execution\Utils;
|
||||
|
||||
use Throwable;
|
||||
use InvalidArgumentException;
|
||||
use Nuwave\Lighthouse\GraphQL;
|
||||
use Nuwave\Lighthouse\Subscriptions\SubscriptionRegistry;
|
||||
use Nuwave\Lighthouse\Schema\SchemaBuilder;
|
||||
use Nuwave\Lighthouse\Subscriptions\Contracts\BroadcastsSubscriptions;
|
||||
use Nuwave\Lighthouse\Subscriptions\Contracts\SubscriptionExceptionHandler;
|
||||
use Nuwave\Lighthouse\Subscriptions\SubscriptionRegistry;
|
||||
use Throwable;
|
||||
|
||||
class Subscription
|
||||
{
|
||||
/**
|
||||
* Broadcast subscription to client(s).
|
||||
*
|
||||
* @param string $subscriptionField
|
||||
* @param mixed $root
|
||||
* @param bool|null $shouldQueue
|
||||
* @return void
|
||||
*
|
||||
* @throws \InvalidArgumentException
|
||||
*/
|
||||
public static function broadcast(string $subscriptionField, $root, ?bool $shouldQueue = null): void
|
||||
{
|
||||
// Ensure we have a schema and registered subscription fields
|
||||
// in the event we are calling this method in code.
|
||||
/** @var \Nuwave\Lighthouse\GraphQL $graphQL */
|
||||
$graphQL = app(GraphQL::class);
|
||||
$graphQL->prepSchema();
|
||||
/** @var \Nuwave\Lighthouse\Schema\SchemaBuilder $schemaBuilder */
|
||||
$schemaBuilder = app(SchemaBuilder::class);
|
||||
$schemaBuilder->schema();
|
||||
|
||||
/** @var \Nuwave\Lighthouse\Subscriptions\SubscriptionRegistry $registry */
|
||||
$registry = app(SubscriptionRegistry::class);
|
||||
@@ -39,25 +34,22 @@ class Subscription
|
||||
/** @var \Nuwave\Lighthouse\Subscriptions\Contracts\BroadcastsSubscriptions $broadcaster */
|
||||
$broadcaster = app(BroadcastsSubscriptions::class);
|
||||
|
||||
$shouldQueue = $shouldQueue === null
|
||||
? config('lighthouse.subscriptions.queue_broadcasts', false)
|
||||
: $shouldQueue;
|
||||
// Default to the configuration setting if not specified
|
||||
if ($shouldQueue === null) {
|
||||
$shouldQueue = config('lighthouse.subscriptions.queue_broadcasts', false);
|
||||
}
|
||||
|
||||
$method = $shouldQueue
|
||||
? 'queueBroadcast'
|
||||
: 'broadcast';
|
||||
$subscription = $registry->subscription($subscriptionField);
|
||||
|
||||
try {
|
||||
call_user_func(
|
||||
[$broadcaster, $method],
|
||||
$registry->subscription($subscriptionField),
|
||||
$subscriptionField,
|
||||
$root
|
||||
);
|
||||
if ($shouldQueue) {
|
||||
$broadcaster->queueBroadcast($subscription, $subscriptionField, $root);
|
||||
} else {
|
||||
$broadcaster->broadcast($subscription, $subscriptionField, $root);
|
||||
}
|
||||
} catch (Throwable $e) {
|
||||
/** @var \Nuwave\Lighthouse\Subscriptions\Contracts\SubscriptionExceptionHandler $exceptionHandler */
|
||||
$exceptionHandler = app(SubscriptionExceptionHandler::class);
|
||||
|
||||
$exceptionHandler->handleBroadcastError($e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
namespace Nuwave\Lighthouse\Execution;
|
||||
|
||||
use Closure;
|
||||
use GraphQL\Error\Error;
|
||||
use Illuminate\Validation\ValidationException as LaravelValidationException;
|
||||
use Nuwave\Lighthouse\Exceptions\ValidationException;
|
||||
|
||||
/**
|
||||
* Wrap native Laravel validation exceptions, adding structured data to extensions.
|
||||
*/
|
||||
class ValidationErrorHandler implements ErrorHandler
|
||||
{
|
||||
public function __invoke(?Error $error, Closure $next): ?array
|
||||
{
|
||||
if ($error === null) {
|
||||
return $next(null);
|
||||
}
|
||||
|
||||
$underlyingException = $error->getPrevious();
|
||||
if ($underlyingException instanceof LaravelValidationException) {
|
||||
return $next(new Error(
|
||||
$error->getMessage(),
|
||||
// @phpstan-ignore-next-line graphql-php and phpstan disagree with themselves
|
||||
$error->getNodes(),
|
||||
$error->getSource(),
|
||||
$error->getPositions(),
|
||||
$error->getPath(),
|
||||
ValidationException::fromLaravel($underlyingException)
|
||||
));
|
||||
}
|
||||
|
||||
return $next($error);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
namespace Nuwave\Lighthouse\Execution;
|
||||
|
||||
use GraphQL\Validator\DocumentValidator;
|
||||
use GraphQL\Validator\Rules\DisableIntrospection;
|
||||
use GraphQL\Validator\Rules\QueryComplexity;
|
||||
use GraphQL\Validator\Rules\QueryDepth;
|
||||
use Illuminate\Contracts\Config\Repository as ConfigRepository;
|
||||
use Nuwave\Lighthouse\Support\Contracts\ProvidesValidationRules;
|
||||
|
||||
class ValidationRulesProvider implements ProvidesValidationRules
|
||||
{
|
||||
/**
|
||||
* @var \Illuminate\Contracts\Config\Repository
|
||||
*/
|
||||
protected $configRepository;
|
||||
|
||||
public function __construct(ConfigRepository $configRepository)
|
||||
{
|
||||
$this->configRepository = $configRepository;
|
||||
}
|
||||
|
||||
public function validationRules(): ?array
|
||||
{
|
||||
return [
|
||||
QueryComplexity::class => new QueryComplexity($this->configRepository->get('lighthouse.security.max_query_complexity', 0)),
|
||||
QueryDepth::class => new QueryDepth($this->configRepository->get('lighthouse.security.max_query_depth', 0)),
|
||||
DisableIntrospection::class => new DisableIntrospection($this->configRepository->get('lighthouse.security.disable_introspection', 0)),
|
||||
] + DocumentValidator::defaultRules();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user