Skip to content

Commit c6fe556

Browse files
committed
Implement Nexus Clock
1 parent 72f562e commit c6fe556

File tree

10 files changed

+385
-9
lines changed

10 files changed

+385
-9
lines changed

composer.json

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@
1717
"source": "https://github.com/NexusPHP/framework"
1818
},
1919
"require": {
20-
"php": "^8.2"
20+
"php": "^8.2",
21+
"psr/clock": "^1.0"
2122
},
2223
"require-dev": {
2324
"nexusphp/tachycardia": "^2.3",
@@ -29,8 +30,12 @@
2930
"phpunit/phpunit": "^11.2"
3031
},
3132
"replace": {
33+
"nexusphp/clock": "self.version",
3234
"nexusphp/option": "self.version"
3335
},
36+
"provide": {
37+
"psr/clock-implementation": "1.0"
38+
},
3439
"minimum-stability": "dev",
3540
"prefer-stable": true,
3641
"autoload": {

src/Nexus/Clock/Clock.php

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/**
6+
* This file is part of the Nexus framework.
7+
*
8+
* (c) John Paul E. Balandan, CPA <[email protected]>
9+
*
10+
* For the full copyright and license information, please view
11+
* the LICENSE file that was distributed with this source code.
12+
*/
13+
14+
namespace Nexus\Clock;
15+
16+
use Psr\Clock\ClockInterface;
17+
18+
interface Clock extends ClockInterface
19+
{
20+
/**
21+
* Lets the clock sleep for an amount of seconds.
22+
*/
23+
public function sleep(float|int $seconds): void;
24+
}

src/Nexus/Clock/FrozenClock.php

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/**
6+
* This file is part of the Nexus framework.
7+
*
8+
* (c) John Paul E. Balandan, CPA <[email protected]>
9+
*
10+
* For the full copyright and license information, please view
11+
* the LICENSE file that was distributed with this source code.
12+
*/
13+
14+
namespace Nexus\Clock;
15+
16+
/**
17+
* A clock that always shows the same time.
18+
*/
19+
final class FrozenClock implements Clock
20+
{
21+
public function __construct(
22+
private \DateTimeImmutable $now,
23+
) {}
24+
25+
public function now(): \DateTimeImmutable
26+
{
27+
return $this->now;
28+
}
29+
30+
public function sleep(float|int $seconds): void
31+
{
32+
$now = $this->now->format('U.u') + $seconds;
33+
$now = \sprintf('@%0.6F', $now);
34+
$timezone = $this->now->getTimezone();
35+
36+
$this->now = (new \DateTimeImmutable($now))->setTimezone($timezone);
37+
}
38+
}

src/Nexus/Clock/LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2024 John Paul E. Balandan, CPA <[email protected]>
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

src/Nexus/Clock/README.md

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
# Nexus Clock
2+
3+
Nexus Clock decouples applications from the system clock for better testing.
4+
5+
> Provides an abstraction for a [PSR-20](https://www.php-fig.org/psr/psr-20/)-compatible clock.
6+
7+
## Installation
8+
9+
composer require nexusphp/clock
10+
11+
## Getting Started
12+
13+
```php
14+
<?php
15+
16+
use Nexus\Clock\Clock;
17+
18+
class TimeWarp
19+
{
20+
public function __construct(
21+
private Clock $clock
22+
) {}
23+
24+
public function warp(): void
25+
{
26+
$now = $this->clock->now();
27+
// $now is a \DateTimeImmutable object
28+
29+
$this->clock->sleep(60); // Sleep for 1 minute
30+
31+
var_dump($this->clock->now()->getTimestamp() - $now->getTimestamp());
32+
}
33+
}
34+
35+
$warper = new TimeWarp(new SystemClock('UTC'));
36+
$warper->warp(); // Outputs: int(60)
37+
38+
```
39+
40+
## License
41+
42+
Nexus Option is licensed under the [MIT License][1].
43+
44+
## Resources
45+
46+
* [Report issues][2] and [send pull requests][3] in the [main Nexus repository][4]
47+
48+
[1]: LICENSE
49+
[2]: https://github.com/NexusPHP/framework/issues
50+
[3]: https://github.com/NexusPHP/framework/pulls
51+
[4]: https://github.com/NexusPHP/framework

src/Nexus/Clock/SystemClock.php

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/**
6+
* This file is part of the Nexus framework.
7+
*
8+
* (c) John Paul E. Balandan, CPA <[email protected]>
9+
*
10+
* For the full copyright and license information, please view
11+
* the LICENSE file that was distributed with this source code.
12+
*/
13+
14+
namespace Nexus\Clock;
15+
16+
/**
17+
* A clock that relies on the system time.
18+
*
19+
* @immutable
20+
*/
21+
final readonly class SystemClock implements Clock
22+
{
23+
private \DateTimeZone $timezone;
24+
25+
public function __construct(\DateTimeZone|string $timezone)
26+
{
27+
$this->timezone = \is_string($timezone) ? new \DateTimeZone($timezone) : $timezone;
28+
}
29+
30+
public function now(): \DateTimeImmutable
31+
{
32+
return new \DateTimeImmutable('now', $this->timezone);
33+
}
34+
35+
public function sleep(float|int $seconds): void
36+
{
37+
if ($seconds <= 0) {
38+
return;
39+
}
40+
41+
$microseconds = (int) ($seconds * 1_000_000);
42+
$seconds = (int) floor($microseconds / 1_000_000);
43+
$microseconds %= 1_000_000;
44+
45+
sleep($seconds);
46+
usleep($microseconds);
47+
}
48+
}

src/Nexus/Clock/composer.json

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
{
2+
"name": "nexusphp/clock",
3+
"description": "Nexus Clock decouples applications from the system clock for better testing.",
4+
"license": "MIT",
5+
"type": "library",
6+
"keywords": [
7+
"nexus",
8+
"clock",
9+
"psr-20"
10+
],
11+
"authors": [
12+
{
13+
"name": "John Paul E. Balandan, CPA",
14+
"email": "[email protected]"
15+
}
16+
],
17+
"support": {
18+
"issues": "https://github.com/NexusPHP/framework/issues",
19+
"source": "https://github.com/NexusPHP/framework"
20+
},
21+
"require": {
22+
"php": "^8.2",
23+
"psr/clock": "^1.0"
24+
},
25+
"provide": {
26+
"psr/clock-implementation": "1.0"
27+
},
28+
"minimum-stability": "dev",
29+
"autoload": {
30+
"psr-4": {
31+
"Nexus\\Clock\\": ""
32+
}
33+
},
34+
"config": {
35+
"optimize-autoloader": true,
36+
"preferred-install": "dist",
37+
"sort-packages": true
38+
}
39+
}

tests/Clock/FrozenClockTest.php

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/**
6+
* This file is part of the Nexus framework.
7+
*
8+
* (c) John Paul E. Balandan, CPA <[email protected]>
9+
*
10+
* For the full copyright and license information, please view
11+
* the LICENSE file that was distributed with this source code.
12+
*/
13+
14+
namespace Nexus\Tests\Clock;
15+
16+
use Nexus\Clock\FrozenClock;
17+
use Nexus\Clock\SystemClock;
18+
use PHPUnit\Framework\Attributes\CoversClass;
19+
use PHPUnit\Framework\Attributes\Group;
20+
use PHPUnit\Framework\TestCase;
21+
22+
/**
23+
* @internal
24+
*/
25+
#[CoversClass(FrozenClock::class)]
26+
#[Group('unit')]
27+
final class FrozenClockTest extends TestCase
28+
{
29+
public function testFrozenClockAlwaysReturnTheSameDate(): void
30+
{
31+
$now = new \DateTimeImmutable('now', new \DateTimeZone('UTC'));
32+
$clock = new FrozenClock($now);
33+
34+
self::assertSame($now, $clock->now());
35+
self::assertSame('UTC', $clock->now()->getTimezone()->getName());
36+
37+
(new SystemClock('UTC'))->sleep(0.50);
38+
self::assertSame($now, $clock->now());
39+
self::assertSame('UTC', $clock->now()->getTimezone()->getName());
40+
}
41+
42+
public function testFrozenClockSleepingJustAdvancesTheDate(): void
43+
{
44+
$timezone = new \DateTimeZone('Asia/Manila');
45+
$clock = new FrozenClock(new \DateTimeImmutable('2024-08-05 17:59:59.999', $timezone));
46+
self::assertSame('2024-08-05 17:59:59.999000', $clock->now()->format('Y-m-d H:i:s.u'));
47+
self::assertSame($timezone->getName(), $clock->now()->getTimezone()->getName());
48+
49+
$clock->sleep(3.141592); // pi seconds
50+
self::assertSame('2024-08-05 18:00:03.140592', $clock->now()->format('Y-m-d H:i:s.u'));
51+
self::assertSame($timezone->getName(), $clock->now()->getTimezone()->getName());
52+
53+
$clock->sleep(60 * 60 * 4); // 4 hours
54+
self::assertSame('2024-08-05 22:00:03.140592', $clock->now()->format('Y-m-d H:i:s.u'));
55+
self::assertSame($timezone->getName(), $clock->now()->getTimezone()->getName());
56+
}
57+
}

tests/Clock/SystemClockTest.php

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/**
6+
* This file is part of the Nexus framework.
7+
*
8+
* (c) John Paul E. Balandan, CPA <[email protected]>
9+
*
10+
* For the full copyright and license information, please view
11+
* the LICENSE file that was distributed with this source code.
12+
*/
13+
14+
namespace Nexus\Tests\Clock;
15+
16+
use Nexus\Clock\SystemClock;
17+
use Nexus\PHPUnit\Tachycardia\Attribute\TimeLimit;
18+
use PHPUnit\Framework\Attributes\CoversClass;
19+
use PHPUnit\Framework\Attributes\Group;
20+
use PHPUnit\Framework\TestCase;
21+
22+
/**
23+
* @internal
24+
*/
25+
#[CoversClass(SystemClock::class)]
26+
#[Group('unit')]
27+
final class SystemClockTest extends TestCase
28+
{
29+
public function testTimezoneOfSystemClock(): void
30+
{
31+
$clock = new SystemClock('UTC');
32+
self::assertSame('UTC', $clock->now()->getTimezone()->getName());
33+
34+
$clock = new SystemClock(date_default_timezone_get());
35+
self::assertSame(date_default_timezone_get(), $clock->now()->getTimezone()->getName());
36+
37+
$clock = new SystemClock(new \DateTimeZone('Asia/Manila'));
38+
self::assertSame('Asia/Manila', $clock->now()->getTimezone()->getName());
39+
}
40+
41+
public function testNowMovesWithTime(): void
42+
{
43+
$clock = new SystemClock('UTC');
44+
$before = new \DateTimeImmutable();
45+
usleep(10);
46+
$now = $clock->now();
47+
usleep(10);
48+
$after = new \DateTimeImmutable();
49+
50+
self::assertGreaterThan($before, $now);
51+
self::assertLessThan($after, $now);
52+
}
53+
54+
#[TimeLimit(2.10)]
55+
public function testClockSleeps(): void
56+
{
57+
$clock = new SystemClock('UTC');
58+
$tz = $clock->now()->getTimezone()->getName();
59+
60+
$before = (float) $clock->now()->format('U.u');
61+
$clock->sleep(2.0);
62+
$now = (float) $clock->now()->format('U.u');
63+
$clock->sleep(0.0001);
64+
$after = (float) $clock->now()->format('U.u');
65+
66+
self::assertEqualsWithDelta(2.0, $now - $before, 0.05);
67+
self::assertLessThan($after, $now);
68+
self::assertSame($tz, $clock->now()->getTimezone()->getName());
69+
}
70+
71+
public function testClockDoesNotSleepOnNegativeOrZeroSeconds(): void
72+
{
73+
$clock = new SystemClock('UTC');
74+
$tz = $clock->now()->getTimezone()->getName();
75+
76+
$before = (float) $clock->now()->format('U.u');
77+
$clock->sleep(-2.0);
78+
$now = (float) $clock->now()->format('U.u');
79+
$clock->sleep(0.0);
80+
$after = (float) $clock->now()->format('U.u');
81+
82+
// account for latency in execution time
83+
self::assertEqualsWithDelta(0.0, $now - $before, 0.01);
84+
self::assertEqualsWithDelta(0.0, $after - $now, 0.01);
85+
self::assertSame($tz, $clock->now()->getTimezone()->getName());
86+
}
87+
}

0 commit comments

Comments
 (0)