From a5a09a55d21bb1f0c43f2f1eb1b6f71089016637 Mon Sep 17 00:00:00 2001 From: Veaceslav Medvedev Date: Thu, 16 Nov 2017 17:26:12 +0100 Subject: [PATCH] Object serialization (#2) * Add DateRange serialziation * Requere JSON PHP extension in composer --- composer.json | 3 +- src/DateRange.php | 65 +++++++++++++++- src/DateRangeImmutable.php | 56 +++++++++++++- tests/DateRangeImmutableTest.php | 36 +++++++++ tests/DateRangeTest.php | 124 +++++++++++++++++++++++-------- 5 files changed, 250 insertions(+), 34 deletions(-) diff --git a/composer.json b/composer.json index 1e05d07..b7bf209 100644 --- a/composer.json +++ b/composer.json @@ -12,7 +12,8 @@ } ], "require": { - "php": "^7.1" + "php": "^7.1", + "ext-json": "*" }, "require-dev": { "phpunit/phpunit": "~6.4.4" diff --git a/src/DateRange.php b/src/DateRange.php index 48bbd29..9fa6276 100644 --- a/src/DateRange.php +++ b/src/DateRange.php @@ -12,11 +12,13 @@ use DateTimeImmutable; use DateTimeInterface; +use JsonSerializable; +use Serializable; /** * Date range implementation. */ -final class DateRange implements DateRangeInterface +final class DateRange implements DateRangeInterface, Serializable, JsonSerializable { /** * @var DateTimeInterface @@ -40,6 +42,27 @@ public function __construct(DateTimeInterface $begin, DateTimeInterface $end = n $this->end = $end; } + /** + * {@inheritdoc} + */ + public function __toString() + { + return $this->serialize(); + } + + /** + * {@inheritdoc} + */ + public function __debugInfo() + { + return [ + 'begin' => $this->getBegin()->format('c'), + 'end' => $this->isFinite() + ? $this->getEnd()->format('c') + : '-', + ]; + } + /** * {@inheritdoc} */ @@ -120,6 +143,46 @@ public function endAt(DateTimeInterface $time): DateRangeInterface return $this; } + /** + * {@inheritdoc} + */ + public function serialize() + { + return sprintf( + '%s/%s', + $this->getBegin()->format('c'), + $this->isFinite() + ? $this->getEnd()->format('c') + : '-' + ); + } + + /** + * {@inheritdoc} + */ + public function unserialize($serialized) + { + $times = explode('/', $serialized, 2); + + if (count($times) !== 2) { + throw new DateRangeException('Invalid range format'); + } + + $this->begin = new DateTimeImmutable($times[0]); + + if ($times[1] !== '-') { + $this->end = new DateTimeImmutable($times[1]); + } + } + + /** + * {@inheritdoc} + */ + public function jsonSerialize() + { + return $this->serialize(); + } + /** * @param DateTimeInterface $begin * @param DateTimeInterface|null $end diff --git a/src/DateRangeImmutable.php b/src/DateRangeImmutable.php index e14ece1..9d2d8e4 100644 --- a/src/DateRangeImmutable.php +++ b/src/DateRangeImmutable.php @@ -10,7 +10,10 @@ namespace Zee\DateRange; +use DateTimeImmutable; use DateTimeInterface; +use JsonSerializable; +use Serializable; /** * Immutable implementation of date range. @@ -18,7 +21,7 @@ * This class behaves the same as `{@see DateRange}` * except it never modifies itself but returns a new object instead. */ -final class DateRangeImmutable implements DateRangeInterface +final class DateRangeImmutable implements DateRangeInterface, Serializable, JsonSerializable { /** * @var DateRange @@ -34,6 +37,22 @@ public function __construct(DateTimeInterface $begin, DateTimeInterface $end = n $this->storage = new DateRange($begin, $end); } + /** + * {@inheritdoc} + */ + public function __toString() + { + return $this->storage->__toString(); + } + + /** + * {@inheritdoc} + */ + public function __debugInfo() + { + return $this->storage->__debugInfo(); + } + /** * Clones internal storage. */ @@ -119,4 +138,39 @@ public function endAt(DateTimeInterface $time): DateRangeInterface return $clone; } + + /** + * {@inheritdoc} + */ + public function serialize() + { + return $this->storage->serialize(); + } + + /** + * {@inheritdoc} + */ + public function unserialize($serialized) + { + $times = explode('/', $serialized, 2); + + if (count($times) !== 2) { + throw new DateRangeException('Invalid range format'); + } + + $this->storage = new DateRange( + new DateTimeImmutable($times[0]), + $times[1] !== '-' + ? new DateTimeImmutable($times[1]) + : null + ); + } + + /** + * {@inheritdoc} + */ + public function jsonSerialize() + { + return $this->storage->jsonSerialize(); + } } diff --git a/tests/DateRangeImmutableTest.php b/tests/DateRangeImmutableTest.php index 4b3d7aa..f660b55 100644 --- a/tests/DateRangeImmutableTest.php +++ b/tests/DateRangeImmutableTest.php @@ -12,6 +12,7 @@ use DateTime; use DateTimeImmutable; +use DomainException; use PHPUnit\Framework\TestCase; /** @@ -49,4 +50,39 @@ public function checkImmutability() self::assertNotSame($initial, $actual); self::assertSame($soon, $actual->getEnd()); } + + /** + * @test + */ + public function checkImmutableCopyMutableObjectBehaviors() + { + $yesterday = new DateTime('-1 day'); + $tomorrow = new DateTime('+1 day'); + + $immutable = new DateRangeImmutable($yesterday, $tomorrow); + $mutable = new DateRange($yesterday, $tomorrow); + + self::assertSame((string) $mutable, (string) $immutable); + self::assertSame(json_encode($mutable), json_encode($immutable)); + self::assertSame($mutable->__debugInfo(), $immutable->__debugInfo()); + + $immutable = unserialize(serialize($immutable)); + $mutable = unserialize(serialize($mutable)); + + self::assertEquals($mutable->getBegin(), $immutable->getBegin()); + self::assertEquals($mutable->getEnd(), $immutable->getEnd()); + } + + /** + * @test + */ + public function handleInvalidSerializedValue() + { + $range = new DateRangeImmutable(new DateTime()); + + $this->expectException(DomainException::class); + $this->expectExceptionMessage('Invalid range format'); + + $range->unserialize(''); + } } diff --git a/tests/DateRangeTest.php b/tests/DateRangeTest.php index b377cdc..546c7a1 100644 --- a/tests/DateRangeTest.php +++ b/tests/DateRangeTest.php @@ -25,11 +25,11 @@ class DateRangeTest extends TestCase */ public function initInfiniteRange() { - $period = new DateRange(new DateTime()); + $range = new DateRange(new DateTime()); - self::assertFalse($period->isFinite()); - self::assertTrue($period->isStarted()); - self::assertFalse($period->isEnded()); + self::assertFalse($range->isFinite()); + self::assertTrue($range->isStarted()); + self::assertFalse($range->isEnded()); } /** @@ -39,12 +39,12 @@ public function initFiniteRange() { $soon = new DateTimeImmutable('+1 day'); $nextMonth = new DateTimeImmutable('+1 month'); - $period = new DateRange($soon, $nextMonth); + $range = new DateRange($soon, $nextMonth); - self::assertFalse($period->isStarted()); - self::assertTrue($period->isFinite()); - self::assertFalse($period->isEnded()); - self::assertTrue($period->isEndAt($nextMonth)); + self::assertFalse($range->isStarted()); + self::assertTrue($range->isFinite()); + self::assertFalse($range->isEnded()); + self::assertTrue($range->isEndAt($nextMonth)); } /** @@ -56,15 +56,15 @@ public function initEndedRange() : DateRange { $justNow = new DateTimeImmutable('-1 second'); $yesterday = new DateTimeImmutable('-1 day'); - $period = new DateRange($yesterday, $justNow); + $range = new DateRange($yesterday, $justNow); - self::assertTrue($period->isStarted()); - self::assertTrue($period->isFinite()); - self::assertTrue($period->isEnded()); - self::assertSame($yesterday, $period->getBegin()); - self::assertSame($justNow, $period->getEnd()); + self::assertTrue($range->isStarted()); + self::assertTrue($range->isFinite()); + self::assertTrue($range->isEnded()); + self::assertSame($yesterday, $range->getBegin()); + self::assertSame($justNow, $range->getEnd()); - return $period; + return $range; } /** @@ -73,11 +73,11 @@ public function initEndedRange() : DateRange public function useBackdatingBegin() { $justNow = new DateTimeImmutable('-1 second'); - $period = new DateRange($justNow); + $range = new DateRange($justNow); - self::assertTrue($period->isStarted()); - self::assertFalse($period->isEnded()); - self::assertSame($justNow, $period->getBegin()); + self::assertTrue($range->isStarted()); + self::assertFalse($range->isEnded()); + self::assertSame($justNow, $range->getBegin()); } /** @@ -115,13 +115,13 @@ public function setEndBeforeBegin() public function useFutureBegin() : DateRange { $soon = new DateTimeImmutable('+1 day'); - $period = new DateRange($soon); + $range = new DateRange($soon); - self::assertFalse($period->isStarted()); - self::assertFalse($period->isEnded()); - self::assertTrue($period->isbeginAt($soon)); + self::assertFalse($range->isStarted()); + self::assertFalse($range->isEnded()); + self::assertTrue($range->isbeginAt($soon)); - return $period; + return $range; } /** @@ -132,16 +132,16 @@ public function useFutureBegin() : DateRange */ public function changeBegin(DateRange $initial) { - $period = $initial->beginAt($initial->getBegin()); + $range = $initial->beginAt($initial->getBegin()); - self::assertSame($initial->getBegin(), $period->getBegin()); + self::assertSame($initial->getBegin(), $range->getBegin()); $nextMonth = new DateTimeImmutable('+1 month'); - $period = $initial->beginAt($nextMonth); + $range = $initial->beginAt($nextMonth); - self::assertFalse($period->isStarted()); - self::assertFalse($period->isEnded()); - self::assertSame($nextMonth, $period->getBegin()); + self::assertFalse($range->isStarted()); + self::assertFalse($range->isEnded()); + self::assertSame($nextMonth, $range->getBegin()); } /** @@ -164,4 +164,66 @@ public function changeEnd(DateRange $initial) self::assertFalse($period->isEnded()); self::assertSame($nextMonth, $period->getEnd()); } + + /** + * @test + * @dataProvider provideDateRanges + * + * @param DateRange $initial + */ + public function serializeRange(DateRange $initial) + { + $actual = unserialize(serialize($initial)); + + if ($actual instanceof DateRange) { + self::assertTrue($actual->isBeginAt($initial->getBegin())); + + if ($initial->isFinite()) { + self::assertTrue($actual->isEndAt($initial->getEnd())); + } + } else { + self::fail('Cannot serialize/deserialize date range'); + } + } + + /** + * @test + */ + public function handleInvalidSerializedValue() + { + $range = new DateRange(new DateTime()); + + $this->expectException(DomainException::class); + $this->expectExceptionMessage('Invalid range format'); + + $range->unserialize(''); + } + + /** + * @test + * @dataProvider provideDateRanges + * + * @param DateRange $range + */ + public function jsonEncodeRange(DateRange $range) + { + $expected = json_encode((string) $range); + $actual = json_encode($range); + + self::assertSame($expected, $actual); + } + + /** + * @return array + */ + public function provideDateRanges(): array + { + $now = new DateTimeImmutable(); + $soon = new DateTimeImmutable('+1 day'); + + return [ + [new DateRange($now)], + [new DateRange($now, $soon)], + ]; + } }