Skip to content

Commit

Permalink
Support DateTime instances
Browse files Browse the repository at this point in the history
  • Loading branch information
Tigrov committed Aug 5, 2023
1 parent c5aa0b1 commit e71d368
Show file tree
Hide file tree
Showing 5 changed files with 136 additions and 3 deletions.
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 @@ abstract class AbstractColumnSchema implements ColumnSchemaInterface
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 @@ public function computed(bool $value): void
$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 @@ public function dbTypecast(mixed $value): mixed
* 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 @@ public function getComment(): string|null
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 @@ public function getType(): string
return $this->type;
}

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

public function isAllowNull(): bool
{
return $this->allowNull;
Expand Down Expand Up @@ -195,8 +250,23 @@ public function phpType(string|null $value): void
$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

0 comments on commit e71d368

Please sign in to comment.