From df7339fefa2130e997018c937c70ca57588b8615 Mon Sep 17 00:00:00 2001 From: Marco Rieser Date: Wed, 10 Jun 2026 11:08:05 +0200 Subject: [PATCH] wip --- src/Query/Builder.php | 19 ++++++---- src/Query/Concerns/NormalizesDateValues.php | 16 ++++++++ src/Query/EloquentQueryBuilder.php | 13 +++++-- src/Query/IteratorBuilder.php | 3 +- src/Stache/Query/Builder.php | 3 +- tests/Data/Entries/EntryQueryBuilderTest.php | 40 ++++++++++++++++++++ 6 files changed, 81 insertions(+), 13 deletions(-) create mode 100644 src/Query/Concerns/NormalizesDateValues.php diff --git a/src/Query/Builder.php b/src/Query/Builder.php index e6c1511e83d..78560ec69db 100644 --- a/src/Query/Builder.php +++ b/src/Query/Builder.php @@ -14,6 +14,7 @@ use Statamic\Extensions\Pagination\LengthAwarePaginator; use Statamic\Facades\Pattern; use Statamic\Query\Concerns\FakesQueries; +use Statamic\Query\Concerns\NormalizesDateValues; use Statamic\Query\Concerns\QueriesRelationships; use Statamic\Query\Exceptions\MultipleRecordsFoundException; use Statamic\Query\Exceptions\RecordsNotFoundException; @@ -21,7 +22,7 @@ abstract class Builder implements Contract { - use AppliesScopes, FakesQueries, QueriesRelationships; + use AppliesScopes, FakesQueries, NormalizesDateValues, QueriesRelationships; protected $columns; protected $limit; @@ -138,6 +139,8 @@ public function where($column, $operator = null, $value = null, $boolean = 'and' [$value, $operator] = [$operator, '=']; } + $value = $this->normalizeWhereDateValue($value); + $type = 'Basic'; $this->wheres[] = compact('type', 'column', 'value', 'operator', 'boolean'); @@ -393,6 +396,8 @@ public function whereBetween($column, $values, $boolean = 'and', $not = false) throw new InvalidArgumentException('Values should be an array of length 2'); } + $values = array_map(fn ($v) => $this->normalizeWhereDateValue($v), $values); + $this->wheres[] = [ 'type' => ($not ? 'Not' : '').'Between', 'column' => $column, @@ -428,9 +433,9 @@ public function whereDate($column, $operator, $value = null, $boolean = 'and') throw new InvalidArgumentException('Illegal operator for date comparison'); } - if (! ($value instanceof DateTimeInterface)) { - $value = Carbon::parse($value); - } + $value = $value instanceof DateTimeInterface + ? $this->normalizeWhereDateValue($value) + : Carbon::parse($value); $value = Carbon::parse($value->format('Y-m-d')); // we only care about the date part @@ -554,9 +559,9 @@ public function whereTime($column, $operator, $value = null, $boolean = 'and') throw new InvalidArgumentException('Illegal operator for date comparison'); } - if (! ($value instanceof DateTimeInterface)) { - $value = Carbon::parse($value); - } + $value = $value instanceof DateTimeInterface + ? $this->normalizeWhereDateValue($value) + : Carbon::parse($value); $value = $value->format('H:i:s'); // we only care about the time part diff --git a/src/Query/Concerns/NormalizesDateValues.php b/src/Query/Concerns/NormalizesDateValues.php new file mode 100644 index 00000000000..8db54a9c64d --- /dev/null +++ b/src/Query/Concerns/NormalizesDateValues.php @@ -0,0 +1,16 @@ +setTimezone(config('app.timezone')) + : $value; + } +} diff --git a/src/Query/EloquentQueryBuilder.php b/src/Query/EloquentQueryBuilder.php index a4bc452a232..be97bcaf09a 100644 --- a/src/Query/EloquentQueryBuilder.php +++ b/src/Query/EloquentQueryBuilder.php @@ -11,6 +11,7 @@ use Statamic\Contracts\Query\Builder; use Statamic\Extensions\Pagination\LengthAwarePaginator; use Statamic\Facades\Blink; +use Statamic\Query\Concerns\NormalizesDateValues; use Statamic\Query\Concerns\QueriesRelationships; use Statamic\Query\Exceptions\MultipleRecordsFoundException; use Statamic\Query\Exceptions\RecordsNotFoundException; @@ -19,7 +20,7 @@ abstract class EloquentQueryBuilder implements Builder { - use AppliesScopes, QueriesRelationships; + use AppliesScopes, NormalizesDateValues, QueriesRelationships; protected $builder; protected $columns; @@ -317,6 +318,8 @@ public function orWhereNotNull($column) public function whereBetween($column, $values, $boolean = 'and', $not = false) { + $values = array_map(fn ($v) => $this->normalizeWhereDateValue($v), $values); + $this->builder->whereBetween($this->column($column), $values, $boolean, $not); return $this; @@ -343,9 +346,9 @@ public function whereDate($column, $operator, $value = null, $boolean = 'and') $value, $operator, func_num_args() === 2 ); - if (! ($value instanceof DateTimeInterface)) { - $value = Carbon::parse($value); - } + $value = $value instanceof DateTimeInterface + ? $this->normalizeWhereDateValue($value) + : Carbon::parse($value, config('app.timezone')); $this->builder->whereDate($this->column($column), $operator, $value, $boolean); @@ -411,6 +414,8 @@ public function whereTime($column, $operator, $value = null, $boolean = 'and') $value, $operator, func_num_args() === 2 ); + $value = $this->normalizeWhereDateValue($value); + $this->builder->whereTime($this->column($column), $operator, $value, $boolean); return $this; diff --git a/src/Query/IteratorBuilder.php b/src/Query/IteratorBuilder.php index 0eb6c983d91..194d2d42533 100644 --- a/src/Query/IteratorBuilder.php +++ b/src/Query/IteratorBuilder.php @@ -259,7 +259,7 @@ protected function filterWhereDate($entries, $where) return false; } - return $value->copy()->startOfDay()->$method($where['value']); + return $value->copy()->setTimezone(config('app.timezone'))->startOfDay()->$method($where['value']); }); } @@ -319,6 +319,7 @@ protected function filterWhereTime($entries, $where) return false; } + $value = $value->copy()->setTimezone(config('app.timezone')); $compareValue = $value->copy()->setTimeFromTimeString($where['value']); return $value->$method($compareValue); diff --git a/src/Stache/Query/Builder.php b/src/Stache/Query/Builder.php index 0f3a7fba9f3..ebf187ab801 100644 --- a/src/Stache/Query/Builder.php +++ b/src/Stache/Query/Builder.php @@ -206,7 +206,7 @@ protected function filterWhereDate($values, $where) return false; } - return $value->copy()->startOfDay()->$method($where['value']); + return $value->copy()->setTimezone(config('app.timezone'))->startOfDay()->$method($where['value']); }); } @@ -258,6 +258,7 @@ protected function filterWhereTime($values, $where) return false; } + $value = $value->copy()->setTimezone(config('app.timezone')); $compareValue = $value->copy()->setTimeFromTimeString($where['value']); return $value->$method($compareValue); diff --git a/tests/Data/Entries/EntryQueryBuilderTest.php b/tests/Data/Entries/EntryQueryBuilderTest.php index 67f7168413e..9ced72cb342 100644 --- a/tests/Data/Entries/EntryQueryBuilderTest.php +++ b/tests/Data/Entries/EntryQueryBuilderTest.php @@ -251,6 +251,46 @@ public function entries_are_found_using_where_time() $this->assertEquals(['Post 2'], $entries->map->title->all()); } + #[Test] + public function carbon_instances_in_where_date_are_converted_to_app_timezone() + { + // Entries are interpreted in app timezone when stored. + // A UTC Carbon that falls on a *different calendar date* in the app timezone + // must match entries for the app-timezone date, not the UTC date. + config(['app.timezone' => 'America/New_York']); + + $this->createWhereDateTestEntries(); + + // Post 1 stored as '2021-11-15 20:31:04' NY → UTC '2021-11-16 01:31:04'. + // Passing a UTC Carbon for that exact instant: UTC date = Nov 16, NY date = Nov 15. + // With app-tz normalisation we should find entries on NY Nov 15 (Posts 1 and 3). + $utcCarbon = Carbon::parse('2021-11-16 01:31:04', 'UTC'); + + $entries = Entry::query()->whereDate('test_date', $utcCarbon)->get(); + + $this->assertCount(2, $entries); + $this->assertEquals(['Post 1', 'Post 3'], $entries->map->title->all()); + } + + #[Test] + public function carbon_instances_in_where_time_are_converted_to_app_timezone() + { + // Post 2 is stored as '2021-11-14 09:00:00' interpreted in NY time → UTC '2021-11-14 14:00:00'. + // Passing the UTC Carbon representing that instant: UTC time = 14:00, NY time = 09:00. + // The query converts it to NY (09:00) and should find Post 2 (the only entry at 09:00 NY). + config(['app.timezone' => 'America/New_York']); + + $this->createWhereDateTestEntries(); + + // UTC 14:00 = NY 09:00 (EST, UTC-5). + $utcCarbon = Carbon::parse('2021-11-14 14:00:00', 'UTC'); + + $entries = Entry::query()->whereTime('test_date', $utcCarbon)->get(); + + $this->assertCount(1, $entries); + $this->assertEquals(['Post 2'], $entries->map->title->all()); + } + private function createWhereDateTestEntries() { $blueprint = Blueprint::makeFromFields(['test_date' => ['type' => 'date', 'time_enabled' => true]]);