Skip to content

Commit 70599b5

Browse files
committed
Support BackedEnum in query parameters
Moved cast() and encode() to be internal.
1 parent d349473 commit 70599b5

8 files changed

+203
-160
lines changed

src/Internal/functions.php

+50-2
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,6 @@
22

33
namespace Amp\Postgres\Internal;
44

5-
use function Amp\Postgres\cast;
6-
75
/** @internal */
86
const STATEMENT_PARAM_REGEX = <<<'REGEX'
97
[
@@ -92,3 +90,53 @@ function replaceNamedParams(array $params, array $names): array
9290

9391
return $values;
9492
}
93+
94+
/**
95+
* @internal
96+
*
97+
* Casts a PHP value to a representation that is understood by Postgres, including encoding arrays.
98+
*
99+
* @throws \Error If $value is an object which is not a BackedEnum or Stringable, a resource, or an unknown type.
100+
*/
101+
function cast(mixed $value): string|int|float|null
102+
{
103+
return match (\gettype($value)) {
104+
"NULL", "integer", "double", "string" => $value,
105+
"boolean" => $value ? 't' : 'f',
106+
"array" => '{' . \implode(',', \array_map(encodeArrayItem(...), $value)) . '}',
107+
"object" => match (true) {
108+
$value instanceof \BackedEnum => $value->value,
109+
$value instanceof \Stringable => (string) $value,
110+
default => throw new \ValueError(
111+
"An object in parameter values must be a BackedEnum or implement Stringable; got instance of "
112+
. \get_debug_type($value)
113+
),
114+
},
115+
default => throw new \ValueError(\sprintf(
116+
"Invalid value type '%s' in parameter values",
117+
\get_debug_type($value),
118+
)),
119+
};
120+
}
121+
122+
/**
123+
* @internal
124+
*
125+
* Wraps string in double-quotes for inclusion in an array.
126+
*/
127+
function encodeArrayItem(mixed $value): mixed
128+
{
129+
return match (\gettype($value)) {
130+
"NULL" => "NULL",
131+
"string" => '"' . \str_replace(['\\', '"'], ['\\\\', '\\"'], $value) . '"',
132+
"object" => match (true) {
133+
$value instanceof \BackedEnum => encodeArrayItem($value->value),
134+
$value instanceof \Stringable => encodeArrayItem((string) $value),
135+
default => throw new \ValueError(
136+
"An object in parameter arrays must be a BackedEnum or implement Stringable; "
137+
. "got instance of " . \get_debug_type($value)
138+
),
139+
},
140+
default => cast($value),
141+
};
142+
}

src/functions.php

-65
Original file line numberDiff line numberDiff line change
@@ -41,68 +41,3 @@ function connect(PostgresConfig $config, ?Cancellation $cancellation = null): Po
4141
{
4242
return postgresConnector()->connect($config, $cancellation);
4343
}
44-
45-
/**
46-
* Casts a PHP value to a representation that is understood by Postgres, including encoding arrays.
47-
*
48-
* @throws \Error If $value is an object without a __toString() method, a resource, or an unknown type.
49-
*/
50-
function cast(mixed $value): string|int|float|null
51-
{
52-
switch ($type = \gettype($value)) {
53-
case "NULL":
54-
case "integer":
55-
case "double":
56-
case "string":
57-
return $value; // No casting necessary for numerics, strings, and null.
58-
59-
case "boolean":
60-
return $value ? 't' : 'f';
61-
62-
case "array":
63-
return encode($value);
64-
65-
case "object":
66-
if (!\method_exists($value, "__toString")) {
67-
throw new \Error("Object without a __toString() method included in parameter values");
68-
}
69-
70-
return (string) $value;
71-
72-
default:
73-
throw new \Error("Invalid value type '$type' in parameter values");
74-
}
75-
}
76-
77-
/**
78-
* Encodes an array into a PostgreSQL representation of the array.
79-
*
80-
* @return string The serialized representation of the array.
81-
*
82-
* @throws \Error If $array contains an object without a __toString() method, a resource, or an unknown type.
83-
*/
84-
function encode(array $array): string
85-
{
86-
$array = \array_map(function ($value) {
87-
switch (\gettype($value)) {
88-
case "NULL":
89-
return "NULL";
90-
91-
case "object":
92-
if (!\method_exists($value, "__toString")) {
93-
throw new \Error("Object without a __toString() method in array");
94-
}
95-
96-
$value = (string) $value;
97-
// no break
98-
99-
case "string":
100-
return '"' . \str_replace(['\\', '"'], ['\\\\', '\\"'], $value) . '"';
101-
102-
default:
103-
return cast($value); // Recursively encodes arrays and errors on invalid values.
104-
}
105-
}, $array);
106-
107-
return '{' . \implode(',', $array) . '}';
108-
}

test/CastTest.php

+149
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
<?php declare(strict_types=1);
2+
3+
namespace Amp\Postgres\Test;
4+
5+
use PHPUnit\Framework\TestCase;
6+
use function Amp\Postgres\Internal\cast;
7+
8+
enum IntegerEnum: int
9+
{
10+
case One = 1;
11+
case Two = 2;
12+
case Three = 3;
13+
}
14+
15+
enum StringEnum: string
16+
{
17+
case One = 'one';
18+
case Two = 'two';
19+
case Three = 'three';
20+
}
21+
22+
enum UnitEnum
23+
{
24+
case Case;
25+
}
26+
27+
class CastTest extends TestCase
28+
{
29+
public function testSingleDimensionalStringArray(): void
30+
{
31+
$array = ["one", "two", "three"];
32+
$string = '{"one","two","three"}';
33+
34+
$this->assertSame($string, cast($array));
35+
}
36+
37+
public function testMultiDimensionalStringArray(): void
38+
{
39+
$array = ["one", "two", ["three", "four"], "five"];
40+
$string = '{"one","two",{"three","four"},"five"}';
41+
42+
$this->assertSame($string, cast($array));
43+
}
44+
45+
public function testQuotedStrings(): void
46+
{
47+
$array = ["one", "two", ["three", "four"], "five"];
48+
$string = '{"one","two",{"three","four"},"five"}';
49+
50+
$this->assertSame($string, cast($array));
51+
}
52+
53+
public function testEscapedQuoteDelimiter(): void
54+
{
55+
$array = ['va"lue1', 'value"2'];
56+
$string = '{"va\\"lue1","value\\"2"}';
57+
58+
$this->assertSame($string, cast($array));
59+
}
60+
61+
public function testNullValue(): void
62+
{
63+
$array = ["one", null, "three"];
64+
$string = '{"one",NULL,"three"}';
65+
66+
$this->assertSame($string, cast($array));
67+
}
68+
69+
public function testSingleDimensionalIntegerArray(): void
70+
{
71+
$array = [1, 2, 3];
72+
$string = '{' . \implode(',', $array) . '}';
73+
74+
$this->assertSame($string, cast($array));
75+
}
76+
77+
public function testIntegerArrayWithNull(): void
78+
{
79+
$array = [1, 2, null, 3];
80+
$string = '{1,2,NULL,3}';
81+
82+
$this->assertSame($string, cast($array));
83+
}
84+
85+
public function testMultidimensionalIntegerArray(): void
86+
{
87+
$array = [1, 2, [3, 4], [5], 6, 7, [[8, 9], 10]];
88+
$string = '{1,2,{3,4},{5},6,7,{{8,9},10}}';
89+
90+
$this->assertSame($string, cast($array));
91+
}
92+
93+
public function testEscapedBackslashesInQuotedValue(): void
94+
{
95+
$array = ["test\\ing", "esca\\ped\\"];
96+
$string = '{"test\\\\ing","esca\\\\ped\\\\"}';
97+
98+
$this->assertSame($string, cast($array));
99+
}
100+
101+
public function testBackedEnum(): void
102+
{
103+
$this->assertSame(3, cast(IntegerEnum::Three));
104+
$this->assertSame('three', cast(StringEnum::Three));
105+
}
106+
107+
public function testBackedEnumInArray(): void
108+
{
109+
$array = [
110+
[IntegerEnum::One, IntegerEnum::Two, IntegerEnum::Three],
111+
[StringEnum::One, StringEnum::Two, StringEnum::Three],
112+
];
113+
$string = '{{1,2,3},{"one","two","three"}}';
114+
115+
$this->assertSame($string, cast($array));
116+
}
117+
118+
public function testUnitEnum(): void
119+
{
120+
$this->expectException(\ValueError::class);
121+
$this->expectExceptionMessage('An object in parameter values must be');
122+
123+
cast(UnitEnum::Case);
124+
}
125+
126+
public function testUnitEnumInArray(): void
127+
{
128+
$this->expectException(\ValueError::class);
129+
$this->expectExceptionMessage('An object in parameter arrays must be');
130+
131+
cast([UnitEnum::Case]);
132+
}
133+
134+
public function testObjectWithoutToStringMethod(): void
135+
{
136+
$this->expectException(\ValueError::class);
137+
$this->expectExceptionMessage('An object in parameter values must be');
138+
139+
cast(new \stdClass);
140+
}
141+
142+
public function testObjectWithoutToStringMethodInArray(): void
143+
{
144+
$this->expectException(\ValueError::class);
145+
$this->expectExceptionMessage('An object in parameter arrays must be');
146+
147+
cast([new \stdClass]);
148+
}
149+
}

test/EncodeTest.php

-89
This file was deleted.

test/PgSqlConnectionTest.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
use Amp\Postgres\PgSqlConnection;
66
use Amp\Postgres\PostgresLink;
77
use Revolt\EventLoop;
8-
use function Amp\Postgres\cast;
8+
use function Amp\Postgres\Internal\cast;
99

1010
/**
1111
* @requires extension pgsql

test/PgSqlPoolTest.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
use Amp\Sql\Common\ConnectionPool;
1010
use Amp\Sql\SqlConnector;
1111
use Revolt\EventLoop;
12-
use function Amp\Postgres\cast;
12+
use function Amp\Postgres\Internal\cast;
1313

1414
/**
1515
* @requires extension pgsql

test/PqConnectionTest.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
use Amp\Postgres\Internal\PqUnbufferedResultSet;
77
use Amp\Postgres\PostgresLink;
88
use Amp\Postgres\PqConnection;
9-
use function Amp\Postgres\cast;
9+
use function Amp\Postgres\Internal\cast;
1010

1111
/**
1212
* @requires extension pq

0 commit comments

Comments
 (0)