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

Add ToDateTime parameter attribute #77

Merged
merged 22 commits into from
Apr 3, 2024
Merged
Show file tree
Hide file tree
Changes from 20 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ jobs:
uses: yiisoft/actions/.github/workflows/phpunit.yml@master
with:
ini-values: pcov.directory=$GITHUB_WORKSPACE, pcov.exclude=#^(?!($GITHUB_WORKSPACE/config/|$GITHUB_WORKSPACE/src/)).*#
extensions: intl
os: >-
['ubuntu-latest', 'windows-latest']
php: >-
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/mutation.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,6 @@ jobs:
os: >-
['ubuntu-latest']
php: >-
['8.2']
['8.3']
secrets:
STRYKER_DASHBOARD_API_KEY: ${{ secrets.STRYKER_DASHBOARD_API_KEY }}
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

## 1.2.0 under development

- New #77: Add `ToDateTimeImmutable` parameter attribute (@vjik)
- Enh #76: Raise the minimum version of PHP to 8.1 (@vjik)

## 1.1.0 February 09, 2024
Expand Down
3 changes: 2 additions & 1 deletion composer-require-checker.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
{
"symbol-whitelist": [
"Yiisoft\\Router\\CurrentRoute"
"Yiisoft\\Router\\CurrentRoute",
"IntlDateFormatter"
]
}
3 changes: 3 additions & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,9 @@
"yiisoft/dummy-provider": "^1.0",
"yiisoft/test-support": "^3.0"
},
"suggest": {
"ext-intl": "Allows using `ToDateTimeImmutable` parameter attribute"
},
"autoload": {
"psr-4": {
"Yiisoft\\Hydrator\\": "src"
Expand Down
17 changes: 17 additions & 0 deletions docs/guide/en/typecasting.md
Original file line number Diff line number Diff line change
Expand Up @@ -138,3 +138,20 @@ $money = $hydrator->create(Money::class, [
'currency' => 'AMD',
]);
```

To cast a value to `DateTimeImmutable` object explicitly, you can use `ToDateTimeImmutable` attribute:

```php
use DateTimeImmutable;
use Yiisoft\Hydrator\Attribute\Parameter\ToDateTimeImmutable;

class Person
{
public function __construct(
#[ToDateTimeImmutable(locale: 'ru')]
private ?DateTimeImmutable $birthday = null,
) {}
}

$person = $hydrator->create(Person::class, ['birthday' => '27.01.1986']);
```
17 changes: 17 additions & 0 deletions docs/guide/ru/typecasting.md
Original file line number Diff line number Diff line change
Expand Up @@ -138,3 +138,20 @@ $money = $hydrator->create(Money::class, [
'currency' => 'AMD',
]);
```

Для приведения значения к объекту `DateTimeImmutable` явно, вы можете использовать атрибут `ToDateTimeImmutable`:

```php
use DateTimeImmutable;
use Yiisoft\Hydrator\Attribute\Parameter\ToDateTimeImmutable;

class Person
{
public function __construct(
#[ToDateTimeImmutable(locale: 'ru')]
private ?DateTimeImmutable $birthday = null,
) {}
}

$person = $hydrator->create(Person::class, ['birthday' => '27.01.1986']);
```
5 changes: 3 additions & 2 deletions psalm.xml
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,13 @@
xsi:schemaLocation="https://getpsalm.org/schema/config vendor/vimeo/psalm/config.xsd"
>
<projectFiles>
<directory name="src"/>
<directory name="src" />
<ignoreFiles>
<directory name="vendor"/>
<directory name="vendor" />
</ignoreFiles>
</projectFiles>
<issueHandlers>
<MixedAssignment errorLevel="suppress" />
<RiskyTruthyFalsyComparison errorLevel="suppress" />
</issueHandlers>
</psalm>
36 changes: 36 additions & 0 deletions src/Attribute/Parameter/ToDateTimeImmutable.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<?php

declare(strict_types=1);

namespace Yiisoft\Hydrator\Attribute\Parameter;

use Attribute;
use IntlDateFormatter;

/**
* Converts the resolved value to `DateTimeImmutable` object. Non-resolved and invalid values are skipped.
*
* @psalm-type IntlDateFormatterFormat = IntlDateFormatter::FULL | IntlDateFormatter::LONG | IntlDateFormatter::MEDIUM | IntlDateFormatter::SHORT | IntlDateFormatter::NONE
*/
#[Attribute(Attribute::TARGET_PROPERTY | Attribute::TARGET_PARAMETER | Attribute::IS_REPEATABLE)]
final class ToDateTimeImmutable implements ParameterAttributeInterface
{
/**
* @psalm-param IntlDateFormatterFormat|null $dateType
* @psalm-param IntlDateFormatterFormat|null $timeType
* @psalm-param non-empty-string|null $timeZone
*/
public function __construct(
public readonly ?string $format = null,
public readonly ?int $dateType = null,
public readonly ?int $timeType = null,
public readonly ?string $timeZone = null,
public readonly ?string $locale = null,
) {
}

public function getResolver(): string
{
return ToDateTimeImmutableResolver::class;
}
}
141 changes: 141 additions & 0 deletions src/Attribute/Parameter/ToDateTimeImmutableResolver.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
<?php

declare(strict_types=1);

namespace Yiisoft\Hydrator\Attribute\Parameter;

use DateTimeImmutable;
use DateTimeInterface;
use DateTimeZone;
use IntlDateFormatter;
use Yiisoft\Hydrator\AttributeHandling\Exception\UnexpectedAttributeException;
use Yiisoft\Hydrator\AttributeHandling\ParameterAttributeResolveContext;
use Yiisoft\Hydrator\Result;

/**
* @psalm-import-type IntlDateFormatterFormat from ToDateTimeImmutable
*/
final class ToDateTimeImmutableResolver implements ParameterAttributeResolverInterface
{
/**
* @psalm-param IntlDateFormatterFormat $dateType
* @psalm-param IntlDateFormatterFormat $timeType
* @psalm-param non-empty-string|null $timeZone
*/
public function __construct(
private readonly ?string $format = null,
private readonly int $dateType = IntlDateFormatter::SHORT,
private readonly int $timeType = IntlDateFormatter::SHORT,
private readonly ?string $timeZone = null,
private readonly ?string $locale = null,
) {
}

public function getParameterValue(
ParameterAttributeInterface $attribute,
ParameterAttributeResolveContext $context
): Result {
if (!$attribute instanceof ToDateTimeImmutable) {
throw new UnexpectedAttributeException(ToDateTimeImmutable::class, $attribute);
}

if (!$context->isResolved()) {
return Result::fail();
}

$resolvedValue = $context->getResolvedValue();

if ($resolvedValue instanceof DateTimeImmutable) {
return Result::success($resolvedValue);
}

if ($resolvedValue instanceof DateTimeInterface) {
return Result::success(DateTimeImmutable::createFromInterface($resolvedValue));
}

$timeZone = $attribute->timeZone ?? $this->timeZone;
if ($timeZone !== null) {
$timeZone = new DateTimeZone($timeZone);
}

if (is_int($resolvedValue)) {
return Result::success(
$this->makeDateTimeFromTimestamp($resolvedValue, $timeZone)
);
}

if (is_string($resolvedValue) && !empty($resolvedValue)) {
$format = $attribute->format ?? $this->format;
if (is_string($format) && str_starts_with($format, 'php:')) {
return $this->parseWithPhpFormat($resolvedValue, substr($format, 4), $timeZone);
}
return $this->parseWithIntlFormat(
$resolvedValue,
$format,
$attribute->dateType ?? $this->dateType,
$attribute->timeType ?? $this->timeType,
$timeZone,
$attribute->locale ?? $this->locale,
);
}

return Result::fail();
}

/**
* @psalm-param non-empty-string $resolvedValue
*/
private function parseWithPhpFormat(string $resolvedValue, string $format, ?DateTimeZone $timeZone): Result
{
$date = DateTimeImmutable::createFromFormat($format, $resolvedValue, $timeZone);
if ($date === false) {
return Result::fail();
}

$errors = DateTimeImmutable::getLastErrors();
if (!empty($errors['warning_count'])) {
return Result::fail();
}

// If no time was provided in the format string set time to 0
if (!strpbrk($format, 'aAghGHisvuU')) {
$date = $date->setTime(0, 0);
}

return Result::success($date);
}

/**
* @psalm-param non-empty-string $resolvedValue
* @psalm-param IntlDateFormatterFormat $dateType
* @psalm-param IntlDateFormatterFormat $timeType
*/
private function parseWithIntlFormat(
string $resolvedValue,
?string $format,
int $dateType,
int $timeType,
?DateTimeZone $timeZone,
?string $locale,
): Result {
$formatter = $format === null
? new IntlDateFormatter($locale, $dateType, $timeType, $timeZone)
: new IntlDateFormatter(
$locale,
IntlDateFormatter::NONE,
IntlDateFormatter::NONE,
$timeZone,
pattern: $format
);
$formatter->setLenient(false);
$timestamp = $formatter->parse($resolvedValue);
return is_int($timestamp)
? Result::success($this->makeDateTimeFromTimestamp($timestamp, $timeZone))
: Result::fail();
}

private function makeDateTimeFromTimestamp(int $timestamp, ?DateTimeZone $timeZone): DateTimeImmutable
{
return (new DateTimeImmutable(timezone: $timeZone))->setTimestamp($timestamp);
}
}
Loading
Loading