A src/DateTimeImmutable/DateTimeImmutableFactory.php => src/DateTimeImmutable/DateTimeImmutableFactory.php +31 -0
@@ 0,0 1,31 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Ancarda\HighTestCoverage\DateTimeImmutable;
+
+use DateTimeImmutable;
+use DateTimeZone;
+
+/**
+ * Factory that produces a DateTimeImmutable instance
+ *
+ * You should typehint with this interface in all your code. A typical use
+ * would be to have a constructor accept an instance like so:
+ *
+ * function __construct(DateTimeImmutableFactory $dateTimeImmutable)
+ *
+ * Which is then used throughout a class. Your Dependency Injection container
+ * would then have an entry that resolves to Real:
+ *
+ * :DateTimeImmutableFactory:class => Real::class,
+ *
+ * When that class is under test, you'll instead give it a class like Fixed.
+ */
+interface DateTimeImmutableFactory
+{
+ public function __invoke(
+ string $datetime = "now",
+ ?DateTimeZone $timezone = null
+ ): DateTimeImmutable;
+}
A src/DateTimeImmutable/Fixed.php => src/DateTimeImmutable/Fixed.php +32 -0
@@ 0,0 1,32 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Ancarda\HighTestCoverage\DateTimeImmutable;
+
+use DateTimeImmutable;
+use DateTimeZone;
+
+/**
+ * Return a predetermined fixed value every time
+ *
+ * This is the simplest possible implementation of DateTimeImmutable.
+ * The value given in the constructor is returned from invoke every time.
+ */
+final class Fixed implements DateTimeImmutableFactory
+{
+ /** @var DateTimeImmutable */
+ private $value;
+
+ public function __construct(DateTimeImmutable $value)
+ {
+ $this->value = $value;
+ }
+
+ public function __invoke(
+ string $datetime = "now",
+ ?DateTimeZone $timezone = null
+ ): DateTimeImmutable {
+ return $this->value;
+ }
+}
A src/DateTimeImmutable/Real.php => src/DateTimeImmutable/Real.php +24 -0
@@ 0,0 1,24 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Ancarda\HighTestCoverage\DateTimeImmutable;
+
+use DateTimeImmutable;
+use DateTimeZone;
+
+/**
+ * Produce a real DateTimeImmutable instance
+ *
+ * This class just wraps DateTimeImmutable::__construct and is intended to be
+ * used in production.
+ */
+final class Real implements DateTimeImmutableFactory
+{
+ public function __invoke(
+ string $datetime = "now",
+ ?DateTimeZone $timezone = null
+ ): DateTimeImmutable {
+ return new DateTimeImmutable($datetime, $timezone);
+ }
+}
A src/DateTimeImmutable/Succession.php => src/DateTimeImmutable/Succession.php +61 -0
@@ 0,0 1,61 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Ancarda\HighTestCoverage\DateTimeImmutable;
+
+use DateTimeImmutable;
+use DateTimeZone;
+use LogicException;
+
+/**
+ * Return the next item in a set of predetermined fixed values every time
+ *
+ * This implementation takes a list of DateTimeImmutable instances in the
+ * constructor. Each time a DateTimeImmutable is requested, the next item in
+ * the list is returned and the pointer is moved one place.
+ *
+ * When the list is exhausted, the pointer wraps around.
+ */
+final class Succession implements DateTimeImmutableFactory
+{
+ /** @var array<int, DateTimeImmutable> */
+ private $succession = [];
+
+ /** @var int */
+ private $cursor = 0;
+
+ /** @var int */
+ private $last = 0;
+
+ /**
+ * @param array<int, DateTimeImmutable> $succession Non-Empty array
+ * @throws LogicException If given an empty array of integers
+ */
+ public function __construct(array $succession)
+ {
+ if (count($succession) === 0) {
+ throw new LogicException('succession cannot be empty');
+ }
+
+ $this->last = count($succession) - 1;
+ $this->succession = $succession;
+ }
+
+ public function __invoke(
+ string $datetime = "now",
+ ?DateTimeZone $timezone = null
+ ): DateTimeImmutable {
+ if ($this->cursor === $this->last) {
+ $this->cursor = 0;
+ return $this->succession[$this->last];
+ }
+
+ return $this->succession[$this->cursor++];
+ }
+
+ public function rewind(): void
+ {
+ $this->cursor = 0;
+ }
+}
A tests/DateTimeImmutable/FixedTest.php => tests/DateTimeImmutable/FixedTest.php +24 -0
@@ 0,0 1,24 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Tests\DateTimeImmutable;
+
+use Ancarda\HighTestCoverage\DateTimeImmutable\Fixed;
+use DateTimeZone;
+use DateTimeImmutable;
+use PHPUnit\Framework\TestCase;
+
+final class FixedTest extends TestCase
+{
+ public function testFixed(): void
+ {
+ $dt = new DateTimeImmutable();
+ $fixed = new Fixed($dt);
+
+ $a = $fixed();
+ $b = $fixed();
+
+ self::assertSame($a, $b);
+ }
+}
A tests/DateTimeImmutable/RealTest.php => tests/DateTimeImmutable/RealTest.php +18 -0
@@ 0,0 1,18 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Tests\DateTimeImmutable;
+
+use Ancarda\HighTestCoverage\DateTimeImmutable\Real;
+use DateTimeZone;
+use PHPUnit\Framework\TestCase;
+
+final class RealTest extends TestCase
+{
+ public function testReal(): void
+ {
+ $dt = (new Real())('2000-01-01T00:00:00Z', new DateTimeZone('UTC'));
+ self::assertSame('2000-01-01T00:00:00+00:00', $dt->format('c'));
+ }
+}
A tests/DateTimeImmutable/SuccessionTest.php => tests/DateTimeImmutable/SuccessionTest.php +70 -0
@@ 0,0 1,70 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Tests\DateTimeImmutable;
+
+use Ancarda\HighTestCoverage\DateTimeImmutable\Succession;
+use DateTimeImmutable;
+use DateTimeZone;
+use LogicException;
+use PHPUnit\Framework\TestCase;
+
+final class SuccessionTest extends TestCase
+{
+ public function testSuccession(): void
+ {
+ $a = new DateTimeImmutable('2000-01-01T00:00:00Z');
+ $b = new DateTimeImmutable('2001-01-01T00:00:00Z');
+ $c = new DateTimeImmutable('2002-01-01T00:00:00Z');
+ $d = new DateTimeImmutable('2003-01-01T00:00:00Z');
+
+ $succession = new Succession([$a, $b, $c, $d]);
+
+ self::assertSame($a, $succession());
+ self::assertSame($b, $succession());
+ self::assertSame($c, $succession());
+ self::assertSame($d, $succession());
+ }
+
+ public function testSuccessionWrapsAround(): void
+ {
+ $a = new DateTimeImmutable('2000-01-01T00:00:00Z');
+ $b = new DateTimeImmutable('2001-01-01T00:00:00Z');
+ $c = new DateTimeImmutable('2002-01-01T00:00:00Z');
+
+ $succession = new Succession([$a, $b, $c]);
+
+ for ($i = 0; $i <= 3; $i++) {
+ self::assertSame($a, $succession());
+ self::assertSame($b, $succession());
+ self::assertSame($c, $succession());
+ }
+ }
+
+ public function testRewind(): void
+ {
+ $a = new DateTimeImmutable('2000-01-01T00:00:00Z');
+ $b = new DateTimeImmutable('2001-01-01T00:00:00Z');
+ $c = new DateTimeImmutable('2002-01-01T00:00:00Z');
+ $d = new DateTimeImmutable('2003-01-01T00:00:00Z');
+
+ $succession = new Succession([$a, $b, $c, $d]);
+
+ self::assertSame($a, $succession());
+ self::assertSame($b, $succession());
+ $succession->rewind();
+ self::assertSame($a, $succession());
+ self::assertSame($b, $succession());
+ self::assertSame($c, $succession());
+ self::assertSame($d, $succession());
+ }
+
+ public function testSuccessionRejectsEmptyArrays(): void
+ {
+ $this->expectException(LogicException::class);
+ $this->expectExceptionMessage('succession cannot be empty');
+
+ new Succession([]);
+ }
+}