11<?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 );
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