diff --git a/CHANGELOG.md b/CHANGELOG.md index 940e9885..f39b091e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +### v4.25.1 (2026-08-11) +* * * +### Bug Fixes: +- Filter operators `in`, `not_in` and `between` are now sent as a single form field by `URLFormEncoder`, at any nesting depth. Filters on export operations, such as `ramp[effective_from][between]`, were index-encoded and therefore ignored by the API. + ### v4.25.0 (2026-07-30) * * * ### New Resources: diff --git a/VERSION b/VERSION index 4d9fbcf2..72e19f2f 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -4.25.0 +4.25.1 diff --git a/src/ValueObjects/Encoders/URLFormEncoder.php b/src/ValueObjects/Encoders/URLFormEncoder.php index 733c5510..ff4a2b4e 100644 --- a/src/ValueObjects/Encoders/URLFormEncoder.php +++ b/src/ValueObjects/Encoders/URLFormEncoder.php @@ -6,6 +6,12 @@ class URLFormEncoder implements ParamEncoderInterface { + /** + * Filter operators whose value is the whole array, sent as a single field + * (e.g. updated_at[between]=[1704067200,1717199999]). + */ + private const ARRAY_OPERATORS = ['in', 'not_in', 'between']; + /** * @param array $params. * @param array $jsonKeys. @@ -33,6 +39,12 @@ private static function serialize($value, $prefix = null, $idx = null, $jsonKeys (!is_null($prefix) ? '[' . $usK . ']' : $usK) . (!is_null($idx) ? '[' . $idx . ']' : ''); $serialized[$key] = is_string($v) ? $v : json_encode((is_array($v) && $v === []) ? (object)[] : $v); + } else if (is_array($v) && $v !== [] && !is_int($k) && in_array(Util::toUnderscoreFromCamelCase($k), self::ARRAY_OPERATORS, true)) { + // An empty filter is not a filter, so it falls through to the + // recursion below and leaves nothing in the request. + $usK = Util::toUnderscoreFromCamelCase($k); + $key = (!is_null($prefix) ? $prefix . '[' . $usK . ']' : $usK) . (!is_null($idx) ? '[' . $idx . ']' : ''); + $serialized[$key] = json_encode($v); } else if (is_array($v) && !is_int($k)) { $tempPrefix = (!is_null($prefix)) ? $prefix . '[' . Util::toUnderscoreFromCamelCase($k) . ']' : Util::toUnderscoreFromCamelCase($k); $serialized = array_merge($serialized, self::serialize($v, $tempPrefix, null, $jsonKeys, $level + 1)); diff --git a/src/Version.php b/src/Version.php index d50c6817..aa6a6013 100644 --- a/src/Version.php +++ b/src/Version.php @@ -4,7 +4,7 @@ final class Version { - const VERSION = '4.25.0'; + const VERSION = '4.25.1'; } ?> \ No newline at end of file diff --git a/tests/ValueObjects/Encoder/URLFormEncoderTest.php b/tests/ValueObjects/Encoder/URLFormEncoderTest.php index 5f4b3178..880876f0 100644 --- a/tests/ValueObjects/Encoder/URLFormEncoderTest.php +++ b/tests/ValueObjects/Encoder/URLFormEncoderTest.php @@ -217,4 +217,56 @@ public function testEncodeParamsShouldNotUseArrayBasedIndexingForJsonArray(): vo $this->assertIsString($encoded); $this->assertSame("id=foo&name=foo&discount_percentage=10&apply_on=each_specified_item&item_constraints%5Bconstraint%5D%5B0%5D=specific&item_constraints%5Bitem_type%5D%5B0%5D=plan&item_constraints%5Bitem_price_ids%5D%5B0%5D=%5B%22some_price_id%22%5D", $encoded); } + + /** Filter operators carry the whole array in one field, at any nesting depth. */ + /** ramp[effective_from][between]=[1704067200,1717199999]&ramp[status][in]=["scheduled","draft"] */ + public function testEncodeParamsWithFilterArrayOperators(): void + { + $params = [ + 'export_type' => 'import_friendly_data', + 'ramp' => [ + 'effective_from' => [ + 'between' => [1704067200, 1717199999], + ], + 'status' => [ + 'in' => ['scheduled', 'draft'], + ], + ], + ]; + $encoded = URLFormEncoder::encode($params); + $this->assertIsString($encoded); + $this->assertSame( + "export_type=import_friendly_data&ramp%5Beffective_from%5D%5Bbetween%5D=%5B1704067200%2C1717199999%5D&ramp%5Bstatus%5D%5Bin%5D=%5B%22scheduled%22%2C%22draft%22%5D", + $encoded + ); + } + + /** Top-level filters on non-list requests get the same treatment. */ + /** updated_at[between]=[1704067200,1717199999] */ + public function testEncodeParamsWithTopLevelFilterArrayOperator(): void + { + $params = [ + 'updated_at' => [ + 'between' => [1704067200, 1717199999], + ], + ]; + $encoded = URLFormEncoder::encode($params); + $this->assertIsString($encoded); + $this->assertSame("updated_at%5Bbetween%5D=%5B1704067200%2C1717199999%5D", $encoded); + } + + /** An empty filter operator is left out of the request entirely. */ + /** limit=5 */ + public function testEncodeParamsWithEmptyFilterArrayOperator(): void + { + $params = [ + 'limit' => 5, + 'updated_at' => [ + 'between' => [], + ], + ]; + $encoded = URLFormEncoder::encode($params); + $this->assertIsString($encoded); + $this->assertSame("limit=5", $encoded); + } }