|
| 1 | +# PostgreSQL Range Types |
| 2 | + |
| 3 | +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. |
| 4 | + |
| 5 | +## Available Range Types |
| 6 | + |
| 7 | +| Range Type | PostgreSQL Type | Value Type | Description | |
| 8 | +|---|---|---|---| |
| 9 | +| DateRange | DATERANGE | DateTimeInterface | Date ranges (without time) | |
| 10 | +| Int4Range | INT4RANGE | int | 4-byte integer ranges | |
| 11 | +| Int8Range | INT8RANGE | int | 8-byte integer ranges | |
| 12 | +| NumRange | NUMRANGE | int/float | Numeric ranges with arbitrary precision | |
| 13 | +| TsRange | TSRANGE | DateTimeInterface | Timestamp ranges without timezone | |
| 14 | +| TstzRange | TSTZRANGE | DateTimeInterface | Timestamp ranges with timezone | |
| 15 | + |
| 16 | +## Basic Usage |
| 17 | + |
| 18 | +### Registration |
| 19 | + |
| 20 | +First, register the range types you need: |
| 21 | + |
| 22 | +```php |
| 23 | +use Doctrine\DBAL\Types\Type; |
| 24 | + |
| 25 | +Type::addType('daterange', "MartinGeorgiev\\Doctrine\\DBAL\\Types\\DateRange"); |
| 26 | +Type::addType('int4range', "MartinGeorgiev\\Doctrine\\DBAL\\Types\\Int4Range"); |
| 27 | +Type::addType('int8range', "MartinGeorgiev\\Doctrine\\DBAL\\Types\\Int8Range"); |
| 28 | +Type::addType('numrange', "MartinGeorgiev\\Doctrine\\DBAL\\Types\\NumRange"); |
| 29 | +Type::addType('tsrange', "MartinGeorgiev\\Doctrine\\DBAL\\Types\\TsRange"); |
| 30 | +Type::addType('tstzrange', "MartinGeorgiev\\Doctrine\\DBAL\\Types\\TstzRange"); |
| 31 | +``` |
| 32 | + |
| 33 | +### Entity Usage |
| 34 | + |
| 35 | +```php |
| 36 | +use Doctrine\ORM\Mapping as ORM; |
| 37 | +use MartinGeorgiev\Doctrine\DBAL\Types\ValueObject\DateRange; |
| 38 | +use MartinGeorgiev\Doctrine\DBAL\Types\ValueObject\NumericRange; |
| 39 | + |
| 40 | +#[ORM\Entity] |
| 41 | +class Product |
| 42 | +{ |
| 43 | + #[ORM\Column(type: 'numrange')] |
| 44 | + private ?NumericRange $priceRange = null; |
| 45 | + |
| 46 | + #[ORM\Column(type: 'daterange')] |
| 47 | + private ?DateRange $availabilityPeriod = null; |
| 48 | + |
| 49 | + public function setPriceRange(float $min, float $max): void |
| 50 | + { |
| 51 | + $this->priceRange = new NumericRange($min, $max); |
| 52 | + } |
| 53 | + |
| 54 | + public function setAvailabilityPeriod(\DateTimeInterface $start, \DateTimeInterface $end): void |
| 55 | + { |
| 56 | + $this->availabilityPeriod = new DateRange($start, $end); |
| 57 | + } |
| 58 | +} |
| 59 | +``` |
| 60 | + |
| 61 | +## Range Construction |
| 62 | + |
| 63 | +### Inclusive vs Exclusive Bounds |
| 64 | + |
| 65 | +Ranges support both inclusive `[` and exclusive `(` bounds: |
| 66 | + |
| 67 | +```php |
| 68 | +use MartinGeorgiev\Doctrine\DBAL\Types\ValueObject\NumericRange; |
| 69 | + |
| 70 | +// [1.0, 10.0) - includes 1.0, excludes 10.0 |
| 71 | +$range = new NumericRange(1.0, 10.0, true, false); |
| 72 | + |
| 73 | +// (0, 100] - excludes 0, includes 100 |
| 74 | +$range = new NumericRange(0, 100, false, true); |
| 75 | + |
| 76 | +// [5, 15] - includes both bounds |
| 77 | +$range = new NumericRange(5, 15, true, true); |
| 78 | +``` |
| 79 | + |
| 80 | +### Infinite Ranges |
| 81 | + |
| 82 | +Ranges can be unbounded on either side: |
| 83 | + |
| 84 | +```php |
| 85 | +// [10, ∞) - from 10 to infinity |
| 86 | +$range = new NumericRange(10, null, true, false); |
| 87 | + |
| 88 | +// (-∞, 100] - from negative infinity to 100 |
| 89 | +$range = new NumericRange(null, 100, false, true); |
| 90 | + |
| 91 | +// (-∞, ∞) - infinite range |
| 92 | +$range = NumericRange::infinite(); |
| 93 | +``` |
| 94 | + |
| 95 | +### Empty Ranges |
| 96 | + |
| 97 | +```php |
| 98 | +// Create an explicitly empty range |
| 99 | +$range = NumericRange::empty(); |
| 100 | + |
| 101 | +// Check if a range is empty |
| 102 | +if ($range->isEmpty()) { |
| 103 | + // Handle empty range |
| 104 | +} |
| 105 | +``` |
| 106 | + |
| 107 | +## Numeric Ranges (NUMRANGE) |
| 108 | + |
| 109 | +For arbitrary precision numeric values: |
| 110 | + |
| 111 | +```php |
| 112 | +use MartinGeorgiev\Doctrine\DBAL\Types\ValueObject\NumericRange; |
| 113 | + |
| 114 | +// Price range from €10.50 to €99.99 |
| 115 | +$priceRange = new NumericRange(10.50, 99.99); |
| 116 | + |
| 117 | +// Check if a price is in range |
| 118 | +if ($priceRange->contains(25.00)) { |
| 119 | + echo "Price is in range"; |
| 120 | +} |
| 121 | + |
| 122 | +// Create from PostgreSQL string |
| 123 | +$range = NumericRange::fromString('[10.5,99.99)'); |
| 124 | +``` |
| 125 | + |
| 126 | +## Integer Ranges |
| 127 | + |
| 128 | +### Int4Range (4-byte integers) |
| 129 | + |
| 130 | +```php |
| 131 | +use MartinGeorgiev\Doctrine\DBAL\Types\ValueObject\Int4Range; |
| 132 | + |
| 133 | +// Age range |
| 134 | +$ageRange = new Int4Range(18, 65); |
| 135 | + |
| 136 | +// Check if age is valid |
| 137 | +if ($ageRange->contains(25)) { |
| 138 | + echo "Age is valid"; |
| 139 | +} |
| 140 | +``` |
| 141 | + |
| 142 | +### Int8Range (8-byte integers) |
| 143 | + |
| 144 | +```php |
| 145 | +use MartinGeorgiev\Doctrine\DBAL\Types\ValueObject\Int8Range; |
| 146 | + |
| 147 | +// Large number range |
| 148 | +$range = new Int8Range(PHP_INT_MIN, PHP_INT_MAX); |
| 149 | +``` |
| 150 | + |
| 151 | +## Date Ranges (DATERANGE) |
| 152 | + |
| 153 | +For date-only ranges without time components: |
| 154 | + |
| 155 | +```php |
| 156 | +use MartinGeorgiev\Doctrine\DBAL\Types\ValueObject\DateRange; |
| 157 | + |
| 158 | +// Event period |
| 159 | +$eventPeriod = new DateRange( |
| 160 | + new \DateTimeImmutable('2024-01-01'), |
| 161 | + new \DateTimeImmutable('2024-12-31') |
| 162 | +); |
| 163 | + |
| 164 | +// Convenience methods |
| 165 | +$singleDay = DateRange::singleDay(new \DateTimeImmutable('2024-06-15')); |
| 166 | +$year2024 = DateRange::year(2024); |
| 167 | +$june2024 = DateRange::month(2024, 6); |
| 168 | + |
| 169 | +// Check if a date falls within the range |
| 170 | +$checkDate = new \DateTimeImmutable('2024-06-15'); |
| 171 | +if ($eventPeriod->contains($checkDate)) { |
| 172 | + echo "Date is within event period"; |
| 173 | +} |
| 174 | +``` |
| 175 | + |
| 176 | +## Timestamp Ranges |
| 177 | + |
| 178 | +### TsRange (without timezone) |
| 179 | + |
| 180 | +```php |
| 181 | +use MartinGeorgiev\Doctrine\DBAL\Types\ValueObject\TsRange; |
| 182 | + |
| 183 | +// Working hours |
| 184 | +$workingHours = new TsRange( |
| 185 | + new \DateTimeImmutable('2024-01-01 09:00:00'), |
| 186 | + new \DateTimeImmutable('2024-01-01 17:00:00') |
| 187 | +); |
| 188 | +``` |
| 189 | + |
| 190 | +### TstzRange (with timezone) |
| 191 | + |
| 192 | +```php |
| 193 | +use MartinGeorgiev\Doctrine\DBAL\Types\ValueObject\TstzRange; |
| 194 | + |
| 195 | +// Meeting time across UTC timezone |
| 196 | +$meetingTime = new TstzRange( |
| 197 | + new \DateTimeImmutable('2024-01-01 14:00:00+00:00'), |
| 198 | + new \DateTimeImmutable('2024-01-01 15:00:00+00:00') |
| 199 | +); |
| 200 | +``` |
| 201 | + |
| 202 | +## Range Operations |
| 203 | + |
| 204 | +### Contains Check |
| 205 | + |
| 206 | +```php |
| 207 | +$range = new NumericRange(1, 10); |
| 208 | + |
| 209 | +if ($range->contains(5)) { |
| 210 | + echo "5 is in the range [1, 10)"; |
| 211 | +} |
| 212 | +``` |
| 213 | + |
| 214 | +### String Representation |
| 215 | + |
| 216 | +```php |
| 217 | +$range = new NumericRange(1.5, 10.7); |
| 218 | +echo $range; // Outputs: [1.5,10.7) |
| 219 | + |
| 220 | +$range = new DateRange( |
| 221 | + new \DateTimeImmutable('2024-01-01'), |
| 222 | + new \DateTimeImmutable('2024-12-31') |
| 223 | +); |
| 224 | +echo $range; // Outputs: [2024-01-01,2024-12-31) |
| 225 | +``` |
| 226 | + |
| 227 | +### Parsing from String Values |
| 228 | + |
| 229 | +```php |
| 230 | +// Parse PostgreSQL range strings |
| 231 | +$numRange = NumericRange::fromString('[1.5,10.7)'); |
| 232 | +$dateRange = DateRange::fromString('[2024-01-01,2024-12-31)'); |
| 233 | +$emptyRange = NumericRange::fromString('empty'); |
| 234 | +``` |
| 235 | + |
| 236 | +## DQL Usage with Range Functions |
| 237 | + |
| 238 | +Register range functions for DQL queries: |
| 239 | + |
| 240 | +```php |
| 241 | +$configuration->addCustomStringFunction('DATERANGE', MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\Daterange::class); |
| 242 | +$configuration->addCustomStringFunction('INT4RANGE', MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\Int4range::class); |
| 243 | +$configuration->addCustomStringFunction('INT8RANGE', MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\Int8range::class); |
| 244 | +$configuration->addCustomStringFunction('NUMRANGE', MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\Numrange::class); |
| 245 | +$configuration->addCustomStringFunction('TSRANGE', MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\Tsrange::class); |
| 246 | +$configuration->addCustomStringFunction('TSTZRANGE', MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\Tstzrange::class); |
| 247 | +``` |
| 248 | + |
| 249 | +Use in DQL: |
| 250 | + |
| 251 | +```php |
| 252 | +// Find products with overlapping price ranges |
| 253 | +$dql = " |
| 254 | + SELECT p |
| 255 | + FROM Product p |
| 256 | + WHERE OVERLAPS(p.priceRange, NUMRANGE(20, 50)) = TRUE |
| 257 | +"; |
| 258 | + |
| 259 | +// Find events in a date range |
| 260 | +$dql = " |
| 261 | + SELECT e |
| 262 | + FROM Event e |
| 263 | + WHERE CONTAINS(e.period, DATERANGE('2024-06-01', '2024-06-30')) = TRUE |
| 264 | +"; |
| 265 | +``` |
| 266 | + |
| 267 | +## Common Use Cases |
| 268 | + |
| 269 | +### Price Ranges |
| 270 | + |
| 271 | +```php |
| 272 | +#[ORM\Entity] |
| 273 | +class Product |
| 274 | +{ |
| 275 | + #[ORM\Column(type: 'numrange')] |
| 276 | + private ?NumericRange $priceRange = null; |
| 277 | + |
| 278 | + public function setPriceRange(float $min, float $max): void |
| 279 | + { |
| 280 | + $this->priceRange = new NumericRange($min, $max, true, false); |
| 281 | + } |
| 282 | + |
| 283 | + public function isInPriceRange(float $price): bool |
| 284 | + { |
| 285 | + return $this->priceRange?->contains($price) ?? false; |
| 286 | + } |
| 287 | +} |
| 288 | +``` |
| 289 | + |
| 290 | +### Availability Periods |
| 291 | + |
| 292 | +```php |
| 293 | +#[ORM\Entity] |
| 294 | +class Room |
| 295 | +{ |
| 296 | + #[ORM\Column(type: 'tstzrange')] |
| 297 | + private ?TstzRange $availabilityWindow = null; |
| 298 | + |
| 299 | + public function setAvailability(\DateTimeInterface $start, \DateTimeInterface $end): void |
| 300 | + { |
| 301 | + $this->availabilityWindow = new TstzRange($start, $end); |
| 302 | + } |
| 303 | + |
| 304 | + public function isAvailableAt(\DateTimeInterface $time): bool |
| 305 | + { |
| 306 | + return $this->availabilityWindow?->contains($time) ?? false; |
| 307 | + } |
| 308 | +} |
| 309 | +``` |
| 310 | + |
| 311 | +### Age Restrictions |
| 312 | + |
| 313 | +```php |
| 314 | +#[ORM\Entity] |
| 315 | +class Event |
| 316 | +{ |
| 317 | + #[ORM\Column(type: 'int4range')] |
| 318 | + private ?Int4Range $ageRestriction = null; |
| 319 | + |
| 320 | + public function setAgeRestriction(int $minAge, int $maxAge): void |
| 321 | + { |
| 322 | + $this->ageRestriction = new Int4Range($minAge, $maxAge, true, true); |
| 323 | + } |
| 324 | + |
| 325 | + public function isAgeAllowed(int $age): bool |
| 326 | + { |
| 327 | + return $this->ageRestriction?->contains($age) ?? true; |
| 328 | + } |
| 329 | +} |
| 330 | +``` |
0 commit comments