99use Doctrine \ORM \EntityManagerInterface ;
1010use Doctrine \ORM \NonUniqueResultException ;
1111use Doctrine \ORM \NoResultException ;
12+ use Doctrine \ORM \QueryBuilder ;
13+ use JsonException ;
1214use RuntimeException ;
1315
1416final class ORMContext implements Context
@@ -22,6 +24,8 @@ public function __construct(EntityManagerInterface $manager)
2224
2325 /**
2426 * @And I see :count entities :entityClass
27+ *
28+ * @param class-string $entityClass
2529 */
2630 public function andISeeInRepository (int $ count , string $ entityClass ): void
2731 {
@@ -30,6 +34,8 @@ public function andISeeInRepository(int $count, string $entityClass): void
3034
3135 /**
3236 * @Then I see :count entities :entityClass
37+ *
38+ * @param class-string $entityClass
3339 */
3440 public function thenISeeInRepository (int $ count , string $ entityClass ): void
3541 {
@@ -38,6 +44,8 @@ public function thenISeeInRepository(int $count, string $entityClass): void
3844
3945 /**
4046 * @And I see entity :entity with id :id
47+ *
48+ * @param class-string $entityClass
4149 */
4250 public function andISeeEntityInRepositoryWithId (string $ entityClass , string $ id ): void
4351 {
@@ -46,6 +54,8 @@ public function andISeeEntityInRepositoryWithId(string $entityClass, string $id)
4654
4755 /**
4856 * @Then I see entity :entity with id :id
57+ *
58+ * @param class-string $entityClass
4959 */
5060 public function thenISeeEntityInRepositoryWithId (string $ entityClass , string $ id ): void
5161 {
@@ -54,6 +64,8 @@ public function thenISeeEntityInRepositoryWithId(string $entityClass, string $id
5464
5565 /**
5666 * @Then I see entity :entity with properties:
67+ *
68+ * @param class-string $entityClass
5769 */
5870 public function andISeeEntityInRepositoryWithProperties (string $ entityClass , PyStringNode $ string ): void
5971 {
@@ -62,6 +74,7 @@ public function andISeeEntityInRepositoryWithProperties(string $entityClass, PyS
6274 }
6375
6476 /**
77+ * @param class-string $entityClass
6578 * @param array<string, mixed> $params
6679 *
6780 * @throws NonUniqueResultException
@@ -74,12 +87,20 @@ private function seeInRepository(int $count, string $entityClass, ?array $params
7487 ->select ('count(e) ' );
7588
7689 if (null !== $ params ) {
90+ $ metadata = $ this ->manager ->getClassMetadata ($ entityClass );
91+
7792 foreach ($ params as $ columnName => $ columnValue ) {
7893 if ($ columnValue === null ) {
7994 $ query ->andWhere (sprintf ('e.%s IS NULL ' , $ columnName ));
8095 } else {
81- $ query ->andWhere (sprintf ('e.%s = :%s ' , $ columnName , $ columnName ))
82- ->setParameter ($ columnName , $ columnValue );
96+ if ($ this ->isJsonField ($ metadata , $ columnName )) {
97+ // Handle JSON fields with proper DQL
98+ $ this ->addJsonFieldCondition ($ query , $ columnName , $ columnValue );
99+ } else {
100+ // Regular field comparison
101+ $ query ->andWhere (sprintf ('e.%s = :%s ' , $ columnName , $ columnName ))
102+ ->setParameter ($ columnName , $ columnValue );
103+ }
83104 }
84105 }
85106 }
@@ -93,4 +114,74 @@ private function seeInRepository(int $count, string $entityClass, ?array $params
93114 );
94115 }
95116 }
117+
118+ /**
119+ * Check if a field is mapped as JSON type
120+ *
121+ * @param \Doctrine\ORM\Mapping\ClassMetadata<object> $metadata
122+ */
123+ private function isJsonField (\Doctrine \ORM \Mapping \ClassMetadata $ metadata , string $ fieldName ): bool
124+ {
125+ if (!$ metadata ->hasField ($ fieldName )) {
126+ return false ;
127+ }
128+
129+ $ fieldMapping = $ metadata ->getFieldMapping ($ fieldName );
130+
131+ return \in_array ($ fieldMapping ['type ' ], ['json ' , 'json_array ' ], true );
132+ }
133+
134+ /**
135+ * Add JSON field condition using DQL-compatible functions
136+ * Uses CONCAT for PostgreSQL to convert JSON to string for comparison
137+ *
138+ * @param mixed $expectedValue
139+ */
140+ private function addJsonFieldCondition (QueryBuilder $ query , string $ fieldName , $ expectedValue ): void
141+ {
142+ $ platform = $ this ->manager ->getConnection ()->getDatabasePlatform ();
143+ $ platformName = $ platform ->getName ();
144+
145+ // Normalize JSON value - ensure consistent encoding
146+ $ expectedJson = $ this ->normalizeJsonValue ($ expectedValue );
147+ $ paramName = $ fieldName . '_json ' ;
148+
149+ if ($ platformName === 'postgresql ' ) {
150+ // PostgreSQL: Use CONCAT to convert JSON to string for comparison
151+ // CONCAT('', field) effectively casts JSON to text in a DQL-compatible way
152+ $ query ->andWhere (sprintf ('CONCAT( \'\', e.%s) = :%s ' , $ fieldName , $ paramName ))
153+ ->setParameter ($ paramName , $ expectedJson );
154+ } elseif ($ platformName === 'mysql ' ) {
155+ // MySQL: Use JSON_UNQUOTE to extract JSON as string
156+ $ query ->andWhere (sprintf ('JSON_UNQUOTE(e.%s) = :%s ' , $ fieldName , $ paramName ))
157+ ->setParameter ($ paramName , $ expectedJson );
158+ } else {
159+ // Fallback for other databases (SQLite, etc.)
160+ $ query ->andWhere (sprintf ('e.%s = :%s ' , $ fieldName , $ paramName ))
161+ ->setParameter ($ paramName , $ expectedJson );
162+ }
163+ }
164+
165+ /**
166+ * Normalize JSON value to ensure consistent comparison
167+ * This handles arrays, objects, and already-encoded JSON strings
168+ *
169+ * @param mixed $value
170+ */
171+ private function normalizeJsonValue ($ value ): string
172+ {
173+ if (is_string ($ value )) {
174+ // If it's already a JSON string, decode and re-encode for normalization
175+ try {
176+ $ decoded = json_decode ($ value , true , 512 , JSON_THROW_ON_ERROR );
177+ return json_encode ($ decoded , JSON_THROW_ON_ERROR | JSON_UNESCAPED_SLASHES );
178+ } catch (JsonException $ e ) {
179+ // If it's not valid JSON, treat as regular string
180+ return json_encode ($ value , JSON_THROW_ON_ERROR | JSON_UNESCAPED_SLASHES );
181+ }
182+ }
183+
184+ // For arrays/objects, encode with consistent flags
185+ return json_encode ($ value , JSON_THROW_ON_ERROR | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE );
186+ }
96187}
0 commit comments