Skip to content
Merged
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
17 changes: 14 additions & 3 deletions src/Planner/SelectionSetPlanner.php
Original file line number Diff line number Diff line change
Expand Up @@ -1130,9 +1130,20 @@ private function collectRequiredFieldsFromSelectionSet(
) : void {
foreach ($selectionSet->selections as $selection) {
if ($selection instanceof FieldNode && $selection->name->value !== '__typename') {
// Hook fields are synthesized client-side and never present in the raw response,
// so they must not be used as presence guards for discriminating union/interface variants.
if ($this->directiveProcessor->getHookDirective($selection->directives) !== null) {
// The synthetic hook field itself is never present in the raw response, so it
// cannot guard the variant. But the data the hook `requires` IS injected into
// the operation and returned by the server, so those fields must guard the
// variant — otherwise the parent's optional payload offsets are never narrowed
// for the leaf class that consumes them.
$hookName = $this->directiveProcessor->getHookDirective($selection->directives);

if ($hookName !== null) {
$hook = $this->config->hooks[$hookName] ?? null;

if ($hook !== null) {
$this->collectRequiredFieldsFromSelectionSet($hook->requiresFragment->selectionSet, $requiredFields);
}

continue;
}

Expand Down
31 changes: 31 additions & 0 deletions tests/HooksInterface/FindOwnerHook.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?php

declare(strict_types=1);

namespace Ruudk\GraphQLCodeGenerator\HooksInterface;

use Ruudk\GraphQLCodeGenerator\Attribute\Hook;
use Ruudk\GraphQLCodeGenerator\HooksInterface\Generated\Hook\ProjectOwnerId;

#[Hook(
name: 'findOwner',
requires: <<<'GRAPHQL'
fragment ProjectOwnerId on Project {
ownerId
}
GRAPHQL
)]
final readonly class FindOwnerHook
{
/**
* @param array<string, User> $users
*/
public function __construct(
private array $users = [],
) {}

public function __invoke(ProjectOwnerId $project) : ?User
{
return $this->users[$project->ownerId] ?? null;
}
}
24 changes: 24 additions & 0 deletions tests/HooksInterface/Generated/Hook/ProjectOwnerId.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?php

declare(strict_types=1);

namespace Ruudk\GraphQLCodeGenerator\HooksInterface\Generated\Hook;

// This file was automatically generated and should not be edited.

final class ProjectOwnerId
{
public string $ownerId {
get => $this->ownerId ??= $this->data['ownerId'];
}

/**
* @param array{
* 'ownerId': string,
* ...,
* } $data
*/
public function __construct(
private readonly array $data,
) {}
}
50 changes: 50 additions & 0 deletions tests/HooksInterface/Generated/Query/Test/Data.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
<?php

declare(strict_types=1);

namespace Ruudk\GraphQLCodeGenerator\HooksInterface\Generated\Query\Test;

use Ruudk\GraphQLCodeGenerator\HooksInterface\FindOwnerHook;
use Ruudk\GraphQLCodeGenerator\HooksInterface\Generated\Query\Test\Data\Node;

// This file was automatically generated and should not be edited.

final class Data
{
public Node $node {
get => $this->node ??= new Node($this->data['node'], $this->hooks);
}

/**
* @var list<Error>
*/
public readonly array $errors;

/**
* @param array{
* 'node': array{
* '__typename': string,
* 'ownerId'?: string,
* ...,
* },
* ...,
* } $data
* @param list<array{
* 'code': string,
* 'debugMessage'?: string,
* 'message': string,
* ...,
* }> $errors
* @param array{
* 'findOwner': FindOwnerHook,
* ...,
* } $hooks
*/
public function __construct(
private readonly array $data,
array $errors,
private readonly array $hooks,
) {
$this->errors = array_map(fn(array $error) => new Error($error), $errors);
}
}
55 changes: 55 additions & 0 deletions tests/HooksInterface/Generated/Query/Test/Data/Node.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
<?php

declare(strict_types=1);

namespace Ruudk\GraphQLCodeGenerator\HooksInterface\Generated\Query\Test\Data;

use Ruudk\GraphQLCodeGenerator\HooksInterface\FindOwnerHook;
use Ruudk\GraphQLCodeGenerator\HooksInterface\Generated\Query\Test\Data\Node\AsProject;

// This file was automatically generated and should not be edited.

final class Node
{
public ?AsProject $asProject {
get {
if (isset($this->asProject)) {
return $this->asProject;
}

if ($this->data['__typename'] !== 'Project') {
return $this->asProject = null;
}

if (! array_key_exists('ownerId', $this->data)) {
return $this->asProject = null;
}

return $this->asProject = new AsProject($this->data, $this->hooks);
}
}

/**
* @api
* @phpstan-assert-if-true !null $this->asProject
*/
public bool $isProject {
get => $this->isProject ??= $this->data['__typename'] === 'Project';
}

/**
* @param array{
* '__typename': string,
* 'ownerId'?: string,
* ...,
* } $data
* @param array{
* 'findOwner': FindOwnerHook,
* ...,
* } $hooks
*/
public function __construct(
private readonly array $data,
private readonly array $hooks,
) {}
}
42 changes: 42 additions & 0 deletions tests/HooksInterface/Generated/Query/Test/Data/Node/AsProject.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<?php

declare(strict_types=1);

namespace Ruudk\GraphQLCodeGenerator\HooksInterface\Generated\Query\Test\Data\Node;

use Ruudk\GraphQLCodeGenerator\HooksInterface\FindOwnerHook;
use Ruudk\GraphQLCodeGenerator\HooksInterface\Generated\Hook\ProjectOwnerId;
use Ruudk\GraphQLCodeGenerator\HooksInterface\User;

// This file was automatically generated and should not be edited.

final class AsProject
{
public ?User $owner {
get => $this->owner ??= $this->hooks['findOwner']->__invoke($this->buildProjectOwnerId());
}

/**
* @param array{
* '__typename': 'Project',
* 'ownerId': string,
* ...,
* } $data
* @param array{
* 'findOwner': FindOwnerHook,
* ...,
* } $hooks
*/
public function __construct(
private readonly array $data,
private readonly array $hooks,
) {}

/**
* @internal
*/
public function buildProjectOwnerId() : ProjectOwnerId
{
return new ProjectOwnerId($this->data);
}
}
24 changes: 24 additions & 0 deletions tests/HooksInterface/Generated/Query/Test/Error.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?php

declare(strict_types=1);

namespace Ruudk\GraphQLCodeGenerator\HooksInterface\Generated\Query\Test;

// This file was automatically generated and should not be edited.

final readonly class Error
{
public string $message;

/**
* @param array{
* 'debugMessage'?: string,
* 'message': string,
* ...,
* } $error
*/
public function __construct(array $error)
{
$this->message = $error['debugMessage'] ?? $error['message'];
}
}
57 changes: 57 additions & 0 deletions tests/HooksInterface/Generated/Query/Test/TestQuery.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
<?php

declare(strict_types=1);

namespace Ruudk\GraphQLCodeGenerator\HooksInterface\Generated\Query\Test;

use Ruudk\GraphQLCodeGenerator\HooksInterface\FindOwnerHook;
use Ruudk\GraphQLCodeGenerator\TestClient;

// This file was automatically generated and should not be edited.

final readonly class TestQuery {
public const string OPERATION_NAME = 'Test';
public const string OPERATION_DEFINITION = <<<'GRAPHQL'
query Test {
node {
__typename
... on Project {
... on Project {
ownerId
}
}
}
}

GRAPHQL;

/**
* @param array{
* 'findOwner': FindOwnerHook,
* ...,
* } $hooks
*/
public function __construct(
private TestClient $client,
private array $hooks,
) {}

/**
* @api
*/
public function execute() : Data
{
$data = $this->client->graphql(
self::OPERATION_DEFINITION,
[
],
self::OPERATION_NAME,
);

return new Data(
$data['data'] ?? [], // @phpstan-ignore argument.type
$data['errors'] ?? [], // @phpstan-ignore argument.type
$this->hooks,
);
}
}
50 changes: 50 additions & 0 deletions tests/HooksInterface/HooksInterfaceTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
<?php

declare(strict_types=1);

namespace Ruudk\GraphQLCodeGenerator\HooksInterface;

use Override;
use Ruudk\GraphQLCodeGenerator\Config\Config;
use Ruudk\GraphQLCodeGenerator\GraphQLTestCase;
use Ruudk\GraphQLCodeGenerator\HooksInterface\Generated\Query\Test\TestQuery;

final class HooksInterfaceTest extends GraphQLTestCase
{
#[Override]
public function getConfig() : Config
{
return parent::getConfig()
->withHook(FindOwnerHook::class);
}

public function testGenerate() : void
{
$this->assertActualMatchesExpected();
}

public function testQuery() : void
{
$findOwner = new FindOwnerHook([
'user-123' => new User('user-123'),
]);

$result = new TestQuery(
$this->getClient([
'data' => [
'node' => [
'__typename' => 'Project',
'ownerId' => 'user-123',
],
],
]),
[
'findOwner' => $findOwner,
],
)->execute();

self::assertNotNull($result->node->asProject);
self::assertNotNull($result->node->asProject->owner);
self::assertSame('user-123', $result->node->asProject->owner->id);
}
}
22 changes: 22 additions & 0 deletions tests/HooksInterface/Schema.graphql
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
type Query {
node: Node!
}

interface Node {
id: ID!
}

type Project implements Node {
id: ID!
name: String!
ownerId: ID!
}

type Organization implements Node {
id: ID!
title: String!
}

type User {
id: ID!
}
7 changes: 7 additions & 0 deletions tests/HooksInterface/Test.graphql
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
query Test {
node {
... on Project {
owner @hook(name: "findOwner")
}
}
}
Loading
Loading