28
28
use Doctrine \Common \Persistence \ManagerRegistry ;
29
29
use Doctrine \ORM \EntityManagerInterface ;
30
30
use Doctrine \ORM \Mapping \ClassMetadataInfo ;
31
+ use Doctrine \ORM \QueryBuilder ;
31
32
32
33
/**
33
34
* Subresource data provider for the Doctrine ORM.
@@ -79,82 +80,10 @@ public function getSubresource(string $resourceClass, array $identifiers, array
79
80
throw new ResourceClassNotSupportedException ('The given resource class is not a subresource. ' );
80
81
}
81
82
82
- $ originAlias = 'o ' ;
83
- $ queryBuilder = $ repository ->createQueryBuilder ($ originAlias );
84
83
$ queryNameGenerator = new QueryNameGenerator ();
85
- $ previousQueryBuilder = null ;
86
- $ previousAlias = null ;
87
-
88
- $ num = \count ($ context ['identifiers ' ]);
89
-
90
- while ($ num --) {
91
- list ($ identifier , $ identifierResourceClass ) = $ context ['identifiers ' ][$ num ];
92
- $ previousAssociationProperty = $ context ['identifiers ' ][$ num + 1 ][0 ] ?? $ context ['property ' ];
93
-
94
- $ manager = $ this ->managerRegistry ->getManagerForClass ($ identifierResourceClass );
95
-
96
- if (!$ manager instanceof EntityManagerInterface) {
97
- throw new RuntimeException ("The manager for $ identifierResourceClass must be an EntityManager. " );
98
- }
99
-
100
- $ classMetadata = $ manager ->getClassMetadata ($ identifierResourceClass );
101
-
102
- if (!$ classMetadata instanceof ClassMetadataInfo) {
103
- throw new RuntimeException ("The class metadata for $ identifierResourceClass must be an instance of ClassMetadataInfo. " );
104
- }
105
-
106
- $ qb = $ manager ->createQueryBuilder ();
107
- $ alias = $ queryNameGenerator ->generateJoinAlias ($ identifier );
108
- $ relationType = $ classMetadata ->getAssociationMapping ($ previousAssociationProperty )['type ' ];
109
- $ normalizedIdentifiers = isset ($ identifiers [$ identifier ]) ? $ this ->normalizeIdentifiers ($ identifiers [$ identifier ], $ manager , $ identifierResourceClass ) : [];
110
-
111
- switch ($ relationType ) {
112
- //MANY_TO_MANY relations need an explicit join so that the identifier part can be retrieved
113
- case ClassMetadataInfo::MANY_TO_MANY :
114
- $ joinAlias = $ queryNameGenerator ->generateJoinAlias ($ previousAssociationProperty );
115
-
116
- $ qb ->select ($ joinAlias )
117
- ->from ($ identifierResourceClass , $ alias )
118
- ->innerJoin ("$ alias. $ previousAssociationProperty " , $ joinAlias );
119
-
120
- break ;
121
- case ClassMetadataInfo::ONE_TO_MANY :
122
- $ mappedBy = $ classMetadata ->getAssociationMapping ($ previousAssociationProperty )['mappedBy ' ];
123
-
124
- // first pass, o.property instead of alias.property
125
- if (null === $ previousQueryBuilder ) {
126
- $ originAlias = "$ originAlias. $ mappedBy " ;
127
- } else {
128
- $ previousAlias = "$ previousAlias. $ mappedBy " ;
129
- }
130
-
131
- $ qb ->select ($ alias )
132
- ->from ($ identifierResourceClass , $ alias );
133
- break ;
134
- default :
135
- $ qb ->select ("IDENTITY( $ alias. $ previousAssociationProperty) " )
136
- ->from ($ identifierResourceClass , $ alias );
137
- }
138
-
139
- // Add where clause for identifiers
140
- foreach ($ normalizedIdentifiers as $ key => $ value ) {
141
- $ placeholder = $ queryNameGenerator ->generateParameterName ($ key );
142
- $ qb ->andWhere ("$ alias. $ key = : $ placeholder " );
143
- $ queryBuilder ->setParameter ($ placeholder , $ value );
144
- }
145
-
146
- // recurse queries
147
- if (null === $ previousQueryBuilder ) {
148
- $ previousQueryBuilder = $ qb ;
149
- } else {
150
- $ previousQueryBuilder ->andWhere ($ qb ->expr ()->in ($ previousAlias , $ qb ->getDQL ()));
151
- }
152
-
153
- $ previousAlias = $ alias ;
154
- }
155
84
156
85
/*
157
- * The following translate to this pseudo-dql:
86
+ * The following recursively translates to this pseudo-dql:
158
87
*
159
88
* SELECT thirdLevel WHERE thirdLevel IN (
160
89
* SELECT thirdLevel FROM relatedDummies WHERE relatedDummies = ? AND relatedDummies IN (
@@ -164,9 +93,7 @@ public function getSubresource(string $resourceClass, array $identifiers, array
164
93
*
165
94
* By using subqueries, we're forcing the SQL execution plan to go through indexes on doctrine identifiers.
166
95
*/
167
- $ queryBuilder ->where (
168
- $ queryBuilder ->expr ()->in ($ originAlias , $ previousQueryBuilder ->getDQL ())
169
- );
96
+ $ queryBuilder = $ this ->buildQuery ($ identifiers , $ context , $ queryNameGenerator , $ repository ->createQueryBuilder ($ alias = 'o ' ), $ alias , \count ($ context ['identifiers ' ]));
170
97
171
98
if (true === $ context ['collection ' ]) {
172
99
foreach ($ this ->collectionExtensions as $ extension ) {
@@ -195,4 +122,75 @@ public function getSubresource(string $resourceClass, array $identifiers, array
195
122
196
123
return $ context ['collection ' ] ? $ query ->getResult () : $ query ->getOneOrNullResult ();
197
124
}
125
+
126
+ /**
127
+ * @throws RuntimeException
128
+ */
129
+ private function buildQuery (array $ identifiers , array $ context , QueryNameGenerator $ queryNameGenerator , QueryBuilder $ previousQueryBuilder , string $ previousAlias , int $ remainingIdentifiers , QueryBuilder $ topQueryBuilder = null ): QueryBuilder
130
+ {
131
+ if ($ remainingIdentifiers <= 0 ) {
132
+ return $ previousQueryBuilder ;
133
+ }
134
+
135
+ $ topQueryBuilder = $ topQueryBuilder ?? $ previousQueryBuilder ;
136
+
137
+ list ($ identifier , $ identifierResourceClass ) = $ context ['identifiers ' ][$ remainingIdentifiers - 1 ];
138
+ $ previousAssociationProperty = $ context ['identifiers ' ][$ remainingIdentifiers ][0 ] ?? $ context ['property ' ];
139
+
140
+ $ manager = $ this ->managerRegistry ->getManagerForClass ($ identifierResourceClass );
141
+
142
+ if (!$ manager instanceof EntityManagerInterface) {
143
+ throw new RuntimeException ("The manager for $ identifierResourceClass must be an EntityManager. " );
144
+ }
145
+
146
+ $ classMetadata = $ manager ->getClassMetadata ($ identifierResourceClass );
147
+
148
+ if (!$ classMetadata instanceof ClassMetadataInfo) {
149
+ throw new RuntimeException (
150
+ "The class metadata for $ identifierResourceClass must be an instance of ClassMetadataInfo. "
151
+ );
152
+ }
153
+
154
+ $ qb = $ manager ->createQueryBuilder ();
155
+ $ alias = $ queryNameGenerator ->generateJoinAlias ($ identifier );
156
+ $ relationType = $ classMetadata ->getAssociationMapping ($ previousAssociationProperty )['type ' ];
157
+ $ normalizedIdentifiers = isset ($ identifiers [$ identifier ]) ? $ this ->normalizeIdentifiers (
158
+ $ identifiers [$ identifier ],
159
+ $ manager ,
160
+ $ identifierResourceClass
161
+ ) : [];
162
+
163
+ switch ($ relationType ) {
164
+ // MANY_TO_MANY relations need an explicit join so that the identifier part can be retrieved
165
+ case ClassMetadataInfo::MANY_TO_MANY :
166
+ $ joinAlias = $ queryNameGenerator ->generateJoinAlias ($ previousAssociationProperty );
167
+
168
+ $ qb ->select ($ joinAlias )
169
+ ->from ($ identifierResourceClass , $ alias )
170
+ ->innerJoin ("$ alias. $ previousAssociationProperty " , $ joinAlias );
171
+ break ;
172
+ case ClassMetadataInfo::ONE_TO_MANY :
173
+ $ mappedBy = $ classMetadata ->getAssociationMapping ($ previousAssociationProperty )['mappedBy ' ];
174
+ $ previousAlias = "$ previousAlias. $ mappedBy " ;
175
+
176
+ $ qb ->select ($ alias )
177
+ ->from ($ identifierResourceClass , $ alias );
178
+ break ;
179
+ default :
180
+ $ qb ->select ("IDENTITY( $ alias. $ previousAssociationProperty) " )
181
+ ->from ($ identifierResourceClass , $ alias );
182
+ }
183
+
184
+ // Add where clause for identifiers
185
+ foreach ($ normalizedIdentifiers as $ key => $ value ) {
186
+ $ placeholder = $ queryNameGenerator ->generateParameterName ($ key );
187
+ $ qb ->andWhere ("$ alias. $ key = : $ placeholder " );
188
+ $ topQueryBuilder ->setParameter ($ placeholder , $ value );
189
+ }
190
+
191
+ // Recurse queries
192
+ $ qb = $ this ->buildQuery ($ identifiers , $ context , $ queryNameGenerator , $ qb , $ alias , --$ remainingIdentifiers , $ topQueryBuilder );
193
+
194
+ return $ previousQueryBuilder ->andWhere ($ qb ->expr ()->in ($ previousAlias , $ qb ->getDQL ()));
195
+ }
198
196
}
0 commit comments