Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor DateTimeFormatter Filter #160

Merged
merged 11 commits into from
Sep 3, 2024
42 changes: 42 additions & 0 deletions docs/book/v3/standard-filters.md
Original file line number Diff line number Diff line change
Expand Up @@ -642,6 +642,48 @@ All options can be set at instantiation or by using a related method. For exampl
methods for `target` are `getTarget()` and `setTarget()`. You can also use the `setOptions()` method
which accepts an array of all options.

## DateTimeFormatter

This filter formats either a `DateTimeInterface` object, a string, or integer that `DateTime` will understand to a date
and/or time string in the configured format.

### Supported Options

The following options are supported for `Laminas\Filter\DateTimeFormatter`

- `format`: a valid [date format](https://www.php.net/manual/datetime.format.php) to use when formatting a string, for example `l jS F Y`. This option defaults to `DateTimeInterface::ATOM` when unspecified
- `timezone` : The default timezone to apply when converting a string or integer argument to a `DateTime` instance. This option falls back to the system timezone when unspecified

### Basic Usage

#### Without Any Options

```php
$filter = new \Laminas\Filter\DateTimeFormatter();

echo $filter->filter('2024-01-01'); // => 2024-01-01T00:00:00+00:00
echo $filter->filter(1_359_739_801); // => 2013-02-01T17:30:01+00:00
echo $filter->filter(new DateTimeImmutable('2024-01-01')) // => 2024-01-01T00:00:00+00:00
```

#### With `format` Option

```php
$filter = new \Laminas\Filter\DateTimeFormatter([
'format' => 'd-m-Y'
]);
echo $filter->filter('2024-08-16 00:00:00'); // => 16-08-2024
```

#### With `timezone` Option

```php
$filter = new \Laminas\Filter\DateTimeFormatter([
'timezone' => 'Europe/Paris'
]);
echo $filter->filter('2024-01-01'); // => 2024-01-01T00:00:00+01:00
```

## DenyList

This filter will return `null` if the value being filtered is present in the filter's list of
Expand Down
8 changes: 0 additions & 8 deletions psalm-baseline.xml
Original file line number Diff line number Diff line change
Expand Up @@ -206,14 +206,6 @@
<code><![CDATA[$res !== true]]></code>
</TypeDoesNotContainType>
</file>
<file src="src/DateTimeFormatter.php">
<MixedAssignment>
<code><![CDATA[$result]]></code>
</MixedAssignment>
<PossiblyInvalidArgument>
<code><![CDATA[$e->getCode()]]></code>
</PossiblyInvalidArgument>
</file>
<file src="src/DateTimeSelect.php">
<MixedReturnStatement>
<code><![CDATA[$value]]></code>
Expand Down
96 changes: 40 additions & 56 deletions src/DateTimeFormatter.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,99 +4,83 @@

namespace Laminas\Filter;

use DateTime;
use DateTimeImmutable;
use DateTimeInterface;
use DateTimeZone;
use Laminas\Filter\Exception\InvalidArgumentException;
use Throwable;
use Traversable;

use function date_default_timezone_get;
use function is_int;
use function is_string;

/**
* @psalm-type Options = array{
* format?: string,
* ...
* format?: non-empty-string,
* timezone?: non-empty-string,
* }
* @extends AbstractFilter<Options>
* @implements FilterInterface<string>
*/
final class DateTimeFormatter extends AbstractFilter
final class DateTimeFormatter implements FilterInterface
{
/**
* A valid format string accepted by date()
*
* @var string
*/
protected $format = DateTime::ISO8601;
private readonly string $format;

/**
* Sets filter options
*
* @param array|Traversable $options
* A valid timezone string
*/
public function __construct($options = null)
{
if ($options !== null) {
$this->setOptions($options);
}
}
private readonly DateTimeZone $timezone;

/**
* Set the format string accepted by date() to use when formatting a string
*
* @param string $format
* @return self
* @param Options $options
*/
public function setFormat($format)
public function __construct(array $options = [])
{
$this->format = $format;

return $this;
}
$this->format = $options['format'] ?? DateTimeInterface::ATOM;

/**
* Filter a datetime string by normalizing it to the filters specified format
*
* @param DateTime|string|int|mixed $value
* @throws Exception\InvalidArgumentException
*/
public function filter(mixed $value): mixed
{
try {
$result = $this->normalizeDateTime($value);
$this->timezone = new DateTimeZone(
$options['timezone'] ?? date_default_timezone_get()
);
} catch (Throwable $e) {
// DateTime threw an exception, an invalid date string was provided
throw new Exception\InvalidArgumentException('Invalid date string provided', $e->getCode(), $e);
throw new InvalidArgumentException('Invalid timezone provided');
}

if ($result === false) {
return $value;
}

return $result;
}

/**
* Normalize the provided value to a formatted string
* Filter a datetime string by normalizing it to the filters specified format
*
* @return string|mixed
* @inheritDoc
*/
protected function normalizeDateTime(mixed $value)
public function filter(mixed $value): mixed
{
if ($value === '' || $value === null) {
if (
! (is_string($value) && $value !== '')
&& ! is_int($value)
&& ! $value instanceof DateTimeInterface
) {
return $value;
}

if (! is_string($value) && ! is_int($value) && ! $value instanceof DateTimeInterface) {
return $value;
}
try {
if (is_int($value)) {
$value = '@' . (string) $value;
}

if (is_int($value)) {
//timestamp
$value = new DateTime('@' . $value);
} elseif (! $value instanceof DateTimeInterface) {
$value = new DateTime($value);
if (is_string($value)) {
$value = new DateTimeImmutable($value, $this->timezone);
}
} catch (Throwable $e) {
throw new InvalidArgumentException('Invalid date/time string provided');
}

return $value->format($this->format);
}

public function __invoke(mixed $value): mixed
{
return $this->filter($value);
}
}
88 changes: 67 additions & 21 deletions test/DateTimeFormatterTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
use DateTime;
use DateTimeImmutable;
use DateTimeInterface;
use DateTimeZone;
use Laminas\Filter\DateTimeFormatter;
use Laminas\Filter\Exception;
use PHPUnit\Framework\Attributes\DataProvider;
Expand Down Expand Up @@ -35,8 +36,6 @@ public function tearDown(): void
public static function returnUnfilteredDataProvider(): array
{
return [
[null],
[''],
[new stdClass()],
[
[
Expand All @@ -45,6 +44,7 @@ public static function returnUnfilteredDataProvider(): array
],
],
[0.53],
[true],
];
}

Expand All @@ -64,7 +64,7 @@ public function testFormatterFormatsZero(): void

$filter = new DateTimeFormatter();
$result = $filter->filter(0);
self::assertSame('1970-01-01T00:00:00+0000', $result);
self::assertSame('1970-01-01T00:00:00+00:00', $result);
}

public function testDateTimeFormatted(): void
Expand All @@ -73,32 +73,76 @@ public function testDateTimeFormatted(): void

$filter = new DateTimeFormatter();
$result = $filter->filter('2012-01-01');
self::assertSame('2012-01-01T00:00:00+0000', $result);
self::assertSame('2012-01-01T00:00:00+00:00', $result);
}

public function testReturnExceptionOnInvalidTimezone(): void
{
date_default_timezone_set('UTC');

self::expectException(Exception\InvalidArgumentException::class);

new DateTimeFormatter([
'timezone' => 'Continent/City',
]);
}

public function testDateTimeFormattedWithAlternateTimezones(): void
{
$filter = new DateTimeFormatter();
date_default_timezone_set('UTC');

date_default_timezone_set('Europe/Paris');
$filterParis = new DateTimeFormatter([
'timezone' => 'Europe/Paris',
]);

$resultParis = $filter->filter('2012-01-01');
self::assertSame('2012-01-01T00:00:00+0100', $resultParis);
$resultParis = $filterParis->filter('2012-01-01');
self::assertSame('2012-01-01T00:00:00+01:00', $resultParis);

date_default_timezone_set('America/New_York');
$filterNewYork = new DateTimeFormatter([
'timezone' => 'America/New_York',
]);

$resultNewYork = $filter->filter('2012-01-01');
self::assertSame('2012-01-01T00:00:00-0500', $resultNewYork);
$resultNewYork = $filterNewYork->filter('2012-01-01');
self::assertSame('2012-01-01T00:00:00-05:00', $resultNewYork);
}

/**
* @throws \Exception
*/
public function testTimezoneRemainUnchangedOnDateTimeInterfaceInput(): void
{
date_default_timezone_set('UTC');

$filter = new DateTimeFormatter([
'timezone' => 'UTC',
]);

$datetime = new DateTimeImmutable('2024-01-01 00:00:00', new DateTimeZone('America/New_York'));

$result = $filter->filter($datetime);

self::assertSame('2024-01-01T00:00:00-05:00', $result);
}

public function testSetFormat(): void
{
date_default_timezone_set('UTC');

$filter = new DateTimeFormatter();
$filter->setFormat(DateTimeInterface::RFC1036);
$result = $filter->filter('2012-01-01');
self::assertSame('Sun, 01 Jan 12 00:00:00 +0000', $result);
$filter = new DateTimeFormatter([
'format' => DateTimeInterface::RFC1036,
]);
self::assertSame('Sun, 01 Jan 12 00:00:00 +0000', $filter->filter('2012-01-01'));

$filter = new DateTimeFormatter([
'format' => 'd-m-Y',
]);
self::assertSame('16-08-2024', $filter->filter('2024-08-16 00:00:00'));

$filter = new DateTimeFormatter([
'format' => 'asd Y W',
]);

self::assertSame('am0016 2024 33', $filter->filter('2024-08-16 00:00:00'));
}

public function testFormatDateTimeFromTimestamp(): void
Expand All @@ -107,7 +151,7 @@ public function testFormatDateTimeFromTimestamp(): void

$filter = new DateTimeFormatter();
$result = $filter->filter(1_359_739_801);
self::assertSame('2013-02-01T17:30:01+0000', $result);
self::assertSame('2013-02-01T17:30:01+00:00', $result);
}

public function testAcceptDateTimeValue(): void
Expand All @@ -116,27 +160,29 @@ public function testAcceptDateTimeValue(): void

$filter = new DateTimeFormatter();
$result = $filter->filter(new DateTime('2012-01-01'));
self::assertSame('2012-01-01T00:00:00+0000', $result);
self::assertSame('2012-01-01T00:00:00+00:00', $result);
}

public function testInvalidArgumentExceptionThrownOnInvalidInput(): void
public function testThrowInvalidArgumentOnInvalidInput(): void
{
$filter = new DateTimeFormatter();
$this->expectException(Exception\InvalidArgumentException::class);
self::expectException(Exception\InvalidArgumentException::class);
$filter->filter('2013-31-31');
}

public function testAcceptDateTimeInterface(): void
{
date_default_timezone_set('UTC');

$filter = new DateTimeFormatter();

self::assertSame(
'2024-08-09T00:00:00+0000',
'2024-08-09T00:00:00+00:00',
$filter->filter(new DateTimeImmutable('2024-08-09'))
);

self::assertSame(
'2024-08-09T00:00:00+0000',
'2024-08-09T00:00:00+00:00',
$filter->filter(new DateTime('2024-08-09'))
);
}
Expand Down