PostgreSQL range types represent ranges of values of some element type (called the range’s subtype). This library provides support for all PostgreSQL built-in range types.
Ranges support both inclusive [ and exclusive ( bounds:
useMartinGeorgiev\Doctrine\DBAL\Types\ValueObject\NumericRange;// [1.0, 10.0) - includes 1.0, excludes 10.0$range=newNumericRange(1.0,10.0,true,false);// (0, 100] - excludes 0, includes 100$range=newNumericRange(0,100,false,true);// [5, 15] - includes both bounds$range=newNumericRange(5,15,true,true);
Infinite Ranges
Ranges can be unbounded on either side:
// [10, ∞) - from 10 to infinity$range=newNumericRange(10,null,true,false);// (-∞, 100] - from negative infinity to 100$range=newNumericRange(null,100,false,true);// (-∞, ∞) - infinite range$range=NumericRange::infinite();
Empty Ranges
// Create an explicitly empty range$range=NumericRange::empty();// Check if a range is emptyif($range->isEmpty()){// Handle empty range}
Numeric Ranges (NUMRANGE)
For arbitrary precision numeric values:
useMartinGeorgiev\Doctrine\DBAL\Types\ValueObject\NumericRange;// Price range from €10.50 to €99.99$priceRange=newNumericRange(10.50,99.99);// Check if a price is in rangeif($priceRange->contains(25.00)){echo"Price is in range";}// Create from PostgreSQL string$range=NumericRange::fromString('[10.5,99.99)');
Integer Ranges
Int4Range (4-byte integers)
useMartinGeorgiev\Doctrine\DBAL\Types\ValueObject\Int4Range;// Age range$ageRange=newInt4Range(18,65);// Check if age is validif($ageRange->contains(25)){echo"Age is valid";}
Int8Range (8-byte integers)
useMartinGeorgiev\Doctrine\DBAL\Types\ValueObject\Int8Range;// Large number range$range=newInt8Range(PHP_INT_MIN,PHP_INT_MAX);
Date Ranges (DATERANGE)
For date-only ranges without time components:
useMartinGeorgiev\Doctrine\DBAL\Types\ValueObject\DateRange;// Event period$eventPeriod=newDateRange(new\DateTimeImmutable('2024-01-01'),new\DateTimeImmutable('2024-12-31'));// Convenience methods$singleDay=DateRange::singleDay(new\DateTimeImmutable('2024-06-15'));$year2024=DateRange::year(2024);$june2024=DateRange::month(2024,6);// Check if a date falls within the range$checkDate=new\DateTimeImmutable('2024-06-15');if($eventPeriod->contains($checkDate)){echo"Date is within event period";}
Timestamp Ranges
TsRange (without timezone)
useMartinGeorgiev\Doctrine\DBAL\Types\ValueObject\TsRange;// Working hours$workingHours=newTsRange(new\DateTimeImmutable('2024-01-01 09:00:00'),new\DateTimeImmutable('2024-01-01 17:00:00'));
TstzRange (with timezone)
useMartinGeorgiev\Doctrine\DBAL\Types\ValueObject\TstzRange;// Meeting time across UTC timezone$meetingTime=newTstzRange(new\DateTimeImmutable('2024-01-01 14:00:00+00:00'),new\DateTimeImmutable('2024-01-01 15:00:00+00:00'));
Range Operations
Contains Check
$range=newNumericRange(1,10);if($range->contains(5)){echo"5 is in the range [1, 10)";}
// Parse PostgreSQL range strings$numRange=NumericRange::fromString('[1.5,10.7)');$dateRange=DateRange::fromString('[2024-01-01,2024-12-31)');$emptyRange=NumericRange::fromString('empty');
Infinity Support
PostgreSQL distinguishes between unbounded ranges and ranges bounded by infinity:
Unbounded: [0,) - no upper bound
Bounded by infinity: [0,infinity) - explicitly bounded by the infinity value
All range types that support infinity (NUMRANGE, TSRANGE, TSTZRANGE, DATERANGE) provide a unified API:
useMartinGeorgiev\Doctrine\DBAL\Types\ValueObject\NumericRange;useMartinGeorgiev\Doctrine\DBAL\Types\ValueObject\DateRange;// Using infinity flags in constructor (7th parameter = upper infinity)$numRange=newNumericRange(0,null,true,false,false,false,true);$dateRange=newDateRange(new\DateTimeImmutable('2024-01-01'),null,true,false,false,false,true);echo$numRange;// [0,Infinity)echo$dateRange;// [2024-01-01,infinity)// Parsing from PostgreSQL format$range=NumericRange::fromString('[0,infinity)');$range->isUpperBoundedInfinity();// true$range->isLowerBoundedInfinity();// false
Spelling follows the element type, matching PostgreSQL:
Range type
Accepted bound spellings
Emitted spelling
NUMRANGE
infinity, -infinity, inf, -inf, in any case; a positive bound may also be written +infinity or +inf
Infinity / -Infinity
DATERANGE, TSRANGE, TSTZRANGE
infinity and -infinity, in any case; a positive bound may also be written +infinity. The inf abbreviation is rejected
infinity / -infinity
// The numeric family reads the abbreviation, which a date or timestamp range rejectsechoNumericRange::fromString('[1,inf)');// [1,Infinity)DateRange::fromString('[2024-01-01,inf)');// throws - PostgreSQL rejects it too
NumericRange convenience: Accepts PHP’s INF constant as shorthand:
useMartinGeorgiev\Doctrine\DBAL\Types\ValueObject\NumericRange;$range=newNumericRange(0,INF);echo$range;// [0,Infinity)// Equivalent to using flags explicitly$same=newNumericRange(0,null,true,false,false,false,true);
Note: Integer ranges (INT4RANGE, INT8RANGE) do not support infinity values in PostgreSQL.
NaN Bounds
NUMRANGE and NUMMULTIRANGE also take NaN as a bound. It is not an open end: PostgreSQL gives numeric a total order in which NaN sits above every other value, Infinity included, and equals itself. NaN therefore travels as the bound value, using PHP’s NAN constant rather than a flag:
useMartinGeorgiev\Doctrine\DBAL\Types\ValueObject\NumericRange;$range=newNumericRange(1,NAN);echo$range;// [1,NaN)\is_nan($range->getUpper());// trueNumericRange::fromString('[1,nan)');// any case; a sign is rejected, as PostgreSQL rejects '-nan'::numeric
The ordering decides emptiness and containment, exactly as PostgreSQL does:
NumericRange::fromString('[1,NaN]')->contains(NAN);// trueNumericRange::fromString('[1,NaN)')->contains(NAN);// false - the bound is exclusiveNumericRange::fromString('[1,NaN)')->contains(1e300);// true - every finite value sorts below NaNNumericRange::fromString('[1,Infinity)')->contains(NAN);// false - NaN sorts above InfinityNumericRange::fromString('[1,)')->contains(NAN);// true - an open end has nothing to sort againstechoNumericRange::fromString('[NaN,NaN)');// empty - equal bounds, exclusive brackets
Note: Only the numeric range types take a NaN bound. NaN belongs to PostgreSQL’s numeric and floating-point types, so a date, timestamp or integer range rejects it — DateRange::fromString('[2024-01-01,NaN)') throws, exactly as SELECT '[2024-01-01,NaN)'::daterange errors.
📖 See also: Infinity Values for why a bound of infinity is a flag here, a native INF for floats and an enum for datetime array items.
// Find products with overlapping price ranges$dql="
SELECT p
FROM Product p
WHERE OVERLAPS(p.priceRange, NUMRANGE('20', '50')) = TRUE
";// Find events in a date range$dql="
SELECT e
FROM Event e
WHERE CONTAINS(e.period, DATERANGE('2024-06-01', '2024-06-30')) = TRUE
";