11 Commits
Author SHA1 Message Date
Jérôme Gamez 0754922206 Release 4.0.0 2026-08-23 23:06:53 +02:00
Jérôme Gamez df585ab815 Update changelog 2026-08-23 20:09:20 +02:00
Jérôme Gamez c094cf87e3 Don't assume the local time zone is not Europe/Berlin 2026-08-23 20:03:38 +02:00
Jérôme Gamez c2243a5934 Harden WrappingClock validation
Wrapping an object should not call it just to validate the return value.
Instead, we check the method signature when the object is wrapped, and
the return value at runtime. We were implicitly checking it already
with the `assert()` calls.
2026-08-23 20:03:38 +02:00
Jérôme Gamez e8f20c112e Don't auto-merge dependabot PRs 2026-08-23 19:35:54 +02:00
Jérôme Gamez 7db7032723 Type time zone input 2026-08-23 19:33:15 +02:00
Jérôme Gamez 8f5274748c Update README 2026-08-23 19:30:12 +02:00
Jérôme Gamez e73aacc19c Update to PHPUnit 12 2026-08-22 23:51:52 +02:00
Jérôme Gamez c062aff727 Drop support for PHP versions before 8.3 2026-08-22 23:51:52 +02:00
Jérôme Gamez b4408036c1 Move source and test files up one level 2026-08-22 23:24:55 +02:00
Jérôme Gamez 0a55b7a019 Remove deprecated Clock interface 2026-08-22 23:24:55 +02:00
20 changed files with 234 additions and 270 deletions
@@ -1,23 +0,0 @@
name: "Dependabot auto-merge"
on: [pull_request_target] # yamllint disable-line rule:truthy
permissions:
contents: "write"
jobs:
dependabot:
runs-on: "ubuntu-latest"
if: "${{ github.actor == 'dependabot[bot]' }}"
steps:
- name: "Dependabot metadata"
id: "metadata"
uses: "dependabot/[email protected]"
with:
github-token: "${{ secrets.GITHUB_TOKEN }}"
- name: "Enable auto-merge for Dependabot PRs"
if: "${{ steps.dependabot-metadata.outputs.update-type != 'version-update:semver-major' }}"
run: "gh pr merge --auto --merge \"$PR_URL\""
env:
PR_URL: "${{github.event.pull_request.html_url}}"
GITHUB_TOKEN: "${{secrets.GITHUB_TOKEN}}"
+1 -6
View File
@@ -4,9 +4,7 @@ on:
pull_request: pull_request:
push: push:
branches: branches:
- '1.x' - '4.x'
- '2.x'
- '3.x'
jobs: jobs:
tests: tests:
@@ -16,9 +14,6 @@ jobs:
strategy: strategy:
matrix: matrix:
php: php:
- "8.0"
- "8.1"
- "8.2"
- "8.3" - "8.3"
- "8.4" - "8.4"
- "8.5" - "8.5"
+10 -1
View File
@@ -2,9 +2,18 @@
## Unreleased ## Unreleased
## 4.0.0 - 2026-08-23
* Dropped support for PHP <8.3.
* Removed the deprecated `Beste\Clock` interface. Clock implementations now implement
`Psr\Clock\ClockInterface` directly.
* `WrappingClock` will throw an `InvalidArgumentException` if the given object doesn't behave like
a PSR-20 clock and will throw an `UnexpectedValueException` if a later call does not return
`DateTimeImmutable`.
## 3.1.0 - 2026-08-22 ## 3.1.0 - 2026-08-22
Deprecates `Beste\Clock` in favor of `Psr\Clock\ClockInterface`. The implementations continue to implement Deprecated `Beste\Clock` in favor of `Psr\Clock\ClockInterface`. The implementations continue to implement
the deprecated interface for backwards compatibility. Please type against `Psr\Clock\ClockInterface`. the deprecated interface for backwards compatibility. Please type against `Psr\Clock\ClockInterface`.
`Beste\Clock` will be removed in 4.0. `Beste\Clock` will be removed in 4.0.
+4 -6
View File
@@ -114,8 +114,8 @@ printf("\nAfter turning back the clock 5 minutes, it's %s", $frozenClock->now()-
### `MinuteClock` ### `MinuteClock`
In some cases, microseconds, milliseconds, or even seconds are too precise for some use cases - sometimes it's just In some cases, microseconds, milliseconds, or even seconds are too precise. Sometimes it is enough to know that
enough if something happened in the same minute. Using the minute something happened within the same minute. `MinuteClock` removes seconds and fractions of a second from another clock.
```php ```php
# examples/minute_clock.php # examples/minute_clock.php
@@ -141,10 +141,8 @@ printf("For %s, the minute clock still returns %s\n",
### `WrappingClock` ### `WrappingClock`
If you already have an object with a `now()` method returning a `DateTimeImmutable` object, you can wrap it If you already have an object with a `now()` method returning a `DateTimeImmutable`, you can wrap it in a
in a `WrappingClock` to make it a "real" Clock. `WrappingClock` to make it a PSR-20 clock.
as a "real" clock.
```php ```php
# examples/wrapping_clock.php # examples/wrapping_clock.php
+5 -8
View File
@@ -11,7 +11,7 @@
} }
], ],
"require": { "require": {
"php": "^8.0", "php": "^8.3",
"psr/clock": "^1.0" "psr/clock": "^1.0"
}, },
"require-dev": { "require-dev": {
@@ -19,19 +19,16 @@
"phpstan/phpstan": "^2.1", "phpstan/phpstan": "^2.1",
"phpstan/phpstan-phpunit": "^2.0", "phpstan/phpstan-phpunit": "^2.0",
"phpstan/phpstan-strict-rules": "^2.0", "phpstan/phpstan-strict-rules": "^2.0",
"phpunit/phpunit": "^9.6.20" "phpunit/phpunit": "^12.5"
}, },
"autoload": { "autoload": {
"psr-4": { "psr-4": {
"Beste\\Clock\\": "src/Clock" "Beste\\Clock\\": "src"
}, }
"files": [
"src/Clock.php"
]
}, },
"autoload-dev": { "autoload-dev": {
"psr-4": { "psr-4": {
"Beste\\Clock\\Tests\\": "tests/Clock" "Beste\\Clock\\Tests\\": "tests"
} }
}, },
"provide": { "provide": {
+7 -7
View File
@@ -1,9 +1,9 @@
<?xml version="1.0"?> <?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" <phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd" xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/12.5/phpunit.xsd"
bootstrap="vendor/autoload.php" bootstrap="vendor/autoload.php"
cacheResultFile=".build/phpunit/phpunit.cache" cacheDirectory=".build/phpunit/cache"
beStrictAboutCoversAnnotation="true" beStrictAboutCoverageMetadata="true"
colors="true" colors="true"
> >
<testsuites> <testsuites>
@@ -12,10 +12,10 @@
</testsuite> </testsuite>
</testsuites> </testsuites>
<coverage> <source>
<include> <include>
<directory>src/Clock</directory> <directory>src</directory>
</include> </include>
</coverage> </source>
</phpunit> </phpunit>
-15
View File
@@ -1,15 +0,0 @@
<?php
declare(strict_types=1);
namespace Beste;
use Psr\Clock\ClockInterface;
/**
* @deprecated since 3.1.0, use {@see ClockInterface} instead.
*/
interface Clock extends ClockInterface
{
}
@@ -4,12 +4,11 @@ declare(strict_types=1);
namespace Beste\Clock; namespace Beste\Clock;
use Beste\Clock;
use DateTimeImmutable; use DateTimeImmutable;
use DateTimeZone; use DateTimeZone;
use Psr\Clock\ClockInterface; use Psr\Clock\ClockInterface;
final class FrozenClock implements Clock final class FrozenClock implements ClockInterface
{ {
private DateTimeImmutable $frozenAt; private DateTimeImmutable $frozenAt;
@@ -4,13 +4,13 @@ declare(strict_types=1);
namespace Beste\Clock; namespace Beste\Clock;
use Beste\Clock;
use DateTimeImmutable; use DateTimeImmutable;
use DateTimeZone; use DateTimeZone;
use InvalidArgumentException; use InvalidArgumentException;
use Psr\Clock\ClockInterface;
use Throwable; use Throwable;
final class LocalizedClock implements Clock final class LocalizedClock implements ClockInterface
{ {
private DateTimeZone $timeZone; private DateTimeZone $timeZone;
@@ -19,10 +19,7 @@ final class LocalizedClock implements Clock
$this->timeZone = $timeZone; $this->timeZone = $timeZone;
} }
/** public static function in(DateTimeZone|string $timeZone): self
* @param DateTimeZone|string $timeZone
*/
public static function in($timeZone): self
{ {
if (is_string($timeZone)) { if (is_string($timeZone)) {
try { try {
@@ -4,11 +4,10 @@ declare(strict_types=1);
namespace Beste\Clock; namespace Beste\Clock;
use Beste\Clock;
use DateTimeImmutable; use DateTimeImmutable;
use Psr\Clock\ClockInterface; use Psr\Clock\ClockInterface;
final class MinuteClock implements Clock final class MinuteClock implements ClockInterface
{ {
private ClockInterface $clock; private ClockInterface $clock;
@@ -4,10 +4,10 @@ declare(strict_types=1);
namespace Beste\Clock; namespace Beste\Clock;
use Beste\Clock;
use DateTimeImmutable; use DateTimeImmutable;
use Psr\Clock\ClockInterface;
final class SystemClock implements Clock final class SystemClock implements ClockInterface
{ {
private function __construct() private function __construct()
{ {
+2 -2
View File
@@ -4,11 +4,11 @@ declare(strict_types=1);
namespace Beste\Clock; namespace Beste\Clock;
use Beste\Clock;
use DateTimeImmutable; use DateTimeImmutable;
use DateTimeZone; use DateTimeZone;
use Psr\Clock\ClockInterface;
final class UTCClock implements Clock final class UTCClock implements ClockInterface
{ {
private DateTimeZone $timeZone; private DateTimeZone $timeZone;
@@ -4,12 +4,14 @@ declare(strict_types=1);
namespace Beste\Clock; namespace Beste\Clock;
use Beste\Clock; use Closure;
use DateTimeImmutable; use DateTimeImmutable;
use InvalidArgumentException; use InvalidArgumentException;
use Psr\Clock\ClockInterface; use Psr\Clock\ClockInterface;
use ReflectionMethod;
use UnexpectedValueException;
final class WrappingClock implements Clock final class WrappingClock implements ClockInterface
{ {
private ClockInterface $wrappedClock; private ClockInterface $wrappedClock;
@@ -18,6 +20,9 @@ final class WrappingClock implements Clock
$this->wrappedClock = $wrappedClock; $this->wrappedClock = $wrappedClock;
} }
/**
* @throws InvalidArgumentException when the given object doesn't behave like a clock.
*/
public static function wrapping(object $clock): self public static function wrapping(object $clock): self
{ {
if ($clock instanceof ClockInterface) { if ($clock instanceof ClockInterface) {
@@ -25,28 +30,30 @@ final class WrappingClock implements Clock
} }
if (!method_exists($clock, 'now')) { if (!method_exists($clock, 'now')) {
throw new InvalidArgumentException('$clock must implement StellaMaris\Clock\ClockInterface or have a now() method'); throw new InvalidArgumentException('$clock must implement Psr\Clock\ClockInterface or have a now() method');
} }
if (!($clock->now() instanceof DateTimeImmutable)) { $method = new ReflectionMethod($clock, 'now');
throw new InvalidArgumentException('$clock->now() must return a DateTimeImmutable');
if (!$method->isPublic() || $method->getNumberOfRequiredParameters() > 0) {
throw new InvalidArgumentException('$clock->now() must be public and accept no required parameters');
} }
$wrappedClock = new class($clock) implements ClockInterface { $wrappedClock = new class($method->getClosure($clock)) implements ClockInterface {
private object $clock; private Closure $now;
public function __construct(object $clock) public function __construct(Closure $now)
{ {
$this->clock = $clock; $this->now = $now;
} }
public function now(): DateTimeImmutable public function now(): DateTimeImmutable
{ {
assert(method_exists($this->clock, 'now')); $now = ($this->now)();
$now = $this->clock->now(); if (!$now instanceof DateTimeImmutable) {
throw new UnexpectedValueException('$clock->now() must return a DateTimeImmutable');
assert($now instanceof DateTimeImmutable); }
return $now; return $now;
} }
-88
View File
@@ -1,88 +0,0 @@
<?php
declare(strict_types=1);
namespace Beste\Clock\Tests;
use Beste\Clock\FrozenClock;
use Beste\Clock\WrappingClock;
use DateTimeImmutable;
use InvalidArgumentException;
use PHPUnit\Framework\TestCase;
/**
* @internal
* @coversDefaultClass \Beste\Clock\WrappingClock
*/
final class WrappingClockTest extends TestCase
{
/**
* @test
*
* @covers ::__construct
* @covers ::wrapping
* @covers ::now
*/
public function itWrapsAClockInterface(): void
{
$clock = FrozenClock::fromUTC();
$wrappedClock = WrappingClock::wrapping($clock);
self::assertSame(
$clock->now()->format(DATE_ATOM),
$wrappedClock->now()->format(DATE_ATOM)
);
}
/**
* @test
*
* @covers ::wrapping
*/
public function itWrapsAnObjectWithANowMethod(): void
{
$now = FrozenClock::fromUTC()->now();
$clock = new class($now) {
private DateTimeImmutable $now;
public function __construct(DateTimeImmutable $now)
{
$this->now = $now;
}
public function now(): DateTimeImmutable
{
return $this->now;
}
};
$wrappedClock = WrappingClock::wrapping($clock);
self::assertSame(
$now->format(DATE_ATOM),
$wrappedClock->now()->format(DATE_ATOM)
);
}
/**
* @test
*
* @covers ::wrapping
*/
public function itRejectsObjectsWithANowMethodReturningANonDateTimeImmutable(): void
{
$clock = new class() {
public function now(): string
{
return 'foo';
}
};
$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage('$clock->now() must return a DateTimeImmutable');
WrappingClock::wrapping($clock);
}
}
@@ -6,21 +6,18 @@ namespace Beste\Clock\Tests;
use Beste\Clock\FrozenClock; use Beste\Clock\FrozenClock;
use DateTimeImmutable; use DateTimeImmutable;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
use Psr\Clock\ClockInterface; use Psr\Clock\ClockInterface;
/** /**
* @internal * @internal
* @coversDefaultClass \Beste\Clock\FrozenClock
*/ */
#[CoversClass(FrozenClock::class)]
final class FrozenClockTest extends TestCase final class FrozenClockTest extends TestCase
{ {
/** #[Test]
* @test
* @covers ::__construct
* @covers ::at
* @covers ::now
*/
public function itFreezesTime(): void public function itFreezesTime(): void
{ {
$now = new DateTimeImmutable('2021-03-21 22:16:00'); $now = new DateTimeImmutable('2021-03-21 22:16:00');
@@ -29,14 +26,7 @@ final class FrozenClockTest extends TestCase
self::assertEquals($now, $clock->now()); self::assertEquals($now, $clock->now());
} }
/** #[Test]
* @test
*
* @uses ::__construct
* @uses ::fromUTC
*
* @covers ::now
*/
public function itReturnsAnEqualNowButNotTheSame(): void public function itReturnsAnEqualNowButNotTheSame(): void
{ {
$clock = FrozenClock::fromUTC(); $clock = FrozenClock::fromUTC();
@@ -48,14 +38,7 @@ final class FrozenClockTest extends TestCase
self::assertNotSame($first, $second); self::assertNotSame($first, $second);
} }
/** #[Test]
* @test
*
* @uses ::__construct
* @uses ::now
*
* @covers ::withNowFrom
*/
public function itFreezesTimeFromAnotherClock(): void public function itFreezesTimeFromAnotherClock(): void
{ {
$now = new DateTimeImmutable('now'); $now = new DateTimeImmutable('now');
@@ -71,14 +54,7 @@ final class FrozenClockTest extends TestCase
self::assertEquals($now, $frozenClock->now()); self::assertEquals($now, $frozenClock->now());
} }
/** #[Test]
* @test
*
* @uses ::__construct
* @uses ::now
*
* @covers ::fromUTC
*/
public function itFreezesTheCurrentUTCTime(): void public function itFreezesTheCurrentUTCTime(): void
{ {
$clock = FrozenClock::fromUTC(); $clock = FrozenClock::fromUTC();
@@ -87,15 +63,7 @@ final class FrozenClockTest extends TestCase
self::assertEquals($clock->now(), $clock->now()); self::assertEquals($clock->now(), $clock->now());
} }
/** #[Test]
* @test
*
* @uses ::__construct
* @uses ::at
* @uses ::now
*
* @covers ::setTo
*/
public function itCanBeSet(): void public function itCanBeSet(): void
{ {
$now = new DateTimeImmutable('2021-03-21 18:18:18'); $now = new DateTimeImmutable('2021-03-21 18:18:18');
@@ -7,18 +7,17 @@ namespace Beste\Clock\Tests;
use Beste\Clock\LocalizedClock; use Beste\Clock\LocalizedClock;
use DateTimeZone; use DateTimeZone;
use InvalidArgumentException; use InvalidArgumentException;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
/** /**
* @internal * @internal
* @coversDefaultClass \Beste\Clock\LocalizedClock
*/ */
#[CoversClass(LocalizedClock::class)]
final class LocalizedClockTest extends TestCase final class LocalizedClockTest extends TestCase
{ {
/** #[Test]
* @test
* @covers ::in
*/
public function itRejectsAnInvalidTimeZone(): void public function itRejectsAnInvalidTimeZone(): void
{ {
$this->expectException(InvalidArgumentException::class); $this->expectException(InvalidArgumentException::class);
@@ -26,12 +25,7 @@ final class LocalizedClockTest extends TestCase
LocalizedClock::in('invalid'); LocalizedClock::in('invalid');
} }
/** #[Test]
* @test
* @covers ::__construct
* @covers ::in
* @covers ::now
*/
public function itUsesTheGivenTimeZone(): void public function itUsesTheGivenTimeZone(): void
{ {
$timeZone = new DateTimeZone('Asia/Bangkok'); $timeZone = new DateTimeZone('Asia/Bangkok');
@@ -41,12 +35,7 @@ final class LocalizedClockTest extends TestCase
self::assertSame($timeZone->getName(), $now->getTimezone()->getName()); self::assertSame($timeZone->getName(), $now->getTimezone()->getName());
} }
/** #[Test]
* @test
* @covers ::__construct
* @covers ::in
* @covers ::now
*/
public function itAcceptsTheTimeZoneAsAString(): void public function itAcceptsTheTimeZoneAsAString(): void
{ {
$timeZone = 'Pacific/Guam'; $timeZone = 'Pacific/Guam';
@@ -6,30 +6,23 @@ namespace Beste\Clock\Tests;
use Beste\Clock\MinuteClock; use Beste\Clock\MinuteClock;
use DateTimeImmutable; use DateTimeImmutable;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
use Psr\Clock\ClockInterface; use Psr\Clock\ClockInterface;
/** /**
* @internal * @internal
* @coversDefaultClass \Beste\Clock\MinuteClock
*/ */
#[CoversClass(MinuteClock::class)]
final class MinuteClockTest extends TestCase final class MinuteClockTest extends TestCase
{ {
/** #[Test]
* @test #[DataProvider('dateTimeStringProvider')]
*
* @covers ::__construct
* @covers ::wrapping
* @covers ::now
* @covers ::floor
*
* @uses \Beste\Clock\FrozenClock
*
* @dataProvider dateTimeStringProvider
*/
public function itDiscardsEverythingSmallerThanMinutes(string $format, string $input, string $expected): void public function itDiscardsEverythingSmallerThanMinutes(string $format, string $input, string $expected): void
{ {
$mockClock = $this->createMock(ClockInterface::class); $mockClock = self::createStub(ClockInterface::class);
$mockClock->method('now')->willReturn(new DateTimeImmutable($input)); $mockClock->method('now')->willReturn(new DateTimeImmutable($input));
// Just to be sure that the test input is valid ^^ // Just to be sure that the test input is valid ^^
@@ -43,7 +36,7 @@ final class MinuteClockTest extends TestCase
/** /**
* @return array<string, string[]> * @return array<string, string[]>
*/ */
public function dateTimeStringProvider(): array public static function dateTimeStringProvider(): array
{ {
return [ return [
'microseconds' => [ 'microseconds' => [
@@ -5,12 +5,14 @@ declare(strict_types=1);
namespace Beste\Clock\Tests; namespace Beste\Clock\Tests;
use Beste\Clock\SystemClock; use Beste\Clock\SystemClock;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
/** /**
* @internal * @internal
* @coversDefaultClass \Beste\Clock\SystemClock
*/ */
#[CoversClass(SystemClock::class)]
final class SystemClockTest extends TestCase final class SystemClockTest extends TestCase
{ {
private string $defaultTimeZone; private string $defaultTimeZone;
@@ -25,16 +27,10 @@ final class SystemClockTest extends TestCase
date_default_timezone_set($this->defaultTimeZone); date_default_timezone_set($this->defaultTimeZone);
} }
/** #[Test]
* @test
*
* @covers ::__construct
* @covers ::create
* @covers ::now
*/
public function itUsesTheSystemTimeZone(): void public function itUsesTheSystemTimeZone(): void
{ {
$timeZone = 'Europe/Berlin'; $timeZone = $this->defaultTimeZone === 'Europe/Berlin' ? 'UTC' : 'Europe/Berlin';
self::assertNotSame($timeZone, $this->defaultTimeZone); self::assertNotSame($timeZone, $this->defaultTimeZone);
date_default_timezone_set($timeZone); date_default_timezone_set($timeZone);
@@ -5,12 +5,14 @@ declare(strict_types=1);
namespace Beste\Clock\Tests; namespace Beste\Clock\Tests;
use Beste\Clock\UTCClock; use Beste\Clock\UTCClock;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
/** /**
* @internal * @internal
* @coversDefaultClass \Beste\Clock\UTCClock
*/ */
#[CoversClass(UTCClock::class)]
final class UTCClockTest extends TestCase final class UTCClockTest extends TestCase
{ {
private string $defaultTimeZone; private string $defaultTimeZone;
@@ -25,13 +27,7 @@ final class UTCClockTest extends TestCase
date_default_timezone_set($this->defaultTimeZone); date_default_timezone_set($this->defaultTimeZone);
} }
/** #[Test]
* @test
*
* @covers ::__construct
* @covers ::create
* @covers ::now
*/
public function itUsesUTCRegardlessOfTheSystemTimeZone(): void public function itUsesUTCRegardlessOfTheSystemTimeZone(): void
{ {
date_default_timezone_set('Europe/Berlin'); date_default_timezone_set('Europe/Berlin');
+147
View File
@@ -0,0 +1,147 @@
<?php
declare(strict_types=1);
namespace Beste\Clock\Tests;
use Beste\Clock\FrozenClock;
use Beste\Clock\WrappingClock;
use DateTimeImmutable;
use DateTimeZone;
use InvalidArgumentException;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\Attributes\UsesClass;
use PHPUnit\Framework\TestCase;
use UnexpectedValueException;
/**
* @internal
*/
#[CoversClass(WrappingClock::class)]
#[UsesClass(FrozenClock::class)]
final class WrappingClockTest extends TestCase
{
#[Test]
public function itWrapsAClockInterface(): void
{
$clock = FrozenClock::fromUTC();
$wrappedClock = WrappingClock::wrapping($clock);
self::assertSame(
$clock->now()->format(DATE_ATOM),
$wrappedClock->now()->format(DATE_ATOM)
);
}
#[Test]
public function itWrapsAnObjectWithANowMethod(): void
{
$now = FrozenClock::fromUTC()->now();
$clock = new class($now) {
private DateTimeImmutable $now;
public function __construct(DateTimeImmutable $now)
{
$this->now = $now;
}
public function now(): DateTimeImmutable
{
return $this->now;
}
};
$wrappedClock = WrappingClock::wrapping($clock);
self::assertSame(
$now->format(DATE_ATOM),
$wrappedClock->now()->format(DATE_ATOM)
);
}
#[Test]
public function itDoesNotCallNowWhileWrapping(): void
{
$clock = new class() {
public int $calls = 0;
public function now(): DateTimeImmutable
{
++$this->calls;
return new DateTimeImmutable();
}
};
$wrappedClock = WrappingClock::wrapping($clock);
self::assertSame(0, $clock->calls);
$wrappedClock->now();
self::assertSame(1, $clock->calls);
}
#[Test]
public function itRejectsObjectsWithANowMethodReturningANonDateTimeImmutable(): void
{
$clock = new class() {
public function now(): string
{
return 'foo';
}
};
$this->expectException(UnexpectedValueException::class);
$this->expectExceptionMessage('$clock->now() must return a DateTimeImmutable');
WrappingClock::wrapping($clock)->now();
}
#[Test]
public function itRejectsObjectsWithoutANowMethod(): void
{
$clock = new class() {
};
$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage('$clock must implement Psr\Clock\ClockInterface or have a now() method');
WrappingClock::wrapping($clock);
}
#[Test]
public function itRejectsObjectsWithANonPublicNowMethod(): void
{
$clock = new class() {
protected function now(): DateTimeImmutable
{
return new DateTimeImmutable();
}
};
$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage('$clock->now() must be public and accept no required parameters');
WrappingClock::wrapping($clock);
}
#[Test]
public function itRejectsObjectsWithANowMethodRequiringArguments(): void
{
$clock = new class() {
public function now(string $timeZone): DateTimeImmutable
{
return new DateTimeImmutable('now', new DateTimeZone($timeZone));
}
};
$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage('$clock->now() must be public and accept no required parameters');
WrappingClock::wrapping($clock);
}
}