-
-
Notifications
You must be signed in to change notification settings - Fork 74
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat!: introduce constructor for custom date formats
A new constructor can be registered to declare which format(s) are supported during the mapping of a date object. By default, any valid timestamp or ATOM-formatted value will be accepted. ```php (new \CuyZ\Valinor\MapperBuilder()) // Both COOKIE and ATOM formats will be accepted ->registerConstructor( new \CuyZ\Valinor\Mapper\Object\DateTimeFormatConstructor(DATE_COOKIE, DATE_ATOM) ) ->mapper() ->map(DateTimeInterface::class, 'Monday, 08-Nov-1971 13:37:42 UTC'); ``` The previously very opinionated behaviour has been removed, but can be temporarily used to help with the migration. ```php (new \CuyZ\Valinor\MapperBuilder()) ->registerConstructor( new \CuyZ\Valinor\Mapper\Object\BackwardCompatibilityDateTimeConstructor() ) ->mapper() ->map(DateTimeInterface::class, 'Monday, 08-Nov-1971 13:37:42 UTC'); ```
- Loading branch information
Showing
16 changed files
with
528 additions
and
275 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,52 @@ | ||
# Dealing with dates | ||
|
||
When the mapper builds a date object, it has to know which format(s) are | ||
supported. By default, any valid timestamp or ATOM-formatted value will be | ||
accepted. | ||
|
||
If other formats are to be supported, they need to be registered using the | ||
following constructor: | ||
|
||
```php | ||
(new \CuyZ\Valinor\MapperBuilder()) | ||
// Both `Cookie` and `ATOM` formats will be accepted | ||
->registerConstructor( | ||
new \CuyZ\Valinor\Mapper\Object\DateTimeFormatConstructor(DATE_COOKIE, DATE_ATOM) | ||
) | ||
->mapper() | ||
->map(DateTimeInterface::class, 'Monday, 08-Nov-1971 13:37:42 UTC'); | ||
``` | ||
|
||
## Custom date class implementation | ||
|
||
By default, the library will map a `DateTimeInterface` to a `DateTimeImmutable` | ||
instance. If other implementations are to be supported, custom constructors can | ||
be used. | ||
|
||
Here is an implementation example for the [nesbot/carbon] library: | ||
|
||
```php | ||
(new MapperBuilder()) | ||
// When the mapper meets a `DateTimeInterface` it will convert it to Carbon | ||
->infer(DateTimeInterface::class, fn () => \Carbon\Carbon::class) | ||
|
||
// We teach the mapper how to create a Carbon instance | ||
->registerConstructor(function (string $time): \Carbon\Carbon { | ||
// Only `Cookie` format will be accepted | ||
return Carbon::createFromFormat(DATE_COOKIE, $time); | ||
}) | ||
|
||
// Carbon uses its own exceptions, so we need to wrap it for the mapper | ||
->filterExceptions(function (Throwable $exception) { | ||
if ($exception instanceof \Carbon\Exceptions\Exception) { | ||
return \CuyZ\Valinor\Mapper\Tree\Message\ThrowableMessage::from($exception); | ||
} | ||
|
||
throw $exception; | ||
}) | ||
|
||
->mapper() | ||
->map(DateTimeInterface::class, 'Monday, 08-Nov-1971 13:37:42 UTC'); | ||
``` | ||
|
||
[nesbot/carbon]: https://github.com/briannesbitt/Carbon |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
95 changes: 95 additions & 0 deletions
95
src/Mapper/Object/BackwardCompatibilityDateTimeConstructor.php
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,95 @@ | ||
<?php | ||
|
||
declare(strict_types=1); | ||
|
||
namespace CuyZ\Valinor\Mapper\Object; | ||
|
||
use CuyZ\Valinor\Mapper\Object\Exception\CannotParseToBackwardCompatibilityDateTime; | ||
use DateTime; | ||
use DateTimeImmutable; | ||
use DateTimeInterface; | ||
|
||
/** | ||
* @deprecated This class is only here to support the old datetime mapping | ||
* behaviour. It can be used to temporarily have the same behaviour | ||
* during the mapping. A migration to {@see DateTimeFormatConstructor} is | ||
* strongly recommended. | ||
* | ||
* Usage: | ||
* | ||
* ```php | ||
* (new \CuyZ\Valinor\MapperBuilder()) | ||
* ->registerConstructor(new BackwardCompatibilityDateTimeConstructor()) | ||
* ->mapper() | ||
* ->map(SomeClass::class, […]); | ||
* ``` | ||
* | ||
* @api | ||
*/ | ||
final class BackwardCompatibilityDateTimeConstructor | ||
{ | ||
public const DATE_MYSQL = 'Y-m-d H:i:s'; | ||
public const DATE_PGSQL = 'Y-m-d H:i:s.u'; | ||
public const DATE_WITHOUT_TIME = '!Y-m-d'; | ||
|
||
/** | ||
* @param class-string<DateTime|DateTimeImmutable> $className | ||
* @param non-empty-string|positive-int|array{datetime: non-empty-string|positive-int, format?: ?non-empty-string} $value | ||
*/ | ||
#[DynamicConstructor] | ||
public function __invoke(string $className, $value): DateTimeInterface | ||
{ | ||
$datetime = $value; | ||
$format = null; | ||
|
||
if (is_array($datetime)) { | ||
$format = $datetime['format'] ?? null; | ||
$datetime = $datetime['datetime']; | ||
} | ||
|
||
if ($format) { | ||
$date = $this->tryFormat($className, (string)$datetime, $format); | ||
} elseif (is_int($datetime)) { | ||
$date = $this->tryFormat($className, (string)$datetime, 'U'); | ||
} else { | ||
$date = $this->tryAllFormats($className, $datetime); | ||
} | ||
|
||
if (! $date) { | ||
// @PHP8.0 use throw exception expression | ||
throw new CannotParseToBackwardCompatibilityDateTime($datetime); | ||
} | ||
|
||
return $date; | ||
} | ||
|
||
/** | ||
* @param class-string<DateTime|DateTimeImmutable> $className | ||
*/ | ||
private function tryAllFormats(string $className, string $value): ?DateTimeInterface | ||
{ | ||
$formats = [ | ||
self::DATE_MYSQL, self::DATE_PGSQL, DATE_ATOM, DATE_RFC850, DATE_COOKIE, | ||
DATE_RFC822, DATE_RFC1036, DATE_RFC1123, DATE_RFC2822, DATE_RFC3339, | ||
DATE_RFC3339_EXTENDED, DATE_RFC7231, DATE_RSS, DATE_W3C, self::DATE_WITHOUT_TIME, | ||
]; | ||
|
||
foreach ($formats as $format) { | ||
$date = $this->tryFormat($className, $value, $format); | ||
|
||
if ($date instanceof DateTimeInterface) { | ||
return $date; | ||
} | ||
} | ||
|
||
return null; | ||
} | ||
|
||
/** | ||
* @param class-string<DateTime|DateTimeImmutable> $className | ||
*/ | ||
private function tryFormat(string $className, string $value, string $format): ?DateTimeInterface | ||
{ | ||
return $className::createFromFormat($format, $value) ?: null; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,63 @@ | ||
<?php | ||
|
||
declare(strict_types=1); | ||
|
||
namespace CuyZ\Valinor\Mapper\Object; | ||
|
||
use CuyZ\Valinor\Mapper\Object\Exception\CannotParseToDateTime; | ||
use DateTime; | ||
use DateTimeImmutable; | ||
use DateTimeInterface; | ||
|
||
/** | ||
* Can be given to {@see MapperBuilder::registerConstructor()} to describe which | ||
* date formats should be allowed during mapping. | ||
* | ||
* By default, if this constructor is never registered, the dates will accept | ||
* any valid timestamp or ATOM-formatted value. | ||
* | ||
* Usage: | ||
* | ||
* ```php | ||
* (new \CuyZ\Valinor\MapperBuilder()) | ||
* // Both `Cookie` and `ATOM` formats will be accepted | ||
* ->registerConstructor(new DateTimeFormatConstructor(DATE_COOKIE, DATE_ATOM)) | ||
* ->mapper() | ||
* ->map(DateTimeInterface::class, 'Monday, 08-Nov-1971 13:37:42 UTC'); | ||
* ``` | ||
* | ||
* @api | ||
*/ | ||
final class DateTimeFormatConstructor | ||
{ | ||
/** @var non-empty-array<non-empty-string> */ | ||
private array $formats; | ||
|
||
/** | ||
* @param non-empty-string $format | ||
* @param non-empty-string ...$formats | ||
*/ | ||
public function __construct(string $format, string ...$formats) | ||
{ | ||
$this->formats = [$format, ...$formats]; | ||
} | ||
|
||
/** | ||
* @param class-string<DateTime|DateTimeImmutable> $className | ||
* @param non-empty-string|positive-int $value | ||
* @PHP8.0 union | ||
*/ | ||
#[DynamicConstructor] | ||
public function __invoke(string $className, $value): DateTimeInterface | ||
{ | ||
foreach ($this->formats as $format) { | ||
$date = $className::createFromFormat($format, (string)$value) ?: null; | ||
|
||
if ($date) { | ||
return $date; | ||
} | ||
} | ||
|
||
throw new CannotParseToDateTime($value, $this->formats); | ||
} | ||
} |
This file was deleted.
Oops, something went wrong.
42 changes: 42 additions & 0 deletions
42
src/Mapper/Object/Exception/CannotParseToBackwardCompatibilityDateTime.php
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,42 @@ | ||
<?php | ||
|
||
declare(strict_types=1); | ||
|
||
namespace CuyZ\Valinor\Mapper\Object\Exception; | ||
|
||
use CuyZ\Valinor\Mapper\Tree\Message\ErrorMessage; | ||
use CuyZ\Valinor\Mapper\Tree\Message\HasParameters; | ||
use CuyZ\Valinor\Utility\String\StringFormatter; | ||
use CuyZ\Valinor\Utility\ValueDumper; | ||
use RuntimeException; | ||
|
||
/** @internal */ | ||
final class CannotParseToBackwardCompatibilityDateTime extends RuntimeException implements ErrorMessage, HasParameters | ||
{ | ||
private string $body = 'Value {value} does not match a valid date format.'; | ||
|
||
/** @var array<string, string> */ | ||
private array $parameters; | ||
|
||
/** | ||
* @param string|int $datetime | ||
*/ | ||
public function __construct($datetime) | ||
{ | ||
$this->parameters = [ | ||
'value' => ValueDumper::dump($datetime), | ||
]; | ||
|
||
parent::__construct(StringFormatter::for($this), 1659706547); | ||
} | ||
|
||
public function body(): string | ||
{ | ||
return $this->body; | ||
} | ||
|
||
public function parameters(): array | ||
{ | ||
return $this->parameters; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.