2222use ApiPlatform \Metadata \Operation ;
2323use ApiPlatform \Metadata \ParameterProviderFilterInterface ;
2424use ApiPlatform \State \ParameterProvider \IriConverterParameterProvider ;
25+ use Doctrine \ORM \Mapping \ClassMetadata ;
26+ use Doctrine \ORM \Mapping \FieldMapping ;
2527use Doctrine \ORM \QueryBuilder ;
2628
2729/**
@@ -57,6 +59,13 @@ public function apply(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $q
5759 $ ormLeafMetadata = $ this ->resolveLeafMetadataAtRuntime ($ queryBuilder , $ resourceClass , $ parameter ->getProperty (), $ property );
5860 }
5961
62+ // Backed enum exposed as a resource: compare the scalar column to the resolved enum.
63+ if (null === $ ormLeafMetadata ) {
64+ $ this ->applyEnumComparison ($ queryBuilder , $ alias , $ property , $ parameterName , $ value , $ context );
65+
66+ return ;
67+ }
68+
6069 // Collection associations (OneToMany/ManyToMany) require a JOIN to compare individual elements.
6170 if ($ ormLeafMetadata ['is_collection_valued ' ]) {
6271 $ queryBuilder ->join (\sprintf ('%s.%s ' , $ alias , $ property ), $ parameterName );
@@ -102,6 +111,25 @@ public static function getParameterProvider(): string
102111 return IriConverterParameterProvider::class;
103112 }
104113
114+ private function applyEnumComparison (QueryBuilder $ queryBuilder , string $ alias , string $ property , string $ parameterName , mixed $ value , array $ context ): void
115+ {
116+ $ propertyExpr = \sprintf ('%s.%s ' , $ alias , $ property );
117+ $ normalize = static fn (mixed $ v ): mixed => $ v instanceof \BackedEnum ? $ v ->value : $ v ;
118+
119+ if (is_iterable ($ value )) {
120+ $ values = \is_array ($ value ) ? $ value : iterator_to_array ($ value );
121+ $ queryBuilder
122+ ->{$ context ['whereClause ' ] ?? 'andWhere ' }(\sprintf ('%s IN (:%s) ' , $ propertyExpr , $ parameterName ))
123+ ->setParameter ($ parameterName , array_map ($ normalize , $ values ));
124+
125+ return ;
126+ }
127+
128+ $ queryBuilder
129+ ->{$ context ['whereClause ' ] ?? 'andWhere ' }(\sprintf ('%s = :%s ' , $ propertyExpr , $ parameterName ))
130+ ->setParameter ($ parameterName , $ normalize ($ value ));
131+ }
132+
105133 /**
106134 * @return array{is_collection_valued: bool, association_target_class: string, identifier_type: ?string}|null
107135 */
@@ -122,9 +150,12 @@ private function getOrmLeafMetadata(mixed $parameter): ?array
122150 * Resolves leaf metadata at runtime by walking the association chain.
123151 * Used as fallback when precomputed orm_leaf_metadata is not available.
124152 *
125- * @return array{is_collection_valued: bool, association_target_class: string, identifier_type: ?string}
153+ * Returns null when the leaf is a backed enum column so the caller compares it directly;
154+ * a non-association, non-enum leaf is left to raise a mapping error.
155+ *
156+ * @return array{is_collection_valued: bool, association_target_class: string, identifier_type: ?string}|null
126157 */
127- private function resolveLeafMetadataAtRuntime (QueryBuilder $ queryBuilder , string $ resourceClass , string $ originalProperty , string $ leafProperty ): array
158+ private function resolveLeafMetadataAtRuntime (QueryBuilder $ queryBuilder , string $ resourceClass , string $ originalProperty , string $ leafProperty ): ? array
128159 {
129160 $ em = $ queryBuilder ->getEntityManager ();
130161 $ metadata = $ em ->getClassMetadata ($ resourceClass );
@@ -135,6 +166,10 @@ private function resolveLeafMetadataAtRuntime(QueryBuilder $queryBuilder, string
135166 $ metadata = $ em ->getClassMetadata ($ associationMapping ['targetEntity ' ]);
136167 }
137168
169+ if (!$ metadata ->hasAssociation ($ leafProperty ) && $ this ->isEnumField ($ metadata , $ leafProperty )) {
170+ return null ;
171+ }
172+
138173 $ isCollectionValued = $ metadata ->isCollectionValuedAssociation ($ leafProperty );
139174 $ associationMapping = $ metadata ->getAssociationMapping ($ leafProperty );
140175 $ targetClass = $ associationMapping ['targetEntity ' ];
@@ -152,4 +187,19 @@ private function resolveLeafMetadataAtRuntime(QueryBuilder $queryBuilder, string
152187 'identifier_type ' => $ identifierType ,
153188 ];
154189 }
190+
191+ private function isEnumField (ClassMetadata $ metadata , string $ field ): bool
192+ {
193+ if (!isset ($ metadata ->fieldMappings [$ field ])) {
194+ return false ;
195+ }
196+
197+ $ fieldMapping = $ metadata ->fieldMappings [$ field ];
198+ // Doctrine ORM 2.x returns an array and Doctrine ORM 3.x returns a FieldMapping object.
199+ if ($ fieldMapping instanceof FieldMapping) {
200+ $ fieldMapping = (array ) $ fieldMapping ;
201+ }
202+
203+ return null !== ($ fieldMapping ['enumType ' ] ?? null );
204+ }
155205}
0 commit comments