Skip to content

Commit 025069d

Browse files
authored
Merge pull request #138 from Kharhamel/datetime
implemented safe versions of the classes DateTime and DateTimeImmutable
2 parents 277af72 + 9084748 commit 025069d

File tree

8 files changed

+476
-4
lines changed

8 files changed

+476
-4
lines changed

CONTRIBUTING.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,13 @@ $ cd generator
3636
$ php ./safe.php generate
3737
```
3838

39+
## Special cases
40+
41+
In some cases, automatic generation is too difficult to execute and the function has to be written manually.
42+
This should however only be done exceptionally in order to keep the project easy to maintain.
43+
The most important examples are all the functions of the classes `DateTime` and `DateTimeImmutable`, since the entire classes have to be overloaded manually.
44+
All custom objects must be located in lib/ and custom functions must be in lib/special_cases.php.
45+
3946
### Submitting a PR
4047

4148
The continuous integration hooks will regenerate all the functions and check that the result is exactly what has been

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,9 @@ $content = file_get_contents('foobar.json');
5959
$foobar = json_decode($content);
6060
```
6161

62+
All PHP functions that can return `false` on error are part of Safe.
63+
In addition, Safe also provide 2 'Safe' classes: `Safe\DateTime` and `Safe\DateTimeImmutable` whose methods will throw exceptions instead of returning false.
64+
6265
## PHPStan integration
6366

6467
> Yeah... but I must explicitly think about importing the "safe" variant of the function, for each and every file of my application.

generated/apcu.php

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -53,14 +53,15 @@ function apcu_cas(string $key, int $old, int $new): void
5353
* @param int $step The step, or value to decrease.
5454
* @param bool $success Optionally pass the success or fail boolean value to
5555
* this referenced variable.
56+
* @param int $ttl TTL to use if the operation inserts a new value (rather than decrementing an existing one).
5657
* @return int Returns the current value of key's value on success
5758
* @throws ApcuException
5859
*
5960
*/
60-
function apcu_dec(string $key, int $step = 1, ?bool &$success = null): int
61+
function apcu_dec(string $key, int $step = 1, ?bool &$success = null, int $ttl = 0): int
6162
{
6263
error_clear_last();
63-
$result = \apcu_dec($key, $step, $success);
64+
$result = \apcu_dec($key, $step, $success, $ttl);
6465
if ($result === false) {
6566
throw ApcuException::createFromPhpError();
6667
}
@@ -96,14 +97,15 @@ function apcu_delete($key): void
9697
* @param int $step The step, or value to increase.
9798
* @param bool $success Optionally pass the success or fail boolean value to
9899
* this referenced variable.
100+
* @param int $ttl TTL to use if the operation inserts a new value (rather than incrementing an existing one).
99101
* @return int Returns the current value of key's value on success
100102
* @throws ApcuException
101103
*
102104
*/
103-
function apcu_inc(string $key, int $step = 1, ?bool &$success = null): int
105+
function apcu_inc(string $key, int $step = 1, ?bool &$success = null, int $ttl = 0): int
104106
{
105107
error_clear_last();
106-
$result = \apcu_inc($key, $step, $success);
108+
$result = \apcu_inc($key, $step, $success, $ttl);
107109
if ($result === false) {
108110
throw ApcuException::createFromPhpError();
109111
}
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
<?php
2+
3+
4+
namespace Safe;
5+
6+
7+
use PHPUnit\Framework\TestCase;
8+
use Safe\Exceptions\DatetimeException;
9+
10+
class DateTimeImmutableTest extends TestCase
11+
{
12+
protected function setUp()
13+
{
14+
require_once __DIR__ . '/../../lib/Exceptions/SafeExceptionInterface.php';
15+
require_once __DIR__ . '/../../lib/Exceptions/AbstractSafeException.php';
16+
require_once __DIR__ . '/../../generated/Exceptions/DatetimeException.php';
17+
require_once __DIR__ . '/../../lib/DateTimeImmutable.php';
18+
}
19+
20+
public function testCreateFromFormatCrashOnError(): void
21+
{
22+
$this->expectException(DatetimeException::class);
23+
$datetime = DateTimeImmutable::createFromFormat('lol', 'super');
24+
}
25+
26+
public function testConstructorPreserveTimeAndTimezone(): void
27+
{
28+
$timezone = new \DateTimeZone('Pacific/Chatham');
29+
$datetime = new DateTimeImmutable('now', $timezone);
30+
$this->assertInstanceOf(DateTimeImmutable::class, $datetime);
31+
$this->assertEquals($timezone, $datetime->getTimezone());
32+
}
33+
34+
public function testCreateFromFormatPreserveTimeAndTimezone(): void
35+
{
36+
$timezone = new \DateTimeZone('Pacific/Chatham');
37+
$datetime = DateTimeImmutable::createFromFormat('d-m-Y', '20-03-2006', $timezone);
38+
$this->assertInstanceOf(DateTimeImmutable::class, $datetime);
39+
$this->assertEquals('20-03-2006', $datetime->format('d-m-Y'));
40+
$this->assertEquals($timezone, $datetime->getTimezone());
41+
}
42+
43+
public function testSafeDatetimeImmutableIsImmutable(): void
44+
{
45+
$datetime1 = new DateTimeImmutable();
46+
$datetime2 = $datetime1->add(new \DateInterval('P1W'));
47+
48+
$this->assertNotSame($datetime1, $datetime2);
49+
}
50+
51+
public function testSetDate(): void
52+
{
53+
$datetime = new \DateTimeImmutable();
54+
$safeDatetime = new DateTimeImmutable();
55+
$datetime = $datetime->setDate(2017, 4, 6);
56+
$safeDatetime = $safeDatetime->setDate(2017, 4, 6);
57+
$this->assertInstanceOf(DateTimeImmutable::class, $safeDatetime);
58+
$this->assertEquals($datetime->format('Y-m-d'), $safeDatetime->format('Y-m-d'));
59+
}
60+
61+
public function testSetIsoDate(): void
62+
{
63+
$datetime = new \DateTimeImmutable();
64+
$safeDatetime = new DateTimeImmutable();
65+
$datetime = $datetime->setISODate(2017, 4, 6);
66+
$safeDatetime = $safeDatetime->setISODate(2017, 4, 6);
67+
$this->assertInstanceOf(DateTimeImmutable::class, $safeDatetime);
68+
$this->assertEquals($datetime->format('Y-m-d'), $safeDatetime->format('Y-m-d'));
69+
}
70+
71+
public function testModify(): void
72+
{
73+
$datetime = new \DateTimeImmutable();
74+
$datetime = $datetime->setDate(2017, 4, 6);
75+
$datetime = $datetime->modify('+1 day');
76+
$safeDatime = new DateTimeImmutable();
77+
$safeDatime = $safeDatime->setDate(2017, 4, 6);
78+
$safeDatime = $safeDatime->modify('+1 day');
79+
$this->assertInstanceOf(DateTimeImmutable::class, $safeDatime);
80+
$this->assertEquals($datetime->format('j-n-Y'), $safeDatime->format('j-n-Y'));
81+
}
82+
83+
public function testSetTimestamp(): void
84+
{
85+
$datetime = new \DateTimeImmutable('2000-01-01');
86+
$safeDatime = new DateTimeImmutable('2000-01-01');
87+
$datetime = $datetime = $datetime->setTimestamp(12);
88+
$safeDatime = $safeDatime->setTimestamp(12);
89+
90+
$this->assertEquals($datetime->getTimestamp(), $safeDatime->getTimestamp());
91+
}
92+
93+
public function testSetTimezone(): void
94+
{
95+
$timezone = new \DateTimeZone('Pacific/Chatham');
96+
$datetime = new \DateTimeImmutable('2000-01-01');
97+
$safeDatime = new DateTimeImmutable('2000-01-01');
98+
$datetime = $datetime->setTimezone($timezone);
99+
$safeDatime = $safeDatime->setTimezone($timezone);
100+
101+
$this->assertEquals($datetime->getTimezone(), $safeDatime->getTimezone());
102+
}
103+
104+
public function testSetTime(): void
105+
{
106+
$datetime = new \DateTimeImmutable('2000-01-01');
107+
$safeDatime = new DateTimeImmutable('2000-01-01');
108+
$datetime = $datetime->setTime(2, 3, 1, 5);
109+
$safeDatime = $safeDatime->setTime(2, 3, 1, 5);
110+
111+
$this->assertEquals($datetime->format('H-i-s-u'), $safeDatime->format('H-i-s-u'));
112+
}
113+
114+
public function testAdd(): void
115+
{
116+
$interval = new \DateInterval('P1M');
117+
$datetime = new \DateTimeImmutable('2000-01-01');
118+
$safeDatime = new DateTimeImmutable('2000-01-01');
119+
$datetime = $datetime->add($interval);
120+
$safeDatime = $safeDatime->add($interval);
121+
122+
$this->assertEquals($datetime->getTimestamp(), $safeDatime->getTimestamp());
123+
}
124+
125+
public function testSub(): void
126+
{
127+
$interval = new \DateInterval('P1M');
128+
$datetime = new \DateTimeImmutable('2000-01-01');
129+
$safeDatime = new DateTimeImmutable('2000-01-01');
130+
$datetime = $datetime->sub($interval);
131+
$safeDatime = $safeDatime->sub($interval);
132+
133+
$this->assertEquals($datetime->getTimestamp(), $safeDatime->getTimestamp());
134+
}
135+
136+
public function testSerialize()
137+
{
138+
$timezone = new \DateTimeZone('Pacific/Chatham');
139+
$safeDatetime = DateTimeImmutable::createFromFormat('d-m-Y', '20-03-2006', $timezone);
140+
/** @var DateTimeImmutable $newDatetime */
141+
$newDatetime = unserialize(serialize($safeDatetime));
142+
143+
$this->assertEquals($safeDatetime->getTimestamp(), $newDatetime->getTimestamp());
144+
$this->assertEquals($safeDatetime->getTimezone(), $newDatetime->getTimezone());
145+
}
146+
}

generator/tests/DateTimeTest.php

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
<?php
2+
3+
4+
namespace Safe;
5+
6+
7+
use PHPUnit\Framework\TestCase;
8+
use Safe\Exceptions\DatetimeException;
9+
10+
class DateTimeTest extends TestCase
11+
{
12+
protected function setUp()
13+
{
14+
require_once __DIR__ . '/../../lib/Exceptions/SafeExceptionInterface.php';
15+
require_once __DIR__ . '/../../lib/Exceptions/AbstractSafeException.php';
16+
require_once __DIR__ . '/../../generated/Exceptions/DatetimeException.php';
17+
require_once __DIR__ . '/../../lib/DateTime.php';
18+
}
19+
20+
public function testSafeDatetimeCrashOnError(): void
21+
{
22+
$this->expectException(DatetimeException::class);
23+
$datetime = DateTime::createFromFormat('lol', 'super');
24+
}
25+
26+
public function testCreateFromFormatPreserveTimeAndTimezone(): void
27+
{
28+
$timezone = new \DateTimeZone('Pacific/Chatham');
29+
$datetime = DateTime::createFromFormat('d-m-Y', '20-03-2006', $timezone);
30+
$this->assertInstanceOf(DateTime::class, $datetime);
31+
$this->assertEquals('20-03-2006', $datetime->format('d-m-Y'));
32+
$this->assertEquals($timezone, $datetime->getTimezone());
33+
}
34+
35+
public function testSetDate(): void
36+
{
37+
$datetime = new DateTime();
38+
$datetime = $datetime->setDate(2017, 4, 6);
39+
$this->assertInstanceOf(DateTime::class, $datetime);
40+
$this->assertEquals(2017, $datetime->format('Y'));
41+
$this->assertEquals(4, $datetime->format('n'));
42+
$this->assertEquals(6, $datetime->format('j'));
43+
44+
//todo: test an error case
45+
}
46+
47+
public function testModify(): void
48+
{
49+
$datetime = new DateTime();
50+
$datetime = $datetime->setDate(2017, 4, 6);
51+
$datetime = $datetime->modify('+1 day');
52+
$this->assertInstanceOf(DateTime::class, $datetime);
53+
$this->assertEquals('7-4-2017', $datetime->format('j-n-Y'));
54+
}
55+
}

generator/tests/SpecialCasesTest.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
class SpecialCasesTest extends TestCase
99
{
10+
1011
public function testPregReplace()
1112
{
1213
require_once __DIR__.'/../../lib/special_cases.php';

lib/DateTime.php

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
<?php
2+
3+
namespace Safe;
4+
5+
use DateInterval;
6+
use DateTimeInterface;
7+
use DateTimeZone;
8+
use Safe\Exceptions\DatetimeException;
9+
10+
/** this class implements a safe version of the Datetime class */
11+
class DateTime extends \DateTime
12+
{
13+
//switch from regular datetime to safe version
14+
private static function createFromRegular(\DateTime $datetime): self
15+
{
16+
return new self($datetime->format('Y-m-d H:i:s'), $datetime->getTimezone());
17+
}
18+
19+
/**
20+
* @param string $format
21+
* @param string $time
22+
* @param DateTimeZone|null $timezone
23+
*/
24+
public static function createFromFormat($format, $time, $timezone = null): self
25+
{
26+
$datetime = parent::createFromFormat($format, $time, $timezone);
27+
if ($datetime === false) {
28+
throw DatetimeException::createFromPhpError();
29+
}
30+
return self::createFromRegular($datetime);
31+
}
32+
33+
/**
34+
* @param DateTimeInterface $datetime2 The date to compare to.
35+
* @param boolean $absolute [optional] Whether to return absolute difference.
36+
* @return DateInterval The DateInterval object representing the difference between the two dates.
37+
*/
38+
public function diff($datetime2, $absolute = false): DateInterval
39+
{
40+
/** @var \DateInterval|false $result */
41+
$result = parent::diff($datetime2, $absolute);
42+
if ($result === false) {
43+
throw DatetimeException::createFromPhpError();
44+
}
45+
return $result;
46+
}
47+
48+
/**
49+
* @param string $modify A date/time string. Valid formats are explained in <a href="https://secure.php.net/manual/en/datetime.formats.php">Date and Time Formats</a>.
50+
* @return DateTime Returns the DateTime object for method chaining.
51+
*/
52+
public function modify($modify): self
53+
{
54+
/** @var DateTime|false $result */
55+
$result = parent::modify($modify);
56+
if ($result === false) {
57+
throw DatetimeException::createFromPhpError();
58+
}
59+
return $result;
60+
}
61+
62+
/**
63+
* @param int $year
64+
* @param int $month
65+
* @param int $day
66+
* @return DateTime
67+
*/
68+
public function setDate($year, $month, $day): self
69+
{
70+
/** @var DateTime|false $result */
71+
$result = parent::setDate($year, $month, $day);
72+
if ($result === false) {
73+
throw DatetimeException::createFromPhpError();
74+
}
75+
return $result;
76+
}
77+
}

0 commit comments

Comments
 (0)