Author SHA1 Message Date
dependabot[bot] aa4266b61f github-actions(deps): bump actions/checkout from 6 to 7
Bumps [actions/checkout](https://github.com/actions/checkout) from 6 to 7.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v6...v7)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <[email protected]>
2026-06-18 18:22:28 +00:00
20 changed files with 269 additions and 247 deletions
@@ -0,0 +1,23 @@
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}}"
+9 -7
View File
@@ -4,7 +4,9 @@ on:
pull_request:
push:
branches:
- '4.x'
- '1.x'
- '2.x'
- '3.x'
jobs:
tests:
@@ -14,16 +16,18 @@ jobs:
strategy:
matrix:
php:
- "8.0"
- "8.1"
- "8.2"
- "8.3"
- "8.4"
- "8.5"
include:
- php: "8.6"
composer-options: "--ignore-platform-req=php+"
- php: "8.5"
composer-options: "--ignore-platform-reqs"
steps:
- name: Checkout code
uses: actions/checkout@v6
uses: actions/checkout@v7
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
@@ -34,8 +38,6 @@ jobs:
COMPOSER_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- uses: "ramsey/composer-install@v4"
with:
composer-options: "${{ matrix.composer-options }}"
- name: Setup problem matchers for PHP
run: echo "::add-matcher::${{ runner.tool_cache }}/php.json"
-17
View File
@@ -1,22 +1,5 @@
# CHANGELOG
## 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
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`.
`Beste\Clock` will be removed in 4.0.
## 3.0.0 - 2022-11-26
This release replaces `stella-maris/clock` with `psr/clock`.
+6 -4
View File
@@ -114,8 +114,8 @@ printf("\nAfter turning back the clock 5 minutes, it's %s", $frozenClock->now()-
### `MinuteClock`
In some cases, microseconds, milliseconds, or even seconds are too precise. Sometimes it is enough to know that
something happened within the same minute. `MinuteClock` removes seconds and fractions of a second from another clock.
In some cases, microseconds, milliseconds, or even seconds are too precise for some use cases - sometimes it's just
enough if something happened in the same minute. Using the minute
```php
# examples/minute_clock.php
@@ -141,8 +141,10 @@ printf("For %s, the minute clock still returns %s\n",
### `WrappingClock`
If you already have an object with a `now()` method returning a `DateTimeImmutable`, you can wrap it in a
`WrappingClock` to make it a PSR-20 clock.
If you already have an object with a `now()` method returning a `DateTimeImmutable` object, you can wrap it
in a `WrappingClock` to make it a "real" Clock.
as a "real" clock.
```php
# examples/wrapping_clock.php
+8 -5
View File
@@ -11,7 +11,7 @@
}
],
"require": {
"php": "^8.3",
"php": "^8.0",
"psr/clock": "^1.0"
},
"require-dev": {
@@ -19,16 +19,19 @@
"phpstan/phpstan": "^2.1",
"phpstan/phpstan-phpunit": "^2.0",
"phpstan/phpstan-strict-rules": "^2.0",
"phpunit/phpunit": "^12.5"
"phpunit/phpunit": "^9.6.20"
},
"autoload": {
"psr-4": {
"Beste\\Clock\\": "src"
}
"Beste\\Clock\\": "src/Clock"
},
"files": [
"src/Clock.php"
]
},
"autoload-dev": {
"psr-4": {
"Beste\\Clock\\Tests\\": "tests"
"Beste\\Clock\\Tests\\": "tests/Clock"
}
},
"provide": {
+7 -7
View File
@@ -1,9 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<?xml version="1.0"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/12.5/phpunit.xsd"
xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd"
bootstrap="vendor/autoload.php"
cacheDirectory=".build/phpunit/cache"
beStrictAboutCoverageMetadata="true"
cacheResultFile=".build/phpunit/phpunit.cache"
beStrictAboutCoversAnnotation="true"
colors="true"
>
<testsuites>
@@ -12,10 +12,10 @@
</testsuite>
</testsuites>
<source>
<coverage>
<include>
<directory>src</directory>
<directory>src/Clock</directory>
</include>
</source>
</coverage>
</phpunit>
+12
View File
@@ -0,0 +1,12 @@
<?php
declare(strict_types=1);
namespace Beste;
use Psr\Clock\ClockInterface;
interface Clock extends ClockInterface
{
}
@@ -4,11 +4,12 @@ declare(strict_types=1);
namespace Beste\Clock;
use Beste\Clock;
use DateTimeImmutable;
use DateTimeZone;
use Psr\Clock\ClockInterface;
final class FrozenClock implements ClockInterface
final class FrozenClock implements Clock
{
private DateTimeImmutable $frozenAt;
@@ -4,13 +4,13 @@ declare(strict_types=1);
namespace Beste\Clock;
use Beste\Clock;
use DateTimeImmutable;
use DateTimeZone;
use InvalidArgumentException;
use Psr\Clock\ClockInterface;
use Throwable;
final class LocalizedClock implements ClockInterface
final class LocalizedClock implements Clock
{
private DateTimeZone $timeZone;
@@ -19,7 +19,10 @@ final class LocalizedClock implements ClockInterface
$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)) {
try {
@@ -4,10 +4,11 @@ declare(strict_types=1);
namespace Beste\Clock;
use Beste\Clock;
use DateTimeImmutable;
use Psr\Clock\ClockInterface;
final class MinuteClock implements ClockInterface
final class MinuteClock implements Clock
{
private ClockInterface $clock;
@@ -4,10 +4,10 @@ declare(strict_types=1);
namespace Beste\Clock;
use Beste\Clock;
use DateTimeImmutable;
use Psr\Clock\ClockInterface;
final class SystemClock implements ClockInterface
final class SystemClock implements Clock
{
private function __construct()
{
+2 -2
View File
@@ -4,11 +4,11 @@ declare(strict_types=1);
namespace Beste\Clock;
use Beste\Clock;
use DateTimeImmutable;
use DateTimeZone;
use Psr\Clock\ClockInterface;
final class UTCClock implements ClockInterface
final class UTCClock implements Clock
{
private DateTimeZone $timeZone;
@@ -4,14 +4,12 @@ declare(strict_types=1);
namespace Beste\Clock;
use Closure;
use Beste\Clock;
use DateTimeImmutable;
use InvalidArgumentException;
use Psr\Clock\ClockInterface;
use ReflectionMethod;
use UnexpectedValueException;
final class WrappingClock implements ClockInterface
final class WrappingClock implements Clock
{
private ClockInterface $wrappedClock;
@@ -20,9 +18,6 @@ final class WrappingClock implements ClockInterface
$this->wrappedClock = $wrappedClock;
}
/**
* @throws InvalidArgumentException when the given object doesn't behave like a clock.
*/
public static function wrapping(object $clock): self
{
if ($clock instanceof ClockInterface) {
@@ -30,30 +25,28 @@ final class WrappingClock implements ClockInterface
}
if (!method_exists($clock, 'now')) {
throw new InvalidArgumentException('$clock must implement Psr\Clock\ClockInterface or have a now() method');
throw new InvalidArgumentException('$clock must implement StellaMaris\Clock\ClockInterface or have a now() method');
}
$method = new ReflectionMethod($clock, 'now');
if (!$method->isPublic() || $method->getNumberOfRequiredParameters() > 0) {
throw new InvalidArgumentException('$clock->now() must be public and accept no required parameters');
if (!($clock->now() instanceof DateTimeImmutable)) {
throw new InvalidArgumentException('$clock->now() must return a DateTimeImmutable');
}
$wrappedClock = new class($method->getClosure($clock)) implements ClockInterface {
private Closure $now;
$wrappedClock = new class($clock) implements ClockInterface {
private object $clock;
public function __construct(Closure $now)
public function __construct(object $clock)
{
$this->now = $now;
$this->clock = $clock;
}
public function now(): DateTimeImmutable
{
$now = ($this->now)();
assert(method_exists($this->clock, 'now'));
if (!$now instanceof DateTimeImmutable) {
throw new UnexpectedValueException('$clock->now() must return a DateTimeImmutable');
}
$now = $this->clock->now();
assert($now instanceof DateTimeImmutable);
return $now;
}
@@ -6,18 +6,21 @@ namespace Beste\Clock\Tests;
use Beste\Clock\FrozenClock;
use DateTimeImmutable;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\TestCase;
use Psr\Clock\ClockInterface;
/**
* @internal
* @coversDefaultClass \Beste\Clock\FrozenClock
*/
#[CoversClass(FrozenClock::class)]
final class FrozenClockTest extends TestCase
{
#[Test]
/**
* @test
* @covers ::__construct
* @covers ::at
* @covers ::now
*/
public function itFreezesTime(): void
{
$now = new DateTimeImmutable('2021-03-21 22:16:00');
@@ -26,7 +29,14 @@ final class FrozenClockTest extends TestCase
self::assertEquals($now, $clock->now());
}
#[Test]
/**
* @test
*
* @uses ::__construct
* @uses ::fromUTC
*
* @covers ::now
*/
public function itReturnsAnEqualNowButNotTheSame(): void
{
$clock = FrozenClock::fromUTC();
@@ -38,7 +48,14 @@ final class FrozenClockTest extends TestCase
self::assertNotSame($first, $second);
}
#[Test]
/**
* @test
*
* @uses ::__construct
* @uses ::now
*
* @covers ::withNowFrom
*/
public function itFreezesTimeFromAnotherClock(): void
{
$now = new DateTimeImmutable('now');
@@ -54,7 +71,14 @@ final class FrozenClockTest extends TestCase
self::assertEquals($now, $frozenClock->now());
}
#[Test]
/**
* @test
*
* @uses ::__construct
* @uses ::now
*
* @covers ::fromUTC
*/
public function itFreezesTheCurrentUTCTime(): void
{
$clock = FrozenClock::fromUTC();
@@ -63,7 +87,15 @@ final class FrozenClockTest extends TestCase
self::assertEquals($clock->now(), $clock->now());
}
#[Test]
/**
* @test
*
* @uses ::__construct
* @uses ::at
* @uses ::now
*
* @covers ::setTo
*/
public function itCanBeSet(): void
{
$now = new DateTimeImmutable('2021-03-21 18:18:18');
@@ -7,17 +7,18 @@ namespace Beste\Clock\Tests;
use Beste\Clock\LocalizedClock;
use DateTimeZone;
use InvalidArgumentException;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\TestCase;
/**
* @internal
* @coversDefaultClass \Beste\Clock\LocalizedClock
*/
#[CoversClass(LocalizedClock::class)]
final class LocalizedClockTest extends TestCase
{
#[Test]
/**
* @test
* @covers ::in
*/
public function itRejectsAnInvalidTimeZone(): void
{
$this->expectException(InvalidArgumentException::class);
@@ -25,7 +26,12 @@ final class LocalizedClockTest extends TestCase
LocalizedClock::in('invalid');
}
#[Test]
/**
* @test
* @covers ::__construct
* @covers ::in
* @covers ::now
*/
public function itUsesTheGivenTimeZone(): void
{
$timeZone = new DateTimeZone('Asia/Bangkok');
@@ -35,7 +41,12 @@ final class LocalizedClockTest extends TestCase
self::assertSame($timeZone->getName(), $now->getTimezone()->getName());
}
#[Test]
/**
* @test
* @covers ::__construct
* @covers ::in
* @covers ::now
*/
public function itAcceptsTheTimeZoneAsAString(): void
{
$timeZone = 'Pacific/Guam';
@@ -6,23 +6,30 @@ namespace Beste\Clock\Tests;
use Beste\Clock\MinuteClock;
use DateTimeImmutable;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\TestCase;
use Psr\Clock\ClockInterface;
/**
* @internal
* @coversDefaultClass \Beste\Clock\MinuteClock
*/
#[CoversClass(MinuteClock::class)]
final class MinuteClockTest extends TestCase
{
#[Test]
#[DataProvider('dateTimeStringProvider')]
/**
* @test
*
* @covers ::__construct
* @covers ::wrapping
* @covers ::now
* @covers ::floor
*
* @uses \Beste\Clock\FrozenClock
*
* @dataProvider dateTimeStringProvider
*/
public function itDiscardsEverythingSmallerThanMinutes(string $format, string $input, string $expected): void
{
$mockClock = self::createStub(ClockInterface::class);
$mockClock = $this->createMock(ClockInterface::class);
$mockClock->method('now')->willReturn(new DateTimeImmutable($input));
// Just to be sure that the test input is valid ^^
@@ -36,7 +43,7 @@ final class MinuteClockTest extends TestCase
/**
* @return array<string, string[]>
*/
public static function dateTimeStringProvider(): array
public function dateTimeStringProvider(): array
{
return [
'microseconds' => [
@@ -5,14 +5,12 @@ declare(strict_types=1);
namespace Beste\Clock\Tests;
use Beste\Clock\SystemClock;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\TestCase;
/**
* @internal
* @coversDefaultClass \Beste\Clock\SystemClock
*/
#[CoversClass(SystemClock::class)]
final class SystemClockTest extends TestCase
{
private string $defaultTimeZone;
@@ -27,10 +25,16 @@ final class SystemClockTest extends TestCase
date_default_timezone_set($this->defaultTimeZone);
}
#[Test]
/**
* @test
*
* @covers ::__construct
* @covers ::create
* @covers ::now
*/
public function itUsesTheSystemTimeZone(): void
{
$timeZone = $this->defaultTimeZone === 'Europe/Berlin' ? 'UTC' : 'Europe/Berlin';
$timeZone = 'Europe/Berlin';
self::assertNotSame($timeZone, $this->defaultTimeZone);
date_default_timezone_set($timeZone);
@@ -5,14 +5,12 @@ declare(strict_types=1);
namespace Beste\Clock\Tests;
use Beste\Clock\UTCClock;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\TestCase;
/**
* @internal
* @coversDefaultClass \Beste\Clock\UTCClock
*/
#[CoversClass(UTCClock::class)]
final class UTCClockTest extends TestCase
{
private string $defaultTimeZone;
@@ -27,7 +25,13 @@ final class UTCClockTest extends TestCase
date_default_timezone_set($this->defaultTimeZone);
}
#[Test]
/**
* @test
*
* @covers ::__construct
* @covers ::create
* @covers ::now
*/
public function itUsesUTCRegardlessOfTheSystemTimeZone(): void
{
date_default_timezone_set('Europe/Berlin');
+88
View File
@@ -0,0 +1,88 @@
<?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);
}
}
-147
View File
@@ -1,147 +0,0 @@
<?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);
}
}