Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 48 additions & 0 deletions phpstan-baseline.neon
Original file line number Diff line number Diff line change
Expand Up @@ -462,6 +462,24 @@ parameters:
count: 1
path: src/Support/SelectFields.php

-
rawMessage: 'Method Rebing\GraphQL\Support\SelectFields::handleMorphToRelation() has parameter $field with no value type specified in iterable type array.'
identifier: missingType.iterableValue
count: 1
path: src/Support/SelectFields.php

-
rawMessage: 'Method Rebing\GraphQL\Support\SelectFields::handleMorphToRelation() has parameter $queryArgs with no value type specified in iterable type array.'
identifier: missingType.iterableValue
count: 1
path: src/Support/SelectFields.php

-
rawMessage: 'Method Rebing\GraphQL\Support\SelectFields::handleMorphToRelation() has parameter $with with no value type specified in iterable type array.'
identifier: missingType.iterableValue
count: 1
path: src/Support/SelectFields.php

-
rawMessage: 'Method Rebing\GraphQL\Support\SelectFields::handleRelation() has parameter $field with no value type specified in iterable type array.'
identifier: missingType.iterableValue
Expand Down Expand Up @@ -1032,6 +1050,36 @@ parameters:
count: 1
path: tests/Database/SelectFields/ValidateFieldTests/ValidateFieldsQuery.php

-
rawMessage: 'Method Rebing\GraphQL\Tests\Database\SelectFields\UnionMorphTests\CommentsQuery::resolve() has parameter $args with no type specified.'
identifier: missingType.parameter
count: 1
path: tests/Database/SelectFields/UnionMorphTests/CommentsQuery.php

-
rawMessage: 'Method Rebing\GraphQL\Tests\Database\SelectFields\UnionMorphTests\CommentsQuery::resolve() has parameter $context with no type specified.'
identifier: missingType.parameter
count: 1
path: tests/Database/SelectFields/UnionMorphTests/CommentsQuery.php

-
rawMessage: 'Method Rebing\GraphQL\Tests\Database\SelectFields\UnionMorphTests\CommentsQuery::resolve() has parameter $root with no type specified.'
identifier: missingType.parameter
count: 1
path: tests/Database/SelectFields/UnionMorphTests/CommentsQuery.php

-
rawMessage: 'Method Rebing\GraphQL\Tests\Database\SelectFields\UnionMorphTests\CommentsQuery::resolve() return type has no value type specified in iterable type array.'
identifier: missingType.iterableValue
count: 1
path: tests/Database/SelectFields/UnionMorphTests/CommentsQuery.php

-
rawMessage: 'Method Rebing\GraphQL\Tests\Database\SelectFields\UnionMorphTests\CommentsQuery::resolve() return type has no value type specified in iterable type array|Illuminate\Database\Eloquent\Collection.'
identifier: missingType.iterableValue
count: 1
path: tests/Database/SelectFields/UnionMorphTests/CommentsQuery.php

-
rawMessage: 'Method Rebing\GraphQL\Tests\Support\Objects\CustomExamplesQuery::resolve() has no return type specified.'
identifier: missingType.return
Expand Down
191 changes: 170 additions & 21 deletions src/Support/SelectFields.php
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,19 @@ protected static function handleFields(
}
// With

elseif (\is_array($field['fields']) && !empty($field['fields']) && $queryable) {
elseif (is_a($parentTypeUnwrapped, \GraphQL\Type\Definition\InterfaceType::class)) {
static::handleInterfaceFields(
$queryArgs,
$field,
$parentTypeUnwrapped,
$select,
$with,
$ctx,
$fieldObject,
$key,
$customQuery
);
} elseif (\is_array($field['fields']) && !empty($field['fields']) && $queryable) {
if (isset($parentType->config['model'])) {
// Get the next parent type, so that 'with' queries could be made
// Both keys for the relation are required (e.g 'id' <-> 'user_id')
Expand All @@ -207,26 +219,29 @@ protected static function handleFields(

static::addAlwaysFields($fieldObject, $field, $parentTable, true);

$with[$relationsKey] = static::getSelectableFieldsAndRelations(
$queryArgs,
$field,
$newParentType,
$customQuery,
false,
$ctx
);
} elseif (is_a($parentTypeUnwrapped, \GraphQL\Type\Definition\InterfaceType::class)) {
static::handleInterfaceFields(
$queryArgs,
$field,
$parentTypeUnwrapped,
$select,
$with,
$ctx,
$fieldObject,
$key,
$customQuery
);
// Check if this is a MorphTo relation
if (is_a($relation, MorphTo::class)) {
// For MorphTo relations, we need to handle them specially
// because they can have different models, and we need to eager load based on the query
static::handleMorphToRelation(
$queryArgs,
$field,
$with,
$ctx,
$fieldObject,
$key,
$customQuery
);
} else {
$with[$relationsKey] = static::getSelectableFieldsAndRelations(
$queryArgs,
$field,
$newParentType,
$customQuery,
false,
$ctx
);
}
} else {
static::handleFields($queryArgs, $field, $fieldObject->config['type'], $select, $with, $ctx);
}
Expand Down Expand Up @@ -265,6 +280,31 @@ protected static function isMongodbInstance(GraphqlType $parentType): bool
return isset($parentType->config['model']) ? app($parentType->config['model']) instanceof $mongoType : false;
}

/**
* Get types from UnionType or InterfaceType.
*
* @return GraphqlType[]
*/
protected static function getTypesFromUnionOrInterface(GraphqlType $type): array
{
if ($type instanceof UnionType) {
return $type->getTypes();
}

if ($type instanceof \GraphQL\Type\Definition\InterfaceType) {
// For InterfaceType, get types from the config
// @phpstan-ignore-next-line - InterfaceType can have custom 'types' config
if (isset($type->config['types']) && \is_callable($type->config['types'])) {
/** @var callable(): array<int, GraphqlType> $typesCallable */
$typesCallable = $type->config['types'];

return $typesCallable();
}
}

return [];
}

/**
* @param string|Expression $field
* @param array $select Passed by reference, adds further fields to select
Expand Down Expand Up @@ -451,6 +491,23 @@ protected static function handleInterfaceFields(
): void {
$relationsKey = Arr::get($fieldObject->config, 'alias', $key);

// Check if this field is actually a relation
$queryable = static::isQueryable($fieldObject->config);
$isRelation = $queryable && \is_array($field['fields']) && !empty($field['fields']);

// If it's not a relation, handle it as a regular field
if (!$isRelation) {
// @phpstan-ignore-next-line - Custom config key
$key = $fieldObject->config['alias'] ?? $key;
// @phpstan-ignore-next-line - alias can be Closure or string
$key = $key instanceof Closure ? $key() : $key;
$parentTable = static::isMongodbInstance($parentType) ? null : static::getTableNameFromParentType($parentType);
static::addFieldToSelect($key, $select, $parentTable, false);
static::addAlwaysFields($fieldObject, $select, $parentTable);

return;
}

$with[$relationsKey] = function ($query) use (
$queryArgs,
$field,
Expand All @@ -461,6 +518,11 @@ protected static function handleInterfaceFields(
$key,
$fieldObject
) {
// Check if $query is actually a relation
if (!($query instanceof Relation)) {
return $query;
}

$parentTable = static::isMongodbInstance($parentType) ? null : static::getTableNameFromParentType($parentType);

static::handleRelation($select, $query, $parentTable, $field);
Expand Down Expand Up @@ -518,6 +580,93 @@ function (GraphqlType $type) use ($query) {
};
}

/**
* Handle MorphTo relations
* @param mixed $ctx
*/
protected static function handleMorphToRelation(
array $queryArgs,
array $field,
array &$with,
$ctx,
FieldDefinition $fieldObject,
string $key,
?Closure $customQuery
): void {
$relationsKey = Arr::get($fieldObject->config, 'alias', $key);

/* @var GraphqlType $fieldType */
$fieldType = $fieldObject->config['type'];

if ($fieldType instanceof WrappingType) {
$fieldType = $fieldType->getInnermostType();
}

$relationNames = (isset($fieldType->config['relationName']) && \is_callable($fieldType->config['relationName']))
? $fieldType->config['relationName']()
: null;

// @phpstan-ignore-next-line - getInnermostType returns Type which is GraphqlType
$types = static::getTypesFromUnionOrInterface($fieldType);
$isInterface = $fieldType instanceof \GraphQL\Type\Definition\InterfaceType;

$with[$relationsKey] = function ($relation) use ($queryArgs, $field, $types, $relationNames, $customQuery, $ctx, $isInterface) {
// Check if $relation is actually a MorphTo relation
if (!($relation instanceof MorphTo)) {
return $relation;
}

$morphRelation = [];

foreach ($types as $type) {
// Get the model class name for the morph type
if (isset($type->config['model'])) {
$modelClass = $type->config['model'];
} else {
// Fallback to type name if no model is configured
$typeName = $type instanceof \GraphQL\Type\Definition\NamedType ? $type->name() : '';
$modelClass = $relationNames[$typeName] ?? $typeName;
}

/** @var callable $callable */
$callable = static::getSelectableFieldsAndRelations(
$queryArgs,
$field,
$type,
$customQuery,
false,
$ctx
);

// If the field type is an interface, wrap the callable to select * instead
// This is necessary because interfaces can have different implementations
// with different fields, so we need to select all fields
if ($isInterface) {
$originalCallable = $callable;
$callable = function ($query) use ($originalCallable) {
// Call the original callable first to set up relations
$originalCallable($query);
// Override select to use * for interface types
// We need to clear existing columns and set to * to ensure
// all fields from the concrete type are selected
$query->getQuery()->columns = null;
$query->select('*');

return $query;
};
}

$morphRelation[$modelClass] = $callable;
}

if (!empty($morphRelation)) {
$relation->constrain($morphRelation);
}

return $relation;
};
}

public function getSelect(): array
{
return $this->select;
Expand Down
4 changes: 4 additions & 0 deletions src/Support/UnionType.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ public function getAttributes(): array
$attributes['resolveType'] = [$this, 'resolveType'];
}

if (method_exists($this, 'relationName')) {
$attributes['relationName'] = [$this, 'relationName'];
}

return $attributes;
}

Expand Down
38 changes: 38 additions & 0 deletions tests/Database/SelectFields/UnionMorphTests/CommentType.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<?php

declare(strict_types = 1);
namespace Rebing\GraphQL\Tests\Database\SelectFields\UnionMorphTests;

use GraphQL\Type\Definition\Type;
use Rebing\GraphQL\Support\Facades\GraphQL;
use Rebing\GraphQL\Support\Type as GraphQLType;
use Rebing\GraphQL\Tests\Support\Models\Comment;

class CommentType extends GraphQLType
{
protected $attributes = [
'name' => 'Comment',
'model' => Comment::class,
];

public function fields(): array
{
return [
'id' => [
'type' => Type::nonNull(Type::id()),
],
'title' => [
'type' => Type::nonNull(Type::string()),
],
'body' => [
'type' => Type::string(),
],
'file' => [
'type' => GraphQL::type('File'),
],
'commentable' => [
'type' => GraphQL::type('CommentableUnion'),
],
];
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
<?php

declare(strict_types = 1);
namespace Rebing\GraphQL\Tests\Database\SelectFields\UnionMorphTests;

use GraphQL\Type\Definition\Type as GraphqlType;
use Rebing\GraphQL\Support\Facades\GraphQL;
use Rebing\GraphQL\Support\UnionType;
use Rebing\GraphQL\Tests\Support\Models\Post;
use Rebing\GraphQL\Tests\Support\Models\Product;

class CommentableUnionType extends UnionType
{
protected $attributes = [
'name' => 'CommentableUnion',
];

public function types(): array
{
return [
GraphQL::type('Post'),
GraphQL::type('Product'),
];
}

/**
* @return array<string, string>
*/
public function relationName(): array
{
return [
Post::class => 'post',
Product::class => 'product',
];
}

public function resolveType(object $value): ?GraphqlType
{
if ($value instanceof Post) {
return GraphQL::type('Post');
}

if ($value instanceof Product) {
return GraphQL::type('Product');
}

return null;
}
}
Loading