Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support DateTime instances #736

Draft
wants to merge 1 commit into
base: master
Choose a base branch
from
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
70 changes: 70 additions & 0 deletions src/Schema/AbstractColumnSchema.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@

namespace Yiisoft\Db\Schema;

use DateTimeImmutable;
use DateTimeInterface;
use DateTimeZone;
use Yiisoft\Db\Expression\Expression;
use Yiisoft\Db\Expression\ExpressionInterface;
use Yiisoft\Db\Helper\DbStringHelper;

Expand Down Expand Up @@ -45,6 +49,7 @@
private bool $autoIncrement = false;
private string|null $comment = null;
private bool $computed = false;
private string|null $dateTimeFormat = null;
private string|null $dbType = null;
private mixed $defaultValue = null;
private array|null $enumValues = null;
Expand Down Expand Up @@ -81,6 +86,11 @@
$this->computed = $value;
}

public function dateTimeFormat(string|null $value): void
{
$this->dateTimeFormat = $value;
}

public function dbType(string|null $value): void
{
$this->dbType = $value;
Expand All @@ -92,6 +102,41 @@
* The default implementation does the same as casting for PHP, but it should be possible to override this with
* annotation of an explicit PDO type.
*/

if ($this->dateTimeFormat !== null) {
if (empty($value) || $value instanceof Expression) {
return $value;
}

if (!$this->hasTimezone() && $this->type !== SchemaInterface::TYPE_DATE) {
// if data type does not have timezone DB stores datetime without timezone
// convert datetime to UTC to avoid timezone issues
if (!$value instanceof DateTimeImmutable) {
// make a copy of $value if change timezone
if ($value instanceof DateTimeInterface) {
$value = DateTimeImmutable::createFromInterface($value);

Check warning on line 117 in src/Schema/AbstractColumnSchema.php

View check run for this annotation

Codecov / codecov/patch

src/Schema/AbstractColumnSchema.php#L117

Added line #L117 was not covered by tests
} elseif (is_string($value)) {
$value = date_create_immutable($value) ?: $value;
}
}

if ($value instanceof DateTimeImmutable) { // DateTimeInterface does not have the method setTimezone()
$value = $value->setTimezone(new DateTimeZone('UTC'));
// Known possible issues:
// MySQL converts `TIMESTAMP` values from the current time zone to UTC for storage, and back from UTC to the current time zone when retrieve data.
// Oracle `TIMESTAMP WITH LOCAL TIME ZONE` data stored in the database is normalized to the database time zone. And returns it in the users' local session time zone.
// Both of them do not store time zone offset and require to convert DateTime to local DB timezone instead of UTC before insert.
// To solve the issue it requires to set local DB timezone to UTC if the types are in use
}
}

if ($value instanceof DateTimeInterface) {
return $value->format($this->dateTimeFormat);
}

return (string) $value;
}

return $this->typecast($value);
}

Expand All @@ -115,6 +160,11 @@
return $this->comment;
}

public function getDateTimeFormat(): string|null
{
return $this->dateTimeFormat;
}

public function getDbType(): string|null
{
return $this->dbType;
Expand Down Expand Up @@ -165,6 +215,11 @@
return $this->type;
}

public function hasTimezone(): bool
{
return false;
}

public function isAllowNull(): bool
{
return $this->allowNull;
Expand Down Expand Up @@ -195,8 +250,23 @@
$this->phpType = $value;
}

/**
* @throws \Exception
*/
public function phpTypecast(mixed $value): mixed
{
if (is_string($value) && $this->dateTimeFormat !== null) {
if (!$this->hasTimezone()) {
// if data type does not have timezone datetime was converted to UTC before insert
$datetime = new DateTimeImmutable($value, new DateTimeZone('UTC'));

// convert datetime to PHP timezone
return $datetime->setTimezone(new DateTimeZone(date_default_timezone_get()));
}

return new DateTimeImmutable($value);
}

return $this->typecast($value);
}

Expand Down
31 changes: 31 additions & 0 deletions src/Schema/AbstractSchema.php
Original file line number Diff line number Diff line change
Expand Up @@ -418,6 +418,11 @@ protected function getColumnPhpType(ColumnSchemaInterface $column): string
SchemaInterface::TYPE_DOUBLE => SchemaInterface::PHP_TYPE_DOUBLE,
SchemaInterface::TYPE_BINARY => SchemaInterface::PHP_TYPE_RESOURCE,
SchemaInterface::TYPE_JSON => SchemaInterface::PHP_TYPE_ARRAY,
SchemaInterface::TYPE_DATETIME => SchemaInterface::PHP_TYPE_DATE_TIME,
SchemaInterface::TYPE_TIMESTAMP => SchemaInterface::PHP_TYPE_DATE_TIME,
SchemaInterface::TYPE_DATE => SchemaInterface::PHP_TYPE_DATE_TIME,
SchemaInterface::TYPE_TIME => SchemaInterface::PHP_TYPE_DATE_TIME,

default => SchemaInterface::PHP_TYPE_STRING,
};
}
Expand Down Expand Up @@ -648,4 +653,30 @@ public function getViewNames(string $schema = '', bool $refresh = false): array

return (array) $this->viewNames[$schema];
}

protected function getDateTimeFormat(ColumnSchemaInterface $column): string|null
{
return match ($column->getType()) {
self::TYPE_TIMESTAMP,
self::TYPE_DATETIME => 'Y-m-d H:i:s'
. $this->getMillisecondsFormat($column)
. ($column->hasTimezone() ? 'P' : ''),
self::TYPE_DATE => 'Y-m-d',
self::TYPE_TIME => 'H:i:s'
. $this->getMillisecondsFormat($column)
. ($column->hasTimezone() ? 'P' : ''),
default => null,
};
}

protected function getMillisecondsFormat(ColumnSchemaInterface $column): string
{
$precision = $column->getPrecision();

return match (true) {
$precision > 3 => '.u',
$precision > 0 => '.v',
default => '',
};
}
}
19 changes: 19 additions & 0 deletions src/Schema/ColumnSchemaInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,13 @@ public function comment(string|null $value): void;
*/
public function computed(bool $value): void;

/**
* The datetime format to convert value from `DateTimeInterface` to a database representation.
*
* It defines from table schema.
*/
public function dateTimeFormat(string|null $value): void;

/**
* The database data-type of column.
*
Expand Down Expand Up @@ -134,6 +141,13 @@ public function extra(string|null $value): void;
*/
public function getComment(): string|null;

/**
* @return string|null The datetime format.
*
* @see dateTimeFormat()
*/
public function getDateTimeFormat(): string|null;

/**
* @return string|null The database type of the column.
* Null means the column has no type in the database.
Expand Down Expand Up @@ -206,6 +220,11 @@ public function getSize(): int|null;
*/
public function getType(): string;

/**
* @return bool True if the datetime type has a timezone, false otherwise.
*/
public function hasTimezone(): bool;

/**
* Whether this column is nullable.
*
Expand Down
5 changes: 5 additions & 0 deletions src/Schema/SchemaInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

namespace Yiisoft\Db\Schema;

use DateTimeInterface;
use Throwable;
use Yiisoft\Db\Command\DataType;
use Yiisoft\Db\Constraint\ConstraintSchemaInterface;
Expand Down Expand Up @@ -247,6 +248,10 @@ interface SchemaInterface extends ConstraintSchemaInterface
* Define the php type as `array` for cast to php value.
*/
public const PHP_TYPE_ARRAY = 'array';
/**
* Define the php type as `DateTimeInterface` for cast to php value.
*/
public const PHP_TYPE_DATE_TIME = DateTimeInterface::class;
/**
* Define the php type as `null` for cast to php value.
*/
Expand Down
14 changes: 11 additions & 3 deletions tests/Common/CommonSchemaTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -885,9 +885,9 @@ protected function columnSchema(array $columns, string $table): void
$column->getDefaultValue(),
"defaultValue of column $name is expected to be an object but it is not."
);
$this->assertSame(
(string) $expected['defaultValue'],
(string) $column->getDefaultValue(),
$this->assertEquals(
$expected['defaultValue'],
$column->getDefaultValue(),
"defaultValue of column $name does not match."
);
} else {
Expand All @@ -907,6 +907,14 @@ protected function columnSchema(array $columns, string $table): void
"dimension of column $name does not match"
);
}

if (isset($expected['dateTimeFormat'])) {
$this->assertSame(
$expected['dateTimeFormat'],
$column->getDateTimeFormat(),
"dateTimeFormat of column $name does not match"
);
}
}

$db->close();
Expand Down
Loading