1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
/* |
4
|
|
|
* This file is part of the LoopBackApiBundle package. |
5
|
|
|
* |
6
|
|
|
* (c) Théo FIDRY <[email protected]> |
7
|
|
|
* |
8
|
|
|
* For the full copyright and license information, please view the LICENSE |
9
|
|
|
* file that was distributed with this source code. |
10
|
|
|
*/ |
11
|
|
|
|
12
|
|
|
namespace Fidry\LoopBackApiBundle\Filter; |
13
|
|
|
|
14
|
|
|
use Doctrine\Common\Persistence\ManagerRegistry; |
15
|
|
|
use Doctrine\Common\Persistence\Mapping\ClassMetadata; |
16
|
|
|
use Doctrine\ORM\Query\Expr; |
17
|
|
|
use Doctrine\ORM\QueryBuilder; |
18
|
|
|
use Dunglas\ApiBundle\Api\IriConverterInterface; |
19
|
|
|
use Dunglas\ApiBundle\Api\ResourceInterface; |
20
|
|
|
use Dunglas\ApiBundle\Doctrine\Orm\Filter\FilterInterface; |
21
|
|
|
use Fidry\LoopBackApiBundle\Http\Request\FilterQueryExtractorInterface; |
22
|
|
|
use Symfony\Component\HttpFoundation\RequestStack; |
23
|
|
|
use Symfony\Component\PropertyAccess\PropertyAccessorInterface; |
24
|
|
|
|
25
|
|
|
/** |
26
|
|
|
* Class SearchFilter. |
27
|
|
|
* |
28
|
|
|
* @author Théo FIDRY <[email protected]> |
29
|
|
|
*/ |
30
|
|
|
class WhereFilter implements FilterInterface |
31
|
|
|
{ |
32
|
|
|
const PARAMETER_OPERATOR_OR = 'or'; |
33
|
|
|
const PARAMETER_OPERATOR_GT = 'gt'; |
34
|
|
|
const PARAMETER_OPERATOR_GTE = 'gte'; |
35
|
|
|
const PARAMETER_OPERATOR_LT = 'lt'; |
36
|
|
|
const PARAMETER_OPERATOR_LTE = 'lte'; |
37
|
|
|
const PARAMETER_OPERATOR_BETWEEN = 'between'; |
38
|
|
|
const PARAMETER_OPERATOR_NEQ = 'neq'; |
39
|
|
|
const PARAMETER_OPERATOR_LIKE = 'like'; |
40
|
|
|
const PARAMETER_OPERATOR_NLIKE = 'nlike'; |
41
|
|
|
|
42
|
|
|
const PARAMETER_ID_KEY = 'id'; |
43
|
|
|
const PARAMETER_NULL_VALUE = 'null'; |
44
|
|
|
|
45
|
|
|
/** |
46
|
|
|
* @var IriConverterInterface |
47
|
|
|
*/ |
48
|
|
|
private $iriConverter; |
49
|
|
|
|
50
|
|
|
/** |
51
|
|
|
* @var FilterQueryExtractorInterface |
52
|
|
|
*/ |
53
|
|
|
private $queryExtractor; |
54
|
|
|
|
55
|
|
|
/** |
56
|
|
|
* @var ManagerRegistry |
57
|
|
|
*/ |
58
|
|
|
private $managerRegistry; |
59
|
|
|
|
60
|
|
|
/** |
61
|
|
|
* @var array|null |
62
|
|
|
*/ |
63
|
|
|
private $properties; |
64
|
|
|
|
65
|
|
|
/** |
66
|
|
|
* @var PropertyAccessorInterface |
67
|
|
|
*/ |
68
|
|
|
private $propertyAccessor; |
69
|
|
|
|
70
|
|
|
/** |
71
|
|
|
* @var RequestStack |
72
|
|
|
*/ |
73
|
|
|
private $requestStack; |
74
|
|
|
|
75
|
|
|
/** |
76
|
|
|
* @param ManagerRegistry $managerRegistry |
77
|
|
|
* @param RequestStack $requestStack |
78
|
|
|
* @param IriConverterInterface $iriConverter |
79
|
|
|
* @param PropertyAccessorInterface $propertyAccessor |
80
|
|
|
* @param FilterQueryExtractorInterface $queryExtractor |
81
|
|
|
* @param null|array $properties Null to allow filtering on all properties with the exact strategy or a map of property name with strategy. |
82
|
|
|
*/ |
83
|
|
|
public function __construct( |
84
|
|
|
ManagerRegistry $managerRegistry, |
85
|
|
|
RequestStack $requestStack, |
86
|
|
|
IriConverterInterface $iriConverter, |
87
|
|
|
PropertyAccessorInterface $propertyAccessor, |
88
|
|
|
FilterQueryExtractorInterface $queryExtractor, |
89
|
|
|
array $properties = null |
90
|
|
|
) { |
91
|
|
|
$this->managerRegistry = $managerRegistry; |
92
|
|
|
$this->iriConverter = $iriConverter; |
93
|
|
|
$this->propertyAccessor = $propertyAccessor; |
94
|
|
|
$this->queryExtractor = $queryExtractor; |
95
|
|
|
$this->requestStack = $requestStack; |
96
|
|
|
$this->properties = $properties; |
97
|
|
|
} |
98
|
|
|
|
99
|
|
|
/** |
100
|
|
|
* {@inheritdoc} |
101
|
|
|
*/ |
102
|
|
|
public function apply(ResourceInterface $resource, QueryBuilder $queryBuilder) |
103
|
|
|
{ |
104
|
|
|
if (null === $request = $this->requestStack->getCurrentRequest()) { |
105
|
|
|
return null; |
106
|
|
|
} |
107
|
|
|
|
108
|
|
|
$queryValues = $this->queryExtractor->extractProperties($request); |
109
|
|
|
$metadata = $this->getClassMetadata($resource); |
110
|
|
|
$queryExpr = []; |
111
|
|
|
$aliases = []; |
112
|
|
|
$associationsMetadata = []; |
113
|
|
|
|
114
|
|
|
// Retrieve all doctrine query expressions |
115
|
|
|
foreach ($queryValues as $key => $value) { |
116
|
|
|
if (self::PARAMETER_OPERATOR_OR === $key && is_array($value)) { |
117
|
|
|
/* |
118
|
|
|
* OR operator case |
119
|
|
|
* |
120
|
|
|
* At this point $dataSet is expected to equal to something like this: |
121
|
|
|
* |
122
|
|
|
* $value = [ |
123
|
|
|
* 0 => [ |
124
|
|
|
* 0 => [ |
125
|
|
|
* 'property' => [ |
126
|
|
|
* 'operator' => 'operand' |
127
|
|
|
* ] |
128
|
|
|
* ], |
129
|
|
|
* 1 => [ |
130
|
|
|
* 'property' => value |
131
|
|
|
* ] |
132
|
|
|
* ], |
133
|
|
|
* 1 => [...], |
134
|
|
|
* ... |
135
|
|
|
* ] |
136
|
|
|
*/ |
137
|
|
|
foreach ($value as $index => $dataSet) { |
138
|
|
|
// Expect $dataSet to be an array containing 2 parameters |
139
|
|
|
if (is_array($dataSet) && 2 === count($dataSet)) { |
140
|
|
|
$queries = []; |
141
|
|
|
|
142
|
|
|
// Handle each "query" of $dataSet |
143
|
|
|
$count = 0; |
144
|
|
|
foreach ($dataSet as $dataSetElem) { |
145
|
|
|
if (false === is_array($dataSetElem)) { |
146
|
|
|
continue; |
147
|
|
|
} |
148
|
|
|
$property = key($dataSetElem); |
149
|
|
|
|
150
|
|
|
// At this point the value may be either a value or an array (for operators) |
151
|
|
|
$expr = $this->handleFilter( |
152
|
|
|
$queryBuilder, |
153
|
|
|
$metadata, |
154
|
|
|
$aliases, |
155
|
|
|
$associationsMetadata, |
156
|
|
|
$property, |
157
|
|
|
$dataSetElem[$property], |
158
|
|
|
sprintf('or_%s%d%d', $property, $index, $count) |
159
|
|
|
); |
160
|
|
|
|
161
|
|
|
$queries = array_merge($queries, $expr); |
162
|
|
|
++$count; |
163
|
|
|
} |
164
|
|
|
|
165
|
|
|
if (2 === count($queries)) { |
166
|
|
|
$queryExpr[] = $queryBuilder->expr()->orX($queries[0], $queries[1]); |
167
|
|
|
} |
168
|
|
|
} |
169
|
|
|
} |
170
|
|
|
} else { |
171
|
|
|
$queryExpr = array_merge( |
172
|
|
|
$queryExpr, |
173
|
|
|
$this->handleFilter($queryBuilder, $metadata, $aliases, $associationsMetadata, $key, $value) |
174
|
|
|
); |
175
|
|
|
} |
176
|
|
|
} |
177
|
|
|
|
178
|
|
|
foreach ($queryExpr as $expr) { |
179
|
|
|
$queryBuilder->andWhere($expr); |
180
|
|
|
} |
181
|
|
|
} |
182
|
|
|
|
183
|
|
|
/** |
184
|
|
|
* Handles the given filter to call the proper operator. At this point, it's unclear if the value passed is the real |
185
|
|
|
* value operator. |
186
|
|
|
* |
187
|
|
|
* @param QueryBuilder $queryBuilder |
188
|
|
|
* @param ClassMetadata $resourceMetadata |
189
|
|
|
* @param string[] $aliases |
190
|
|
|
* @param ClassMetadata[] $associationsMetadata |
191
|
|
|
* @param string $property |
192
|
|
|
* @param array|string $value |
193
|
|
|
* @param string|null $parameter If is string is used to construct the parameter to avoid parameter conflicts. |
194
|
|
|
* |
195
|
|
|
* @return array |
196
|
|
|
*/ |
197
|
|
|
private function handleFilter( |
198
|
|
|
QueryBuilder $queryBuilder, |
199
|
|
|
ClassMetadata $resourceMetadata, |
200
|
|
|
array $aliases, |
201
|
|
|
array $associationsMetadata, |
202
|
|
|
$property, |
203
|
|
|
$value, |
204
|
|
|
$parameter = null |
205
|
|
|
) { |
206
|
|
|
$queryExpr = []; |
207
|
|
|
|
208
|
|
|
/* |
209
|
|
|
* simple (case 1): |
210
|
|
|
* $property = name |
211
|
|
|
* |
212
|
|
|
* relation (case 2): |
213
|
|
|
* $property = relatedDummy_name |
214
|
|
|
* $property = relatedDymmy_id |
215
|
|
|
* $property = relatedDummy_user_id |
216
|
|
|
* $property = relatedDummy_user_name |
217
|
|
|
*/ |
218
|
|
|
if (false !== strpos($property, '.')) { |
219
|
|
|
$explodedProperty = explode('.', $property); |
220
|
|
|
} else { |
221
|
|
|
$explodedProperty = explode('_', $property); |
222
|
|
|
} |
223
|
|
|
// we are in case 2 |
224
|
|
|
$property = array_pop($explodedProperty); |
225
|
|
|
$alias = $this->getResourceAliasForProperty($aliases, $explodedProperty); |
226
|
|
|
$aliasMetadata = $this->getAssociationMetadataForProperty( |
227
|
|
|
$resourceMetadata, |
228
|
|
|
$associationsMetadata, |
229
|
|
|
$explodedProperty |
230
|
|
|
); |
231
|
|
|
|
232
|
|
|
if (true === $aliasMetadata->hasField($property)) { |
233
|
|
|
// Entity has the property |
234
|
|
|
if (is_array($value)) { |
235
|
|
|
foreach ($value as $operator => $operand) { |
236
|
|
|
// Case where there is an operator |
237
|
|
|
$queryExpr[] = $this->handleOperator( |
238
|
|
|
$queryBuilder, |
239
|
|
|
$alias, |
240
|
|
|
$aliasMetadata, |
241
|
|
|
$property, |
242
|
|
|
$operator, |
243
|
|
|
$operand, |
244
|
|
|
$parameter |
245
|
|
|
); |
246
|
|
|
} |
247
|
|
|
} else { |
248
|
|
|
// Simple where |
249
|
|
|
$value = $this->normalizeValue($aliasMetadata, $property, $value); |
250
|
|
|
if (null === $value) { |
251
|
|
|
$queryExpr[] = $queryBuilder->expr()->isNull(sprintf('%s.%s', $alias, $property)); |
252
|
|
|
} else { |
253
|
|
|
if (null === $parameter) { |
254
|
|
|
$parameter = $property; |
255
|
|
|
} |
256
|
|
|
$queryExpr[] = $queryBuilder->expr()->eq( |
257
|
|
|
sprintf('%s.%s', $alias, $property), |
258
|
|
|
sprintf(':%s', $parameter) |
259
|
|
|
); |
260
|
|
|
$queryBuilder->setParameter($parameter, $value); |
261
|
|
|
} |
262
|
|
|
} |
263
|
|
|
} |
264
|
|
|
|
265
|
|
|
return $queryExpr; |
266
|
|
|
} |
267
|
|
|
|
268
|
|
|
/** |
269
|
|
|
* Gets the proper query expression for the set of data given. |
270
|
|
|
* |
271
|
|
|
* @param QueryBuilder $queryBuilder |
272
|
|
|
* @param string $alias alias of the entity to which belongs the property |
273
|
|
|
* @param ClassMetadata $aliasMetadata |
274
|
|
|
* @param string $property |
275
|
|
|
* @param string $operator |
276
|
|
|
* @param string|array $value |
277
|
|
|
* @param string|null $parameter If is string is used to construct the parameter to avoid parameter conflicts. |
278
|
|
|
* |
279
|
|
|
* @return Expr|null |
280
|
|
|
*/ |
281
|
|
|
private function handleOperator( |
282
|
|
|
QueryBuilder $queryBuilder, |
283
|
|
|
$alias, |
284
|
|
|
ClassMetadata $aliasMetadata, |
285
|
|
|
$property, |
286
|
|
|
$operator, |
287
|
|
|
$value, |
288
|
|
|
$parameter = |
289
|
|
|
null |
290
|
|
|
) { |
291
|
|
|
$queryExpr = null; |
292
|
|
|
if (null === $parameter) { |
293
|
|
|
$parameter = $property; |
294
|
|
|
} |
295
|
|
|
|
296
|
|
|
// Only particular case: the between operator |
297
|
|
|
if (self::PARAMETER_OPERATOR_BETWEEN === $operator |
298
|
|
|
&& is_array($value) |
299
|
|
|
&& 2 === count($value) |
300
|
|
|
) { |
301
|
|
|
$value = array_values($value); |
302
|
|
|
$paramBefore = sprintf(':between_before_%s', $parameter); |
303
|
|
|
$paramAfter = sprintf(':between_after_%s', $parameter); |
304
|
|
|
|
305
|
|
|
$queryExpr = $queryBuilder->expr()->between( |
306
|
|
|
sprintf('%s.%s', $alias, $property), |
307
|
|
|
$paramBefore, |
308
|
|
|
$paramAfter |
309
|
|
|
); |
310
|
|
|
|
311
|
|
|
$queryBuilder |
312
|
|
|
->setParameter($paramBefore, $value[0]) |
313
|
|
|
->setParameter($paramAfter, $value[1]) |
314
|
|
|
; |
315
|
|
|
|
316
|
|
|
return $queryExpr; |
317
|
|
|
} |
318
|
|
|
|
319
|
|
|
// Expect $value to be a string |
320
|
|
|
if (false === is_string($value)) { |
321
|
|
|
return null; |
322
|
|
|
} |
323
|
|
|
|
324
|
|
|
// Normalize $value before using it |
325
|
|
|
$value = $this->normalizeValue($aliasMetadata, $property, $value); |
326
|
|
|
$parameterValue = (self::PARAMETER_OPERATOR_LIKE === $operator || self::PARAMETER_OPERATOR_NLIKE === $operator) |
327
|
|
|
? sprintf('%%%s%%', $value) |
328
|
|
|
: $value; |
329
|
|
|
|
330
|
|
|
switch ($operator) { |
331
|
|
View Code Duplication |
case self::PARAMETER_OPERATOR_GT: |
|
|
|
|
332
|
|
|
$queryExpr = $queryBuilder->expr()->gt(sprintf('%s.%s', $alias, $property), sprintf(':%s', $parameter)); |
333
|
|
|
break; |
334
|
|
|
|
335
|
|
View Code Duplication |
case self::PARAMETER_OPERATOR_GTE: |
|
|
|
|
336
|
|
|
$queryExpr = $queryBuilder->expr()->gte(sprintf('%s.%s', $alias, $property), sprintf(':%s', |
337
|
|
|
$parameter)); |
338
|
|
|
break; |
339
|
|
|
|
340
|
|
View Code Duplication |
case self::PARAMETER_OPERATOR_LT: |
|
|
|
|
341
|
|
|
$queryExpr = $queryBuilder->expr()->lt(sprintf('%s.%s', $alias, $property), sprintf(':%s', $parameter)); |
342
|
|
|
break; |
343
|
|
|
|
344
|
|
View Code Duplication |
case self::PARAMETER_OPERATOR_LTE: |
|
|
|
|
345
|
|
|
$queryExpr = $queryBuilder->expr()->lte(sprintf('%s.%s', $alias, $property), sprintf(':%s', |
346
|
|
|
$parameter)); |
347
|
|
|
break; |
348
|
|
|
|
349
|
|
|
case self::PARAMETER_OPERATOR_NEQ: |
350
|
|
|
if (null === $value) { |
351
|
|
|
// Skip the set parameter that takes place after the switch case |
352
|
|
|
return $queryBuilder->expr()->isNotNull(sprintf('%s.%s', $alias, $property)); |
353
|
|
|
} else { |
354
|
|
|
$queryExpr = $queryBuilder->expr()->neq(sprintf('%s.%s', $alias, $property), sprintf(':%s', $parameter)); |
355
|
|
|
} |
356
|
|
|
break; |
357
|
|
|
|
358
|
|
View Code Duplication |
case self::PARAMETER_OPERATOR_LIKE: |
|
|
|
|
359
|
|
|
$queryExpr = $queryBuilder->expr()->like(sprintf('%s.%s', $alias, $property), sprintf(':%s', |
360
|
|
|
$parameter)); |
361
|
|
|
break; |
362
|
|
|
|
363
|
|
View Code Duplication |
case self::PARAMETER_OPERATOR_NLIKE: |
|
|
|
|
364
|
|
|
$queryExpr = $queryBuilder->expr()->notLike(sprintf('%s.%s', $alias, $property), sprintf(':%s', $parameter)); |
365
|
|
|
break; |
366
|
|
|
} |
367
|
|
|
|
368
|
|
|
if (null === $queryBuilder->getParameter($parameter)) { |
369
|
|
|
$queryBuilder->setParameter($parameter, $parameterValue); |
370
|
|
|
} |
371
|
|
|
|
372
|
|
|
return $queryExpr; |
373
|
|
|
} |
374
|
|
|
|
375
|
|
|
/** |
376
|
|
|
* Normalizes the value. If the key is an ID, get the real ID value. If is null, set the value to null. Otherwise |
377
|
|
|
* return unchanged value. |
378
|
|
|
* |
379
|
|
|
* @param ClassMetadata $metadata |
380
|
|
|
* @param string $property |
381
|
|
|
* @param string $value |
382
|
|
|
* |
383
|
|
|
* @return null|string |
384
|
|
|
*/ |
385
|
|
|
private function normalizeValue(ClassMetadata $metadata, $property, $value) |
386
|
|
|
{ |
387
|
|
|
if (self::PARAMETER_ID_KEY === $property) { |
388
|
|
|
return $this->getFilterValueFromUrl($value); |
389
|
|
|
} |
390
|
|
|
|
391
|
|
|
if (self::PARAMETER_NULL_VALUE === $value) { |
392
|
|
|
return null; |
393
|
|
|
} |
394
|
|
|
|
395
|
|
|
switch ($metadata->getTypeOfField($property)) { |
396
|
|
|
case 'boolean': |
397
|
|
|
return (bool) $value; |
398
|
|
|
|
399
|
|
|
case 'integer': |
400
|
|
|
return (int) $value; |
401
|
|
|
|
402
|
|
|
case 'float': |
403
|
|
|
return (float) $value; |
404
|
|
|
|
405
|
|
|
case 'datetime': |
406
|
|
|
// the input has the format `2015-04-28T02:23:50 00:00`, transform it to match the database format |
407
|
|
|
// `2015-04-28 02:23:50` |
408
|
|
|
return preg_replace('/(\d{4}(-\d{2}){2})T(\d{2}(:\d{2}){2}) \d{2}:\d{2}/', '$1 $3', $value); |
409
|
|
|
} |
410
|
|
|
|
411
|
|
|
return $value; |
412
|
|
|
} |
413
|
|
|
|
414
|
|
|
/** |
415
|
|
|
* {@inheritdoc} |
416
|
|
|
* |
417
|
|
|
* TODO |
418
|
|
|
*/ |
419
|
|
|
public function getDescription(ResourceInterface $resource) |
420
|
|
|
{ |
421
|
|
|
return []; |
422
|
|
|
} |
423
|
|
|
|
424
|
|
|
/** |
425
|
|
|
* Gets the ID from an URI or a raw ID. |
426
|
|
|
* |
427
|
|
|
* @param string $value |
428
|
|
|
* |
429
|
|
|
* @return string |
430
|
|
|
*/ |
431
|
|
|
protected function getFilterValueFromUrl($value) |
432
|
|
|
{ |
433
|
|
|
try { |
434
|
|
|
if ($item = $this->iriConverter->getItemFromIri($value)) { |
435
|
|
|
return $this->propertyAccessor->getValue($item, 'id'); |
436
|
|
|
} |
437
|
|
|
} catch (\InvalidArgumentException $e) { |
438
|
|
|
// Do nothing, return the raw value |
439
|
|
|
} |
440
|
|
|
|
441
|
|
|
return $value; |
442
|
|
|
} |
443
|
|
|
|
444
|
|
|
/** |
445
|
|
|
* Gets the alias used for the entity to which the property belongs. |
446
|
|
|
* |
447
|
|
|
* @example |
448
|
|
|
* $property was `name` |
449
|
|
|
* $explodedProperty then is [] |
450
|
|
|
* => 'o' |
451
|
|
|
* |
452
|
|
|
* $property was `relatedDummy_name` |
453
|
|
|
* $explodedProperty then is ['relatedDummy'] |
454
|
|
|
* => WhereFilter_relatedDummyAlias |
455
|
|
|
* |
456
|
|
|
* $property was `relatedDummy_anotherDummy_name` |
457
|
|
|
* $explodedProperty then is ['relatedDummy', 'anotherDummy'] |
458
|
|
|
* => WhereFilter_relatedDummy_anotherDummyAlias |
459
|
|
|
* |
460
|
|
|
* @param string[] $aliases Array containing all the properties for each an alias is used. The key is the |
461
|
|
|
* property and the value the actual alias. |
462
|
|
|
* @param string[] $explodedProperty |
463
|
|
|
* |
464
|
|
|
* @return string alias |
465
|
|
|
*/ |
466
|
|
|
private function getResourceAliasForProperty(array &$aliases, array $explodedProperty) |
467
|
|
|
{ |
468
|
|
|
if (0 === count($explodedProperty)) { |
469
|
|
|
return 'o'; |
470
|
|
|
} |
471
|
|
|
|
472
|
|
|
foreach ($explodedProperty as $property) { |
473
|
|
|
if (false === isset($aliases[$property])) { |
474
|
|
|
$aliases[$property] = sprintf('WhereFilter_%sAlias', implode('_', $explodedProperty)); |
475
|
|
|
} |
476
|
|
|
} |
477
|
|
|
|
478
|
|
|
return $aliases[end($explodedProperty)]; |
479
|
|
|
} |
480
|
|
|
|
481
|
|
|
/** |
482
|
|
|
* Gets the metadata to which belongs the property. |
483
|
|
|
* |
484
|
|
|
* @example |
485
|
|
|
* $property was `name` |
486
|
|
|
* $explodedProperty then is [] |
487
|
|
|
* => $resourceMetadata |
488
|
|
|
* |
489
|
|
|
* $property was `relatedDummy_name` |
490
|
|
|
* $explodedProperty then is ['relatedDummy'] |
491
|
|
|
* => metadata of relatedDummy |
492
|
|
|
* |
493
|
|
|
* $property was `relatedDummy_anotherDummy_name` |
494
|
|
|
* $explodedProperty then is ['relatedDummy', 'anotherDummy'] |
495
|
|
|
* => metadata of anotherDummy |
496
|
|
|
* |
497
|
|
|
* @param ClassMetadata $resourceMetadata |
498
|
|
|
* @param ClassMetadata[] $associationsMetadata |
499
|
|
|
* @param array $explodedProperty |
500
|
|
|
* |
501
|
|
|
* @return ClassMetadata |
502
|
|
|
*/ |
503
|
|
|
private function getAssociationMetadataForProperty( |
504
|
|
|
ClassMetadata $resourceMetadata, |
505
|
|
|
array &$associationsMetadata, |
506
|
|
|
array |
507
|
|
|
$explodedProperty |
508
|
|
|
) { |
509
|
|
|
if (0 === count($explodedProperty)) { |
510
|
|
|
return $resourceMetadata; |
511
|
|
|
} |
512
|
|
|
|
513
|
|
|
$parentResourceMetadata = $resourceMetadata; |
514
|
|
|
foreach ($explodedProperty as $index => $property) { |
515
|
|
|
if (1 <= $index) { |
516
|
|
|
$parentResourceMetadata = $associationsMetadata[$explodedProperty[$index - 1]]; |
517
|
|
|
} |
518
|
|
|
|
519
|
|
|
if (false === $parentResourceMetadata->hasAssociation($property)) { |
520
|
|
|
throw new \RuntimeException(sprintf( |
521
|
|
|
'Class %s::%s is not an association.', |
522
|
|
|
$parentResourceMetadata->getName |
523
|
|
|
(), |
524
|
|
|
$property) |
525
|
|
|
); |
526
|
|
|
} |
527
|
|
|
|
528
|
|
|
if (false === isset($associationsMetadata[$property])) { |
529
|
|
|
$associationsMetadata[$property] = $this->getMetadata( |
530
|
|
|
$parentResourceMetadata->getAssociationTargetClass($property) |
531
|
|
|
); |
532
|
|
|
} |
533
|
|
|
} |
534
|
|
|
|
535
|
|
|
return $associationsMetadata[end($explodedProperty)]; |
536
|
|
|
} |
537
|
|
|
|
538
|
|
|
/** |
539
|
|
|
* Gets class metadata for the given class. |
540
|
|
|
* |
541
|
|
|
* @param string $class |
542
|
|
|
* |
543
|
|
|
* @return ClassMetadata |
544
|
|
|
*/ |
545
|
|
|
private function getMetadata($class) |
546
|
|
|
{ |
547
|
|
|
return $this |
548
|
|
|
->managerRegistry |
549
|
|
|
->getManagerForClass($class) |
550
|
|
|
->getClassMetadata($class) |
551
|
|
|
; |
552
|
|
|
} |
553
|
|
|
|
554
|
|
|
/** |
555
|
|
|
* Gets class metadata for the given resource. |
556
|
|
|
* |
557
|
|
|
* @param ResourceInterface $resource |
558
|
|
|
* |
559
|
|
|
* @return ClassMetadata |
560
|
|
|
*/ |
561
|
|
|
private function getClassMetadata(ResourceInterface $resource) |
562
|
|
|
{ |
563
|
|
|
$entityClass = $resource->getEntityClass(); |
564
|
|
|
|
565
|
|
|
return $this |
566
|
|
|
->managerRegistry |
567
|
|
|
->getManagerForClass($entityClass) |
568
|
|
|
->getClassMetadata($entityClass) |
569
|
|
|
; |
570
|
|
|
} |
571
|
|
|
} |
572
|
|
|
|
Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.
You can also find more detailed suggestions in the “Code” section of your repository.