Skip to content

Commit 91eb962

Browse files
authored
Use DateTimeColumn (#393)
1 parent 4fbb78d commit 91eb962

18 files changed

+489
-66
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
- Bug #388: Set empty `comment` and `extra` properties to `null` when loading table columns (@Tigrov)
3939
- Enh #389, #390: Use `DbArrayHelper::arrange()` instead of `DbArrayHelper::index()` method (@Tigrov)
4040
- New #387: Realize `Schema::loadResultColumn()` method (@Tigrov)
41+
- New #393: Use `DateTimeColumn` class for datetime column types (@Tigrov)
4142

4243
## 1.2.0 March 21, 2024
4344

src/Column/ColumnBuilder.php

+31
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,37 @@
44

55
namespace Yiisoft\Db\Mysql\Column;
66

7+
use Yiisoft\Db\Constant\ColumnType;
8+
79
final class ColumnBuilder extends \Yiisoft\Db\Schema\Column\ColumnBuilder
810
{
11+
public static function timestamp(int|null $size = 0): DateTimeColumn
12+
{
13+
return new DateTimeColumn(ColumnType::TIMESTAMP, size: $size);
14+
}
15+
16+
public static function datetime(int|null $size = 0): DateTimeColumn
17+
{
18+
return new DateTimeColumn(ColumnType::DATETIME, size: $size);
19+
}
20+
21+
public static function datetimeWithTimezone(int|null $size = 0): DateTimeColumn
22+
{
23+
return new DateTimeColumn(ColumnType::DATETIMETZ, size: $size);
24+
}
25+
26+
public static function time(int|null $size = 0): DateTimeColumn
27+
{
28+
return new DateTimeColumn(ColumnType::TIME, size: $size);
29+
}
30+
31+
public static function timeWithTimezone(int|null $size = 0): DateTimeColumn
32+
{
33+
return new DateTimeColumn(ColumnType::TIMETZ, size: $size);
34+
}
35+
36+
public static function date(): DateTimeColumn
37+
{
38+
return new DateTimeColumn(ColumnType::DATE);
39+
}
940
}

src/Column/ColumnDefinitionBuilder.php

+6-4
Original file line numberDiff line numberDiff line change
@@ -45,9 +45,9 @@ final class ColumnDefinitionBuilder extends AbstractColumnDefinitionBuilder
4545
'varbinary',
4646
'blob',
4747
'year',
48-
'time',
49-
'datetime',
5048
'timestamp',
49+
'datetime',
50+
'time',
5151
];
5252

5353
protected const TYPES_WITH_SCALE = [
@@ -87,10 +87,12 @@ protected function getDbType(ColumnInterface $column): string
8787
ColumnType::TEXT => 'text',
8888
ColumnType::BINARY => 'blob',
8989
ColumnType::UUID => 'binary(16)',
90-
ColumnType::DATETIME => 'datetime',
9190
ColumnType::TIMESTAMP => 'timestamp',
92-
ColumnType::DATE => 'date',
91+
ColumnType::DATETIME => 'datetime',
92+
ColumnType::DATETIMETZ => 'datetime',
9393
ColumnType::TIME => 'time',
94+
ColumnType::TIMETZ => 'time',
95+
ColumnType::DATE => 'date',
9496
ColumnType::ARRAY => 'json',
9597
ColumnType::STRUCTURED => 'json',
9698
ColumnType::JSON => 'json',

src/Column/ColumnFactory.php

+17-4
Original file line numberDiff line numberDiff line change
@@ -49,14 +49,27 @@ final class ColumnFactory extends AbstractColumnFactory
4949
'tinyblob' => ColumnType::BINARY,
5050
'mediumblob' => ColumnType::BINARY,
5151
'longblob' => ColumnType::BINARY,
52-
'year' => ColumnType::DATE,
53-
'date' => ColumnType::DATE,
54-
'time' => ColumnType::TIME,
55-
'datetime' => ColumnType::DATETIME,
52+
'year' => ColumnType::SMALLINT,
5653
'timestamp' => ColumnType::TIMESTAMP,
54+
'datetime' => ColumnType::DATETIME,
55+
'time' => ColumnType::TIME,
56+
'date' => ColumnType::DATE,
5757
'json' => ColumnType::JSON,
5858
];
5959

60+
protected function getColumnClass(string $type, array $info = []): string
61+
{
62+
return match ($type) {
63+
ColumnType::TIMESTAMP => DateTimeColumn::class,
64+
ColumnType::DATETIME => DateTimeColumn::class,
65+
ColumnType::DATETIMETZ => DateTimeColumn::class,
66+
ColumnType::TIME => DateTimeColumn::class,
67+
ColumnType::TIMETZ => DateTimeColumn::class,
68+
ColumnType::DATE => DateTimeColumn::class,
69+
default => parent::getColumnClass($type, $info),
70+
};
71+
}
72+
6073
protected function getType(string $dbType, array $info = []): string
6174
{
6275
if ($dbType === 'bit' && isset($info['size']) && $info['size'] === 1) {

src/Column/DateTimeColumn.php

+39
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Yiisoft\Db\Mysql\Column;
6+
7+
use Yiisoft\Db\Constant\ColumnType;
8+
9+
/**
10+
* Represents the metadata for a datetime column.
11+
*
12+
* > [!WARNING]
13+
* > MySQL DBMS converts `TIMESTAMP` column type values from database session time zone to UTC for storage, and back
14+
* > from UTC to the session time zone when retrieve the values.
15+
*
16+
* `TIMESTAMP` database type does not store time zone offset and require to convert datetime values to the database
17+
* session time zone before insert and back to the PHP time zone after retrieve the values. This will be done in the
18+
* {@see dbTypecast()} and {@see phpTypecast()} methods and guarantees that the values are stored in the database
19+
* in the correct time zone.
20+
*
21+
* To avoid possible time zone issues with the datetime values conversion, it is recommended to set the PHP and database
22+
* time zones to UTC.
23+
*/
24+
final class DateTimeColumn extends \Yiisoft\Db\Schema\Column\DateTimeColumn
25+
{
26+
protected function getFormat(): string
27+
{
28+
return $this->format ??= match ($this->getType()) {
29+
ColumnType::DATETIMETZ => 'Y-m-d H:i:s' . $this->getMillisecondsFormat(),
30+
ColumnType::TIMETZ => 'H:i:s' . $this->getMillisecondsFormat(),
31+
default => parent::getFormat(),
32+
};
33+
}
34+
35+
protected function shouldConvertTimezone(): bool
36+
{
37+
return $this->shouldConvertTimezone ??= !empty($this->dbTimezone) && $this->getType() !== ColumnType::DATE;
38+
}
39+
}

src/Connection.php

+6
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
use Psr\Log\LogLevel;
88
use Throwable;
9+
use Yiisoft\Db\Connection\ServerInfoInterface;
910
use Yiisoft\Db\Driver\Pdo\AbstractPdoConnection;
1011
use Yiisoft\Db\Driver\Pdo\PdoCommandInterface;
1112
use Yiisoft\Db\Mysql\Column\ColumnFactory;
@@ -84,4 +85,9 @@ public function getSchema(): SchemaInterface
8485
{
8586
return $this->schema ??= new Schema($this, $this->schemaCache);
8687
}
88+
89+
public function getServerInfo(): ServerInfoInterface
90+
{
91+
return $this->serverInfo ??= new ServerInfo($this);
92+
}
8793
}

src/Schema.php

+10-3
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
use function str_starts_with;
3232
use function strtolower;
3333
use function substr;
34+
use function substr_compare;
3435
use function trim;
3536

3637
use const PHP_INT_SIZE;
@@ -424,6 +425,7 @@ protected function loadResultColumn(array $metadata): ColumnInterface|null
424425
'float', 'double', 'decimal' => $columnInfo['scale'] = $metadata['precision'],
425426
'bigint' => $metadata['len'] === 20 ? $columnInfo['unsigned'] = true : null,
426427
'int' => $metadata['len'] === 10 && PHP_INT_SIZE !== 8 ? $columnInfo['unsigned'] = true : null,
428+
'timestamp' => $columnInfo['dbTimezone'] = $this->db->getServerInfo()->getTimezone(),
427429
default => null,
428430
};
429431

@@ -444,8 +446,7 @@ protected function loadResultColumn(array $metadata): ColumnInterface|null
444446
private function loadColumn(array $info): ColumnInterface
445447
{
446448
$extra = trim(str_ireplace('auto_increment', '', $info['extra'], $autoIncrement));
447-
448-
$column = $this->db->getColumnFactory()->fromDefinition($info['column_type'], [
449+
$columnInfo = [
449450
'autoIncrement' => $autoIncrement > 0,
450451
'comment' => $info['column_comment'] === '' ? null : $info['column_comment'],
451452
'defaultValueRaw' => $info['column_default'],
@@ -456,7 +457,13 @@ private function loadColumn(array $info): ColumnInterface
456457
'schema' => $info['schema'],
457458
'table' => $info['table'],
458459
'unique' => $info['column_key'] === 'UNI',
459-
]);
460+
];
461+
462+
if (substr_compare($info['column_type'], 'timestamp', 0, 9, true) === 0) {
463+
$columnInfo['dbTimezone'] = $this->db->getServerInfo()->getTimezone();
464+
}
465+
466+
$column = $this->db->getColumnFactory()->fromDefinition($info['column_type'], $columnInfo);
460467

461468
if (str_starts_with($extra, 'DEFAULT_GENERATED')) {
462469
$extra = trim(substr($extra, 18));

src/ServerInfo.php

+26
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Yiisoft\Db\Mysql;
6+
7+
use Yiisoft\Db\Driver\Pdo\PdoServerInfo;
8+
9+
final class ServerInfo extends PdoServerInfo
10+
{
11+
/** @psalm-suppress PropertyNotSetInConstructor */
12+
private string $timezone;
13+
14+
public function getTimezone(bool $refresh = false): string
15+
{
16+
/** @psalm-suppress TypeDoesNotContainType */
17+
if (!isset($this->timezone) || $refresh) {
18+
/** @var string */
19+
$this->timezone = $this->db->createCommand(
20+
"SELECT LPAD(TIME_FORMAT(TIMEDIFF(NOW(), UTC_TIMESTAMP), '%H:%i'), 6, '+')"
21+
)->queryScalar();
22+
}
23+
24+
return $this->timezone;
25+
}
26+
}

tests/ColumnBuilderTest.php

+13
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@
44

55
namespace Yiisoft\Db\Mysql\Tests;
66

7+
use PHPUnit\Framework\Attributes\DataProviderExternal;
78
use Yiisoft\Db\Mysql\Column\ColumnBuilder;
9+
use Yiisoft\Db\Mysql\Tests\Provider\ColumnBuilderProvider;
810
use Yiisoft\Db\Mysql\Tests\Support\TestTrait;
911
use Yiisoft\Db\Tests\AbstractColumnBuilderTest;
1012

@@ -19,4 +21,15 @@ public function getColumnBuilderClass(): string
1921
{
2022
return ColumnBuilder::class;
2123
}
24+
25+
#[DataProviderExternal(ColumnBuilderProvider::class, 'buildingMethods')]
26+
public function testBuildingMethods(
27+
string $buildingMethod,
28+
array $args,
29+
string $expectedInstanceOf,
30+
string $expectedType,
31+
array $expectedMethodResults = [],
32+
): void {
33+
parent::testBuildingMethods($buildingMethod, $args, $expectedInstanceOf, $expectedType, $expectedMethodResults);
34+
}
2235
}

0 commit comments

Comments
 (0)