1
1
<?php
2
+
3
+ /*
4
+ * This file is part of the API Platform project.
5
+ *
6
+ * (c) Kévin Dunglas <[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
+ declare (strict_types=1 );
2
13
// ---
3
14
// slug: computed-field
4
15
// name: Compute a field
12
23
// by modifying the SQL query (via `stateOptions`/`handleLinks`), mapping the computed value
13
24
// to the entity object (via `processor`/`process`), and optionally enabling sorting on it
14
25
// using a custom filter configured via `parameters`.
26
+
15
27
namespace App \Filter {
16
28
use ApiPlatform \Doctrine \Orm \Filter \FilterInterface ;
17
29
use ApiPlatform \Doctrine \Orm \Util \QueryNameGeneratorInterface ;
@@ -44,7 +56,7 @@ public function apply(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $q
44
56
*/
45
57
// Defines the OpenAPI/Swagger schema for this filter parameter.
46
58
// Tells API Platform documentation generators that 'sort[totalQuantity]' expects 'asc' or 'desc'.
47
- // This also add constraint violations to the parameter that will reject any wrong values.
59
+ // This also add constraint violations to the parameter that will reject any wrong values.
48
60
public function getSchema (Parameter $ parameter ): array
49
61
{
50
62
return ['type ' => 'string ' , 'enum ' => ['asc ' , 'desc ' ]];
@@ -73,15 +85,15 @@ public function getDescription(string $resourceClass): array
73
85
#[ORM \Entity]
74
86
// Defines the GetCollection operation for Cart, including computed 'totalQuantity'.
75
87
// Recipe involves:
76
- // 1. handleLinks (modify query)
77
- // 2. process (map result)
78
- // 3. parameters (filters)
88
+ // 1. setup the repository method (modify query)
89
+ // 2. process (map result)
90
+ // 3. parameters (filters)
79
91
#[GetCollection(
80
92
normalizationContext: ['hydra_prefix ' => false ],
81
93
paginationItemsPerPage: 3 ,
82
94
paginationPartial: false ,
83
- // stateOptions: Uses handleLinks to modify the query *before* fetching.
84
- stateOptions: new Options (handleLinks: [ self ::class, ' handleLinks ' ] ),
95
+ // stateOptions: Uses repositoryMethod to modify the query *before* fetching. See App\Repository\CartRepository .
96
+ stateOptions: new Options (repositoryMethod: ' getCartsWithTotalQuantity ' ),
85
97
// processor: Uses process to map the result *after* fetching, *before* serialization.
86
98
processor: [self ::class, 'process ' ],
87
99
write: true ,
@@ -99,20 +111,6 @@ public function getDescription(string $resourceClass): array
99
111
)]
100
112
class Cart
101
113
{
102
- // Handles links/joins and modifications to the QueryBuilder *before* data is fetched (via stateOptions).
103
- // Adds SQL logic (JOIN, SELECT aggregate, GROUP BY) to calculate 'totalQuantity' at the database level.
104
- // The alias 'totalQuantity' created here is crucial for the filter and processor.
105
- public static function handleLinks (QueryBuilder $ queryBuilder , array $ uriVariables , QueryNameGeneratorInterface $ queryNameGenerator , array $ context ): void
106
- {
107
- // Get the alias for the root entity (Cart), usually 'o'.
108
- $ rootAlias = $ queryBuilder ->getRootAliases ()[0 ] ?? 'o ' ;
109
- // Generate a unique alias for the joined 'items' relation to avoid conflicts.
110
- $ itemsAlias = $ queryNameGenerator ->generateParameterName ('items ' );
111
- $ queryBuilder ->leftJoin (\sprintf ('%s.items ' , $ rootAlias ), $ itemsAlias )
112
- ->addSelect (\sprintf ('COALESCE(SUM(%s.quantity), 0) AS totalQuantity ' , $ itemsAlias ))
113
- ->addGroupBy (\sprintf ('%s.id ' , $ rootAlias ));
114
- }
115
-
116
114
// Processor function called *after* fetching data, *before* serialization.
117
115
// Maps the raw 'totalQuantity' from Doctrine result onto the Cart entity's property.
118
116
// Handles Doctrine's array result structure: [0 => Entity, 'alias' => computedValue].
@@ -238,6 +236,30 @@ public function setQuantity(int $quantity): self
238
236
}
239
237
}
240
238
239
+ namespace App \Repository {
240
+ use Doctrine \ORM \EntityRepository ;
241
+ use Doctrine \ORM \QueryBuilder ;
242
+
243
+ /**
244
+ * @extends EntityRepository<Cart::class>
245
+ */
246
+ class CartRepository extends EntityRepository
247
+ {
248
+ // This repository method is used via stateOptions to alter the QueryBuilder *before* data is fetched.
249
+ // Adds SQL logic (JOIN, SELECT aggregate, GROUP BY) to calculate 'totalQuantity' at the database level.
250
+ // The alias 'totalQuantity' created here is crucial for the filter and processor.
251
+ public function getCartsWithTotalQuantity (): QueryBuilder
252
+ {
253
+ $ queryBuilder = $ this ->createQueryBuilder ('o ' );
254
+ $ queryBuilder ->leftJoin ('o.items ' , 'items ' )
255
+ ->addSelect ('COALESCE(SUM(items.quantity), 0) AS totalQuantity ' )
256
+ ->addGroupBy ('o.id ' );
257
+
258
+ return $ queryBuilder ;
259
+ }
260
+ }
261
+ }
262
+
241
263
namespace App \Playground {
242
264
use Symfony \Component \HttpFoundation \Request ;
243
265
0 commit comments