Skip to content

Commit 8737bdd

Browse files
committed
Inprove StringCoercionMode::coerce method
1 parent b0f34a1 commit 8737bdd

File tree

3 files changed

+51
-49
lines changed

3 files changed

+51
-49
lines changed

interfaces/QueryString.php

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -192,7 +192,7 @@ private static function composeRecursive(
192192

193193
if (is_object($data)) {
194194
if ($seenObjects->contains($data)) {
195-
QueryComposeMode::Safe !== $composeMode || throw new ValueError('composition failed; object recursion detected.');
195+
QueryComposeMode::Safe !== $composeMode || throw new ValueError('composition failed; circular reference detected.');
196196

197197
return;
198198
}
@@ -201,8 +201,8 @@ private static function composeRecursive(
201201
$data = get_object_vars($data);
202202
}
203203

204-
if (self::isRecursive($data)) {
205-
QueryComposeMode::Safe !== $composeMode || throw new ValueError('composition failed; array recursion detected.');
204+
if (self::hasCircularReference($data)) {
205+
QueryComposeMode::Safe !== $composeMode || throw new ValueError('composition failed; circular reference detected.');
206206

207207
return;
208208
}
@@ -266,7 +266,7 @@ private static function composeRecursive(
266266
* Array recursion detection.
267267
* @see https://stackoverflow.com/questions/9042142/detecting-infinite-array-recursion-in-php
268268
*/
269-
private static function isRecursive(array &$arr): bool
269+
private static function hasCircularReference(array &$arr): bool
270270
{
271271
if (isset($arr[self::RECURSION_MARKER])) {
272272
return true;
@@ -275,7 +275,7 @@ private static function isRecursive(array &$arr): bool
275275
try {
276276
$arr[self::RECURSION_MARKER] = true;
277277
foreach ($arr as $key => &$value) {
278-
if (self::RECURSION_MARKER !== $key && is_array($value) && self::isRecursive($value)) {
278+
if (self::RECURSION_MARKER !== $key && is_array($value) && self::hasCircularReference($value)) {
279279
return true;
280280
}
281281
}

interfaces/StringCoercionMode.php

Lines changed: 38 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,12 @@
1414
namespace League\Uri;
1515

1616
use BackedEnum;
17-
use DateTimeInterface;
18-
use League\Uri\Contracts\FragmentDirective;
1917
use League\Uri\Contracts\UriComponentInterface;
20-
use League\Uri\Contracts\UriInterface;
2118
use Stringable;
2219
use TypeError;
2320
use Uri\Rfc3986\Uri as Rfc3986Uri;
2421
use Uri\WhatWg\Url as WhatWgUrl;
22+
use ValueError;
2523

2624
use function array_is_list;
2725
use function array_map;
@@ -70,7 +68,6 @@ enum StringCoercionMode
7068
* - Backed Enum: converted to their backing value and then stringify see int and string
7169
* - Array as list are flatten into a string list using the "," character as separator
7270
* - Associative array, Unit Enum, any object without stringification semantics is coerced to "[object Object]".
73-
* - DateTimeInterface object are stringify following EcmaScript `Date.prototype.toString()` semantics
7471
*/
7572
case Ecmascript;
7673

@@ -82,30 +79,33 @@ public function isCoercible(mixed $value): bool
8279
? !is_resource($value)
8380
: match (true) {
8481
$value instanceof Rfc3986Uri,
85-
$value instanceof WhatWgUrl,
86-
$value instanceof BackedEnum,
87-
$value instanceof Stringable,
82+
$value instanceof WhatWgUrl,
83+
$value instanceof BackedEnum,
84+
$value instanceof Stringable,
8885
is_scalar($value),
89-
null === $value => true,
86+
null === $value => true,
9087
default => false,
9188
};
9289
}
9390

91+
/**
92+
* @throws TypeError if the type is not supported by the specific case
93+
* @throws ValueError if circular reference is detected
94+
*/
9495
public function coerce(mixed $value): ?string
9596
{
96-
$value = match (true) {
97-
$value instanceof UriComponentInterface,
98-
$value instanceof FragmentDirective => $value->value(),
99-
$value instanceof UriInterface,
100-
$value instanceof WhatWgUrl => $value->toAsciiString(),
101-
$value instanceof Rfc3986Uri => $value->toString(),
102-
$value instanceof BackedEnum => $value->value,
103-
$value instanceof Stringable => (string) $value,
104-
default => $value,
105-
};
106-
107-
if (self::Ecmascript === $this) {
108-
return match (true) {
97+
return match ($this) {
98+
self::Ecmascript => match (true) {
99+
$value instanceof Rfc3986Uri => $value->toString(),
100+
$value instanceof WhatWgUrl => $value->toAsciiString(),
101+
$value instanceof BackedEnum => (string) $value->value,
102+
$value instanceof Stringable => $value->__toString(),
103+
is_object($value) => '[object Object]',
104+
is_array($value) => match (true) {
105+
self::hasCircularReference($value) => throw new ValueError('Recursive array structure detected; unable to coerce value.'),
106+
array_is_list($value) => implode(',', array_map($this->coerce(...), $value)),
107+
default => '[object Object]',
108+
},
109109
true === $value => 'true',
110110
false === $value => 'false',
111111
null === $value => 'null',
@@ -114,31 +114,29 @@ public function coerce(mixed $value): ?string
114114
is_infinite($value) => 0 < $value ? 'Infinity' : '-Infinity',
115115
default => (string) json_encode($value, JSON_PRESERVE_ZERO_FRACTION),
116116
},
117-
is_object($value) => '[object Object]',
118-
is_array($value) => match (true) {
119-
self::isRecursive($value) => throw new TypeError('Recursive array structure detected; unable to coerce value.'),
120-
array_is_list($value) => implode(',', array_map($this->coerce(...), $value)),
121-
default => '[object Object]',
122-
},
123-
!is_scalar($value) => throw new TypeError('Unable to coerce value of type "'.get_debug_type($value).'"'),
124-
default => (string) $value,
125-
};
126-
}
127-
128-
return match (true) {
129-
false === $value => '0',
130-
true === $value => '1',
131-
null === $value => null,
132-
!is_scalar($value) => throw new TypeError('Unable to coerce value of type "'.get_debug_type($value).'"'),
133-
default => (string) $value,
117+
is_scalar($value) => (string) $value,
118+
default => throw new TypeError('Unable to coerce value of type "'.get_debug_type($value).'" with "'.$this->name.'" coercion.'),
119+
},
120+
self::Native => match (true) {
121+
$value instanceof UriComponentInterface => $value->value(),
122+
$value instanceof WhatWgUrl => $value->toAsciiString(),
123+
$value instanceof Rfc3986Uri => $value->toString(),
124+
$value instanceof BackedEnum => (string) $value->value,
125+
$value instanceof Stringable => $value->__toString(),
126+
false === $value => '0',
127+
true === $value => '1',
128+
null === $value => null,
129+
is_scalar($value) => (string) $value,
130+
default => throw new TypeError('Unable to coerce value of type "'.get_debug_type($value).'" with "'.$this->name.'" coercion.'),
131+
},
134132
};
135133
}
136134

137135
/**
138136
* Array recursion detection.
139137
* @see https://stackoverflow.com/questions/9042142/detecting-infinite-array-recursion-in-php
140138
*/
141-
private static function isRecursive(array &$arr): bool
139+
private static function hasCircularReference(array &$arr): bool
142140
{
143141
if (isset($arr[self::RECURSION_MARKER])) {
144142
return true;
@@ -147,7 +145,7 @@ private static function isRecursive(array &$arr): bool
147145
try {
148146
$arr[self::RECURSION_MARKER] = true;
149147
foreach ($arr as $key => &$value) {
150-
if (self::RECURSION_MARKER !== $key && is_array($value) && self::isRecursive($value)) {
148+
if (self::RECURSION_MARKER !== $key && is_array($value) && self::hasCircularReference($value)) {
151149
return true;
152150
}
153151
}

interfaces/StringCoercionModeTest.php

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,13 @@
1414
namespace League\Uri;
1515

1616
use League\Uri\Components\FragmentDirectives\TextDirective;
17+
use League\Uri\Components\Query;
1718
use PHPUnit\Framework\Attributes\DataProvider;
1819
use PHPUnit\Framework\TestCase;
1920
use stdClass;
2021
use Stringable;
2122
use TypeError;
23+
use ValueError;
2224

2325
final class StringCoercionModeTest extends TestCase
2426
{
@@ -53,7 +55,8 @@ public function __toString(): string
5355
}, 'ok'],
5456

5557
[new \Uri\Rfc3986\Uri('https://example.com'), 'https://example.com'],
56-
[new TextDirective('start', 'end'), 'start,end'],
58+
[new TextDirective('start', 'end'), 'text=start,end'],
59+
[Query::new(), null],
5760
];
5861
}
5962

@@ -100,7 +103,8 @@ public function __toString(): string
100103
[TestUnitEnum::One, '[object Object]'],
101104

102105
[new \Uri\WhatWg\Url('https://0:0@0:0/0?0#0'), 'https://0:0@0.0.0.0:0/0?0#0'],
103-
[new TextDirective('start', 'end'), 'start,end'],
106+
[new TextDirective('start', 'end'), 'text=start,end'],
107+
[Query::new(), ''],
104108
];
105109
}
106110

@@ -123,7 +127,7 @@ public function test_it_rejects_direct_recursive_array(): void
123127
$a = [];
124128
$a[] = &$a;
125129

126-
$this->expectException(TypeError::class);
130+
$this->expectException(ValueError::class);
127131

128132
StringCoercionMode::Ecmascript->coerce($a);
129133
}
@@ -136,7 +140,7 @@ public function test_it_rejects_indirect_recursive_array(): void
136140
$b[] = &$a;
137141
$a[] = &$b;
138142

139-
$this->expectException(TypeError::class);
143+
$this->expectException(ValueError::class);
140144

141145
StringCoercionMode::Ecmascript->coerce($a);
142146
}

0 commit comments

Comments
 (0)