diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ca233664..42f94240 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -36,6 +36,13 @@ $ cd generator $ php ./safe.php generate ``` +## Special cases + +In some cases, automatic generation is too difficult to execute and the function has to be written manually. +This should however only be done exceptionally in order to keep the project easy to maintain. +The most important examples are all the functions of the classes `DateTime` and `DateTimeImmutable`, since the entire classes have to be overloaded manually. +All custom objects must be located in lib/ and custom functions must be in lib/special_cases.php. + ### Submitting a PR The continuous integration hooks will regenerate all the functions and check that the result is exactly what has been diff --git a/README.md b/README.md index 20eec751..960ca2eb 100644 --- a/README.md +++ b/README.md @@ -59,6 +59,9 @@ $content = file_get_contents('foobar.json'); $foobar = json_decode($content); ``` +All PHP functions that can return `false` on error are part of Safe. +In addition, Safe also provide 2 'Safe' classes: `Safe\DateTime` and `Safe\DateTimeImmutable` whose methods will throw exceptions instead of returning false. + ## PHPStan integration > Yeah... but I must explicitly think about importing the "safe" variant of the function, for each and every file of my application. diff --git a/generated/apcu.php b/generated/apcu.php index 6c2094c5..fa0ef154 100644 --- a/generated/apcu.php +++ b/generated/apcu.php @@ -53,14 +53,15 @@ function apcu_cas(string $key, int $old, int $new): void * @param int $step The step, or value to decrease. * @param bool $success Optionally pass the success or fail boolean value to * this referenced variable. + * @param int $ttl TTL to use if the operation inserts a new value (rather than decrementing an existing one). * @return int Returns the current value of key's value on success * @throws ApcuException * */ -function apcu_dec(string $key, int $step = 1, ?bool &$success = null): int +function apcu_dec(string $key, int $step = 1, ?bool &$success = null, int $ttl = 0): int { error_clear_last(); - $result = \apcu_dec($key, $step, $success); + $result = \apcu_dec($key, $step, $success, $ttl); if ($result === false) { throw ApcuException::createFromPhpError(); } @@ -96,14 +97,15 @@ function apcu_delete($key): void * @param int $step The step, or value to increase. * @param bool $success Optionally pass the success or fail boolean value to * this referenced variable. + * @param int $ttl TTL to use if the operation inserts a new value (rather than incrementing an existing one). * @return int Returns the current value of key's value on success * @throws ApcuException * */ -function apcu_inc(string $key, int $step = 1, ?bool &$success = null): int +function apcu_inc(string $key, int $step = 1, ?bool &$success = null, int $ttl = 0): int { error_clear_last(); - $result = \apcu_inc($key, $step, $success); + $result = \apcu_inc($key, $step, $success, $ttl); if ($result === false) { throw ApcuException::createFromPhpError(); } diff --git a/generator/tests/DateTimeImmutableTest.php b/generator/tests/DateTimeImmutableTest.php new file mode 100644 index 00000000..d88ee7be --- /dev/null +++ b/generator/tests/DateTimeImmutableTest.php @@ -0,0 +1,146 @@ +expectException(DatetimeException::class); + $datetime = DateTimeImmutable::createFromFormat('lol', 'super'); + } + + public function testConstructorPreserveTimeAndTimezone(): void + { + $timezone = new \DateTimeZone('Pacific/Chatham'); + $datetime = new DateTimeImmutable('now', $timezone); + $this->assertInstanceOf(DateTimeImmutable::class, $datetime); + $this->assertEquals($timezone, $datetime->getTimezone()); + } + + public function testCreateFromFormatPreserveTimeAndTimezone(): void + { + $timezone = new \DateTimeZone('Pacific/Chatham'); + $datetime = DateTimeImmutable::createFromFormat('d-m-Y', '20-03-2006', $timezone); + $this->assertInstanceOf(DateTimeImmutable::class, $datetime); + $this->assertEquals('20-03-2006', $datetime->format('d-m-Y')); + $this->assertEquals($timezone, $datetime->getTimezone()); + } + + public function testSafeDatetimeImmutableIsImmutable(): void + { + $datetime1 = new DateTimeImmutable(); + $datetime2 = $datetime1->add(new \DateInterval('P1W')); + + $this->assertNotSame($datetime1, $datetime2); + } + + public function testSetDate(): void + { + $datetime = new \DateTimeImmutable(); + $safeDatetime = new DateTimeImmutable(); + $datetime = $datetime->setDate(2017, 4, 6); + $safeDatetime = $safeDatetime->setDate(2017, 4, 6); + $this->assertInstanceOf(DateTimeImmutable::class, $safeDatetime); + $this->assertEquals($datetime->format('Y-m-d'), $safeDatetime->format('Y-m-d')); + } + + public function testSetIsoDate(): void + { + $datetime = new \DateTimeImmutable(); + $safeDatetime = new DateTimeImmutable(); + $datetime = $datetime->setISODate(2017, 4, 6); + $safeDatetime = $safeDatetime->setISODate(2017, 4, 6); + $this->assertInstanceOf(DateTimeImmutable::class, $safeDatetime); + $this->assertEquals($datetime->format('Y-m-d'), $safeDatetime->format('Y-m-d')); + } + + public function testModify(): void + { + $datetime = new \DateTimeImmutable(); + $datetime = $datetime->setDate(2017, 4, 6); + $datetime = $datetime->modify('+1 day'); + $safeDatime = new DateTimeImmutable(); + $safeDatime = $safeDatime->setDate(2017, 4, 6); + $safeDatime = $safeDatime->modify('+1 day'); + $this->assertInstanceOf(DateTimeImmutable::class, $safeDatime); + $this->assertEquals($datetime->format('j-n-Y'), $safeDatime->format('j-n-Y')); + } + + public function testSetTimestamp(): void + { + $datetime = new \DateTimeImmutable('2000-01-01'); + $safeDatime = new DateTimeImmutable('2000-01-01'); + $datetime = $datetime = $datetime->setTimestamp(12); + $safeDatime = $safeDatime->setTimestamp(12); + + $this->assertEquals($datetime->getTimestamp(), $safeDatime->getTimestamp()); + } + + public function testSetTimezone(): void + { + $timezone = new \DateTimeZone('Pacific/Chatham'); + $datetime = new \DateTimeImmutable('2000-01-01'); + $safeDatime = new DateTimeImmutable('2000-01-01'); + $datetime = $datetime->setTimezone($timezone); + $safeDatime = $safeDatime->setTimezone($timezone); + + $this->assertEquals($datetime->getTimezone(), $safeDatime->getTimezone()); + } + + public function testSetTime(): void + { + $datetime = new \DateTimeImmutable('2000-01-01'); + $safeDatime = new DateTimeImmutable('2000-01-01'); + $datetime = $datetime->setTime(2, 3, 1, 5); + $safeDatime = $safeDatime->setTime(2, 3, 1, 5); + + $this->assertEquals($datetime->format('H-i-s-u'), $safeDatime->format('H-i-s-u')); + } + + public function testAdd(): void + { + $interval = new \DateInterval('P1M'); + $datetime = new \DateTimeImmutable('2000-01-01'); + $safeDatime = new DateTimeImmutable('2000-01-01'); + $datetime = $datetime->add($interval); + $safeDatime = $safeDatime->add($interval); + + $this->assertEquals($datetime->getTimestamp(), $safeDatime->getTimestamp()); + } + + public function testSub(): void + { + $interval = new \DateInterval('P1M'); + $datetime = new \DateTimeImmutable('2000-01-01'); + $safeDatime = new DateTimeImmutable('2000-01-01'); + $datetime = $datetime->sub($interval); + $safeDatime = $safeDatime->sub($interval); + + $this->assertEquals($datetime->getTimestamp(), $safeDatime->getTimestamp()); + } + + public function testSerialize() + { + $timezone = new \DateTimeZone('Pacific/Chatham'); + $safeDatetime = DateTimeImmutable::createFromFormat('d-m-Y', '20-03-2006', $timezone); + /** @var DateTimeImmutable $newDatetime */ + $newDatetime = unserialize(serialize($safeDatetime)); + + $this->assertEquals($safeDatetime->getTimestamp(), $newDatetime->getTimestamp()); + $this->assertEquals($safeDatetime->getTimezone(), $newDatetime->getTimezone()); + } +} \ No newline at end of file diff --git a/generator/tests/DateTimeTest.php b/generator/tests/DateTimeTest.php new file mode 100644 index 00000000..4e9db911 --- /dev/null +++ b/generator/tests/DateTimeTest.php @@ -0,0 +1,55 @@ +expectException(DatetimeException::class); + $datetime = DateTime::createFromFormat('lol', 'super'); + } + + public function testCreateFromFormatPreserveTimeAndTimezone(): void + { + $timezone = new \DateTimeZone('Pacific/Chatham'); + $datetime = DateTime::createFromFormat('d-m-Y', '20-03-2006', $timezone); + $this->assertInstanceOf(DateTime::class, $datetime); + $this->assertEquals('20-03-2006', $datetime->format('d-m-Y')); + $this->assertEquals($timezone, $datetime->getTimezone()); + } + + public function testSetDate(): void + { + $datetime = new DateTime(); + $datetime = $datetime->setDate(2017, 4, 6); + $this->assertInstanceOf(DateTime::class, $datetime); + $this->assertEquals(2017, $datetime->format('Y')); + $this->assertEquals(4, $datetime->format('n')); + $this->assertEquals(6, $datetime->format('j')); + + //todo: test an error case + } + + public function testModify(): void + { + $datetime = new DateTime(); + $datetime = $datetime->setDate(2017, 4, 6); + $datetime = $datetime->modify('+1 day'); + $this->assertInstanceOf(DateTime::class, $datetime); + $this->assertEquals('7-4-2017', $datetime->format('j-n-Y')); + } +} \ No newline at end of file diff --git a/generator/tests/SpecialCasesTest.php b/generator/tests/SpecialCasesTest.php index 35474214..707e1158 100644 --- a/generator/tests/SpecialCasesTest.php +++ b/generator/tests/SpecialCasesTest.php @@ -7,6 +7,7 @@ class SpecialCasesTest extends TestCase { + public function testPregReplace() { require_once __DIR__.'/../../lib/special_cases.php'; diff --git a/lib/DateTime.php b/lib/DateTime.php new file mode 100644 index 00000000..40cb7595 --- /dev/null +++ b/lib/DateTime.php @@ -0,0 +1,77 @@ +format('Y-m-d H:i:s'), $datetime->getTimezone()); + } + + /** + * @param string $format + * @param string $time + * @param DateTimeZone|null $timezone + */ + public static function createFromFormat($format, $time, $timezone = null): self + { + $datetime = parent::createFromFormat($format, $time, $timezone); + if ($datetime === false) { + throw DatetimeException::createFromPhpError(); + } + return self::createFromRegular($datetime); + } + + /** + * @param DateTimeInterface $datetime2 The date to compare to. + * @param boolean $absolute [optional] Whether to return absolute difference. + * @return DateInterval The DateInterval object representing the difference between the two dates. + */ + public function diff($datetime2, $absolute = false): DateInterval + { + /** @var \DateInterval|false $result */ + $result = parent::diff($datetime2, $absolute); + if ($result === false) { + throw DatetimeException::createFromPhpError(); + } + return $result; + } + + /** + * @param string $modify A date/time string. Valid formats are explained in Date and Time Formats. + * @return DateTime Returns the DateTime object for method chaining. + */ + public function modify($modify): self + { + /** @var DateTime|false $result */ + $result = parent::modify($modify); + if ($result === false) { + throw DatetimeException::createFromPhpError(); + } + return $result; + } + + /** + * @param int $year + * @param int $month + * @param int $day + * @return DateTime + */ + public function setDate($year, $month, $day): self + { + /** @var DateTime|false $result */ + $result = parent::setDate($year, $month, $day); + if ($result === false) { + throw DatetimeException::createFromPhpError(); + } + return $result; + } +} diff --git a/lib/DateTimeImmutable.php b/lib/DateTimeImmutable.php new file mode 100644 index 00000000..8537d56d --- /dev/null +++ b/lib/DateTimeImmutable.php @@ -0,0 +1,181 @@ +innerDateTime = new parent($time, $timezone); + } + + //switch from regular datetime to safe version + private static function createFromRegular(\DateTimeImmutable $datetime): self + { + $safeDatetime = new self(); + $safeDatetime->innerDateTime = $datetime; + return $safeDatetime; + } + + ///////////////////////////////////////////////////////////////////////////// + // overload functions with false errors + + /** + * @param string $format + * @param string $time + * @param DateTimeZone|null $timezone + */ + public static function createFromFormat($format, $time, $timezone = null): self + { + $datetime = parent::createFromFormat($format, $time, $timezone); + if ($datetime === false) { + throw DatetimeException::createFromPhpError(); + } + return self::createFromRegular($datetime); + } + + public function format($format): string + { + /** @var string|false $result */ + $result = $this->innerDateTime->format($format); + if ($result === false) { + throw DatetimeException::createFromPhpError(); + } + return $result; + } + + public function diff($datetime2, $absolute = false): DateInterval + { + /** @var \DateInterval|false $result */ + $result = $this->innerDateTime->diff($datetime2, $absolute); + if ($result === false) { + throw DatetimeException::createFromPhpError(); + } + return $result; + } + + public function modify($modify): self + { + /** @var \DateTimeImmutable|false $result */ + $result = $this->innerDateTime->modify($modify); + if ($result === false) { + throw DatetimeException::createFromPhpError(); + } + return self::createFromRegular($result); //we have to recreate a safe datetime because modify create a new instance of \DateTimeImmutable + } + + public function setDate($year, $month, $day): self + { + /** @var \DateTimeImmutable|false $result */ + $result = $this->innerDateTime->setDate($year, $month, $day); + if ($result === false) { + throw DatetimeException::createFromPhpError(); + } + return self::createFromRegular($result); //we have to recreate a safe datetime because modify create a new instance of \DateTimeImmutable + } + + public function setISODate($year, $week, $day = 1): self + { + /** @var \DateTimeImmutable|false $result */ + $result = $this->innerDateTime->setISODate($year, $week, $day); + if ($result === false) { + throw DatetimeException::createFromPhpError(); + } + return self::createFromRegular($result); //we have to recreate a safe datetime because modify create a new instance of \DateTimeImmutable + } + + public function setTime($hour, $minute, $second = 0, $microseconds = 0): self + { + /** @var \DateTimeImmutable|false $result */ + $result = $this->innerDateTime->setTime($hour, $minute, $second, $microseconds); + if ($result === false) { + throw DatetimeException::createFromPhpError(); + } + return self::createFromRegular($result); + } + + public function setTimestamp($unixtimestamp): self + { + /** @var \DateTimeImmutable|false $result */ + $result = $this->innerDateTime->setTimestamp($unixtimestamp); + if ($result === false) { + throw DatetimeException::createFromPhpError(); + } + return self::createFromRegular($result); + } + + public function setTimezone($timezone): self + { + /** @var \DateTimeImmutable|false $result */ + $result = $this->innerDateTime->setTimezone($timezone); + if ($result === false) { + throw DatetimeException::createFromPhpError(); + } + return self::createFromRegular($result); + } + + public function sub($interval): self + { + /** @var \DateTimeImmutable|false $result */ + $result = $this->innerDateTime->sub($interval); + if ($result === false) { + throw DatetimeException::createFromPhpError(); + } + return self::createFromRegular($result); + } + + public function getOffset(): int + { + /** @var int|false $result */ + $result = $this->innerDateTime->getOffset(); + if ($result === false) { + throw DatetimeException::createFromPhpError(); + } + return $result; + } + + ////////////////////////////////////////////////////////////////////////////////////////// + //overload getters to use the inner datetime immutable instead of itself + + public function add($interval): self + { + return self::createFromRegular($this->innerDateTime->add($interval)); + } + + public static function createFromMutable($dateTime): self + { + return self::createFromRegular(parent::createFromMutable($dateTime)); + } + + public static function __set_state(array $array): self + { + return self::createFromRegular(parent::__set_state($array)); + } + + public function getTimezone(): DateTimeZone + { + return $this->innerDateTime->getTimezone(); + } + + public function getTimestamp(): int + { + return $this->innerDateTime->getTimestamp(); + } +}