Skip to content

Commit

Permalink
Support BackedEnum in query parameters
Browse files Browse the repository at this point in the history
Moved cast() and encode() to be internal.
  • Loading branch information
trowski committed Dec 18, 2022
1 parent d349473 commit 70599b5
Show file tree
Hide file tree
Showing 8 changed files with 203 additions and 160 deletions.
52 changes: 50 additions & 2 deletions src/Internal/functions.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@

namespace Amp\Postgres\Internal;

use function Amp\Postgres\cast;

/** @internal */
const STATEMENT_PARAM_REGEX = <<<'REGEX'
[
Expand Down Expand Up @@ -92,3 +90,53 @@ function replaceNamedParams(array $params, array $names): array

return $values;
}

/**
* @internal
*
* Casts a PHP value to a representation that is understood by Postgres, including encoding arrays.
*
* @throws \Error If $value is an object which is not a BackedEnum or Stringable, a resource, or an unknown type.
*/
function cast(mixed $value): string|int|float|null
{
return match (\gettype($value)) {
"NULL", "integer", "double", "string" => $value,
"boolean" => $value ? 't' : 'f',
"array" => '{' . \implode(',', \array_map(encodeArrayItem(...), $value)) . '}',
"object" => match (true) {
$value instanceof \BackedEnum => $value->value,
$value instanceof \Stringable => (string) $value,
default => throw new \ValueError(
"An object in parameter values must be a BackedEnum or implement Stringable; got instance of "
. \get_debug_type($value)
),
},
default => throw new \ValueError(\sprintf(
"Invalid value type '%s' in parameter values",
\get_debug_type($value),
)),
};
}

/**
* @internal
*
* Wraps string in double-quotes for inclusion in an array.
*/
function encodeArrayItem(mixed $value): mixed
{
return match (\gettype($value)) {
"NULL" => "NULL",
"string" => '"' . \str_replace(['\\', '"'], ['\\\\', '\\"'], $value) . '"',
"object" => match (true) {
$value instanceof \BackedEnum => encodeArrayItem($value->value),
$value instanceof \Stringable => encodeArrayItem((string) $value),
default => throw new \ValueError(
"An object in parameter arrays must be a BackedEnum or implement Stringable; "
. "got instance of " . \get_debug_type($value)
),
},
default => cast($value),
};
}
65 changes: 0 additions & 65 deletions src/functions.php
Original file line number Diff line number Diff line change
Expand Up @@ -41,68 +41,3 @@ function connect(PostgresConfig $config, ?Cancellation $cancellation = null): Po
{
return postgresConnector()->connect($config, $cancellation);
}

/**
* Casts a PHP value to a representation that is understood by Postgres, including encoding arrays.
*
* @throws \Error If $value is an object without a __toString() method, a resource, or an unknown type.
*/
function cast(mixed $value): string|int|float|null
{
switch ($type = \gettype($value)) {
case "NULL":
case "integer":
case "double":
case "string":
return $value; // No casting necessary for numerics, strings, and null.

case "boolean":
return $value ? 't' : 'f';

case "array":
return encode($value);

case "object":
if (!\method_exists($value, "__toString")) {
throw new \Error("Object without a __toString() method included in parameter values");
}

return (string) $value;

default:
throw new \Error("Invalid value type '$type' in parameter values");
}
}

/**
* Encodes an array into a PostgreSQL representation of the array.
*
* @return string The serialized representation of the array.
*
* @throws \Error If $array contains an object without a __toString() method, a resource, or an unknown type.
*/
function encode(array $array): string
{
$array = \array_map(function ($value) {
switch (\gettype($value)) {
case "NULL":
return "NULL";

case "object":
if (!\method_exists($value, "__toString")) {
throw new \Error("Object without a __toString() method in array");
}

$value = (string) $value;
// no break

case "string":
return '"' . \str_replace(['\\', '"'], ['\\\\', '\\"'], $value) . '"';

default:
return cast($value); // Recursively encodes arrays and errors on invalid values.
}
}, $array);

return '{' . \implode(',', $array) . '}';
}
149 changes: 149 additions & 0 deletions test/CastTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
<?php declare(strict_types=1);

namespace Amp\Postgres\Test;

use PHPUnit\Framework\TestCase;
use function Amp\Postgres\Internal\cast;

enum IntegerEnum: int
{
case One = 1;
case Two = 2;
case Three = 3;
}

enum StringEnum: string
{
case One = 'one';
case Two = 'two';
case Three = 'three';
}

enum UnitEnum
{
case Case;
}

class CastTest extends TestCase
{
public function testSingleDimensionalStringArray(): void
{
$array = ["one", "two", "three"];
$string = '{"one","two","three"}';

$this->assertSame($string, cast($array));
}

public function testMultiDimensionalStringArray(): void
{
$array = ["one", "two", ["three", "four"], "five"];
$string = '{"one","two",{"three","four"},"five"}';

$this->assertSame($string, cast($array));
}

public function testQuotedStrings(): void
{
$array = ["one", "two", ["three", "four"], "five"];
$string = '{"one","two",{"three","four"},"five"}';

$this->assertSame($string, cast($array));
}

public function testEscapedQuoteDelimiter(): void
{
$array = ['va"lue1', 'value"2'];
$string = '{"va\\"lue1","value\\"2"}';

$this->assertSame($string, cast($array));
}

public function testNullValue(): void
{
$array = ["one", null, "three"];
$string = '{"one",NULL,"three"}';

$this->assertSame($string, cast($array));
}

public function testSingleDimensionalIntegerArray(): void
{
$array = [1, 2, 3];
$string = '{' . \implode(',', $array) . '}';

$this->assertSame($string, cast($array));
}

public function testIntegerArrayWithNull(): void
{
$array = [1, 2, null, 3];
$string = '{1,2,NULL,3}';

$this->assertSame($string, cast($array));
}

public function testMultidimensionalIntegerArray(): void
{
$array = [1, 2, [3, 4], [5], 6, 7, [[8, 9], 10]];
$string = '{1,2,{3,4},{5},6,7,{{8,9},10}}';

$this->assertSame($string, cast($array));
}

public function testEscapedBackslashesInQuotedValue(): void
{
$array = ["test\\ing", "esca\\ped\\"];
$string = '{"test\\\\ing","esca\\\\ped\\\\"}';

$this->assertSame($string, cast($array));
}

public function testBackedEnum(): void
{
$this->assertSame(3, cast(IntegerEnum::Three));
$this->assertSame('three', cast(StringEnum::Three));
}

public function testBackedEnumInArray(): void
{
$array = [
[IntegerEnum::One, IntegerEnum::Two, IntegerEnum::Three],
[StringEnum::One, StringEnum::Two, StringEnum::Three],
];
$string = '{{1,2,3},{"one","two","three"}}';

$this->assertSame($string, cast($array));
}

public function testUnitEnum(): void
{
$this->expectException(\ValueError::class);
$this->expectExceptionMessage('An object in parameter values must be');

cast(UnitEnum::Case);
}

public function testUnitEnumInArray(): void
{
$this->expectException(\ValueError::class);
$this->expectExceptionMessage('An object in parameter arrays must be');

cast([UnitEnum::Case]);
}

public function testObjectWithoutToStringMethod(): void
{
$this->expectException(\ValueError::class);
$this->expectExceptionMessage('An object in parameter values must be');

cast(new \stdClass);
}

public function testObjectWithoutToStringMethodInArray(): void
{
$this->expectException(\ValueError::class);
$this->expectExceptionMessage('An object in parameter arrays must be');

cast([new \stdClass]);
}
}
89 changes: 0 additions & 89 deletions test/EncodeTest.php

This file was deleted.

2 changes: 1 addition & 1 deletion test/PgSqlConnectionTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
use Amp\Postgres\PgSqlConnection;
use Amp\Postgres\PostgresLink;
use Revolt\EventLoop;
use function Amp\Postgres\cast;
use function Amp\Postgres\Internal\cast;

/**
* @requires extension pgsql
Expand Down
2 changes: 1 addition & 1 deletion test/PgSqlPoolTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
use Amp\Sql\Common\ConnectionPool;
use Amp\Sql\SqlConnector;
use Revolt\EventLoop;
use function Amp\Postgres\cast;
use function Amp\Postgres\Internal\cast;

/**
* @requires extension pgsql
Expand Down
2 changes: 1 addition & 1 deletion test/PqConnectionTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
use Amp\Postgres\Internal\PqUnbufferedResultSet;
use Amp\Postgres\PostgresLink;
use Amp\Postgres\PqConnection;
use function Amp\Postgres\cast;
use function Amp\Postgres\Internal\cast;

/**
* @requires extension pq
Expand Down
Loading

0 comments on commit 70599b5

Please sign in to comment.