12
12
namespace ApiPlatform \Core \Bridge \Doctrine \Orm \Extension ;
13
13
14
14
use ApiPlatform \Core \Bridge \Doctrine \Orm \Util \QueryNameGeneratorInterface ;
15
+ use ApiPlatform \Core \Exception \RuntimeException ;
15
16
use ApiPlatform \Core \Metadata \Property \Factory \PropertyMetadataFactoryInterface ;
16
17
use ApiPlatform \Core \Metadata \Property \Factory \PropertyNameCollectionFactoryInterface ;
18
+ use ApiPlatform \Core \Metadata \Resource \Factory \ResourceMetadataFactoryInterface ;
17
19
use Doctrine \ORM \Mapping \ClassMetadataInfo ;
18
20
use Doctrine \ORM \QueryBuilder ;
19
21
@@ -28,35 +30,94 @@ final class EagerLoadingExtension implements QueryCollectionExtensionInterface,
28
30
{
29
31
private $ propertyNameCollectionFactory ;
30
32
private $ propertyMetadataFactory ;
33
+ private $ resourceMetadataFactory ;
34
+ private $ enabled ;
35
+ private $ maxJoins ;
36
+ private $ eagerOnly ;
31
37
32
- public function __construct (PropertyNameCollectionFactoryInterface $ propertyNameCollectionFactory , PropertyMetadataFactoryInterface $ propertyMetadataFactory )
38
+ public function __construct (PropertyNameCollectionFactoryInterface $ propertyNameCollectionFactory , PropertyMetadataFactoryInterface $ propertyMetadataFactory, ResourceMetadataFactoryInterface $ resourceMetadataFactory , bool $ enabled = true , int $ maxJoins = 30 , bool $ eagerOnly = true )
33
39
{
34
40
$ this ->propertyMetadataFactory = $ propertyMetadataFactory ;
35
41
$ this ->propertyNameCollectionFactory = $ propertyNameCollectionFactory ;
42
+ $ this ->resourceMetadataFactory = $ resourceMetadataFactory ;
43
+ $ this ->enabled = $ enabled ;
44
+ $ this ->maxJoins = $ maxJoins ;
45
+ $ this ->eagerOnly = $ eagerOnly ;
46
+ }
47
+
48
+ /**
49
+ * Gets serializer groups once if available, if not it returns the $options array.
50
+ *
51
+ * @param array $options represents the operation name so that groups are the one of the specific operation
52
+ * @param string $resourceClass
53
+ * @param string $context normalization_context or denormalization_context
54
+ *
55
+ * @return string[]
56
+ */
57
+ private function getSerializerGroups (string $ resourceClass , array $ options , string $ context ): array
58
+ {
59
+ $ resourceMetadata = $ this ->resourceMetadataFactory ->create ($ resourceClass );
60
+
61
+ if (isset ($ options ['collection_operation_name ' ])) {
62
+ $ context = $ resourceMetadata ->getCollectionOperationAttribute ($ options ['collection_operation_name ' ], $ context , null , true );
63
+ } elseif (isset ($ options ['item_operation_name ' ])) {
64
+ $ context = $ resourceMetadata ->getItemOperationAttribute ($ options ['item_operation_name ' ], $ context , null , true );
65
+ } else {
66
+ $ context = $ resourceMetadata ->getAttribute ($ context );
67
+ }
68
+
69
+ if (empty ($ context ['groups ' ])) {
70
+ return $ options ;
71
+ }
72
+
73
+ return ['serializer_groups ' => $ context ['groups ' ]];
36
74
}
37
75
38
76
/**
39
77
* {@inheritdoc}
40
78
*/
41
79
public function applyToCollection (QueryBuilder $ queryBuilder , QueryNameGeneratorInterface $ queryNameGenerator , string $ resourceClass , string $ operationName = null )
42
80
{
81
+ if (false === $ this ->enabled ) {
82
+ return ;
83
+ }
84
+
85
+ $ options = [];
86
+
43
87
if (null !== $ operationName ) {
44
- $ propertyMetadataOptions = ['collection_operation_name ' => $ operationName ];
88
+ $ options = ['collection_operation_name ' => $ operationName ];
45
89
}
46
90
47
- $ this ->joinRelations ($ queryBuilder , $ resourceClass , $ propertyMetadataOptions ?? []);
91
+ $ groups = $ this ->getSerializerGroups ($ resourceClass , $ options , 'normalization_context ' );
92
+
93
+ $ this ->joinRelations ($ queryBuilder , $ resourceClass , $ groups );
48
94
}
49
95
50
96
/**
51
97
* {@inheritdoc}
98
+ * The context may contain serialization groups which helps defining joined entities that are readable.
52
99
*/
53
- public function applyToItem (QueryBuilder $ queryBuilder , QueryNameGeneratorInterface $ queryNameGenerator , string $ resourceClass , array $ identifiers , string $ operationName = null )
100
+ public function applyToItem (QueryBuilder $ queryBuilder , QueryNameGeneratorInterface $ queryNameGenerator , string $ resourceClass , array $ identifiers , string $ operationName = null , array $ context = [] )
54
101
{
102
+ if (false === $ this ->enabled ) {
103
+ return ;
104
+ }
105
+
106
+ $ options = [];
107
+
55
108
if (null !== $ operationName ) {
56
- $ propertyMetadataOptions = ['item_operation_name ' => $ operationName ];
109
+ $ options = ['item_operation_name ' => $ operationName ];
110
+ }
111
+
112
+ if (isset ($ context ['groups ' ])) {
113
+ $ groups = ['serializer_groups ' => $ context ['groups ' ]];
114
+ } elseif (isset ($ context ['resource_class ' ])) {
115
+ $ groups = $ this ->getSerializerGroups ($ context ['resource_class ' ], $ options , isset ($ context ['api_denormalize ' ]) ? 'denormalization_context ' : 'normalization_context ' );
116
+ } else {
117
+ $ groups = $ this ->getSerializerGroups ($ resourceClass , $ options , 'normalization_context ' );
57
118
}
58
119
59
- $ this ->joinRelations ($ queryBuilder , $ resourceClass , $ propertyMetadataOptions ?? [] );
120
+ $ this ->joinRelations ($ queryBuilder , $ resourceClass , $ groups );
60
121
}
61
122
62
123
/**
@@ -68,9 +129,16 @@ public function applyToItem(QueryBuilder $queryBuilder, QueryNameGeneratorInterf
68
129
* @param string $originAlias the current entity alias (first o, then a1, a2 etc.)
69
130
* @param string $relationAlias the previous relation alias to keep it unique
70
131
* @param bool $wasLeftJoin if the relation containing the new one had a left join, we have to force the new one to left join too
132
+ * @param int $joinCount the number of joins
133
+ *
134
+ * @throws RuntimeException when the max number of joins has been reached
71
135
*/
72
- private function joinRelations (QueryBuilder $ queryBuilder , string $ resourceClass , array $ propertyMetadataOptions = [], string $ originAlias = 'o ' , string &$ relationAlias = 'a ' , bool $ wasLeftJoin = false )
136
+ private function joinRelations (QueryBuilder $ queryBuilder , string $ resourceClass , array $ propertyMetadataOptions = [], string $ originAlias = 'o ' , string &$ relationAlias = 'a ' , bool $ wasLeftJoin = false , int & $ joinCount = 0 )
73
137
{
138
+ if ($ joinCount > $ this ->maxJoins ) {
139
+ throw new RuntimeException ('The total number of joined relations has exceeded the specified maximum. Raise the limit if necessary. ' );
140
+ }
141
+
74
142
$ entityManager = $ queryBuilder ->getEntityManager ();
75
143
$ classMetadata = $ entityManager ->getClassMetadata ($ resourceClass );
76
144
$ j = 0 ;
@@ -79,7 +147,11 @@ private function joinRelations(QueryBuilder $queryBuilder, string $resourceClass
79
147
foreach ($ classMetadata ->associationMappings as $ association => $ mapping ) {
80
148
$ propertyMetadata = $ this ->propertyMetadataFactory ->create ($ resourceClass , $ association , $ propertyMetadataOptions );
81
149
82
- if (ClassMetadataInfo::FETCH_EAGER !== $ mapping ['fetch ' ] || false === $ propertyMetadata ->isReadableLink ()) {
150
+ if (true === $ this ->eagerOnly && ClassMetadataInfo::FETCH_EAGER !== $ mapping ['fetch ' ]) {
151
+ continue ;
152
+ }
153
+
154
+ if (false === $ propertyMetadata ->isReadableLink () || false === $ propertyMetadata ->isReadable ()) {
83
155
continue ;
84
156
}
85
157
@@ -97,6 +169,7 @@ private function joinRelations(QueryBuilder $queryBuilder, string $resourceClass
97
169
98
170
$ associationAlias = $ relationAlias .$ i ++;
99
171
$ queryBuilder ->{$ method }($ originAlias .'. ' .$ association , $ associationAlias );
172
+ ++$ joinCount ;
100
173
$ select = [];
101
174
$ targetClassMetadata = $ entityManager ->getClassMetadata ($ mapping ['targetEntity ' ]);
102
175
@@ -118,7 +191,7 @@ private function joinRelations(QueryBuilder $queryBuilder, string $resourceClass
118
191
119
192
$ relationAlias .= ++$ j ;
120
193
121
- $ this ->joinRelations ($ queryBuilder , $ mapping ['targetEntity ' ], $ propertyMetadataOptions , $ associationAlias , $ relationAlias , $ method === 'leftJoin ' );
194
+ $ this ->joinRelations ($ queryBuilder , $ mapping ['targetEntity ' ], $ propertyMetadataOptions , $ associationAlias , $ relationAlias , $ method === 'leftJoin ' , $ joinCount );
122
195
}
123
196
}
124
197
}
0 commit comments