Classes for complex PostgreSQL types
Generally pg_wrapper tries to use native PHP constructs and built-in classes to represent Postgres types:
associative arrays for composite types and hstore
, \DateTimeImmutable
for date and time types, etc.
However, some of the more complex types supported by Postgres do not have suitable PHP equivalents. For these
the package returns instances of the classes described below. Those are immutable and expose the relevant parts
as public readonly
properties or read-only array offsets.
All of the below classes and interfaces, except builtin ones prefixed by a backslash,
belong to \sad_spirit\pg_wrapper\types
namespace, which is omitted for brevity.
ArrayRepresentable
interface
The below classes usually implement both \JsonSerializable
interface and this one,
behaving as a pair for \JsonSerializable
.
The interface defines a single “named constructor” method:
interface ArrayRepresentable
{
public static function createFromArray(array $input): static;
}
The method will always accept an array that was created by \json_decode(\json_encode($object), true)
and the result will be an object equal to the original one. It may also accept additional array formats.
If a class representing Postgres type implements this interface, then type converter
for that type will accept an array as argument for its output()
method passing it to createFromArray()
constructor of a relevant class.
Range types
Range types represent a range of values of some element type (called the range’s subtype). Postgres provides several built-in range types and allows creating custom ones using CREATE TYPE command.
A non-empty range has two bounds, the lower and the upper. All points between these values are included
in the range. An inclusive bound means that the boundary point itself is included in the range,
while an exclusive bound means that it is not included. Using null
for a bound means that
the range is unbounded on that side.
An empty range is the one that does not contain points.
Considering the above, the base Range
class has the following properties
/**
* @template Bound
*/
readonly class Range implements ArrayRepresentable, RangeConstructor, \JsonSerializable
{
/** @var Bound|null */
public mixed $lower;
/** @var Bound|null */
public mixed $upper;
public bool $lowerInclusive;
public bool $upperInclusive;
public bool $empty;
}
RangeConstructor
interface fixes the signature of __construct()
method so that new static()
calls
work as expected in subclasses:
interface RangeConstructor
{
public function __construct(
mixed $lower = null,
mixed $upper = null,
bool $lowerInclusive = true,
bool $upperInclusive = false,
bool $empty = false
);
}
If $empty
constructor argument is true
then an empty range is created, all other values are essentially ignored.
Another way to create an empty range is the static createEmpty()
method.
jsonSerialize()
method returns ['empty' => true]
array for empty ranges and an array with ‘lower’, ‘upper’,
‘lowerInclusive’, and ‘upperInclusive’ keys for non-empty ones. createFromArray()
can process both of these
formats, additionally it will use first two elements of any other array for $lower
and $upper
bounds:
// Both will create an instance of NumericRange with $lower = 1 and $upper = 10
$rangeOne = NumericRange::createFromArray(['upper' => 10, 'lower' => 1]);
$rangeTwo = NumericRange::createFromArray([1, 10, 'this will be ignored']);
Range
has two subclasses that represent the built-in range types of Postgres:
DateTimeRange
Values of
tsrange
,tstzrange
,daterange
types are converted to this. Non-null$lower
and$upper
bounds are instances of\DateTimeImmutable
.NumericRange
Values of
int4range
,int8range
,numrange
types are converted to this. Non-null$lower
and$upper
bounds can beint
,float
ornumeric-string
.
Constructors of these subclasses enforce the types of $lower
and $upper
bound values, check that the $lower
bound is less than or equal to the $upper
(otherwise the range is invalid in Postgres) and create an empty range
if $lower == $upper
and at least one of the bounds is exclusive.
Note
By default, values of custom range types will be converted to instances of base Range
, which means that there
will be no checks of $lower
and $upper
values. It may make sense to create a custom subclass of Range
for such a custom type and configure RangeConverter
/ DefaultTypeConverterFactory
to return instances
of that subclass.
Multirange types
Multirange types, available since Postgres 14, represent lists of ranges. Each range type has a corresponding multirange one.
Classes representing multirange types on PHP side behave like read-only lists of Range
instances, they
extend the base MultiRange
class:
/**
* @template T of Range
*/
readonly class MultiRange
implements ArrayRepresentable, \ArrayAccess, \Countable, \IteratorAggregate, \JsonSerializable
{
/**
* @return class-string<T>
*/
public static function getItemClass(): string;
/**
* @param T ...$items
*/
final public function __construct(Range ...$items);
}
Here ArrayAccess
is implemented read-only, with offsetSet()
and offsetUnset()
throwing exceptions.
Child classes should override getItemClass()
to return the class name of Range
subclass they accept.
As with the Range
class above, MultiRange
has subclasses representing the built-in multirange types:
DateTimeMultiRange
Contains only instances of
DateTimeRange
, values oftsmultirange
,tstzmultirange
,datemultirange
types are converted to this.NumericMultiRange
Contains only instances of
NumericRange
, values ofint4multirange
,int8multirange
,nummultirange
types are converted to this.
Note
By default, values of custom multirange types will be converted to instances of base MultiRange
,
which essentially accepts any Range
instance as its element. It may make sense to create a custom subclass
of MultiRange
that restricts the accepted objects and configure MultiRangeConverter
/
DefaultTypeConverterFactory
to return instances of that subclass.
Geometric types
Postgres geometric types except line
are backed by point
type: it represents a point
in two-dimensional space with its x
and y
coordinates specified as floating-point numbers.
point
values are converted to instances of the Point
class:
final readonly class Point implements ArrayRepresentable, \JsonSerializable
{
public function __construct(
public float $x,
public float $y
) {
}
}
Its jsonSerialize()
method returns an array with ‘x’ and ‘y’ keys. Its createFromArray()
accepts any array
with exactly two elements and either
uses the values with ‘x’ and ‘y’ keys for coordinates or
uses the first array element for
$x
and the second one for$y
if there are no such keys.
// Both will create Point with $x = 1.2 and $y = 3.4
$pointOne = Point::createFromArray(['y' => 3.4, 'x' => 1.2]);
$pointTwo = Point::createFromArray([1.2, 3.4]);
Box
and LineSegment
box
type in Postgres is used for representing a rectangular box and lseg
is for a finite line segment.
Both of these are specified by two point
values: start and end for lseg
and opposite corners for box
.
Those types are converted to Box
and LineSegment
instances
abstract readonly class PointPair implements ArrayRepresentable, \JsonSerializable
{
final public function __construct(
public Point $start,
public Point $end
) {
}
}
final readonly class Box extends PointPair
{
}
final readonly class LineSegment extends PointPair
{
}
jsonSerialize()
method returns an array with ‘start’ and ‘end’ keys. createFromArray()
accepts any array
with exactly two elements and either
uses the values with ‘start’ and ‘end’ keys for relevant points or
uses the first element for
$start
and the second one for$end
if there are no such keys.
Values in the array may be either instances of Point
or arrays suitable for Point::createFromArray()
Path
and Polygon
path
type is a list of connected points. path
can be open, when the first and the last points
are considered not connected and closed, when they are connected. polygon
is represented by a list of points
that are vertices of a polygon, it is quite similar to a closed path
.
Those types are converted to Path
and Polygon
instances which behave like read-only lists of Point
:
abstract readonly class PointList implements \ArrayAccess, \Countable, \IteratorAggregate
{
public function __construct(Point ...$points);
}
final readonly class Path extends PointList implements ArrayRepresentable, \JsonSerializable
{
public function __construct(
public bool $open,
Point ...$points
);
}
final readonly class Polygon extends PointList implements ArrayRepresentable, \JsonSerializable
{
}
ArrayAccess
is implemented read-only, with offsetSet()
and offsetUnset()
throwing exceptions.
Polygon::jsonSerialize()
returns a list of points, its createFromArray()
accepts an array with its elements
being either instances of Point
or arrays suitable for Point::createFromArray()
.
Path::jsonSerialize()
returns a list with the first element being a bool
value representing
the $open
property and all other elements being points. Its createFromArray()
can accept an array
of the same structure, or just an array of points, with $open
defaulting to false
.
Circle
circle
type is represented by a center point
and floating-point radius.
Values of this type are converted to instances of Circle
:
final readonly class Circle implements ArrayRepresentable, \JsonSerializable
{
public function __construct(
public Point $center,
public float $radius
) {
}
}
Its jsonSerialize()
method returns an array with ‘center’ and ‘radius’ keys. Its createFromArray()
accepts
any array with exactly two elements and either
uses the values with ‘center’ and ‘radius’ keys for
$center
and$radius
oruses the first element for
$center
and the second one for$radius
if there are no such keys.
Line
Lines are represented by the linear equation Ax + By + C = 0
, where A
and B
are not both zero. Values
of this type are converted to instances of Line
:
final readonly class Line implements ArrayRepresentable, \JsonSerializable
{
public function __construct(
public float $A,
public float $B,
public float $C
) {
}
}
As usual, its jsonSerialize()
method returns an array with ‘A’, ‘B’, and ‘C’ keys. Its createFromArray()
accepts any array with exactly three elements and either
uses the values with ‘A’, ‘B’, and ‘C’ keys for
$A
,$B
, and$C
oruses the first element for
$A
, the second for$B
, and the third for$C
if there are no such keys.
Note
Postgres also accepts the literal similar to lseg
(two different points on the line) as input
for line
type. You can simply use the LineSegment
class described above and / or its converter to create
such a literal.
Tid
Instances of this are returned for values of Postgres tid
type that represents the physical location of
a tuple (row) within a table.
final readonly class Tid implements ArrayRepresentable, \JsonSerializable
{
public int|string $block;
public int $tuple;
public function __construct(int|string $block, int $tuple);
}
Both $block
(block number) and $tuple
(index of tuple within block) properties are non-negative integers,
$block
may be a string on 32-bit builds of PHP as it is an unsigned 32-bit integer like oid
.
Its jsonSerialize()
method returns an array with ‘block’ and ‘tuple’ keys. Its createFromArray()
accepts
any array with exactly two elements and uses either
the values with ‘block’ and ‘tuple’ keys for corresponding properties,
or the first array element for
$block
and the second one for$tuple
if there are no such keys.