11<?php
2+
3+ /*
4+ * This file is part of the API Platform project.
5+ *
6+ * (c) Kévin Dunglas <dunglas@gmail.com>
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 );
213// ---
314// slug: computed-field
415// name: Compute a field
1223// by modifying the SQL query (via `stateOptions`/`handleLinks`), mapping the computed value
1324// to the entity object (via `processor`/`process`), and optionally enabling sorting on it
1425// using a custom filter configured via `parameters`.
26+
1527namespace App \Filter {
1628 use ApiPlatform \Doctrine \Orm \Filter \FilterInterface ;
1729 use ApiPlatform \Doctrine \Orm \Util \QueryNameGeneratorInterface ;
@@ -44,7 +56,7 @@ public function apply(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $q
4456 */
4557 // Defines the OpenAPI/Swagger schema for this filter parameter.
4658 // 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.
4860 public function getSchema (Parameter $ parameter ): array
4961 {
5062 return ['type ' => 'string ' , 'enum ' => ['asc ' , 'desc ' ]];
@@ -73,15 +85,15 @@ public function getDescription(string $resourceClass): array
7385 #[ORM \Entity]
7486 // Defines the GetCollection operation for Cart, including computed 'totalQuantity'.
7587 // 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)
7991 #[GetCollection(
8092 normalizationContext: ['hydra_prefix ' => false ],
8193 paginationItemsPerPage: 3 ,
8294 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 ' ),
8597 // processor: Uses process to map the result *after* fetching, *before* serialization.
8698 processor: [self ::class, 'process ' ],
8799 write: true ,
@@ -99,20 +111,6 @@ public function getDescription(string $resourceClass): array
99111 )]
100112 class Cart
101113 {
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-
116114 // Processor function called *after* fetching data, *before* serialization.
117115 // Maps the raw 'totalQuantity' from Doctrine result onto the Cart entity's property.
118116 // Handles Doctrine's array result structure: [0 => Entity, 'alias' => computedValue].
@@ -238,6 +236,30 @@ public function setQuantity(int $quantity): self
238236 }
239237}
240238
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+
241263namespace App \Playground {
242264 use Symfony \Component \HttpFoundation \Request ;
243265
0 commit comments