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
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
@@ -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;

@@ -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;
@@ -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;
@@ -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);
} 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);
}

@@ -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;
@@ -165,6 +215,11 @@ public function getType(): string
return $this->type;
}

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

public function isAllowNull(): bool
{
return $this->allowNull;
@@ -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);
}

31 changes: 31 additions & 0 deletions src/Schema/AbstractSchema.php
Original file line number Diff line number Diff line change
@@ -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,
};
}
@@ -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
@@ -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.
*
@@ -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.
@@ -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.
*
5 changes: 5 additions & 0 deletions src/Schema/SchemaInterface.php
Original file line number Diff line number Diff line change
@@ -4,6 +4,7 @@

namespace Yiisoft\Db\Schema;

use DateTimeInterface;
use Throwable;
use Yiisoft\Db\Command\DataType;
use Yiisoft\Db\Constraint\ConstraintSchemaInterface;
@@ -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.
*/
14 changes: 11 additions & 3 deletions tests/Common/CommonSchemaTest.php
Original file line number Diff line number Diff line change
@@ -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 {
@@ -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();