Skip to content

Commit 2346049

Browse files
committed
feat(AppFramework): Add full support for date / time / datetime columns
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 <opensource@fthiessen.de>
1 parent 6fce6fa commit 2346049

8 files changed

Lines changed: 404 additions & 58 deletions

File tree

lib/public/AppFramework/Db/Entity.php

Lines changed: 28 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
*/
88
namespace OCP\AppFramework\Db;
99

10+
use OCP\DB\Types;
11+
1012
use function lcfirst;
1113
use function substr;
1214

@@ -95,24 +97,38 @@ protected function setter(string $name, array $args): void {
9597
// if type definition exists, cast to correct type
9698
if ($args[0] !== null && array_key_exists($name, $this->_fieldTypes)) {
9799
$type = $this->_fieldTypes[$name];
98-
if ($type === 'blob') {
100+
if ($type === Types::BLOB) {
99101
// (B)LOB is treated as string when we read from the DB
100102
if (is_resource($args[0])) {
101103
$args[0] = stream_get_contents($args[0]);
102104
}
103-
$type = 'string';
105+
$type = Types::STRING;
104106
}
105107

106-
if ($type === 'datetime') {
107-
if (!$args[0] instanceof \DateTime) {
108-
$args[0] = new \DateTime($args[0]);
109-
}
110-
} elseif ($type === 'json') {
111-
if (!is_array($args[0])) {
112-
$args[0] = json_decode($args[0], true);
113-
}
114-
} else {
115-
settype($args[0], $type);
108+
switch ($type) {
109+
case Types::TIME:
110+
case Types::DATE:
111+
case Types::DATETIME:
112+
case Types::DATETIME_TZ:
113+
if (!$args[0] instanceof \DateTime) {
114+
$args[0] = new \DateTime($args[0]);
115+
}
116+
break;
117+
case Types::TIME_IMMUTABLE:
118+
case Types::DATE_IMMUTABLE:
119+
case Types::DATETIME_IMMUTABLE:
120+
case Types::DATETIME_TZ_IMMUTABLE:
121+
if (!$args[0] instanceof \DateTimeImmutable) {
122+
$args[0] = new \DateTimeImmutable($args[0]);
123+
}
124+
break;
125+
case TYpes::JSON:
126+
if (!is_array($args[0])) {
127+
$args[0] = json_decode($args[0], true);
128+
}
129+
break;
130+
default:
131+
settype($args[0], $type);
116132
}
117133
}
118134
$this->$name = $args[0];

lib/public/AppFramework/Db/QBMapper.php

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
use Generator;
1111
use OCP\DB\Exception;
1212
use OCP\DB\QueryBuilder\IQueryBuilder;
13+
use OCP\DB\Types;
1314
use OCP\IDBConnection;
1415

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

219220
switch ($types[ $property ]) {
220221
case 'int':
221-
case 'integer':
222+
case Types::INTEGER:
223+
case Types::SMALLINT:
222224
return IQueryBuilder::PARAM_INT;
223-
case 'string':
225+
case Types::STRING:
224226
return IQueryBuilder::PARAM_STR;
225227
case 'bool':
226-
case 'boolean':
228+
case Types::BOOLEAN:
227229
return IQueryBuilder::PARAM_BOOL;
228-
case 'blob':
230+
case Types::BLOB:
229231
return IQueryBuilder::PARAM_LOB;
230-
case 'datetime':
232+
case Types::DATE:
231233
return IQueryBuilder::PARAM_DATE;
232-
case 'json':
234+
case Types::DATETIME:
235+
return IQueryBuilder::PARAM_DATETIME;
236+
case Types::DATETIME_TZ:
237+
return IQueryBuilder::PARAM_DATETIME_TZ;
238+
case Types::DATE_IMMUTABLE:
239+
return IQueryBuilder::PARAM_DATE_IMMUTABLE;
240+
case Types::DATETIME_IMMUTABLE:
241+
return IQueryBuilder::PARAM_DATETIME_IMMUTABLE;
242+
case Types::DATETIME_TZ_IMMUTABLE:
243+
return IQueryBuilder::PARAM_DATETIME_TZ_IMMUTABLE;
244+
case Types::TIME:
245+
return IQueryBuilder::PARAM_TIME;
246+
case Types::TIME_IMMUTABLE:
247+
return IQueryBuilder::PARAM_TIME_IMMUTABLE;
248+
case Types::JSON:
233249
return IQueryBuilder::PARAM_JSON;
234250
}
235251

lib/public/DB/QueryBuilder/IQueryBuilder.php

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
use Doctrine\DBAL\ArrayParameterType;
1111
use Doctrine\DBAL\Connection;
1212
use Doctrine\DBAL\ParameterType;
13+
use Doctrine\DBAL\Types\Types;
1314
use OCP\DB\Exception;
1415
use OCP\DB\IResult;
1516
use OCP\IDBConnection;
@@ -41,10 +42,54 @@ interface IQueryBuilder {
4142
* @since 9.0.0
4243
*/
4344
public const PARAM_LOB = ParameterType::LARGE_OBJECT;
45+
46+
/**
47+
* For passing a \DateTime instance when only interested in the time part (without timezone support)
48+
* @since 31.0.0
49+
*/
50+
public const PARAM_TIME = Types::TIME_MUTABLE;
51+
4452
/**
53+
* For passing a \DateTime instance when only interested in the date part (without timezone support)
4554
* @since 9.0.0
4655
*/
47-
public const PARAM_DATE = 'datetime';
56+
public const PARAM_DATE = Types::DATE_MUTABLE;
57+
58+
/**
59+
* For passing a \DateTime instance (without timezone support)
60+
* @since 31.0.0
61+
*/
62+
public const PARAM_DATETIME = Types::DATETIME_MUTABLE;
63+
64+
/**
65+
* For passing a \DateTime instance with timezone support
66+
* @since 31.0.0
67+
*/
68+
public const PARAM_DATETIME_TZ = Types::DATETIMETZ_MUTABLE;
69+
70+
/**
71+
* For passing a \DateTimeImmutable instance when only interested in the time part (without timezone support)
72+
* @since 31.0.0
73+
*/
74+
public const PARAM_TIME_IMMUTABLE = Types::TIME_MUTABLE;
75+
76+
/**
77+
* For passing a \DateTime instance when only interested in the date part (without timezone support)
78+
* @since 9.0.0
79+
*/
80+
public const PARAM_DATE_IMMUTABLE = Types::DATE_IMMUTABLE;
81+
82+
/**
83+
* For passing a \DateTime instance (without timezone support)
84+
* @since 31.0.0
85+
*/
86+
public const PARAM_DATETIME_IMMUTABLE = Types::DATETIME_IMMUTABLE;
87+
88+
/**
89+
* For passing a \DateTime instance with timezone support
90+
* @since 31.0.0
91+
*/
92+
public const PARAM_DATETIME_TZ_IMMUTABLE = Types::DATETIMETZ_IMMUTABLE;
4893

4994
/**
5095
* @since 24.0.0

lib/public/DB/Types.php

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,17 +41,76 @@ final class Types {
4141
public const BOOLEAN = 'boolean';
4242

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

4954
/**
55+
* An immutable datetime instance with only the date set.
56+
* This will be (de)serialized into a \DateTimeImmutable instance,
57+
* It is recommended to use this over the `DATE` type because
58+
* out `Entity` class works detecting changes through the setter,
59+
* changes on mutable objects can not be detected.
60+
*
61+
* Warning: When deserialized the timezone will be set to the default PHP timezone of the server.
62+
* @var string
63+
* @since 31.0.0
64+
*/
65+
public const DATE_IMMUTABLE = 'date_immutable';
66+
67+
/**
68+
* A datetime instance with date and time support.
69+
* This will be (de)serialized into a \DateTime instance,
70+
* it is recommended to instead use the `DATETIME_IMMUTABLE` instead.
71+
*
72+
* Warning: When deserialized the timezone will be set to the default PHP timezone of the server.
5073
* @var string
5174
* @since 21.0.0
5275
*/
5376
public const DATETIME = 'datetime';
5477

78+
/**
79+
* An immutable datetime instance with date and time set.
80+
* This will be (de)serialized into a \DateTimeImmutable instance,
81+
* It is recommended to use this over the `DATETIME` type because
82+
* out `Entity` class works detecting changes through the setter,
83+
* changes on mutable objects can not be detected.
84+
*
85+
* Warning: When deserialized the timezone will be set to the default PHP timezone of the server.
86+
* @var string
87+
* @since 31.0.0
88+
*/
89+
public const DATETIME_IMMUTABLE = 'datetime_immutable';
90+
91+
92+
/**
93+
* A datetime instance with timezone support
94+
* This will be (de)serialized into a \DateTime instance,
95+
* it is recommended to instead use the `DATETIME_TZ_IMMUTABLE` instead.
96+
*
97+
* @var string
98+
* @since 31.0.0
99+
*/
100+
public const DATETIME_TZ = 'datetimetz';
101+
102+
/**
103+
* An immutable timezone aware datetime instance with date and time set.
104+
* This will be (de)serialized into a \DateTimeImmutable instance,
105+
* It is recommended to use this over the `DATETIME_TZ` type because
106+
* out `Entity` class works detecting changes through the setter,
107+
* changes on mutable objects can not be detected.
108+
*
109+
* @var string
110+
* @since 31.0.0
111+
*/
112+
public const DATETIME_TZ_IMMUTABLE = 'datetimetz_immutable';
113+
55114
/**
56115
* @var string
57116
* @since 21.0.0
@@ -89,11 +148,29 @@ final class Types {
89148
public const TEXT = 'text';
90149

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

161+
/**
162+
* A datetime instance with only the time set.
163+
* This will be (de)serialized into a \DateTime instance.
164+
*
165+
* It is recommended to use this over the `DATETIME_TZ` type because
166+
* out `Entity` class works detecting changes through the setter,
167+
* changes on mutable objects can not be detected.
168+
*
169+
* @var string
170+
* @since 31.0.0
171+
*/
172+
public const TIME_IMMUTABLE = 'time_immutable';
173+
97174
/**
98175
* @var string
99176
* @since 24.0.0

tests/lib/AppFramework/Db/EntityTest.php

Lines changed: 36 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
namespace Test\AppFramework\Db;
1010

1111
use OCP\AppFramework\Db\Entity;
12+
use OCP\DB\Types;
1213
use PHPUnit\Framework\Constraint\IsType;
1314

1415
/**
@@ -30,6 +31,10 @@
3031
* @method void setAnotherBool(bool $anotherBool)
3132
* @method string getLongText()
3233
* @method void setLongText(string $longText)
34+
* @method \DateTime getTime()
35+
* @method void setTime(\DateTime $time)
36+
* @method \DateTimeImmutable getDatetime()
37+
* @method void setDatetime(\DateTimeImmutable $datetime)
3338
*/
3439
class TestEntity extends Entity {
3540
protected $name;
@@ -39,12 +44,16 @@ class TestEntity extends Entity {
3944
protected $trueOrFalse;
4045
protected $anotherBool;
4146
protected $longText;
47+
protected $time;
48+
protected $datetime;
4249

4350
public function __construct($name = null) {
44-
$this->addType('testId', 'integer');
51+
$this->addType('testId', Types::INTEGER);
4552
$this->addType('trueOrFalse', 'bool');
46-
$this->addType('anotherBool', 'boolean');
47-
$this->addType('longText', 'blob');
53+
$this->addType('anotherBool', Types::BOOLEAN);
54+
$this->addType('longText', Types::BLOB);
55+
$this->addType('time', Types::TIME);
56+
$this->addType('datetime', Types::DATETIME_IMMUTABLE);
4857
$this->name = $name;
4958
}
5059
}
@@ -211,23 +220,42 @@ public function testSetterConvertsResourcesToStringProperly() {
211220
$this->assertSame($string, $entity->getLongText());
212221
}
213222

223+
public function testSetterConvertsDatetime() {
224+
$entity = new TestEntity();
225+
$entity->setDatetime('2024-08-19 15:26:00');
226+
$this->assertEquals(new \DateTimeImmutable('2024-08-19 15:26:00'), $entity->getDatetime());
227+
}
228+
229+
public function testSetterDoesNotConvertNullOnDatetime() {
230+
$entity = new TestEntity();
231+
$entity->setDatetime(null);
232+
$this->assertNull($entity->getDatetime());
233+
}
234+
235+
public function testSetterConvertsTime() {
236+
$entity = new TestEntity();
237+
$entity->setTime('15:26:00');
238+
$this->assertEquals(new \DateTime('15:26:00'), $entity->getTime());
239+
}
214240

215241
public function testGetFieldTypes() {
216242
$entity = new TestEntity();
217243
$this->assertEquals([
218-
'id' => 'integer',
219-
'testId' => 'integer',
244+
'id' => Types::INTEGER,
245+
'testId' => Types::INTEGER,
220246
'trueOrFalse' => 'bool',
221-
'anotherBool' => 'boolean',
222-
'longText' => 'blob',
247+
'anotherBool' => Types::BOOLEAN,
248+
'longText' => Types::BLOB,
249+
'time' => Types::TIME,
250+
'datetime' => Types::DATETIME_IMMUTABLE,
223251
], $entity->getFieldTypes());
224252
}
225253

226254

227255
public function testGetItInt() {
228256
$entity = new TestEntity();
229257
$entity->setId(3);
230-
$this->assertEquals('integer', gettype($entity->getId()));
258+
$this->assertEquals(Types::INTEGER, gettype($entity->getId()));
231259
}
232260

233261

0 commit comments

Comments
 (0)