Skip to content

Commit 28eda98

Browse files
authored
Merge pull request #527 from php-enqueue/dsn-impr
[dsn] Add typed methods for query parameters.
2 parents 25d9b9c + aca8633 commit 28eda98

File tree

3 files changed

+248
-17
lines changed

3 files changed

+248
-17
lines changed

pkg/dsn/Dsn.php

+107-17
Original file line numberDiff line numberDiff line change
@@ -89,9 +89,6 @@ public function getSchemeProtocol(): string
8989
return $this->schemeProtocol;
9090
}
9191

92-
/**
93-
* @return string[]
94-
*/
9592
public function getSchemeExtensions(): array
9693
{
9794
return $this->schemeExtensions;
@@ -134,35 +131,72 @@ public function getPort(): ?int
134131
return $this->port;
135132
}
136133

137-
/**
138-
* @return null|string
139-
*/
140134
public function getPath(): ?string
141135
{
142136
return $this->path;
143137
}
144138

145-
/**
146-
* @return null|string
147-
*/
148139
public function getQueryString(): ?string
149140
{
150141
return $this->queryString;
151142
}
152143

153-
/**
154-
* @return array
155-
*/
156144
public function getQuery(): array
157145
{
158146
return $this->query;
159147
}
160148

161-
public function getQueryParameter(string $name, $default = null)
149+
public function getQueryParameter(string $name, string $default = null): ?string
162150
{
163151
return array_key_exists($name, $this->query) ? $this->query[$name] : $default;
164152
}
165153

154+
public function getInt(string $name, int $default = null): ?int
155+
{
156+
$value = $this->getQueryParameter($name);
157+
if (null === $value) {
158+
return $default;
159+
}
160+
161+
if (false == preg_match('/^[\+\-]?[0-9]*$/', $value)) {
162+
throw InvalidQueryParameterTypeException::create($name, 'integer');
163+
}
164+
165+
return (int) $value;
166+
}
167+
168+
public function getFloat(string $name, float $default = null): ?float
169+
{
170+
$value = $this->getQueryParameter($name);
171+
if (null === $value) {
172+
return $default;
173+
}
174+
175+
if (false == is_numeric($value)) {
176+
throw InvalidQueryParameterTypeException::create($name, 'float');
177+
}
178+
179+
return (float) $value;
180+
}
181+
182+
public function getBool(string $name, bool $default = null): ?bool
183+
{
184+
$value = $this->getQueryParameter($name);
185+
if (null === $value) {
186+
return $default;
187+
}
188+
189+
if (in_array($value, ['', '0', 'false'], true)) {
190+
return false;
191+
}
192+
193+
if (in_array($value, ['1', 'true'], true)) {
194+
return true;
195+
}
196+
197+
throw InvalidQueryParameterTypeException::create($name, 'bool');
198+
}
199+
166200
public function toArray()
167201
{
168202
return [
@@ -216,15 +250,71 @@ private function parse(string $dsn): void
216250
}
217251

218252
if ($path = parse_url($dsn, PHP_URL_PATH)) {
219-
$this->path = $path;
253+
$this->path = rawurldecode($path);
220254
}
221255

222256
if ($queryString = parse_url($dsn, PHP_URL_QUERY)) {
223257
$this->queryString = $queryString;
224258

225-
$query = [];
226-
parse_str($queryString, $query);
227-
$this->query = $query;
259+
$this->query = $this->httpParseQuery($queryString, '&', PHP_QUERY_RFC3986);
260+
}
261+
}
262+
263+
/**
264+
* based on http://php.net/manual/en/function.parse-str.php#119484 with some slight modifications.
265+
*/
266+
private function httpParseQuery(string $queryString, string $argSeparator = '&', int $decType = PHP_QUERY_RFC1738): array
267+
{
268+
$result = [];
269+
$parts = explode($argSeparator, $queryString);
270+
271+
foreach ($parts as $part) {
272+
list($paramName, $paramValue) = explode('=', $part, 2);
273+
274+
switch ($decType) {
275+
case PHP_QUERY_RFC3986:
276+
$paramName = rawurldecode($paramName);
277+
$paramValue = rawurldecode($paramValue);
278+
break;
279+
case PHP_QUERY_RFC1738:
280+
default:
281+
$paramName = urldecode($paramName);
282+
$paramValue = urldecode($paramValue);
283+
break;
284+
}
285+
286+
if (preg_match_all('/\[([^\]]*)\]/m', $paramName, $matches)) {
287+
$paramName = substr($paramName, 0, strpos($paramName, '['));
288+
$keys = array_merge([$paramName], $matches[1]);
289+
} else {
290+
$keys = [$paramName];
291+
}
292+
293+
$target = &$result;
294+
295+
foreach ($keys as $index) {
296+
if ('' === $index) {
297+
if (is_array($target)) {
298+
$intKeys = array_filter(array_keys($target), 'is_int');
299+
$index = count($intKeys) ? max($intKeys) + 1 : 0;
300+
} else {
301+
$target = [$target];
302+
$index = 1;
303+
}
304+
} elseif (isset($target[$index]) && !is_array($target[$index])) {
305+
$target[$index] = [$target[$index]];
306+
}
307+
308+
$target = &$target[$index];
309+
}
310+
311+
if (is_array($target)) {
312+
$target[] = $paramValue;
313+
} else {
314+
$target = $paramValue;
315+
}
228316
}
317+
318+
return $result;
229319
}
230320
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<?php
2+
3+
namespace Enqueue\Dsn;
4+
5+
final class InvalidQueryParameterTypeException extends \LogicException
6+
{
7+
public static function create(string $name, string $expectedType): self
8+
{
9+
return new static(sprintf('The query parameter "%s" has invalid type. It must be "%s"', $name, $expectedType));
10+
}
11+
}

pkg/dsn/Tests/DsnTest.php

+130
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
namespace Enqueue\Dsn\Tests;
44

55
use Enqueue\Dsn\Dsn;
6+
use Enqueue\Dsn\InvalidQueryParameterTypeException;
67
use PHPUnit\Framework\TestCase;
78

89
class DsnTest extends TestCase
@@ -73,6 +74,13 @@ public function testShouldParsePath()
7374
$this->assertSame('/thePath', $dsn->getPath());
7475
}
7576

77+
public function testShouldUrlDecodedPath()
78+
{
79+
$dsn = new Dsn('amqp+ext://theUser:thePass@theHost:1267/%2f');
80+
81+
$this->assertSame('//', $dsn->getPath());
82+
}
83+
7684
public function testShouldParseQuery()
7785
{
7886
$dsn = new Dsn('amqp+ext://theUser:thePass@theHost:1267/thePath?foo=fooVal&bar=bar%2fVal');
@@ -81,6 +89,95 @@ public function testShouldParseQuery()
8189
$this->assertSame(['foo' => 'fooVal', 'bar' => 'bar/Val'], $dsn->getQuery());
8290
}
8391

92+
public function testShouldParseQueryShouldPreservePlusSymbol()
93+
{
94+
$dsn = new Dsn('amqp+ext://theUser:thePass@theHost:1267/thePath?foo=fooVal&bar=bar+Val');
95+
96+
$this->assertSame('foo=fooVal&bar=bar+Val', $dsn->getQueryString());
97+
$this->assertSame(['foo' => 'fooVal', 'bar' => 'bar+Val'], $dsn->getQuery());
98+
}
99+
100+
/**
101+
* @dataProvider provideIntQueryParameters
102+
*/
103+
public function testShouldParseQueryParameterAsInt(string $parameter, int $expected)
104+
{
105+
$dsn = new Dsn('foo:?aName='.$parameter);
106+
107+
$this->assertSame($expected, $dsn->getInt('aName'));
108+
}
109+
110+
public function testShouldReturnDefaultIntIfNotSet()
111+
{
112+
$dsn = new Dsn('foo:');
113+
114+
$this->assertNull($dsn->getInt('aName'));
115+
$this->assertSame(123, $dsn->getInt('aName', 123));
116+
}
117+
118+
public function testThrowIfQueryParameterNotInt()
119+
{
120+
$dsn = new Dsn('foo:?aName=notInt');
121+
122+
$this->expectException(InvalidQueryParameterTypeException::class);
123+
$this->expectExceptionMessage('The query parameter "aName" has invalid type. It must be "integer"');
124+
$dsn->getInt('aName');
125+
}
126+
127+
/**
128+
* @dataProvider provideFloatQueryParameters
129+
*/
130+
public function testShouldParseQueryParameterAsFloat(string $parameter, float $expected)
131+
{
132+
$dsn = new Dsn('foo:?aName='.$parameter);
133+
134+
$this->assertSame($expected, $dsn->getFloat('aName'));
135+
}
136+
137+
public function testShouldReturnDefaultFloatIfNotSet()
138+
{
139+
$dsn = new Dsn('foo:');
140+
141+
$this->assertNull($dsn->getFloat('aName'));
142+
$this->assertSame(123., $dsn->getFloat('aName', 123.));
143+
}
144+
145+
public function testThrowIfQueryParameterNotFloat()
146+
{
147+
$dsn = new Dsn('foo:?aName=notFloat');
148+
149+
$this->expectException(InvalidQueryParameterTypeException::class);
150+
$this->expectExceptionMessage('The query parameter "aName" has invalid type. It must be "float"');
151+
$dsn->getFloat('aName');
152+
}
153+
154+
/**
155+
* @dataProvider provideBooleanQueryParameters
156+
*/
157+
public function testShouldParseQueryParameterAsBoolean(string $parameter, bool $expected)
158+
{
159+
$dsn = new Dsn('foo:?aName='.$parameter);
160+
161+
$this->assertSame($expected, $dsn->getBool('aName'));
162+
}
163+
164+
public function testShouldReturnDefaultBoolIfNotSet()
165+
{
166+
$dsn = new Dsn('foo:');
167+
168+
$this->assertNull($dsn->getBool('aName'));
169+
$this->assertTrue($dsn->getBool('aName', true));
170+
}
171+
172+
public function testThrowIfQueryParameterNotBool()
173+
{
174+
$dsn = new Dsn('foo:?aName=notBool');
175+
176+
$this->expectException(InvalidQueryParameterTypeException::class);
177+
$this->expectExceptionMessage('The query parameter "aName" has invalid type. It must be "bool"');
178+
$dsn->getBool('aName');
179+
}
180+
84181
public static function provideSchemes()
85182
{
86183
yield [':', '', '', []];
@@ -99,4 +196,37 @@ public static function provideSchemes()
99196

100197
yield ['amqp+ext+rabbitmq:', 'amqp+ext+rabbitmq', 'amqp', ['ext', 'rabbitmq']];
101198
}
199+
200+
public static function provideIntQueryParameters()
201+
{
202+
yield ['123', 123];
203+
204+
yield ['+123', 123];
205+
206+
yield ['-123', -123];
207+
}
208+
209+
public static function provideFloatQueryParameters()
210+
{
211+
yield ['123', 123.];
212+
213+
yield ['+123', 123.];
214+
215+
yield ['-123', -123.];
216+
217+
yield ['0', 0.];
218+
}
219+
220+
public static function provideBooleanQueryParameters()
221+
{
222+
yield ['', false];
223+
224+
yield ['1', true];
225+
226+
yield ['0', false];
227+
228+
yield ['true', true];
229+
230+
yield ['false', false];
231+
}
102232
}

0 commit comments

Comments
 (0)