Skip to content

[dsn] Add typed methods for query parameters. #527

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Sep 7, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
124 changes: 107 additions & 17 deletions pkg/dsn/Dsn.php
Original file line number Diff line number Diff line change
Expand Up @@ -89,9 +89,6 @@ public function getSchemeProtocol(): string
return $this->schemeProtocol;
}

/**
* @return string[]
*/
public function getSchemeExtensions(): array
{
return $this->schemeExtensions;
Expand Down Expand Up @@ -134,35 +131,72 @@ public function getPort(): ?int
return $this->port;
}

/**
* @return null|string
*/
public function getPath(): ?string
{
return $this->path;
}

/**
* @return null|string
*/
public function getQueryString(): ?string
{
return $this->queryString;
}

/**
* @return array
*/
public function getQuery(): array
{
return $this->query;
}

public function getQueryParameter(string $name, $default = null)
public function getQueryParameter(string $name, string $default = null): ?string
{
return array_key_exists($name, $this->query) ? $this->query[$name] : $default;
}

public function getInt(string $name, int $default = null): ?int
{
$value = $this->getQueryParameter($name);
if (null === $value) {
return $default;
}

if (false == preg_match('/^[\+\-]?[0-9]*$/', $value)) {
throw InvalidQueryParameterTypeException::create($name, 'integer');
}

return (int) $value;
}

public function getFloat(string $name, float $default = null): ?float
{
$value = $this->getQueryParameter($name);
if (null === $value) {
return $default;
}

if (false == is_numeric($value)) {
throw InvalidQueryParameterTypeException::create($name, 'float');
}

return (float) $value;
}

public function getBool(string $name, bool $default = null): ?bool
{
$value = $this->getQueryParameter($name);
if (null === $value) {
return $default;
}

if (in_array($value, ['', '0', 'false'], true)) {
return false;
}

if (in_array($value, ['1', 'true'], true)) {
return true;
}

throw InvalidQueryParameterTypeException::create($name, 'bool');
}

public function toArray()
{
return [
Expand Down Expand Up @@ -216,15 +250,71 @@ private function parse(string $dsn): void
}

if ($path = parse_url($dsn, PHP_URL_PATH)) {
$this->path = $path;
$this->path = rawurldecode($path);
}

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

$query = [];
parse_str($queryString, $query);
$this->query = $query;
$this->query = $this->httpParseQuery($queryString, '&', PHP_QUERY_RFC3986);
}
}

/**
* based on http://php.net/manual/en/function.parse-str.php#119484 with some slight modifications.
*/
private function httpParseQuery(string $queryString, string $argSeparator = '&', int $decType = PHP_QUERY_RFC1738): array
{
$result = [];
$parts = explode($argSeparator, $queryString);

foreach ($parts as $part) {
list($paramName, $paramValue) = explode('=', $part, 2);

switch ($decType) {
case PHP_QUERY_RFC3986:
$paramName = rawurldecode($paramName);
$paramValue = rawurldecode($paramValue);
break;
case PHP_QUERY_RFC1738:
default:
$paramName = urldecode($paramName);
$paramValue = urldecode($paramValue);
break;
}

if (preg_match_all('/\[([^\]]*)\]/m', $paramName, $matches)) {
$paramName = substr($paramName, 0, strpos($paramName, '['));
$keys = array_merge([$paramName], $matches[1]);
} else {
$keys = [$paramName];
}

$target = &$result;

foreach ($keys as $index) {
if ('' === $index) {
if (is_array($target)) {
$intKeys = array_filter(array_keys($target), 'is_int');
$index = count($intKeys) ? max($intKeys) + 1 : 0;
} else {
$target = [$target];
$index = 1;
}
} elseif (isset($target[$index]) && !is_array($target[$index])) {
$target[$index] = [$target[$index]];
}

$target = &$target[$index];
}

if (is_array($target)) {
$target[] = $paramValue;
} else {
$target = $paramValue;
}
}

return $result;
}
}
11 changes: 11 additions & 0 deletions pkg/dsn/InvalidQueryParameterTypeException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?php

namespace Enqueue\Dsn;

final class InvalidQueryParameterTypeException extends \LogicException
{
public static function create(string $name, string $expectedType): self
{
return new static(sprintf('The query parameter "%s" has invalid type. It must be "%s"', $name, $expectedType));
}
}
130 changes: 130 additions & 0 deletions pkg/dsn/Tests/DsnTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace Enqueue\Dsn\Tests;

use Enqueue\Dsn\Dsn;
use Enqueue\Dsn\InvalidQueryParameterTypeException;
use PHPUnit\Framework\TestCase;

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

public function testShouldUrlDecodedPath()
{
$dsn = new Dsn('amqp+ext://theUser:thePass@theHost:1267/%2f');

$this->assertSame('//', $dsn->getPath());
}

public function testShouldParseQuery()
{
$dsn = new Dsn('amqp+ext://theUser:thePass@theHost:1267/thePath?foo=fooVal&bar=bar%2fVal');
Expand All @@ -81,6 +89,95 @@ public function testShouldParseQuery()
$this->assertSame(['foo' => 'fooVal', 'bar' => 'bar/Val'], $dsn->getQuery());
}

public function testShouldParseQueryShouldPreservePlusSymbol()
{
$dsn = new Dsn('amqp+ext://theUser:thePass@theHost:1267/thePath?foo=fooVal&bar=bar+Val');

$this->assertSame('foo=fooVal&bar=bar+Val', $dsn->getQueryString());
$this->assertSame(['foo' => 'fooVal', 'bar' => 'bar+Val'], $dsn->getQuery());
}

/**
* @dataProvider provideIntQueryParameters
*/
public function testShouldParseQueryParameterAsInt(string $parameter, int $expected)
{
$dsn = new Dsn('foo:?aName='.$parameter);

$this->assertSame($expected, $dsn->getInt('aName'));
}

public function testShouldReturnDefaultIntIfNotSet()
{
$dsn = new Dsn('foo:');

$this->assertNull($dsn->getInt('aName'));
$this->assertSame(123, $dsn->getInt('aName', 123));
}

public function testThrowIfQueryParameterNotInt()
{
$dsn = new Dsn('foo:?aName=notInt');

$this->expectException(InvalidQueryParameterTypeException::class);
$this->expectExceptionMessage('The query parameter "aName" has invalid type. It must be "integer"');
$dsn->getInt('aName');
}

/**
* @dataProvider provideFloatQueryParameters
*/
public function testShouldParseQueryParameterAsFloat(string $parameter, float $expected)
{
$dsn = new Dsn('foo:?aName='.$parameter);

$this->assertSame($expected, $dsn->getFloat('aName'));
}

public function testShouldReturnDefaultFloatIfNotSet()
{
$dsn = new Dsn('foo:');

$this->assertNull($dsn->getFloat('aName'));
$this->assertSame(123., $dsn->getFloat('aName', 123.));
}

public function testThrowIfQueryParameterNotFloat()
{
$dsn = new Dsn('foo:?aName=notFloat');

$this->expectException(InvalidQueryParameterTypeException::class);
$this->expectExceptionMessage('The query parameter "aName" has invalid type. It must be "float"');
$dsn->getFloat('aName');
}

/**
* @dataProvider provideBooleanQueryParameters
*/
public function testShouldParseQueryParameterAsBoolean(string $parameter, bool $expected)
{
$dsn = new Dsn('foo:?aName='.$parameter);

$this->assertSame($expected, $dsn->getBool('aName'));
}

public function testShouldReturnDefaultBoolIfNotSet()
{
$dsn = new Dsn('foo:');

$this->assertNull($dsn->getBool('aName'));
$this->assertTrue($dsn->getBool('aName', true));
}

public function testThrowIfQueryParameterNotBool()
{
$dsn = new Dsn('foo:?aName=notBool');

$this->expectException(InvalidQueryParameterTypeException::class);
$this->expectExceptionMessage('The query parameter "aName" has invalid type. It must be "bool"');
$dsn->getBool('aName');
}

public static function provideSchemes()
{
yield [':', '', '', []];
Expand All @@ -99,4 +196,37 @@ public static function provideSchemes()

yield ['amqp+ext+rabbitmq:', 'amqp+ext+rabbitmq', 'amqp', ['ext', 'rabbitmq']];
}

public static function provideIntQueryParameters()
{
yield ['123', 123];

yield ['+123', 123];

yield ['-123', -123];
}

public static function provideFloatQueryParameters()
{
yield ['123', 123.];

yield ['+123', 123.];

yield ['-123', -123.];

yield ['0', 0.];
}

public static function provideBooleanQueryParameters()
{
yield ['', false];

yield ['1', true];

yield ['0', false];

yield ['true', true];

yield ['false', false];
}
}