Skip to content

Commit

Permalink
Added date and datetime property types
Browse files Browse the repository at this point in the history
  • Loading branch information
jaredtking committed Jan 8, 2023
1 parent bed87ae commit 66a96c1
Show file tree
Hide file tree
Showing 13 changed files with 221 additions and 49 deletions.
5 changes: 3 additions & 2 deletions CHANGELOG.md
Expand Up @@ -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`
Expand All @@ -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.
Expand Down Expand Up @@ -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.
Expand Down
4 changes: 2 additions & 2 deletions docs/index.md
Expand Up @@ -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,
),
];
Expand Down Expand Up @@ -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
```
Expand Down
9 changes: 9 additions & 0 deletions docs/model-definitions.md
Expand Up @@ -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)
Expand All @@ -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`
Expand Down Expand Up @@ -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
Expand Down
22 changes: 18 additions & 4 deletions src/Driver/AbstractDriver.php
Expand Up @@ -3,22 +3,36 @@
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
{
/**
* 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);
Expand All @@ -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;
Expand Down
4 changes: 2 additions & 2 deletions src/Driver/DatabaseDriver.php
Expand Up @@ -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());

Expand Down Expand Up @@ -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());

Expand Down
4 changes: 2 additions & 2 deletions src/Driver/DbalDriver.php
Expand Up @@ -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);

Expand Down Expand Up @@ -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)
Expand Down
2 changes: 2 additions & 0 deletions src/Property.php
Expand Up @@ -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;
Expand Down Expand Up @@ -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,
];
}
}
49 changes: 48 additions & 1 deletion src/Type.php
Expand Up @@ -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;

/**
Expand All @@ -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';
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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.
*/
Expand Down
25 changes: 4 additions & 21 deletions tests/Driver/DatabaseDriverTest.php
Expand Up @@ -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);

Expand Down Expand Up @@ -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);
Expand Down
17 changes: 2 additions & 15 deletions tests/Driver/DbalDriverTest.php
Expand Up @@ -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);
Expand All @@ -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);
Expand Down
52 changes: 52 additions & 0 deletions tests/Driver/SerializeValueTestTrait.php
@@ -0,0 +1,52 @@
<?php

namespace Pulsar\Tests\Driver;

use DateTimeImmutable;
use Pulsar\Driver\DatabaseDriver;
use Pulsar\Property;
use Pulsar\Tests\Enums\TestEnumInteger;
use Pulsar\Tests\Enums\TestEnumString;
use Pulsar\Type;
use stdClass;

trait SerializeValueTestTrait
{
public function testSerializeValueString(): void
{
$driver = $this->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')));
}
}

0 comments on commit 66a96c1

Please sign in to comment.