Skip to content

Commit

Permalink
Merge pull request #160 from cPintiuta/3.0.x
Browse files Browse the repository at this point in the history
Refactor `DateTimeFormatter` Filter
  • Loading branch information
gsteel authored Sep 3, 2024
2 parents b8cf55d + 417a5d1 commit e288198
Show file tree
Hide file tree
Showing 4 changed files with 149 additions and 85 deletions.
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

0 comments on commit e288198

Please sign in to comment.