Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 12 additions & 7 deletions src/Query/Builder.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,15 @@
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;
use Statamic\Query\Scopes\AppliesScopes;

abstract class Builder implements Contract
{
use AppliesScopes, FakesQueries, QueriesRelationships;
use AppliesScopes, FakesQueries, NormalizesDateValues, QueriesRelationships;

protected $columns;
protected $limit;
Expand Down Expand Up @@ -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');

Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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

Expand Down
16 changes: 16 additions & 0 deletions src/Query/Concerns/NormalizesDateValues.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?php

namespace Statamic\Query\Concerns;

use DateTimeInterface;
use Illuminate\Support\Carbon;

trait NormalizesDateValues
{
protected function normalizeWhereDateValue($value)
{
return $value instanceof DateTimeInterface
? Carbon::instance($value)->setTimezone(config('app.timezone'))
: $value;
}
}
13 changes: 9 additions & 4 deletions src/Query/EloquentQueryBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -19,7 +20,7 @@

abstract class EloquentQueryBuilder implements Builder
{
use AppliesScopes, QueriesRelationships;
use AppliesScopes, NormalizesDateValues, QueriesRelationships;

protected $builder;
protected $columns;
Expand Down Expand Up @@ -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;
Expand All @@ -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);

Expand Down Expand Up @@ -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;
Expand Down
3 changes: 2 additions & 1 deletion src/Query/IteratorBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -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']);
});
}

Expand Down Expand Up @@ -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);
Expand Down
3 changes: 2 additions & 1 deletion src/Stache/Query/Builder.php
Original file line number Diff line number Diff line change
Expand Up @@ -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']);
});
}

Expand Down Expand Up @@ -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);
Expand Down
40 changes: 40 additions & 0 deletions tests/Data/Entries/EntryQueryBuilderTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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]]);
Expand Down
Loading