Skip to content

Commit 635a4f8

Browse files
committed
rewrote safe\DatetimeImmutable to use an inner datetimeImmutable
1 parent 9f1c30c commit 635a4f8

File tree

6 files changed

+184
-48
lines changed

6 files changed

+184
-48
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 to 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 example are all the functions of the classes DateTime and DateTimeImmutable, since the entire classes have to be overloaded.
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.

generator/tests/DateTimeImmutableTest.php

Lines changed: 104 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,21 @@ protected function setUp()
1717
require_once __DIR__ . '/../../lib/DateTimeImmutable.php';
1818
}
1919

20-
public function testSafeDatetimeImmutableCrashOnError(): void
20+
public function testCreateFromFormatCrashOnError(): void
2121
{
2222
$this->expectException(DatetimeException::class);
2323
$datetime = DateTimeImmutable::createFromFormat('lol', 'super');
2424
}
2525

26-
public function testSafeDatetimeImmutablePreserveTimeAndTimezone(): void
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
2735
{
2836
$timezone = new \DateTimeZone('Pacific/Chatham');
2937
$datetime = DateTimeImmutable::createFromFormat('d-m-Y', '20-03-2006', $timezone);
@@ -32,24 +40,107 @@ public function testSafeDatetimeImmutablePreserveTimeAndTimezone(): void
3240
$this->assertEquals($timezone, $datetime->getTimezone());
3341
}
3442

35-
public function testSafeDatetimeImmutableSetDate(): void
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
3652
{
37-
$datetime = new DateTimeImmutable();
53+
$datetime = new \DateTimeImmutable();
54+
$safeDatetime = new DateTimeImmutable();
3855
$datetime = $datetime->setDate(2017, 4, 6);
39-
$this->assertInstanceOf(DateTimeImmutable::class, $datetime);
40-
$this->assertEquals(2017, $datetime->format('Y'));
41-
$this->assertEquals(4, $datetime->format('n'));
42-
$this->assertEquals(6, $datetime->format('j'));
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+
}
4360

44-
//todo: test an error case
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'));
4569
}
4670

47-
public function testSafeDatetimeImmutableModify(): void
71+
public function testModify(): void
4872
{
49-
$datetime = new DateTimeImmutable();
73+
$datetime = new \DateTimeImmutable();
5074
$datetime = $datetime->setDate(2017, 4, 6);
5175
$datetime = $datetime->modify('+1 day');
52-
$this->assertInstanceOf(DateTimeImmutable::class, $datetime);
53-
$this->assertEquals('7-4-2017', $datetime->format('j-n-Y'));
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());
54145
}
55146
}

generator/tests/DateTimeTest.php

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ public function testSafeDatetimeCrashOnError(): void
2323
$datetime = DateTime::createFromFormat('lol', 'super');
2424
}
2525

26-
public function testSafeDatetimePreserveTimeAndTimezone(): void
26+
public function testCreateFromFormatPreserveTimeAndTimezone(): void
2727
{
2828
$timezone = new \DateTimeZone('Pacific/Chatham');
2929
$datetime = DateTime::createFromFormat('d-m-Y', '20-03-2006', $timezone);
@@ -32,7 +32,7 @@ public function testSafeDatetimePreserveTimeAndTimezone(): void
3232
$this->assertEquals($timezone, $datetime->getTimezone());
3333
}
3434

35-
public function testSafeDatetimeSetDate(): void
35+
public function testSetDate(): void
3636
{
3737
$datetime = new DateTime();
3838
$datetime = $datetime->setDate(2017, 4, 6);
@@ -44,7 +44,7 @@ public function testSafeDatetimeSetDate(): void
4444
//todo: test an error case
4545
}
4646

47-
public function testSafeDatetimeModify(): void
47+
public function testModify(): void
4848
{
4949
$datetime = new DateTime();
5050
$datetime = $datetime->setDate(2017, 4, 6);

lib/DateTime.php

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,12 @@ private static function createFromRegular(\DateTime $datetime): self
1616
return new self($datetime->format('Y-m-d H:i:s'), $datetime->getTimezone());
1717
}
1818

19-
public static function createFromFormat($format, $time, DateTimeZone $timezone = null): self
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
2025
{
2126
$datetime = parent::createFromFormat($format, $time, $timezone);
2227
if ($datetime === false) {

lib/DateTimeImmutable.php

Lines changed: 61 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -8,16 +8,41 @@
88
use DateTimeZone;
99
use Safe\Exceptions\DatetimeException;
1010

11-
//this class is used to implement a safe version of the DatetimeImmutable class
11+
/**
12+
* This class is used to implement a safe version of the DatetimeImmutable class.
13+
* While it technically overloads \DateTimeImmutable for typehint compatibility,
14+
* it is actually used as a wrapper of \DateTimeImmutable, mostly to be able to overwrite functions like getTimestamp() while still being able to edit milliseconds via setTime().
15+
*/
1216
class DateTimeImmutable extends \DateTimeImmutable
1317
{
18+
/**
19+
* @var \DateTimeImmutable
20+
*/
21+
private $innerDateTime;
22+
23+
public function __construct($time = 'now', $timezone = null)
24+
{
25+
parent::__construct();
26+
$this->innerDateTime = new parent($time, $timezone);
27+
}
28+
1429
//switch from regular datetime to safe version
1530
private static function createFromRegular(\DateTimeImmutable $datetime): self
1631
{
17-
return new self($datetime->format('Y-m-d H:i:s'), $datetime->getTimezone());
32+
$safeDatetime = new self();
33+
$safeDatetime->innerDateTime = $datetime;
34+
return $safeDatetime;
1835
}
1936

20-
public static function createFromFormat($format, $time, DateTimeZone $timezone = null): self
37+
/////////////////////////////////////////////////////////////////////////////
38+
// overload functions with false errors
39+
40+
/**
41+
* @param string $format
42+
* @param string $time
43+
* @param DateTimeZone|null $timezone
44+
*/
45+
public static function createFromFormat($format, $time, $timezone = null): self
2146
{
2247
$datetime = parent::createFromFormat($format, $time, $timezone);
2348
if ($datetime === false) {
@@ -28,54 +53,38 @@ public static function createFromFormat($format, $time, DateTimeZone $timezone =
2853

2954
public function format($format): string
3055
{
31-
$result = parent::format($format);
56+
/** @var string|false $result */
57+
$result = $this->innerDateTime->format($format);
3258
if ($result === false) {
3359
throw DatetimeException::createFromPhpError();
3460
}
3561
return $result;
3662
}
3763

38-
/**
39-
* @param DateTimeInterface $datetime2 <p>The date to compare to.</p>
40-
* @param bool $absolute [optional] <p>Should the interval be forced to be positive?</p>
41-
* @return DateInterval
42-
*/
4364
public function diff($datetime2, $absolute = false): DateInterval
4465
{
4566
/** @var \DateInterval|false $result */
46-
$result = parent::diff($datetime2, $absolute);
67+
$result = $this->innerDateTime->diff($datetime2, $absolute);
4768
if ($result === false) {
4869
throw DatetimeException::createFromPhpError();
4970
}
5071
return $result;
5172
}
5273

53-
/**
54-
* @param string $modify <p>A date/time string. Valid formats are explained in
55-
* {@link https://secure.php.net/manual/en/datetime.formats.php Date and Time Formats}.</p>
56-
* @return DateTimeImmutable
57-
*/
5874
public function modify($modify): self
5975
{
6076
/** @var \DateTimeImmutable|false $result */
61-
$result = parent::modify($modify);
77+
$result = $this->innerDateTime->modify($modify);
6278
if ($result === false) {
6379
throw DatetimeException::createFromPhpError();
6480
}
6581
return self::createFromRegular($result); //we have to recreate a safe datetime because modify create a new instance of \DateTimeImmutable
6682
}
6783

68-
/**
69-
* @param int $year <p>Year of the date.</p>
70-
* @param int $month <p>Month of the date.</p>
71-
* @param int $day <p>Day of the date.</p>
72-
* @return DateTimeImmutable
73-
*
74-
*/
7584
public function setDate($year, $month, $day): self
7685
{
7786
/** @var \DateTimeImmutable|false $result */
78-
$result = parent::setDate($year, $month, $day);
87+
$result = $this->innerDateTime->setDate($year, $month, $day);
7988
if ($result === false) {
8089
throw DatetimeException::createFromPhpError();
8190
}
@@ -85,7 +94,7 @@ public function setDate($year, $month, $day): self
8594
public function setISODate($year, $week, $day = 1): self
8695
{
8796
/** @var \DateTimeImmutable|false $result */
88-
$result = parent::setISODate($year, $week, $day);
97+
$result = $this->innerDateTime->setISODate($year, $week, $day);
8998
if ($result === false) {
9099
throw DatetimeException::createFromPhpError();
91100
}
@@ -95,7 +104,7 @@ public function setISODate($year, $week, $day = 1): self
95104
public function setTime($hour, $minute, $second = 0, $microseconds = 0): self
96105
{
97106
/** @var \DateTimeImmutable|false $result */
98-
$result = parent::setTime($hour, $minute, $second, $microseconds);
107+
$result = $this->innerDateTime->setTime($hour, $minute, $second, $microseconds);
99108
if ($result === false) {
100109
throw DatetimeException::createFromPhpError();
101110
}
@@ -105,7 +114,7 @@ public function setTime($hour, $minute, $second = 0, $microseconds = 0): self
105114
public function setTimestamp($unixtimestamp): self
106115
{
107116
/** @var \DateTimeImmutable|false $result */
108-
$result = parent::setTimestamp($unixtimestamp);
117+
$result = $this->innerDateTime->setTimestamp($unixtimestamp);
109118
if ($result === false) {
110119
throw DatetimeException::createFromPhpError();
111120
}
@@ -115,7 +124,7 @@ public function setTimestamp($unixtimestamp): self
115124
public function setTimezone($timezone): self
116125
{
117126
/** @var \DateTimeImmutable|false $result */
118-
$result = parent::setTimezone($timezone);
127+
$result = $this->innerDateTime->setTimezone($timezone);
119128
if ($result === false) {
120129
throw DatetimeException::createFromPhpError();
121130
}
@@ -125,18 +134,29 @@ public function setTimezone($timezone): self
125134
public function sub($interval): self
126135
{
127136
/** @var \DateTimeImmutable|false $result */
128-
$result = parent::sub($interval);
137+
$result = $this->innerDateTime->sub($interval);
129138
if ($result === false) {
130139
throw DatetimeException::createFromPhpError();
131140
}
132141
return self::createFromRegular($result);
133142
}
134143

135-
//theses functions are overload to actually return a safe instance, since datetimeimmutable re-instante itself
144+
public function getOffset(): int
145+
{
146+
/** @var int|false $result */
147+
$result = $this->innerDateTime->getOffset();
148+
if ($result === false) {
149+
throw DatetimeException::createFromPhpError();
150+
}
151+
return $result;
152+
}
153+
154+
//////////////////////////////////////////////////////////////////////////////////////////
155+
//overload getters to use the inner datetime immutable instead of itself
136156

137157
public function add($interval): self
138158
{
139-
return self::createFromRegular(parent::add($interval));
159+
return self::createFromRegular($this->innerDateTime->add($interval));
140160
}
141161

142162
public static function createFromMutable($dateTime): self
@@ -148,4 +168,14 @@ public static function __set_state(array $array): self
148168
{
149169
return self::createFromRegular(parent::__set_state($array));
150170
}
171+
172+
public function getTimezone(): DateTimeZone
173+
{
174+
return $this->innerDateTime->getTimezone();
175+
}
176+
177+
public function getTimestamp(): int
178+
{
179+
return $this->innerDateTime->getTimestamp();
180+
}
151181
}

0 commit comments

Comments
 (0)