Skip to content

Commit 8502a7c

Browse files
committed
Time type complete support #9967
A time in DB uses the `TIME` native MariaDB type, with a precision to the seconds. However, the GraphQL time type is meant to be more human friendly and is only precise to the minute. It also accepts a few formats as input. That should allow the client code to directly forward whatever the human is typing.
1 parent fab7617 commit 8502a7c

File tree

5 files changed

+128
-13
lines changed

5 files changed

+128
-13
lines changed

src/Api/Scalar/TimeType.php

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,15 +14,15 @@
1414

1515
final class TimeType extends ScalarType
1616
{
17-
public ?string $description = 'A time of the day (local time, no timezone).';
17+
public ?string $description = 'A time of the day including only hour and minutes (local time, no timezone). Accepted formats are "14h35", "14:35" or "14h".';
1818

1919
/**
2020
* Serializes an internal value to include in a response.
2121
*/
2222
public function serialize(mixed $value): mixed
2323
{
2424
if ($value instanceof ChronosTime) {
25-
return $value->format('H:i:s.u');
25+
return $value->format('H\hi');
2626
}
2727

2828
return $value;
@@ -41,6 +41,11 @@ public function parseValue(mixed $value): ?ChronosTime
4141
return null;
4242
}
4343

44+
if (!preg_match('~^(?<hour>\d{1,2})(([h:]$)|([h:](?<minute>\d{1,2}))?$)~', trim($value), $m)) {
45+
throw new UnexpectedValueException('Invalid format Chronos time. Expected "14h35", "14:35" or "14h", but got: ' . Utils::printSafe($value));
46+
}
47+
48+
$value = $m['hour'] . ':' . ($m['minute'] ?? '00');
4449
$time = new ChronosTime($value);
4550

4651
return $time;

src/DBAL/Types/TimeType.php

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,20 +5,44 @@
55
namespace Ecodev\Felix\DBAL\Types;
66

77
use Cake\Chronos\ChronosTime;
8-
use DateTimeInterface;
98
use Doctrine\DBAL\Platforms\AbstractPlatform;
9+
use Doctrine\DBAL\Types\ConversionException;
1010

1111
final class TimeType extends \Doctrine\DBAL\Types\TimeType
1212
{
1313
/**
14-
* @param null|ChronosTime|DateTimeInterface|string $value
14+
* @return ($value is null ? null : string)
15+
*/
16+
public function convertToDatabaseValue(mixed $value, AbstractPlatform $platform): ?string
17+
{
18+
if ($value === null) {
19+
return $value;
20+
}
21+
22+
if ($value instanceof ChronosTime) {
23+
return $value->format($platform->getTimeFormatString());
24+
}
25+
26+
throw ConversionException::conversionFailedInvalidType($value, $this->getName(), ['null', 'ChronosTime']);
27+
}
28+
29+
/**
30+
* @return ($value is null ? null : ChronosTime)
1531
*/
1632
public function convertToPHPValue(mixed $value, AbstractPlatform $platform): ?ChronosTime
1733
{
1834
if ($value === null || $value instanceof ChronosTime) {
1935
return $value;
2036
}
2137

38+
if (!is_string($value)) {
39+
throw ConversionException::conversionFailedFormat(
40+
$value,
41+
$this->getName(),
42+
$platform->getTimeFormatString(),
43+
);
44+
}
45+
2246
$val = new ChronosTime($value);
2347

2448
return $val;

tests/Api/Scalar/ChronosTypeTest.php

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ public function testSerialize(): void
3434
}
3535

3636
/**
37-
* @dataProvider providerValue
37+
* @dataProvider providerValues
3838
*/
3939
public function testParseValue(string $input, ?string $expected): void
4040
{
@@ -48,7 +48,7 @@ public function testParseValue(string $input, ?string $expected): void
4848
}
4949

5050
/**
51-
* @dataProvider providerValue
51+
* @dataProvider providerValues
5252
*/
5353
public function testParseLiteral(string $input, ?string $expected): void
5454
{
@@ -72,7 +72,7 @@ public function testParseLiteralAsInt(): void
7272
$type->parseLiteral($ast);
7373
}
7474

75-
public static function providerValue(): array
75+
public static function providerValues(): array
7676
{
7777
return [
7878
'UTC' => ['2018-09-14T22:00:00.000Z', '2018-09-15T00:00:00+02:00'],

tests/Api/Scalar/TimeTypeTest.php

Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,26 @@ public function testSerialize(): void
1717
$type = new TimeType();
1818
$time = new ChronosTime('14:30:25');
1919
$actual = $type->serialize($time);
20-
self::assertSame('14:30:25.000000', $actual);
20+
self::assertSame('14h30', $actual);
2121

2222
// Test serialize with microseconds
2323
$time = new ChronosTime('23:59:59.1254');
2424
$actual = $type->serialize($time);
25-
self::assertSame('23:59:59.001254', $actual);
25+
self::assertSame('23h59', $actual);
26+
}
27+
28+
/**
29+
* @dataProvider providerValues
30+
*/
31+
public function testParseValue(string $input, ?string $expected): void
32+
{
33+
$type = new TimeType();
34+
$actual = $type->parseValue($input);
35+
if ($actual) {
36+
$actual = $actual->__toString();
37+
}
38+
39+
self::assertSame($expected, $actual);
2640
}
2741

2842
/**
@@ -34,8 +48,11 @@ public function testParseLiteral(string $input, ?string $expected): void
3448
$ast = new StringValueNode(['value' => $input]);
3549

3650
$actual = $type->parseLiteral($ast);
37-
self::assertInstanceOf(ChronosTime::class, $actual);
38-
self::assertSame($expected, $actual->format('H:i:s.u'));
51+
if ($actual) {
52+
$actual = $actual->__toString();
53+
}
54+
55+
self::assertSame($expected, $actual);
3956
}
4057

4158
public function testParseLiteralAsInt(): void
@@ -50,8 +67,14 @@ public function testParseLiteralAsInt(): void
5067
public static function providerValues(): array
5168
{
5269
return [
53-
'normal timr' => ['14:30:25', '14:30:25.000000'],
54-
'time with milliseconds' => ['23:45:13.300', '23:45:13.000300'],
70+
'empty string' => ['', null],
71+
'normal time' => ['14:30', '14:30:00'],
72+
'alternative separator' => ['14h30', '14:30:00'],
73+
'only hour' => ['14h', '14:00:00'],
74+
'only hour alternative' => ['14:', '14:00:00'],
75+
'even shorter' => ['9', '09:00:00'],
76+
'spaces are fines' => [' 14h00 ', '14:00:00'],
77+
'a bit weird, but why not' => [' 14h6 ', '14:06:00'],
5578
];
5679
}
5780
}

tests/DBAL/Types/TimeTypeTest.php

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace EcodevTests\Felix\DBAL\Types;
6+
7+
use Cake\Chronos\ChronosTime;
8+
use Doctrine\DBAL\Platforms\AbstractPlatform;
9+
use Doctrine\DBAL\Platforms\MySQLPlatform;
10+
use Doctrine\DBAL\Types\ConversionException;
11+
use Ecodev\Felix\DBAL\Types\TimeType;
12+
use PHPUnit\Framework\TestCase;
13+
14+
class TimeTypeTest extends TestCase
15+
{
16+
private TimeType $type;
17+
18+
private AbstractPlatform $platform;
19+
20+
protected function setUp(): void
21+
{
22+
$this->type = new TimeType();
23+
$this->platform = new MySQLPlatform();
24+
}
25+
26+
public function testConvertToDatabaseValue(): void
27+
{
28+
self::assertSame('TIME', $this->type->getSqlDeclaration(['foo'], $this->platform));
29+
self::assertFalse($this->type->requiresSQLCommentHint($this->platform));
30+
31+
$actual = $this->type->convertToDatabaseValue(new ChronosTime('09:33'), $this->platform);
32+
self::assertSame('09:33:00', $actual, 'support Chronos');
33+
34+
self::assertNull($this->type->convertToDatabaseValue(null, $this->platform), 'support null values');
35+
}
36+
37+
public function testConvertToPHPValue(): void
38+
{
39+
$actualPhp = $this->type->convertToPHPValue('18:59:23', $this->platform);
40+
self::assertInstanceOf(ChronosTime::class, $actualPhp);
41+
self::assertSame('18:59:23', $actualPhp->__toString(), 'support string');
42+
43+
$actualPhp = $this->type->convertToPHPValue(new ChronosTime('18:59:23'), $this->platform);
44+
self::assertInstanceOf(ChronosTime::class, $actualPhp);
45+
self::assertSame('18:59:23', $actualPhp->__toString(), 'support ChronosTime');
46+
47+
self::assertNull($this->type->convertToPHPValue(null, $this->platform), 'support null values');
48+
}
49+
50+
public function testConvertToPHPValueThrowsWithInvalidValue(): void
51+
{
52+
$this->expectException(ConversionException::class);
53+
54+
$this->type->convertToPHPValue(123, $this->platform);
55+
}
56+
57+
public function testConvertToDatabaseValueThrowsWithInvalidValue(): void
58+
{
59+
$this->expectException(ConversionException::class);
60+
61+
$this->type->convertToDatabaseValue(123, $this->platform);
62+
}
63+
}

0 commit comments

Comments
 (0)