diff --git a/CHANGELOG.md b/CHANGELOG.md index ff7b6c9..befac76 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,9 @@ This project adheres to [Semantic Versioning](http://semver.org/). - Doctrine 3 compatibility if using the DBAL driver - Added `Model::deleteOrFail()` method. - The model ID property names can be obtained with `Definition::getIds()` +- Use PHPDoc generics when possible +- Added `enum` model property type. +- Added `date` and `datetime` property types that use `DateTimeInterface` objects. ### Changed - Moved adding event listeners and dispatching events to `EventManager` @@ -32,7 +35,6 @@ This project adheres to [Semantic Versioning](http://semver.org/). - The constructor arguments to `Property` are now typed and promoted to readonly constructor properties. An array of properties is no longer accepted. - Property definitions must return `Property` objects instead of arrays - Renamed the `date` type to `date_unix` -- Use PHPDoc generics when possible ### Fixed - Rollback database transaction after uncaught exception during model persistence. @@ -61,7 +63,6 @@ This project adheres to [Semantic Versioning](http://semver.org/). - Added `Model::beforePersist()` and `Model::afterPersist()` shortcut to install lifecycle event listeners for all create, update, and delete. - Added `Model::getMassAssignmentWhitelist()` and `Model::getMassAssignmentBlacklist()` that can be overriden to define mass assignment rules. - Added `in_array` model definition setting to indicate whether a property is included in the array representation. -- Added `enum` model property type. ### Changed - Make model internal properties private when possible. diff --git a/docs/index.md b/docs/index.md index e9b8e2b..00163df 100644 --- a/docs/index.md +++ b/docs/index.md @@ -105,7 +105,7 @@ class User extends Model type: Type::FLOAT, ), 'last_sign_in' => new Property( - type: Type::DATE_UNIX, + type: Type::DATETIME, null: true, ), ]; @@ -155,7 +155,7 @@ $user = new User([ ]); $user->save(); // creates a new row in Users table -$user->last_sign_in = time(); +$user->last_sign_in = new DateTimeImmutable(); $user->balance = 1000; $user->save(); // changes the `last_sign_in` and `balance` columns ``` diff --git a/docs/model-definitions.md b/docs/model-definitions.md index 25073ca..9f75de3 100644 --- a/docs/model-definitions.md +++ b/docs/model-definitions.md @@ -16,6 +16,7 @@ Standard Options: - [encrypted](#encrypted) - [in_array](#in_array) - [enum_class](#enum_class) +- [date_format](#date_format) Relationships: - [belongs_to](#belongs_to) @@ -36,6 +37,8 @@ The data type of the property. This setting will type cast values when retrieved Supported Types: - `array` - `boolean` +- `date` +- `date_time` - `date_unix` - `enum` - `float` @@ -104,6 +107,12 @@ This is required when the property type is `enum`. The value must be the class n String, Optional, Default: `null` +### date_format + +When using a `date` or `datetime` property type, you can specify the string format for representing these values in the database. If this setting is not used then the default format for `date` is `Y-m-d` and the default format for `datetime` is `Y-m-d H:i:s`. + +String, Optional, Default: `null` + ## Relationships ### belongs_to diff --git a/src/Driver/AbstractDriver.php b/src/Driver/AbstractDriver.php index 88ff034..45bcc4e 100644 --- a/src/Driver/AbstractDriver.php +++ b/src/Driver/AbstractDriver.php @@ -3,8 +3,12 @@ namespace Pulsar\Driver; use BackedEnum; +use DateTimeInterface; use JAQB\Query\SelectQuery; +use Pulsar\Model; +use Pulsar\Property; use Pulsar\Query; +use Pulsar\Type; use UnitEnum; abstract class AbstractDriver implements DriverInterface @@ -12,13 +16,23 @@ abstract class AbstractDriver implements DriverInterface /** * Marshals a value to storage. */ - public function serializeValue(mixed $value): mixed + public function serializeValue(mixed $value, ?Property $property): mixed { // encode backed enums as their backing type if ($value instanceof BackedEnum) { return $value->value; } + // encode datetime objects + if ($value instanceof DateTimeInterface) { + $format = $property?->date_format; + if (!$format) { + $format = $property?->type == Type::DATE ? 'Y-m-d' : 'Y-m-d H:i:s'; + } + + return $value->format($format); + } + // encode arrays/objects as JSON if (is_array($value) || is_object($value)) { return json_encode($value); @@ -30,10 +44,10 @@ public function serializeValue(mixed $value): mixed /** * Serializes an array of values. */ - protected function serialize(array $values): array + protected function serialize(array $values, Model $model): array { - foreach ($values as &$value) { - $value = $this->serializeValue($value); + foreach ($values as $k => &$value) { + $value = $this->serializeValue($value, $model::definition()->get($k)); } return $values; diff --git a/src/Driver/DatabaseDriver.php b/src/Driver/DatabaseDriver.php index 92f9482..3ecfd4d 100644 --- a/src/Driver/DatabaseDriver.php +++ b/src/Driver/DatabaseDriver.php @@ -91,7 +91,7 @@ public function getConnection(?string $id): QueryBuilder public function createModel(Model $model, array $parameters): bool { - $values = $this->serialize($parameters); + $values = $this->serialize($parameters, $model); $tablename = $model->getTablename(); $db = $this->getConnection($model->getConnection()); @@ -140,7 +140,7 @@ public function updateModel(Model $model, array $parameters): bool return true; } - $values = $this->serialize($parameters); + $values = $this->serialize($parameters, $model); $tablename = $model->getTablename(); $db = $this->getConnection($model->getConnection()); diff --git a/src/Driver/DbalDriver.php b/src/Driver/DbalDriver.php index 5f9d560..de049b0 100644 --- a/src/Driver/DbalDriver.php +++ b/src/Driver/DbalDriver.php @@ -34,7 +34,7 @@ public function createModel(Model $model, array $parameters): bool { // build the SQL query $tablename = $model->getTablename(); - $values = $this->serialize($parameters); + $values = $this->serialize($parameters, $model); $dbQuery = new InsertQuery(); $dbQuery->into($tablename)->values($values); @@ -114,7 +114,7 @@ public function updateModel(Model $model, array $parameters): bool // build the SQL query $tablename = $model->getTablename(); - $values = $this->serialize($parameters); + $values = $this->serialize($parameters, $model); $dbQuery = new UpdateQuery(); $dbQuery->table($tablename) ->values($values) diff --git a/src/Property.php b/src/Property.php index da729ab..04a4ccf 100644 --- a/src/Property.php +++ b/src/Property.php @@ -42,6 +42,7 @@ public function __construct( ?string $has_one = null, ?string $has_many = null, public readonly ?string $enum_class = null, + public readonly ?string $date_format = null, ) { $this->hasDefault = $default !== self::MISSING_DEFAULT; @@ -115,6 +116,7 @@ public function toArray(): array 'pivot_tablename' => $this->pivot_tablename, 'morphs_to' => $this->morphs_to, 'enum_class' => $this->enum_class, + 'date_format' => $this->date_format, ]; } } diff --git a/src/Type.php b/src/Type.php index c29184e..5f51fa0 100644 --- a/src/Type.php +++ b/src/Type.php @@ -12,8 +12,11 @@ namespace Pulsar; use BackedEnum; +use DateTimeImmutable; +use DateTimeInterface; use Defuse\Crypto\Crypto; use Defuse\Crypto\Key; +use Pulsar\Exception\ModelException; use stdClass; /** @@ -24,7 +27,7 @@ final class Type const ARRAY = 'array'; const BOOLEAN = 'boolean'; const DATE = 'date'; - const DATE_TIME = 'datetime'; + const DATETIME = 'datetime'; const DATE_UNIX = 'date_unix'; const ENUM = 'enum'; const FLOAT = 'float'; @@ -62,6 +65,14 @@ public static function cast(Property $property, mixed $value): mixed return self::to_enum($value, (string) $property->enum_class); } + if ($type == self::DATE) { + return self::to_date($value, $property->date_format); + } + + if ($type == self::DATETIME) { + return self::to_datetime($value, $property->date_format); + } + $m = 'to_'.$property->type; return self::$m($value); @@ -99,6 +110,42 @@ public static function to_boolean(mixed $value): bool return filter_var($value, FILTER_VALIDATE_BOOLEAN); } + /** + * Casts a date value as a Date object. + */ + public static function to_date(mixed $value, ?string $format): DateTimeInterface + { + if ($value instanceof DateTimeInterface) { + return $value; + } + + $format = $format ?? 'Y-m-d'; + $date = DateTimeImmutable::createFromFormat($format, $value); + if (!$date) { + throw new ModelException('Could not parse date: '.$value); + } + + return $date->setTime(0, 0); + } + + /** + * Casts a datetime value as a Date object. + */ + public static function to_datetime(mixed $value, ?string $format): DateTimeInterface + { + if ($value instanceof DateTimeInterface) { + return $value; + } + + $format = $format ?? 'Y-m-d H:i:s'; + $date = DateTimeImmutable::createFromFormat($format, $value); + if (!$date) { + throw new ModelException('Could not parse datetime: '.$value); + } + + return $date; + } + /** * Casts a date value as a UNIX timestamp. */ diff --git a/tests/Driver/DatabaseDriverTest.php b/tests/Driver/DatabaseDriverTest.php index 53b29d9..df175fb 100644 --- a/tests/Driver/DatabaseDriverTest.php +++ b/tests/Driver/DatabaseDriverTest.php @@ -21,16 +21,16 @@ use Pulsar\Driver\DatabaseDriver; use Pulsar\Exception\DriverException; use Pulsar\Query; -use Pulsar\Tests\Enums\TestEnumInteger; -use Pulsar\Tests\Enums\TestEnumString; use Pulsar\Tests\Models\Group; use Pulsar\Tests\Models\Person; -use stdClass; class DatabaseDriverTest extends MockeryTestCase { - private function getDriver($connection): DatabaseDriver + use SerializeValueTestTrait; + + private function getDriver($connection = null): DatabaseDriver { + $connection = $connection ?: Mockery::mock(QueryBuilder::class); $driver = new DatabaseDriver(); $driver->setConnection($connection); @@ -98,23 +98,6 @@ public function testGetConnectionMissing() $driver->getConnection(false); } - public function testSerializeValue() - { - $driver = new DatabaseDriver(); - - $this->assertEquals('string', $driver->serializeValue('string')); - - $arr = ['test' => true]; - $this->assertEquals('{"test":true}', $driver->serializeValue($arr)); - - $obj = new stdClass(); - $obj->test = true; - $this->assertEquals('{"test":true}', $driver->serializeValue($obj)); - - $this->assertEquals('first', $driver->serializeValue(TestEnumString::First)); - $this->assertEquals(1, $driver->serializeValue(TestEnumInteger::First)); - } - public function testCreateModel() { $db = Mockery::mock(QueryBuilder::class); diff --git a/tests/Driver/DbalDriverTest.php b/tests/Driver/DbalDriverTest.php index 38e422c..d38e49c 100644 --- a/tests/Driver/DbalDriverTest.php +++ b/tests/Driver/DbalDriverTest.php @@ -20,10 +20,11 @@ use Pulsar\Query; use Pulsar\Tests\Models\Group; use Pulsar\Tests\Models\Person; -use stdClass; class DbalDriverTest extends MockeryTestCase { + use SerializeValueTestTrait; + private function getDriver($connection = null): DbalDriver { $connection = $connection ?: Mockery::mock(Connection::class); @@ -44,20 +45,6 @@ public function testGetConnectionFromManagerMissing() $this->getDriver()->getConnection('not_supported'); } - public function testSerializeValue() - { - $driver = $this->getDriver(); - - $this->assertEquals('string', $driver->serializeValue('string')); - - $arr = ['test' => true]; - $this->assertEquals('{"test":true}', $driver->serializeValue($arr)); - - $obj = new stdClass(); - $obj->test = true; - $this->assertEquals('{"test":true}', $driver->serializeValue($obj)); - } - public function testCreateModel() { $db = Mockery::mock(Connection::class); diff --git a/tests/Driver/SerializeValueTestTrait.php b/tests/Driver/SerializeValueTestTrait.php new file mode 100644 index 0000000..018b5c1 --- /dev/null +++ b/tests/Driver/SerializeValueTestTrait.php @@ -0,0 +1,52 @@ +getDriver(); + $this->assertEquals('string', $driver->serializeValue('string', null)); + } + + public function testSerializeValueArray(): void + { + $driver = $this->getDriver(); + $arr = ['test' => true]; + $this->assertEquals('{"test":true}', $driver->serializeValue($arr, null)); + } + + public function testSerializeValueObject(): void + { + $driver = $this->getDriver(); + $obj = new stdClass(); + $obj->test = true; + $this->assertEquals('{"test":true}', $driver->serializeValue($obj, null)); + } + + public function testSerializeValueEnum(): void + { + $driver = $this->getDriver(); + $this->assertEquals('first', $driver->serializeValue(TestEnumString::First, null)); + $this->assertEquals(1, $driver->serializeValue(TestEnumInteger::First, null)); + } + + public function testSerializeValueDateTime(): void + { + date_default_timezone_set('UTC'); + $driver = $this->getDriver(); + $this->assertEquals('2023-01-08 01:02:03', $driver->serializeValue(new DateTimeImmutable('2023-01-08 01:02:03'), null)); + $this->assertEquals('2023-01-08', $driver->serializeValue(new DateTimeImmutable('2023-01-08'), new Property(type: Type::DATE))); + $this->assertEquals('2023-01-08 01:02:03', $driver->serializeValue(new DateTimeImmutable('2023-01-08 01:02:03'), new Property(type: Type::DATETIME))); + $this->assertEquals('1673139723', $driver->serializeValue(new DateTimeImmutable('2023-01-08 01:02:03'), new Property(date_format: 'U'))); + } +} \ No newline at end of file diff --git a/tests/ModelTest.php b/tests/ModelTest.php index 956ab8e..7526696 100644 --- a/tests/ModelTest.php +++ b/tests/ModelTest.php @@ -106,6 +106,7 @@ public function testGetProperties() 'morphs_to' => null, 'in_array' => true, 'enum_class' => null, + 'date_format' => null, ], 'relation' => [ 'type' => Type::INTEGER, @@ -124,6 +125,7 @@ public function testGetProperties() 'morphs_to' => null, 'in_array' => true, 'enum_class' => null, + 'date_format' => null, ], 'answer' => [ 'type' => Type::STRING, @@ -142,6 +144,7 @@ public function testGetProperties() 'morphs_to' => null, 'in_array' => true, 'enum_class' => null, + 'date_format' => null, ], 'test_hook' => [ 'type' => Type::STRING, @@ -160,6 +163,7 @@ public function testGetProperties() 'morphs_to' => null, 'in_array' => true, 'enum_class' => null, + 'date_format' => null, ], 'mutator' => [ 'type' => null, @@ -178,6 +182,7 @@ public function testGetProperties() 'morphs_to' => null, 'in_array' => false, 'enum_class' => null, + 'date_format' => null, ], 'accessor' => [ 'type' => null, @@ -196,6 +201,7 @@ public function testGetProperties() 'morphs_to' => null, 'in_array' => false, 'enum_class' => null, + 'date_format' => null, ], 'encrypted' => [ 'type' => null, @@ -214,6 +220,7 @@ public function testGetProperties() 'morphs_to' => null, 'in_array' => true, 'enum_class' => null, + 'date_format' => null, ], 'appended' => [ 'type' => null, @@ -232,6 +239,7 @@ public function testGetProperties() 'morphs_to' => null, 'in_array' => true, 'enum_class' => null, + 'date_format' => null, ], ]; @@ -260,6 +268,7 @@ public function testPropertiesIdOverwrite() 'morphs_to' => null, 'in_array' => true, 'enum_class' => null, + 'date_format' => null, ]; $this->assertEquals($expected, Person::definition()->get('id')->toArray()); @@ -284,6 +293,7 @@ public function testGetProperty() 'morphs_to' => null, 'in_array' => true, 'enum_class' => null, + 'date_format' => null, ]; $this->assertEquals($expected, TestModel::definition()->get('id')->toArray()); @@ -304,6 +314,7 @@ public function testGetProperty() 'morphs_to' => null, 'in_array' => true, 'enum_class' => null, + 'date_format' => null, ]; $this->assertEquals($expected, TestModel::definition()->get('relation')->toArray()); } @@ -328,6 +339,7 @@ public function testPropertiesAutoTimestamps() 'morphs_to' => null, 'in_array' => true, 'enum_class' => null, + 'date_format' => null, ], 'id2' => [ 'type' => Type::INTEGER, @@ -346,6 +358,7 @@ public function testPropertiesAutoTimestamps() 'morphs_to' => null, 'in_array' => true, 'enum_class' => null, + 'date_format' => null, ], 'default' => [ 'type' => null, @@ -364,6 +377,7 @@ public function testPropertiesAutoTimestamps() 'morphs_to' => null, 'in_array' => true, 'enum_class' => null, + 'date_format' => null, ], 'validate' => [ 'type' => null, @@ -382,6 +396,7 @@ public function testPropertiesAutoTimestamps() 'morphs_to' => null, 'in_array' => true, 'enum_class' => null, + 'date_format' => null, ], 'validate2' => [ 'type' => null, @@ -400,6 +415,7 @@ public function testPropertiesAutoTimestamps() 'morphs_to' => null, 'in_array' => false, 'enum_class' => null, + 'date_format' => null, ], 'unique' => [ 'type' => null, @@ -418,6 +434,7 @@ public function testPropertiesAutoTimestamps() 'morphs_to' => null, 'in_array' => true, 'enum_class' => null, + 'date_format' => null, ], 'required' => [ 'type' => Type::INTEGER, @@ -436,6 +453,7 @@ public function testPropertiesAutoTimestamps() 'morphs_to' => null, 'in_array' => true, 'enum_class' => null, + 'date_format' => null, ], 'hidden' => [ 'type' => Type::BOOLEAN, @@ -454,6 +472,7 @@ public function testPropertiesAutoTimestamps() 'morphs_to' => null, 'in_array' => false, 'enum_class' => null, + 'date_format' => null, ], 'person' => [ 'type' => Type::INTEGER, @@ -472,6 +491,7 @@ public function testPropertiesAutoTimestamps() 'morphs_to' => null, 'in_array' => false, 'enum_class' => null, + 'date_format' => null, ], 'array' => [ 'type' => Type::ARRAY, @@ -494,6 +514,7 @@ public function testPropertiesAutoTimestamps() 'morphs_to' => null, 'in_array' => false, 'enum_class' => null, + 'date_format' => null, ], 'object' => [ 'type' => Type::OBJECT, @@ -512,6 +533,7 @@ public function testPropertiesAutoTimestamps() 'morphs_to' => null, 'in_array' => false, 'enum_class' => null, + 'date_format' => null, ], 'mutable_create_only' => [ 'type' => null, @@ -530,6 +552,7 @@ public function testPropertiesAutoTimestamps() 'morphs_to' => null, 'in_array' => false, 'enum_class' => null, + 'date_format' => null, ], 'protected' => [ 'type' => null, @@ -548,6 +571,7 @@ public function testPropertiesAutoTimestamps() 'morphs_to' => null, 'in_array' => true, 'enum_class' => null, + 'date_format' => null, ], 'created_at' => [ 'type' => Type::DATE_UNIX, @@ -566,6 +590,7 @@ public function testPropertiesAutoTimestamps() 'morphs_to' => null, 'in_array' => true, 'enum_class' => null, + 'date_format' => null, ], 'updated_at' => [ 'type' => Type::DATE_UNIX, @@ -584,6 +609,7 @@ public function testPropertiesAutoTimestamps() 'morphs_to' => null, 'in_array' => true, 'enum_class' => null, + 'date_format' => null, ], ]; $model = new TestModel2(); // forces initialize() @@ -611,6 +637,7 @@ public function testPropertiesSoftDelete() 'morphs_to' => null, 'in_array' => true, 'enum_class' => null, + 'date_format' => null, ], 'name' => [ 'type' => Type::STRING, @@ -629,6 +656,7 @@ public function testPropertiesSoftDelete() 'morphs_to' => null, 'in_array' => true, 'enum_class' => null, + 'date_format' => null, ], 'email' => [ 'type' => Type::STRING, @@ -647,6 +675,7 @@ public function testPropertiesSoftDelete() 'morphs_to' => null, 'in_array' => true, 'enum_class' => null, + 'date_format' => null, ], 'deleted' => [ 'type' => Type::BOOLEAN, @@ -665,6 +694,7 @@ public function testPropertiesSoftDelete() 'morphs_to' => null, 'in_array' => true, 'enum_class' => null, + 'date_format' => null, ], 'deleted_at' => [ 'type' => Type::DATE_UNIX, @@ -683,6 +713,7 @@ public function testPropertiesSoftDelete() 'morphs_to' => null, 'in_array' => true, 'enum_class' => null, + 'date_format' => null, ], 'garage' => [ 'type' => null, @@ -701,6 +732,7 @@ public function testPropertiesSoftDelete() 'morphs_to' => null, 'in_array' => false, 'enum_class' => null, + 'date_format' => null, ], ]; @@ -2319,6 +2351,7 @@ public function testGetPropertiesBelongsTo() 'morphs_to' => null, 'in_array' => true, 'enum_class' => null, + 'date_format' => null, ], 'customer' => [ 'type' => null, @@ -2337,6 +2370,7 @@ public function testGetPropertiesBelongsTo() 'morphs_to' => null, 'in_array' => false, 'enum_class' => null, + 'date_format' => null, ], 'customer_id' => [ 'type' => 'integer', @@ -2355,6 +2389,7 @@ public function testGetPropertiesBelongsTo() 'morphs_to' => null, 'in_array' => true, 'enum_class' => null, + 'date_format' => null, ], ]; diff --git a/tests/TypeTest.php b/tests/TypeTest.php index 5cf9713..1da1597 100644 --- a/tests/TypeTest.php +++ b/tests/TypeTest.php @@ -11,9 +11,11 @@ namespace Pulsar\Tests; +use DateTimeImmutable; use Defuse\Crypto\Crypto; use Defuse\Crypto\Key; use Mockery\Adapter\Phpunit\MockeryTestCase; +use Pulsar\Exception\ModelException; use Pulsar\Property; use Pulsar\Tests\Enums\TestEnumInteger; use Pulsar\Tests\Enums\TestEnumString; @@ -23,6 +25,12 @@ class TypeTest extends MockeryTestCase { + public static function setUpBeforeClass(): void + { + parent::setUpBeforeClass(); + date_default_timezone_set('UTC'); + } + public function testCast(): void { $property = new Property(null: true); @@ -50,6 +58,14 @@ public function testCast(): void $this->assertEquals(123, Type::cast($property, 123)); $this->assertEquals(123, Type::cast($property, '123')); + $property = new Property(type: Type::DATE, null: false); + $this->assertEquals(new DateTimeImmutable('2023-01-08'), Type::cast($property, '2023-01-08')); + $this->assertEquals(new DateTimeImmutable('2023-01-08'), Type::cast($property, new DateTimeImmutable('2023-01-08'))); + + $property = new Property(type: Type::DATETIME, null: false); + $this->assertEquals(new DateTimeImmutable('2010-01-28T17:00:00'), Type::cast($property, '2010-01-28 17:00:00')); + $this->assertEquals(new DateTimeImmutable('2010-01-28T17:00:00'), Type::cast($property, new DateTimeImmutable('2010-01-28T17:00:00'))); + $property = new Property(type: Type::DATE_UNIX, null: false); $this->assertEquals(123, Type::cast($property, 123)); $this->assertEquals(123, Type::cast($property, '123')); @@ -116,6 +132,32 @@ public function testToBoolean(): void } public function testToDate(): void + { + $this->assertEquals(new DateTimeImmutable('2023-01-08'), Type::to_date('2023-01-08', null)); + $this->assertEquals(new DateTimeImmutable('2023-01-08'), Type::to_date(new DateTimeImmutable('2023-01-08'), null)); + $this->assertEquals(new DateTimeImmutable('2015-08-20'), Type::to_date('Aug-20-2015', 'M-j-Y')); + } + + public function testToDateInvalid(): void + { + $this->expectException(ModelException::class); + Type::to_date('not valid', null); + } + + public function testToDateTime(): void + { + $this->assertEquals(new DateTimeImmutable('2023-01-08T01:02:03'), Type::to_datetime('2023-01-08 01:02:03', null)); + $this->assertEquals(new DateTimeImmutable('2023-01-08T01:02:03'), Type::to_datetime(new DateTimeImmutable('2023-01-08 01:02:03'), null)); + $this->assertEquals(new DateTimeImmutable('2015-08-20T01:02:03'), Type::to_datetime('Aug-20-2015 01:02:03', 'M-j-Y h:i:s')); + } + + public function testToDateTimeInvalid(): void + { + $this->expectException(ModelException::class); + Type::to_datetime('not valid', null); + } + + public function testToDateUnix(): void { $this->assertEquals(123, Type::to_date_unix(123)); $this->assertEquals(123, Type::to_date_unix('123'));