Skip to content

Commit

Permalink
feat(AppFramework): Add full support for date / time / datetime columns
Browse files Browse the repository at this point in the history
This adds support for all Doctrine supported types, for the column types only the immutable variants needed to be added.
But especially those types are the important ones, as our **Entity** class works by detecting changes through setters.
Meaning if it is mutable, changes like `$entity->date->modfiy()` can not be detected, so the immutable types make more sense here.

Similar the parameter types needed to be added.

`Enity` and `QBMapper` needed to be adjusted so they support (auto map) those types, required when insert or update an entity.

Also added more tests, especially to make sure the mapper really serializes the values correctly.

Signed-off-by: Ferdinand Thiessen <[email protected]>
  • Loading branch information
susnux committed Aug 19, 2024
1 parent 6fce6fa commit 2346049
Show file tree
Hide file tree
Showing 8 changed files with 404 additions and 58 deletions.
40 changes: 28 additions & 12 deletions lib/public/AppFramework/Db/Entity.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
*/
namespace OCP\AppFramework\Db;

use OCP\DB\Types;

use function lcfirst;
use function substr;

Expand Down Expand Up @@ -95,24 +97,38 @@ protected function setter(string $name, array $args): void {
// if type definition exists, cast to correct type
if ($args[0] !== null && array_key_exists($name, $this->_fieldTypes)) {
$type = $this->_fieldTypes[$name];
if ($type === 'blob') {
if ($type === Types::BLOB) {
// (B)LOB is treated as string when we read from the DB
if (is_resource($args[0])) {
$args[0] = stream_get_contents($args[0]);
}
$type = 'string';
$type = Types::STRING;
}

if ($type === 'datetime') {
if (!$args[0] instanceof \DateTime) {
$args[0] = new \DateTime($args[0]);
}
} elseif ($type === 'json') {
if (!is_array($args[0])) {
$args[0] = json_decode($args[0], true);
}
} else {
settype($args[0], $type);
switch ($type) {
case Types::TIME:
case Types::DATE:
case Types::DATETIME:
case Types::DATETIME_TZ:
if (!$args[0] instanceof \DateTime) {
$args[0] = new \DateTime($args[0]);
}
break;
case Types::TIME_IMMUTABLE:
case Types::DATE_IMMUTABLE:
case Types::DATETIME_IMMUTABLE:
case Types::DATETIME_TZ_IMMUTABLE:
if (!$args[0] instanceof \DateTimeImmutable) {
$args[0] = new \DateTimeImmutable($args[0]);
}
break;
case TYpes::JSON:
if (!is_array($args[0])) {
$args[0] = json_decode($args[0], true);
}
break;
default:
settype($args[0], $type);
}
}
$this->$name = $args[0];
Expand Down
28 changes: 22 additions & 6 deletions lib/public/AppFramework/Db/QBMapper.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
use Generator;
use OCP\DB\Exception;
use OCP\DB\QueryBuilder\IQueryBuilder;
use OCP\DB\Types;
use OCP\IDBConnection;

/**
Expand Down Expand Up @@ -218,18 +219,33 @@ protected function getParameterTypeForProperty(Entity $entity, string $property)

switch ($types[ $property ]) {
case 'int':
case 'integer':
case Types::INTEGER:
case Types::SMALLINT:
return IQueryBuilder::PARAM_INT;
case 'string':
case Types::STRING:
return IQueryBuilder::PARAM_STR;
case 'bool':
case 'boolean':
case Types::BOOLEAN:
return IQueryBuilder::PARAM_BOOL;
case 'blob':
case Types::BLOB:
return IQueryBuilder::PARAM_LOB;
case 'datetime':
case Types::DATE:
return IQueryBuilder::PARAM_DATE;
case 'json':
case Types::DATETIME:
return IQueryBuilder::PARAM_DATETIME;
case Types::DATETIME_TZ:
return IQueryBuilder::PARAM_DATETIME_TZ;
case Types::DATE_IMMUTABLE:
return IQueryBuilder::PARAM_DATE_IMMUTABLE;
case Types::DATETIME_IMMUTABLE:
return IQueryBuilder::PARAM_DATETIME_IMMUTABLE;
case Types::DATETIME_TZ_IMMUTABLE:
return IQueryBuilder::PARAM_DATETIME_TZ_IMMUTABLE;
case Types::TIME:
return IQueryBuilder::PARAM_TIME;
case Types::TIME_IMMUTABLE:
return IQueryBuilder::PARAM_TIME_IMMUTABLE;
case Types::JSON:
return IQueryBuilder::PARAM_JSON;
}

Expand Down
47 changes: 46 additions & 1 deletion lib/public/DB/QueryBuilder/IQueryBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
use Doctrine\DBAL\ArrayParameterType;
use Doctrine\DBAL\Connection;
use Doctrine\DBAL\ParameterType;
use Doctrine\DBAL\Types\Types;
use OCP\DB\Exception;
use OCP\DB\IResult;
use OCP\IDBConnection;
Expand Down Expand Up @@ -41,10 +42,54 @@ interface IQueryBuilder {
* @since 9.0.0
*/
public const PARAM_LOB = ParameterType::LARGE_OBJECT;

/**
* For passing a \DateTime instance when only interested in the time part (without timezone support)
* @since 31.0.0
*/
public const PARAM_TIME = Types::TIME_MUTABLE;

/**
* For passing a \DateTime instance when only interested in the date part (without timezone support)
* @since 9.0.0
*/
public const PARAM_DATE = 'datetime';
public const PARAM_DATE = Types::DATE_MUTABLE;

/**
* For passing a \DateTime instance (without timezone support)
* @since 31.0.0
*/
public const PARAM_DATETIME = Types::DATETIME_MUTABLE;

/**
* For passing a \DateTime instance with timezone support
* @since 31.0.0
*/
public const PARAM_DATETIME_TZ = Types::DATETIMETZ_MUTABLE;

/**
* For passing a \DateTimeImmutable instance when only interested in the time part (without timezone support)
* @since 31.0.0
*/
public const PARAM_TIME_IMMUTABLE = Types::TIME_MUTABLE;

/**
* For passing a \DateTime instance when only interested in the date part (without timezone support)
* @since 9.0.0
*/
public const PARAM_DATE_IMMUTABLE = Types::DATE_IMMUTABLE;

/**
* For passing a \DateTime instance (without timezone support)
* @since 31.0.0
*/
public const PARAM_DATETIME_IMMUTABLE = Types::DATETIME_IMMUTABLE;

/**
* For passing a \DateTime instance with timezone support
* @since 31.0.0
*/
public const PARAM_DATETIME_TZ_IMMUTABLE = Types::DATETIMETZ_IMMUTABLE;

/**
* @since 24.0.0
Expand Down
77 changes: 77 additions & 0 deletions lib/public/DB/Types.php
Original file line number Diff line number Diff line change
Expand Up @@ -41,17 +41,76 @@ final class Types {
public const BOOLEAN = 'boolean';

/**
* A datetime instance with only the date set.
* This will be (de)serialized into a \DateTime instance,
* it is recommended to instead use the `DATE_IMMUTABLE` instead.
*
* Warning: When deserialized the timezone will be set to the default PHP timezone of the server.
* @var string
* @since 21.0.0
*/
public const DATE = 'date';

/**
* An immutable datetime instance with only the date set.
* This will be (de)serialized into a \DateTimeImmutable instance,
* It is recommended to use this over the `DATE` type because
* out `Entity` class works detecting changes through the setter,
* changes on mutable objects can not be detected.
*
* Warning: When deserialized the timezone will be set to the default PHP timezone of the server.
* @var string
* @since 31.0.0
*/
public const DATE_IMMUTABLE = 'date_immutable';

/**
* A datetime instance with date and time support.
* This will be (de)serialized into a \DateTime instance,
* it is recommended to instead use the `DATETIME_IMMUTABLE` instead.
*
* Warning: When deserialized the timezone will be set to the default PHP timezone of the server.
* @var string
* @since 21.0.0
*/
public const DATETIME = 'datetime';

/**
* An immutable datetime instance with date and time set.
* This will be (de)serialized into a \DateTimeImmutable instance,
* It is recommended to use this over the `DATETIME` type because
* out `Entity` class works detecting changes through the setter,
* changes on mutable objects can not be detected.
*
* Warning: When deserialized the timezone will be set to the default PHP timezone of the server.
* @var string
* @since 31.0.0
*/
public const DATETIME_IMMUTABLE = 'datetime_immutable';


/**
* A datetime instance with timezone support
* This will be (de)serialized into a \DateTime instance,
* it is recommended to instead use the `DATETIME_TZ_IMMUTABLE` instead.
*
* @var string
* @since 31.0.0
*/
public const DATETIME_TZ = 'datetimetz';

/**
* An immutable timezone aware datetime instance with date and time set.
* This will be (de)serialized into a \DateTimeImmutable instance,
* It is recommended to use this over the `DATETIME_TZ` type because
* out `Entity` class works detecting changes through the setter,
* changes on mutable objects can not be detected.
*
* @var string
* @since 31.0.0
*/
public const DATETIME_TZ_IMMUTABLE = 'datetimetz_immutable';

/**
* @var string
* @since 21.0.0
Expand Down Expand Up @@ -89,11 +148,29 @@ final class Types {
public const TEXT = 'text';

/**
* A datetime instance with only the time set.
* This will be (de)serialized into a \DateTime instance,
* it is recommended to instead use the `TIME_IMMUTABLE` instead.
*
* Warning: When deserialized the timezone will be set to the default PHP timezone of the server.
* @var string
* @since 21.0.0
*/
public const TIME = 'time';

/**
* A datetime instance with only the time set.
* This will be (de)serialized into a \DateTime instance.
*
* It is recommended to use this over the `DATETIME_TZ` type because
* out `Entity` class works detecting changes through the setter,
* changes on mutable objects can not be detected.
*
* @var string
* @since 31.0.0
*/
public const TIME_IMMUTABLE = 'time_immutable';

/**
* @var string
* @since 24.0.0
Expand Down
44 changes: 36 additions & 8 deletions tests/lib/AppFramework/Db/EntityTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
namespace Test\AppFramework\Db;

use OCP\AppFramework\Db\Entity;
use OCP\DB\Types;
use PHPUnit\Framework\Constraint\IsType;

/**
Expand All @@ -30,6 +31,10 @@
* @method void setAnotherBool(bool $anotherBool)
* @method string getLongText()
* @method void setLongText(string $longText)
* @method \DateTime getTime()
* @method void setTime(\DateTime $time)
* @method \DateTimeImmutable getDatetime()
* @method void setDatetime(\DateTimeImmutable $datetime)
*/
class TestEntity extends Entity {
protected $name;
Expand All @@ -39,12 +44,16 @@ class TestEntity extends Entity {
protected $trueOrFalse;
protected $anotherBool;
protected $longText;
protected $time;
protected $datetime;

public function __construct($name = null) {
$this->addType('testId', 'integer');
$this->addType('testId', Types::INTEGER);
$this->addType('trueOrFalse', 'bool');
$this->addType('anotherBool', 'boolean');
$this->addType('longText', 'blob');
$this->addType('anotherBool', Types::BOOLEAN);
$this->addType('longText', Types::BLOB);
$this->addType('time', Types::TIME);
$this->addType('datetime', Types::DATETIME_IMMUTABLE);
$this->name = $name;
}
}
Expand Down Expand Up @@ -211,23 +220,42 @@ public function testSetterConvertsResourcesToStringProperly() {
$this->assertSame($string, $entity->getLongText());
}

public function testSetterConvertsDatetime() {
$entity = new TestEntity();
$entity->setDatetime('2024-08-19 15:26:00');
$this->assertEquals(new \DateTimeImmutable('2024-08-19 15:26:00'), $entity->getDatetime());
}

public function testSetterDoesNotConvertNullOnDatetime() {
$entity = new TestEntity();
$entity->setDatetime(null);
$this->assertNull($entity->getDatetime());
}

public function testSetterConvertsTime() {
$entity = new TestEntity();
$entity->setTime('15:26:00');
$this->assertEquals(new \DateTime('15:26:00'), $entity->getTime());
}

public function testGetFieldTypes() {
$entity = new TestEntity();
$this->assertEquals([
'id' => 'integer',
'testId' => 'integer',
'id' => Types::INTEGER,
'testId' => Types::INTEGER,
'trueOrFalse' => 'bool',
'anotherBool' => 'boolean',
'longText' => 'blob',
'anotherBool' => Types::BOOLEAN,
'longText' => Types::BLOB,
'time' => Types::TIME,
'datetime' => Types::DATETIME_IMMUTABLE,
], $entity->getFieldTypes());
}


public function testGetItInt() {
$entity = new TestEntity();
$entity->setId(3);
$this->assertEquals('integer', gettype($entity->getId()));
$this->assertEquals(Types::INTEGER, gettype($entity->getId()));
}


Expand Down
Loading

0 comments on commit 2346049

Please sign in to comment.