diff --git a/docs/attributes.rst b/docs/attributes.rst index 9a2af1a..add8676 100644 --- a/docs/attributes.rst +++ b/docs/attributes.rst @@ -100,6 +100,45 @@ itself is resolved as its nearest exposed parent class. A subclass which is exposed is resolved as itself, with its own entity attribute, wherever its rows appear. +Each exposed class has its own type, and the types are not related: a +subclass's type is not an implementation of an interface of its parent's. +A connection of the parent therefore has the parent's type, and a client +cannot ask it for a subclass's own fields, not even with a fragment on the +subclass's type. Expose a subclass's fields through a `computed field +`_ of the subclass's type on the parent, +which returns the entity when it is of the subclass: + +.. code-block:: php + + #[GraphQL\Entity] + #[ORM\Entity] + #[ORM\InheritanceType('SINGLE_TABLE')] + class Animal + { + #[GraphQL\ComputedField(type: Dog::class)] + public function getAsDog(): Dog|null + { + return $this instanceof Dog ? $this : null; + } + } + + #[GraphQL\Entity] + #[ORM\Entity] + class Dog extends Animal + { + #[GraphQL\Field] + #[ORM\Column(type: 'string')] + private string $breed; + } + +.. code-block:: graphql + + { animals { edges { node { name asDog { breed } } } } } + +``asDog`` is null for an animal which is not a dog. The subclass must be +exposed in the group. A subclass inherits the computed field, so a dog's +``asDog`` is the dog itself. + Field ===== diff --git a/docs/computed-fields.rst b/docs/computed-fields.rst index b386f82..51ae927 100644 --- a/docs/computed-fields.rst +++ b/docs/computed-fields.rst @@ -179,7 +179,8 @@ returns a list of them, such as an array or a Doctrine ``Collection``. } This exposes a relation which is not mapped as an association, or which is -derived from one. The entity may be of the same class as the field's own +derived from one, or a subclass's own fields through its parent's type; see +`Inheritance `_. The entity may be of the same class as the field's own entity, and two entities may have computed fields of each other's types. An entity of a class which is not exposed in the group throws ``ApiSkeletons\Doctrine\ORM\GraphQL\Exception\Metadata`` when the metadata diff --git a/test/Entity/InheritanceAnimal.php b/test/Entity/InheritanceAnimal.php index f6c6226..5c4bde3 100644 --- a/test/Entity/InheritanceAnimal.php +++ b/test/Entity/InheritanceAnimal.php @@ -58,4 +58,14 @@ public function getToys(): Collection { return $this->toys; } + + /** + * A subclass's own fields, through a field of the root's type: the + * animal as a dog, or null + */ + #[GraphQL\ComputedField(type: InheritanceDog::class, group: 'InheritanceSubclass')] + public function getAsDog(): InheritanceDog|null + { + return $this instanceof InheritanceDog ? $this : null; + } } diff --git a/test/Feature/Resolve/InheritanceTest.php b/test/Feature/Resolve/InheritanceTest.php index d035066..1d52e3d 100644 --- a/test/Feature/Resolve/InheritanceTest.php +++ b/test/Feature/Resolve/InheritanceTest.php @@ -143,6 +143,34 @@ public function testExposedSubclassIsResolvedAsItself(): void ], $data['dogs']['edges']); } + /** + * A computed field of the subclass's type gives a subclass's own fields + * through the root's type, where a row is of the root's type + */ + #[DataProvider('extractionProvider')] + public function testSubclassFieldsThroughAComputedField(bool $batchAssociations, bool $extractByValue): void + { + $data = $this->execute( + ['group' => 'InheritanceSubclass', 'batchAssociations' => $batchAssociations, 'extractByValue' => $extractByValue], + ['animals' => InheritanceAnimal::class, 'toys' => InheritanceToy::class], + '{ + animals { edges { node { name asDog { breed } } } } + toys { edges { node { name owner { name asDog { breed } } } } } + }', + ); + + $this->assertSame([ + ['node' => ['name' => 'Generic', 'asDog' => null]], + ['node' => ['name' => 'Rex', 'asDog' => ['breed' => 'Labrador']]], + ], $data['animals']['edges']); + + $this->assertSame([ + ['node' => ['name' => 'Ball', 'owner' => ['name' => 'Generic', 'asDog' => null]]], + ['node' => ['name' => 'Bone', 'owner' => ['name' => 'Rex', 'asDog' => ['breed' => 'Labrador']]]], + ['node' => ['name' => 'Rope', 'owner' => ['name' => 'Rex', 'asDog' => ['breed' => 'Labrador']]]], + ], $data['toys']['edges']); + } + public function testExposedClass(): void { $dog = new InheritanceDog('Rex', 'Labrador');