Skip to content

Commit

Permalink
Header\Date no longer extends DateTime
Browse files Browse the repository at this point in the history
  • Loading branch information
olvlvl committed Nov 17, 2024
1 parent cdc6395 commit bb6d62a
Show file tree
Hide file tree
Showing 10 changed files with 141 additions and 117 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -169,3 +169,9 @@ $request = Request::from([ RequestOptions::OPTION_METHOD => RequestMethod::METHO
```

Added `WithRecovery` to replace previous recovery feature. Renamed `RescueEvent` as `RecoverEvent`.



### Other changes

The package `icanboogie/datetime` is only required for development.
4 changes: 2 additions & 2 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,12 @@
"php": ">=8.2",
"ext-mbstring": "*",
"icanboogie/prototype": "^6.0",
"icanboogie/event": "^6.0",
"icanboogie/datetime": "^3.0"
"icanboogie/event": "^6.0"
},
"require-dev": {
"ext-fileinfo": "*",
"ext-gd": "*",
"icanboogie/datetime": "^3.0",
"phpstan/phpstan": "^1.12",
"phpunit/phpunit": "^11.4"
},
Expand Down
5 changes: 2 additions & 3 deletions lib/FileResponse.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

namespace ICanBoogie\HTTP;

use ICanBoogie\DateTime;
use InvalidArgumentException;
use LogicException;
use SplFileInfo;
Expand Down Expand Up @@ -36,7 +35,7 @@ class FileResponse extends Response
public const OPTION_ETAG = 'etag';

/**
* Specifies the expiration date as a {@link DateTime} instance or a relative date
* Specifies the expiration date as a {@see \DateTimeInterface} instance or a relative date
* such as "+3 month", which maps to the `Expires` header field. The `max-age` directive of
* the `Cache-Control` header field is computed from the current time. If it is not
* defined {@link DEFAULT_EXPIRES} is used instead.
Expand Down Expand Up @@ -225,7 +224,7 @@ protected function finalize(Headers &$headers, &$body): void

$headers->expires = $expires;
$headers->cache_control->cacheable = 'public';
$headers->cache_control->max_age = $expires->timestamp - DateTime::now()->timestamp;
$headers->cache_control->max_age = $expires->timestamp - time();

if ($status === ResponseStatus::STATUS_NOT_MODIFIED) {
$this->finalize_for_not_modified($headers);
Expand Down
76 changes: 45 additions & 31 deletions lib/Headers/Date.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,58 +2,72 @@

namespace ICanBoogie\HTTP\Headers;

use DateTimeImmutable;
use DateTimeInterface;
use DateTimeZone;
use ICanBoogie\DateTime;

use function is_numeric;

/**
* A date time object that renders into a string formatted for HTTP header fields.
* Representation of a 'Date' header field.
*
* @property-read bool $is_empty
* Whether the value of the {@see Date} is empty.
* @property-read int|null $timestamp
* The Unix timestamp in seconds, or null if {@see Date} is empty.
*
* @see http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.3.1
*/
class Date extends DateTime
readonly class Date
{
public static function from(
self|\DateTimeInterface|string|null $source,
DateTimeZone|string|null $timezone = null
): static {
DateTimeInterface|int|string|null $source
): self {
$timezone = null;

if ($source === null) {
return static::none();
return new self();
} elseif ($source instanceof DateTimeInterface) {
$timezone = $source->getTimezone();
$source = $source->format('Y-m-d\TH:i:s.u');
} elseif (is_int($source)) {
$timezone = 'UTC';
$source = "@{$source}";
}

return parent::from($source, $timezone);
}

/**
* @param string|int|DateTimeInterface $time If time is provided as a numeric value it is used
* as
* "@{$time}" and the time zone is set to UTC.
* @param DateTimeZone|string $timezone A {@link \DateTimeZone} object representing the desired
* time zone. If the time zone is empty `utc` is used instead.
*/
public function __construct($time = 'now', $timezone = null)
{
if ($time instanceof DateTimeInterface) {
$time = $time->getTimestamp();
if (is_string($timezone)) {
$timezone = new DateTimeZone($timezone);
}

if (is_numeric($time)) {
$time = '@' . $time;
$timezone = null;
}
$datetime = new DateTimeImmutable($source, $timezone);

parent::__construct($time, $timezone ?: 'utc');
return new self($datetime);
}

private function __construct(
public ?DateTimeInterface $delegate = null
) {
}

/**
* Formats the instance according to the RFC 1123.
*
* @inheritdoc
*/
public function __toString(): string
{
return $this->is_empty ? '' : $this->utc->as_rfc1123;
return $this->is_empty
? ''
: str_replace('+0000', 'GMT', $this->delegate->format(DateTimeInterface::RFC1123));
}

/**
* The timestamp of a {@see \DateTime} or {@see DateTimeImmutable} created with "0000-00-00".
*/
private const EMPTY_TIMESTAMP = -62169984000;

public function __get($property)
{
return match ($property) {
'is_empty' => $this->delegate === null || $this->timestamp == self::EMPTY_TIMESTAMP,
'timestamp' => $this->delegate?->getTimestamp(),
default => throw new \BadMethodCallException('Undefined property: ' . get_class($this) . '::' . $property),
};
}
}
2 changes: 1 addition & 1 deletion lib/Response.php
Original file line number Diff line number Diff line change
Expand Up @@ -289,7 +289,7 @@ protected function get_age(): ?int
}

if (!$this->headers->date->is_empty) {
return max(0, time() - $this->headers->date->utc->timestamp);
return max(0, time() - $this->headers->date->timestamp);
}

return null;
Expand Down
13 changes: 10 additions & 3 deletions tests/FileResponseTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace Test\ICanBoogie\HTTP;

use DateTimeInterface;
use ICanBoogie\DateTime;
use ICanBoogie\HTTP\FileResponse;
use ICanBoogie\HTTP\Headers;
Expand Down Expand Up @@ -230,10 +231,16 @@ public static function provide_test_get_etag(): array
}

#[DataProvider('provide_test_get_expires')]
public function test_get_expires(DateTime $expected, string $file, array $options = [], array $headers = []): void
{
public function test_get_expires(
DateTimeInterface $expected,
string $file,
array $options = [],
array $headers = [],
): void {
$response = new FileResponse($file, Request::from(), $options, $headers);
$this->assertGreaterThanOrEqual($expected->utc->format('YmdHi'), $response->expires->utc->format('YmdHi'));
$actual = $response->expires->delegate;

$this->assertGreaterThanOrEqual($expected, $actual);
}

public static function provide_test_get_expires(): array
Expand Down
71 changes: 71 additions & 0 deletions tests/Headers/DateHeaderTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
<?php

/*
* This file is part of the ICanBoogie package.
*
* (c) Olivier Laviale <[email protected]>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Test\ICanBoogie\HTTP\Headers;

use ICanBoogie\DateTime;
use ICanBoogie\HTTP\Headers\Date;
use ICanBoogie\HTTP\Headers\Date as DateHeader;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\TestCase;

class DateHeaderTest extends TestCase
{
public function test_from_datetime(): void
{
$datetime = new \DateTime();
$sut = DateHeader::from($datetime);
$datetime->setTimezone(new \DateTimeZone('GMT'));

$this->assertEquals($datetime->format('D, d M Y H:i:s') . ' GMT', (string) $sut);
}

public function test_from_string(): void
{
$datetime = new \DateTime('now', new \DateTimeZone('GMT'));

$this->assertEquals(
$datetime->format('D, d M Y H:i:s') . ' GMT',
(string) DateHeader::from($datetime->format('D, d M Y H:i:s P'))
);

$this->assertEquals(
$datetime->format('D, d M Y H:i:s') . ' GMT',
(string) DateHeader::from($datetime->format('D, d M Y H:i:s'))
);

$this->assertEquals(
$datetime->format('D, d M Y H:i:s') . ' GMT',
(string) DateHeader::from($datetime->format('Y-m-d H:i:s'))
);
}

#[DataProvider('provide_test_to_string')]
public function test_to_string($expected, $datetime)
{
$field = Date::from($datetime);

$this->assertEquals($expected, (string) $field);
}

public static function provide_test_to_string(): array
{
$now = DateTime::now();

return [

[ $now->as_rfc1123, $now ],
[ '', DateTime::none() ],
[ '', null ]

];
}
}
41 changes: 0 additions & 41 deletions tests/Headers/DateTimeHeaderTest.php

This file was deleted.

38 changes: 3 additions & 35 deletions tests/HeadersTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,38 +16,6 @@

final class HeadersTest extends TestCase
{
public function testDateTimeFromDateTime(): void
{
$datetime = new \DateTime();
$headers_datetime = new DateHeader($datetime);
$datetime->setTimezone(new DateTimeZone('GMT'));

$this->assertEquals($datetime->format('D, d M Y H:i:s') . ' GMT', (string) $headers_datetime);
}

/**
* @throws Exception
*/
public function testDateTimeFromDateTimeString(): void
{
$datetime = new \DateTime('now', new DateTimeZone('GMT'));

$this->assertEquals(
$datetime->format('D, d M Y H:i:s') . ' GMT',
(string) new DateHeader($datetime->format('D, d M Y H:i:s P'))
);

$this->assertEquals(
$datetime->format('D, d M Y H:i:s') . ' GMT',
(string) new DateHeader($datetime->format('D, d M Y H:i:s'))
);

$this->assertEquals(
$datetime->format('D, d M Y H:i:s') . ' GMT',
(string) new DateHeader($datetime->format('Y-m-d H:i:s'))
);
}

public function test_cache_control(): void
{
$headers = new Headers();
Expand Down Expand Up @@ -105,7 +73,7 @@ public function test_date(): void
$now = new DateTime();
$headers->date = $now;
$this->assertInstanceOf(Headers\Date::class, $headers->date);
$this->assertEquals($now, $headers->date);
$this->assertEquals($now, $headers->date->delegate);
}

public function test_etag(): void
Expand All @@ -129,7 +97,7 @@ public function test_last_modified(): void

$value = DateTime::now();
$headers->last_modified = $value;
$this->assertEquals($value, $headers->last_modified);
$this->assertEquals($value->as_rfc1123, (string) $headers->last_modified);

$headers->last_modified = null;
$this->assertEmpty((string) $headers->last_modified);
Expand All @@ -152,7 +120,7 @@ public function test_retry_after(): void

$value = DateTime::now();
$headers->retry_after = $value;
$this->assertEquals($value, $headers->retry_after);
$this->assertEquals($value->as_rfc1123, (string) $headers->retry_after);
}

#[DataProvider('provide_test_date_header')]
Expand Down
2 changes: 1 addition & 1 deletion tests/ResponseTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ public function test_expires(): void

$value = new DateTime('+1 days');
$response->expires = $value;
$this->assertEquals($value->as_iso8601, $response->expires->as_iso8601);
$this->assertEquals($value, $response->expires->delegate);
$this->assertSame(86400, $response->headers->cache_control->max_age);

$response->expires = null;
Expand Down

0 comments on commit bb6d62a

Please sign in to comment.