Skip to content

Commit 798c7d2

Browse files
authored
Merge pull request #199 from TomHAnderson/feature/claude-2
Feature/claude 2
2 parents f79e1df + d9391dd commit 798c7d2

File tree

9 files changed

+561
-49
lines changed

9 files changed

+561
-49
lines changed

README.md

Lines changed: 21 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -46,13 +46,26 @@ Documentation
4646
Full documentation is available at https://doctrine-orm-graphql.apiskeletons.dev or in the [docs](https://github.com/api-skeletons/doctrine-orm-graphql/blob/master/docs) directory.
4747

4848

49-
Versions
49+
Features
5050
--------
5151

52-
* 12.x - Supports [league/event](https://github.com/thephpleague/event) version 3.0 and is PSR-14 compliant
53-
* 11.x - Supports [league/event](https://github.com/thephpleague/event) version 2.2
52+
* Supports all [Doctrine Types](https://doctrine-orm-graphql.apiskeletons.dev/en/latest/types.html#data-type-mappings) and allows custom types
53+
* Pagination with the [GraphQL Complete Connection Model](https://graphql.org/learn/pagination/#complete-connection-model)
54+
* [Filtering of sub-collections](https://doctrine-orm-graphql.apiskeletons.dev/en/latest/queries.html)
55+
* [Events](https://github.com/API-Skeletons/doctrine-orm-graphql#events) for modifying queries, entity types and more
56+
* [Multiple configuration group support](https://doctrine-orm-graphql.apiskeletons.dev/en/latest/driver.html#group)
57+
5458

55-
More information [in the documentation](https://doctrine-orm-graphql.apiskeletons.dev/en/latest/versions.html).
59+
Technical Features
60+
------------------
61+
62+
* Attribute-based metadata
63+
* PHP 8.4 Lazy Ghost Objects for deferred type initialization
64+
* PSR-14 Event-Driven Architecture for query and type customization
65+
* Custom PSR-11 Container with lazy initialization and buildable types
66+
* Advanced hydration system with Doctrine Laminas Hydrator and extraction strategies
67+
* Dynamic QueryBuilder generation with filter translation and event-driven query modification
68+
to solve N+1 query problems
5669

5770

5871
Examples
@@ -63,16 +76,6 @@ The **LDOG Stack**: Laravel, Doctrine ORM, and GraphQL uses this library: https
6376
For an working implementation see https://graphql.lcdb.org
6477

6578

66-
Features
67-
--------
68-
69-
* Supports all [Doctrine Types](https://doctrine-orm-graphql.apiskeletons.dev/en/latest/types.html#data-type-mappings) and allows custom types
70-
* Pagination with the [GraphQL Complete Connection Model](https://graphql.org/learn/pagination/#complete-connection-model)
71-
* [Filtering of sub-collections](https://doctrine-orm-graphql.apiskeletons.dev/en/latest/queries.html)
72-
* [Events](https://github.com/API-Skeletons/doctrine-orm-graphql#events) for modifying queries, entity types and more
73-
* [Multiple configuration group support](https://doctrine-orm-graphql.apiskeletons.dev/en/latest/driver.html#group)
74-
75-
7679
Quick Start
7780
-----------
7881

@@ -277,7 +280,10 @@ You may [exclude any filter](https://doctrine-orm-graphql.apiskeletons.dev/en/la
277280
History
278281
-------
279282

280-
The roots of this project go back to May 2018 with https://github.com/API-Skeletons/zf-doctrine-graphql; written for Zend Framework 2. It was migrated to the framework agnostic https://packagist.org/packages/api-skeletons/doctrine-graphql but the name of that repository was incorrect because it did not specify ORM only. So this repository was created and the others were abandoned.
283+
The roots of this project go back to May 2018 with https://github.com/API-Skeletons/zf-doctrine-graphql; written for
284+
Zend Framework 2. It was migrated to the framework agnostic
285+
https://packagist.org/packages/api-skeletons/doctrine-graphql but the name of that repository was incorrect
286+
because it did not specify ORM only. So this repository was created and the others were abandoned.
281287

282288
This was written for the [Live Concert Database](https://lcdb.org)
283289

docs/events.rst

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,6 @@
22
Events
33
======
44

5-
There are two versions, 11 and 12, of this library which support different event
6-
manager versions. See `Versions and Event Manager Support <versions.html>`_ for
7-
more information.
8-
95
Query Builder Event
106
===================
117

docs/index.rst

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -79,12 +79,6 @@ you'll see, there's a lot of customizable power built in too.
7979
technical/internals
8080
technical/migration-guide
8181

82-
.. toctree::
83-
:maxdepth: 1
84-
:caption: Reference
85-
86-
technical/api-index
87-
technical/changelog
8882

8983
.. role:: raw-html(raw)
9084
:format: html

docs/technical/index.rst

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -246,12 +246,6 @@ Documentation Structure
246246
internals
247247
migration-guide
248248

249-
.. toctree::
250-
:maxdepth: 1
251-
:caption: Reference
252-
253-
api-index
254-
changelog
255249

256250
Conventions Used
257251
================

src/Resolve/ResolveCollectionFactory.php

Lines changed: 7 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@
2121
use League\Event\EventDispatcher;
2222

2323
use function array_flip;
24-
use function array_key_first;
2524
use function count;
2625
use function in_array;
2726

@@ -102,31 +101,22 @@ protected function buildPagination(
102101

103102
// Handle different association types
104103
if (isset($association['joinTable'])) {
105-
// Many-to-many relationship
106-
$identifierValues = $sourceMetadata->getIdentifierValues($source);
107-
$sourceId = $identifierValues[array_key_first($identifierValues)];
108-
109-
$joinTable = $association['joinTable']['name'];
110-
$joinColumns = $association['joinTable']['joinColumns'];
111-
$inverseJoinColumns = $association['joinTable']['inverseJoinColumns'];
112-
113-
$queryBuilder->innerJoin(
114-
$joinTable,
115-
'jt',
116-
'WITH',
117-
'jt.' . $inverseJoinColumns[0]['name'] . ' = entity.id',
118-
);
119-
$queryBuilder->where('jt.' . $joinColumns[0]['name'] . ' = :sourceId');
120-
$queryBuilder->setParameter('sourceId', $sourceId);
104+
// Many-to-many relationship (owning side with join table)
105+
// Use Doctrine's association mapping instead of manual join table handling
106+
$queryBuilder->innerJoin($entityClassName, 'source', 'WITH', ':source MEMBER OF source.' . $associationName);
107+
$queryBuilder->setParameter('source', $source);
121108
} elseif (isset($association['mappedBy'])) {
122109
// One-to-many: target entity has the foreign key
123110
$queryBuilder->where('entity.' . $association['mappedBy'] . ' = :source');
124111
$queryBuilder->setParameter('source', $source);
112+
// @codeCoverageIgnoreStart
125113
} elseif (isset($association['inversedBy'])) {
126114
// Many-to-one from the owning side (less common for collections)
115+
// This is defensively handled here for completeness
127116
$queryBuilder->innerJoin($entityClassName, 'source', 'WITH', 'source.' . $associationName . ' = entity');
128117
$queryBuilder->where('source = :source');
129118
$queryBuilder->setParameter('source', $source);
119+
// @codeCoverageIgnoreEnd
130120
}
131121

132122
// Apply filters using QueryBuilder
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace ApiSkeletonsTest\Doctrine\ORM\GraphQL\Feature\Association;
6+
7+
use ApiSkeletons\Doctrine\ORM\GraphQL\Config;
8+
use ApiSkeletons\Doctrine\ORM\GraphQL\Driver;
9+
use ApiSkeletonsTest\Doctrine\ORM\GraphQL\Entity\Recording;
10+
use ApiSkeletonsTest\Doctrine\ORM\GraphQL\Entity\User;
11+
use ApiSkeletonsTest\Doctrine\ORM\GraphQL\TestCase;
12+
use GraphQL\GraphQL;
13+
use GraphQL\Type\Definition\ObjectType;
14+
use GraphQL\Type\Schema;
15+
16+
class InversedByCollectionTest extends TestCase
17+
{
18+
/**
19+
* Test querying a ManyToMany collection from the owning side (inversedBy)
20+
*/
21+
public function testUserRecordingsCollection(): void
22+
{
23+
$config = new Config(['group' => 'CustomFieldStrategyTest']);
24+
25+
$driver = new Driver($this->getEntityManager(), $config);
26+
27+
$schema = new Schema([
28+
'query' => new ObjectType([
29+
'name' => 'query',
30+
'fields' => [
31+
'user' => $driver->completeConnection(User::class),
32+
'recording' => $driver->completeConnection(Recording::class),
33+
],
34+
]),
35+
]);
36+
37+
// Query users and their recordings
38+
$query = '{
39+
user(pagination: { first: 5 }) {
40+
edges {
41+
node {
42+
name
43+
recordings(pagination: { first: 10 }) {
44+
edges {
45+
node {
46+
source
47+
}
48+
}
49+
totalCount
50+
}
51+
}
52+
}
53+
totalCount
54+
}
55+
}';
56+
57+
$result = GraphQL::executeQuery($schema, $query);
58+
$output = $result->toArray();
59+
60+
$this->assertArrayNotHasKey('errors', $output);
61+
$this->assertIsArray($output['data']['user']['edges']);
62+
}
63+
64+
/**
65+
* Test querying with filters on the inversedBy collection
66+
*/
67+
public function testUserRecordingsWithFilters(): void
68+
{
69+
$config = new Config(['group' => 'CustomFieldStrategyTest']);
70+
71+
$driver = new Driver($this->getEntityManager(), $config);
72+
73+
$schema = new Schema([
74+
'query' => new ObjectType([
75+
'name' => 'query',
76+
'fields' => [
77+
'user' => $driver->completeConnection(User::class),
78+
],
79+
]),
80+
]);
81+
82+
// Query users with filtered recordings
83+
$query = '{
84+
user(pagination: { first: 1 }) {
85+
edges {
86+
node {
87+
name
88+
recordings(
89+
filter: { source: { contains: "tape" } }
90+
pagination: { first: 5 }
91+
) {
92+
edges {
93+
node {
94+
source
95+
}
96+
}
97+
}
98+
}
99+
}
100+
}
101+
}';
102+
103+
$result = GraphQL::executeQuery($schema, $query);
104+
$output = $result->toArray();
105+
106+
$this->assertArrayNotHasKey('errors', $output);
107+
}
108+
}

test/TestCase.php

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
use Doctrine\ORM\Tools\SchemaTool;
1313
use PHPUnit\Framework\TestCase as PHPUnitTestCase;
1414

15+
use function count;
1516
use function date;
1617
use function file_get_contents;
1718

@@ -50,7 +51,8 @@ protected function getEntityManager(): EntityManager
5051

5152
protected function populateData(): void
5253
{
53-
$users = [
54+
$userEntities = [];
55+
$users = [
5456
[
5557
'name' => 'User one',
5658
'email' => 'userOne@gmail.com',
@@ -139,8 +141,10 @@ protected function populateData(): void
139141
$user->setName($userData['name']);
140142
$user->setEmail($userData['email']);
141143
$user->setPassword($userData['password']);
144+
$userEntities[] = $user;
142145
}
143146

147+
$recordingIndex = 0;
144148
foreach ($artists as $name => $performances) {
145149
$artist = (new Entity\Artist())
146150
->setName($name);
@@ -164,6 +168,16 @@ protected function populateData(): void
164168
->setSource($source)
165169
->setPerformance($performance);
166170
self::$entityManager->persist($recording);
171+
172+
// Link recordings to users for testing ManyToMany relationships
173+
if (empty($userEntities)) {
174+
continue;
175+
}
176+
177+
$userIndex = $recordingIndex % count($userEntities);
178+
$userEntities[$userIndex]->addRecording($recording);
179+
$recording->addUser($userEntities[$userIndex]);
180+
$recordingIndex++;
167181
}
168182
}
169183
}

0 commit comments

Comments
 (0)