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
6 changes: 6 additions & 0 deletions src/Attribute/Field.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ public function __construct(
private readonly string|null $hydratorStrategy = null,
private readonly array $excludeFilters = [],
private readonly array $includeFilters = [],
private readonly string|null $extractorMethod = null,
) {
}

Expand Down Expand Up @@ -54,4 +55,9 @@ public function getType(): string|null
{
return $this->type;
}

public function getExtractorMethod(): string|null
{
return $this->extractorMethod;
}
}
5 changes: 5 additions & 0 deletions src/Filter/FilterFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,11 @@ protected function addFields(Entity $targetEntity, array $allowedFilters): array
continue;
}

// Fields with extractorMethod cannot be filtered at the database level
if (! empty($entityMetadata['fields'][$fieldName]['extractorMethod'])) {
continue;
}

$type = $this->typeContainer
->get($entityMetadata['fields'][$fieldName]['type']);

Expand Down
20 changes: 20 additions & 0 deletions src/Hydrator/HydratorContainer.php
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,26 @@ public function get(string $id): mixed
/** @psalm-suppress MixedArgument */
$object->setAllowedFields(array_map('strval', array_keys($metadata['fields'])));

// Register extractorMethod fields as computed overrides
/** @psalm-suppress MixedArrayAccess, MixedAssignment */
foreach ($metadata['fields'] as $fieldName => $fieldMetadata) {
/** @psalm-suppress MixedArrayAccess */
$extractorMethod = $fieldMetadata['extractorMethod'] ?? null;
if ($extractorMethod === null) {
continue;
}

// Use alias as data key when present so FieldResolver can find it by GraphQL field name
/** @psalm-suppress MixedArrayAccess */
$dataKey = $fieldMetadata['alias'] ?? $fieldName;

/** @psalm-suppress MixedArgument, MixedMethodCall */
$object->addComputedField(
$dataKey,
static fn (object $entity): mixed => $entity->$extractorMethod(),
);
}

// Register computed fields
if (isset($metadata['computedFields'])) {
/** @psalm-suppress MixedArrayAccess, MixedAssignment */
Expand Down
3 changes: 2 additions & 1 deletion src/Hydrator/Strategy/Collection.php
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,8 @@ protected function getCollectionFromObjectByValue(): DoctrineCollection
*/
protected function getCollectionFromObjectByReference(): DoctrineCollection
{
$object = $this->getObject();
$object = $this->getObject();
/** @psalm-suppress UndefinedDocblockClass */
$refl = $this->getClassMetadata()->getReflectionClass();
$reflProperty = $refl->getProperty($this->getCollectionName());

Expand Down
1 change: 1 addition & 0 deletions src/Metadata/MetadataFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,7 @@ private function buildMetadataForFields(
'hydratorStrategy' => $instance->getHydratorStrategy() ??
$this->getDefaultStrategy($entityClassMetadata->getTypeOfField($fieldName)),
'excludeFilters' => Filters::toStringArray($instance->getExcludeFilters()),
'extractorMethod' => $instance->getExtractorMethod(),
];

/** @psalm-suppress MixedArrayAssignment */
Expand Down
11 changes: 11 additions & 0 deletions test/Entity/Artist.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
#[GraphQL\Entity(group: 'ExtractionMap', limit: 1)]
#[GraphQL\Entity(group: 'ExtractionMapDuplicate', limit: 1)]
#[GraphQL\Entity(group: 'computedFieldTest')]
#[GraphQL\Entity(group: 'extractorMethodTest')]

#[ORM\Entity]
class Artist
Expand All @@ -45,6 +46,7 @@ class Artist
#[GraphQL\Field(group: 'ExtractionMap', alias: 'title')]
#[GraphQL\Field(group: 'ExtractionMapDuplicate', alias: 'duplicate')]
#[GraphQL\Field(group: 'computedFieldTest')]
#[GraphQL\Field(group: 'extractorMethodTest', extractorMethod: 'getNameUppercased')]

#[ORM\Column(type: 'string', nullable: false)]
private string $name;
Expand All @@ -57,6 +59,7 @@ class Artist
#[GraphQL\Field(group: 'AttributeLimit')]
#[GraphQL\Field(group: 'ExtractionMapDuplicate', alias: 'duplicate')]
#[GraphQL\Field(group: 'computedFieldTest')]
#[GraphQL\Field(group: 'extractorMethodTest')]

#[ORM\Id]
#[ORM\Column(type: 'bigint')]
Expand Down Expand Up @@ -156,6 +159,14 @@ public function getPerformances(): Collection
return $this->performances;
}

/**
* Returns name uppercased — used as extractorMethod target in extractorMethodTest group
*/
public function getNameUppercased(): string
{
return strtoupper($this->name);
}

/**
* Get full name in uppercase (for testing computed fields)
*/
Expand Down
70 changes: 70 additions & 0 deletions test/Feature/ExtractorMethod/ExtractorMethodTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
<?php

declare(strict_types=1);

namespace ApiSkeletonsTest\Doctrine\ORM\GraphQL\Feature\ExtractorMethod;

use ApiSkeletons\Doctrine\ORM\GraphQL\Config;
use ApiSkeletons\Doctrine\ORM\GraphQL\Driver;
use ApiSkeletons\Doctrine\ORM\GraphQL\Type\Entity\EntityTypeContainer;
use ApiSkeletonsTest\Doctrine\ORM\GraphQL\Entity\Artist;
use ApiSkeletonsTest\Doctrine\ORM\GraphQL\TestCase;
use GraphQL\GraphQL;
use GraphQL\Type\Definition\ObjectType;
use GraphQL\Type\Schema;

use function strtoupper;

class ExtractorMethodTest extends TestCase
{
public function testExtractorMethodInMetadata(): void
{
$driver = new Driver($this->getEntityManager(), new Config(['group' => 'extractorMethodTest']));

$driver->type(Artist::class);

$metadata = $driver->get(EntityTypeContainer::class)
->get(Artist::class)
->getMetadata();

$this->assertEquals('getNameUppercased', $metadata['fields']['name']['extractorMethod']);
}

public function testExtractorMethodResolvesToMethodReturn(): void
{
$driver = new Driver($this->getEntityManager(), new Config(['group' => 'extractorMethodTest']));

$schema = new Schema([
'query' => new ObjectType([
'name' => 'query',
'fields' => [
'artists' => $driver->completeConnection(Artist::class),
],
]),
]);

$result = GraphQL::executeQuery($schema, '{ artists { edges { node { id name } } } }')->toArray();

$this->assertEmpty($result['errors'] ?? []);
$this->assertNotEmpty($result['data']['artists']['edges']);

foreach ($result['data']['artists']['edges'] as $edge) {
// name should be the uppercased value from getNameUppercased(), not the raw stored value
$this->assertEquals(strtoupper($edge['node']['name']), $edge['node']['name']);
}
}

public function testExtractorMethodFieldNotInFilters(): void
{
$driver = new Driver($this->getEntityManager(), new Config(['group' => 'extractorMethodTest']));

$filterType = $driver->filter(Artist::class);
$fields = $filterType->getFields();

// name has extractorMethod so must not appear in filters
$this->assertArrayNotHasKey('name', $fields);

// id has no extractorMethod so must still be filterable
$this->assertArrayHasKey('id', $fields);
}
}
3 changes: 2 additions & 1 deletion test/Feature/Metadata/CachingTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ public function testStaticMetadata(): void
'entityClass' => 'ApiSkeletonsTest\Doctrine\ORM\GraphQL\Entity\User',
'byValue' => true,
'limit' => 0,
'description' => '',
'description' => null,
'excludeFilters' => [],
'typeName' => 'ApiSkeletonsTest_Doctrine_ORM_GraphQL_Entity_User_StaticMetadata',
'fields' => [
Expand All @@ -48,6 +48,7 @@ public function testStaticMetadata(): void
'type' => 'string',
'hydratorStrategy' => 'ApiSkeletons\Doctrine\ORM\GraphQL\Hydrator\Strategy\FieldDefault',
'excludeFilters' => [],
'extractorMethod' => null,
],
'recordings' => [
'alias' => null,
Expand Down
Loading