Skip to content

Commit d0cb76b

Browse files
authored
Merge pull request #119 from TomHAnderson/feature/criteria-event
Feature/criteria event
2 parents f1f4db1 + 9f45502 commit d0cb76b

9 files changed

Lines changed: 237 additions & 9 deletions

File tree

README.md

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -298,6 +298,42 @@ $driver->get(EventDispatcher::class)->subscribeTo(Artist::class . '.filterQueryB
298298
);
299299
```
300300

301+
### Filter Criteria (embedded collections)
302+
303+
You may modify the criteria object used to filter associations. For instance, if you use soft
304+
deletes then you would want to filter out deleted rows from an association.
305+
306+
```php
307+
use ApiSkeletons\Doctrine\GraphQL\Attribute as GraphQL;
308+
use ApiSkeletons\Doctrine\GraphQL\Event\FilterCriteria;
309+
use App\ORM\Entity\Artist;
310+
use League\Event\EventDispatcher;
311+
312+
#[GraphQL\Entity]
313+
class Artist
314+
{
315+
#[GraphQL\Field]
316+
public $id;
317+
318+
#[GraphQL\Field]
319+
public $name;
320+
321+
#[GraphQL\Association(filterCriteriaEventName: self::class . '.performances.filterCriteria')]
322+
public $performances;
323+
}
324+
325+
// Add a listener to your driver
326+
$driver->get(EventDispatcher::class)->subscribeTo(
327+
Artist::class . '.performances.filterCriteria',
328+
function (FilterCriteria $event): void {
329+
$event->getCriteria()->andWhere(
330+
$event->getCriteria()->expr()->eq('isDeleted', false)
331+
);
332+
},
333+
);
334+
```
335+
336+
301337
### Entity ObjectType Definition
302338

303339
You may modify the array used to define an entity type before it is created. This can be used for generated data and the like.

src/Attribute/Association.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ public function __construct(
1515
protected string|null $strategy = null,
1616
protected string|null $description = null,
1717
protected array $excludeCriteria = [],
18+
protected string|null $filterCriteriaEventName = null,
1819
) {
1920
}
2021

@@ -38,4 +39,9 @@ public function getExcludeCriteria(): array
3839
{
3940
return $this->excludeCriteria;
4041
}
42+
43+
public function getFilterCriteriaEventName(): string|null
44+
{
45+
return $this->filterCriteriaEventName;
46+
}
4147
}

src/Event/FilterCriteria.php

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace ApiSkeletons\Doctrine\GraphQL\Event;
6+
7+
use Doctrine\Common\Collections\Criteria;
8+
use GraphQL\Type\Definition\ResolveInfo;
9+
use League\Event\HasEventName;
10+
11+
class FilterCriteria implements
12+
HasEventName
13+
{
14+
/** @param mixed[] $args */
15+
public function __construct(
16+
protected Criteria $criteria,
17+
protected string $eventName,
18+
protected mixed $objectValue,
19+
protected array $args,
20+
protected mixed $context,
21+
protected ResolveInfo $info,
22+
) {
23+
}
24+
25+
public function eventName(): string
26+
{
27+
return $this->eventName;
28+
}
29+
30+
public function getCriteria(): Criteria
31+
{
32+
return $this->criteria;
33+
}
34+
35+
public function getObjectValue(): mixed
36+
{
37+
return $this->objectValue;
38+
}
39+
40+
/** @return mixed[] */
41+
public function getArgs(): array
42+
{
43+
return $this->args;
44+
}
45+
46+
public function getContext(): mixed
47+
{
48+
return $this->context;
49+
}
50+
51+
public function getInfo(): ResolveInfo
52+
{
53+
return $this->info;
54+
}
55+
}

src/Metadata/MetadataFactory.php

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -100,8 +100,10 @@ private function globalEnable(array $entityClasses): Metadata
100100
continue;
101101
}
102102

103-
$this->metadataConfig[$entityClass]['fields'][$associationName]['excludeCriteria'] = [];
104-
$this->metadataConfig[$entityClass]['fields'][$associationName]['description'] = $associationName;
103+
$this->metadataConfig[$entityClass]['fields'][$associationName]['excludeCriteria'] = [];
104+
$this->metadataConfig[$entityClass]['fields'][$associationName]['description'] = $associationName;
105+
$this->metadataConfig[$entityClass]['fields'][$associationName]['filterCriteriaEventName']
106+
= null;
105107

106108
// NullifyOwningAssociation is not used for globalEnable
107109
$this->metadataConfig[$entityClass]['fields'][$associationName]['strategy'] =
@@ -257,10 +259,12 @@ private function buildMetadataConfigForAssociations(
257259
);
258260
$associationInstance = $instance;
259261

260-
$this->metadataConfig[$reflectionClass->getName()]['fields'][$associationName]['description'] =
262+
$this->metadataConfig[$reflectionClass->getName()]['fields'][$associationName]['description'] =
261263
$instance->getDescription();
262-
$this->metadataConfig[$reflectionClass->getName()]['fields'][$associationName]['excludeCriteria'] =
264+
$this->metadataConfig[$reflectionClass->getName()]['fields'][$associationName]['excludeCriteria'] =
263265
$instance->getExcludeCriteria();
266+
$this->metadataConfig[$reflectionClass->getName()]['fields'][$associationName]['filterCriteriaEventName'] =
267+
$instance->getFilterCriteriaEventName();
264268

265269
if ($instance->getStrategy()) {
266270
$this->metadataConfig[$reflectionClass->getName()]['fields'][$associationName]['strategy']

src/Resolve/ResolveCollectionFactory.php

Lines changed: 36 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66

77
use ApiSkeletons\Doctrine\GraphQL\Config;
88
use ApiSkeletons\Doctrine\GraphQL\Criteria\Filters as FiltersDef;
9+
use ApiSkeletons\Doctrine\GraphQL\Event\FilterCriteria;
10+
use ApiSkeletons\Doctrine\GraphQL\Metadata\Metadata;
911
use ApiSkeletons\Doctrine\GraphQL\Type\Entity;
1012
use ApiSkeletons\Doctrine\GraphQL\Type\TypeManager;
1113
use Closure;
@@ -15,6 +17,7 @@
1517
use Doctrine\ORM\Mapping\ClassMetadata;
1618
use Doctrine\ORM\PersistentCollection;
1719
use GraphQL\Type\Definition\ResolveInfo;
20+
use League\Event\EventDispatcher;
1821

1922
use function base64_decode;
2023
use function base64_encode;
@@ -27,6 +30,8 @@ public function __construct(
2730
protected Config $config,
2831
protected FieldResolver $fieldResolver,
2932
protected TypeManager $typeManager,
33+
protected EventDispatcher $eventDispatcher,
34+
protected Metadata $metadata,
3035
) {
3136
}
3237

@@ -51,21 +56,29 @@ public function parseArrayValue(ClassMetadata $metadata, string $field, array $v
5156

5257
public function get(Entity $entity): Closure
5358
{
54-
return function ($source, $args, $context, ResolveInfo $resolveInfo) {
59+
return function ($source, array $args, $context, ResolveInfo $info) {
5560
$fieldResolver = $this->fieldResolver;
56-
$collection = $fieldResolver($source, $args, $context, $resolveInfo);
61+
$collection = $fieldResolver($source, $args, $context, $info);
5762

5863
$collectionMetadata = $this->entityManager->getMetadataFactory()
5964
->getMetadataFor(
6065
(string) $this->entityManager->getMetadataFactory()
6166
->getMetadataFor(ClassUtils::getRealClass($source::class))
62-
->getAssociationTargetClass($resolveInfo->fieldName),
67+
->getAssociationTargetClass($info->fieldName),
6368
);
6469

70+
$metadataConfig = $this->metadata->getMetadataConfig();
71+
$entityClass = ClassUtils::getRealClass($source::class);
72+
6573
return $this->buildPagination(
6674
$args['pagination'] ?? [],
6775
$collection,
6876
$this->buildCriteria($args['filter'] ?? [], $collectionMetadata),
77+
$metadataConfig[$entityClass]['fields'][$info->fieldName]['filterCriteriaEventName'],
78+
$source,
79+
$args,
80+
$context,
81+
$info,
6982
);
7083
};
7184
}
@@ -116,8 +129,13 @@ private function buildCriteria(array $filter, ClassMetadata $collectionMetadata)
116129
*
117130
* @return mixed[]
118131
*/
119-
private function buildPagination(array $pagination, PersistentCollection $collection, Criteria $criteria): array
120-
{
132+
private function buildPagination(
133+
array $pagination,
134+
PersistentCollection $collection,
135+
Criteria $criteria,
136+
string|null $filterCriteriaEventName,
137+
mixed ...$resolve,
138+
): array {
121139
$first = 0;
122140
$after = 0;
123141
$last = 0;
@@ -174,6 +192,19 @@ private function buildPagination(array $pagination, PersistentCollection $collec
174192
$criteria->setMaxResults($limit);
175193
}
176194

195+
/**
196+
* Fire the event dispatcher using the passed event name.
197+
*/
198+
if ($filterCriteriaEventName) {
199+
$this->eventDispatcher->dispatch(
200+
new FilterCriteria(
201+
$criteria,
202+
$filterCriteriaEventName,
203+
...$resolve,
204+
),
205+
);
206+
}
207+
177208
// Fetch slice of collection
178209
$items = $collection->matching($criteria);
179210

src/Services.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,8 @@ static function (ContainerInterface $container) {
6969
$container->get(Config::class),
7070
$container->get(Resolve\FieldResolver::class),
7171
$container->get(Type\TypeManager::class),
72+
$container->get(EventDispatcher::class),
73+
$container->get(Metadata\Metadata::class),
7274
);
7375
},
7476
)

test/Entity/Artist.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
#[GraphQL\Entity(group: 'DuplicateGroup')]
2020
#[GraphQL\Entity(group: 'DuplicateGroupField')]
2121
#[GraphQL\Entity(group: 'DuplicateGroupAssociation')]
22+
#[GraphQL\Entity(group: 'FilterCriteriaEvent')]
2223
#[ORM\Entity]
2324
class Artist
2425
{
@@ -29,12 +30,14 @@ class Artist
2930
#[GraphQL\Field(group: 'DuplicateGroup')]
3031
#[GraphQL\Field(group: 'DuplicateGroupField')]
3132
#[GraphQL\Field(group: 'DuplicateGroupField')]
33+
#[GraphQL\Field(group: 'FilterCriteriaEvent')]
3234
#[ORM\Column(type: 'string', nullable: false)]
3335
private string $name;
3436

3537
#[GraphQL\Field(description: 'Primary key')]
3638
#[GraphQL\Field(group: 'ExcludeCriteriaTest')]
3739
#[GraphQL\Field(group: 'TypeNameTest')]
40+
#[GraphQL\Field(group: 'FilterCriteriaEvent')]
3841
#[ORM\Id]
3942
#[ORM\Column(type: 'bigint')]
4043
#[ORM\GeneratedValue(strategy: 'AUTO')]
@@ -47,6 +50,7 @@ class Artist
4750
#[GraphQL\Association(group: 'DuplicateGroup')]
4851
#[GraphQL\Association(group: 'DuplicateGroupAssociation')]
4952
#[GraphQL\Association(group: 'DuplicateGroupAssociation')]
53+
#[GraphQL\Association(group: 'FilterCriteriaEvent', filterCriteriaEventName: self::class . '.performances.filterCriteria')]
5054
#[ORM\OneToMany(targetEntity: 'ApiSkeletonsTest\Doctrine\GraphQL\Entity\Performance', mappedBy: 'artist')]
5155
private Collection $performances;
5256

test/Entity/Performance.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,19 +15,23 @@
1515
*/
1616
#[GraphQL\Entity(typeName: 'performance', description: 'Performances')]
1717
#[GraphQL\Entity(group: 'ExcludeCriteriaTest', excludeCriteria: ['contains'])]
18+
#[GraphQL\Entity(group: 'FilterCriteriaEvent')]
1819
#[ORM\Entity]
1920
class Performance
2021
{
2122
#[GraphQL\Field(description: 'Venue name')]
2223
#[GraphQL\Field(description: 'Venue name', group: 'ExcludeCriteriaTest')]
24+
#[GraphQL\Field(group: 'FilterCriteriaEvent')]
2325
#[ORM\Column(type: 'string', nullable: true)]
2426
private string|null $venue = null;
2527

2628
#[GraphQL\Field(description: 'City name')]
29+
#[GraphQL\Field(group: 'FilterCriteriaEvent')]
2730
#[ORM\Column(type: 'string', nullable: true)]
2831
private string|null $city = null;
2932

3033
#[GraphQL\Field(description: 'State name')]
34+
#[GraphQL\Field(group: 'FilterCriteriaEvent')]
3135
#[ORM\Column(type: 'string', nullable: true)]
3236
private string|null $state = null;
3337

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace ApiSkeletonsTest\Doctrine\GraphQL\Feature\Event;
6+
7+
use ApiSkeletons\Doctrine\GraphQL\Config;
8+
use ApiSkeletons\Doctrine\GraphQL\Driver;
9+
use ApiSkeletons\Doctrine\GraphQL\Event\FilterCriteria;
10+
use ApiSkeletonsTest\Doctrine\GraphQL\AbstractTest;
11+
use ApiSkeletonsTest\Doctrine\GraphQL\Entity\Artist;
12+
use Doctrine\Common\Collections\Criteria;
13+
use GraphQL\GraphQL;
14+
use GraphQL\Type\Definition\ObjectType;
15+
use GraphQL\Type\Definition\ResolveInfo;
16+
use GraphQL\Type\Schema;
17+
use League\Event\EventDispatcher;
18+
19+
use function count;
20+
21+
class FilterCriteriaTest extends AbstractTest
22+
{
23+
public function testEvent(): void
24+
{
25+
$driver = new Driver($this->getEntityManager(), new Config(['group' => 'FilterCriteriaEvent']));
26+
27+
$driver->get(EventDispatcher::class)->subscribeTo(
28+
Artist::class . '.performances.filterCriteria',
29+
function (FilterCriteria $event): void {
30+
$this->assertInstanceOf(Criteria::class, $event->getCriteria());
31+
32+
$event->getCriteria()->andWhere(
33+
$event->getCriteria()->expr()->eq('venue', 'Delta Center'),
34+
);
35+
36+
$this->assertInstanceOf(Artist::class, $event->getObjectValue());
37+
$this->assertEquals('contextTest', $event->getContext());
38+
$this->assertIsArray($event->getArgs());
39+
$this->assertInstanceOf(ResolveInfo::class, $event->getInfo());
40+
},
41+
);
42+
43+
$schema = new Schema([
44+
'query' => new ObjectType([
45+
'name' => 'query',
46+
'fields' => [
47+
'artist' => [
48+
'type' => $driver->connection($driver->type(Artist::class)),
49+
'args' => [
50+
'filter' => $driver->filter(Artist::class),
51+
],
52+
'resolve' => $driver->resolve(Artist::class),
53+
],
54+
],
55+
]),
56+
]);
57+
58+
$query = '{
59+
artist (filter: { id: { eq: 1 } } ) {
60+
edges {
61+
node {
62+
id
63+
name
64+
performances {
65+
edges {
66+
node {
67+
venue
68+
}
69+
}
70+
}
71+
}
72+
}
73+
}
74+
}';
75+
76+
$result = GraphQL::executeQuery($schema, $query, null, 'contextTest');
77+
$data = $result->toArray()['data'];
78+
79+
$this->assertEquals(1, count($data['artist']['edges']));
80+
$this->assertEquals(1, count($data['artist']['edges'][0]['node']['performances']));
81+
$this->assertEquals(
82+
'Delta Center',
83+
$data['artist']['edges'][0]['node']['performances']['edges'][0]['node']['venue'],
84+
);
85+
}
86+
}

0 commit comments

Comments
 (0)