Skip to content

Added default to attribute to specify default values for properties #5

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

Merged
merged 4 commits into from
Dec 27, 2024
Merged
Show file tree
Hide file tree
Changes from all 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
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# PHP Pure Data objects

![Packagist Version](https://img.shields.io/packagist/v/nuxtifyts/php-dto)
![Packagist Version](https://img.shields.io/packagist/v/nuxtifyts/php-dto?style=for-the-badge&cacheSeconds=3600)
![PhpStan Level](https://img.shields.io/badge/PHPStan-level%2010-brightgreen.svg?style=for-the-badge)

This package enabled the creation of data objects which can be used in various ways.
Using modern PHP syntax, it allows you to hydrate data from arrays, objects, and other data sources.
Expand Down
87 changes: 87 additions & 0 deletions docs/DefaultValues.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
Default Values
=

Sometimes, we may need to specify that a property has a default value,
we can achieve that using plain PHP for some property types but not all of them.

```php
use Nuxtifyts\PhpDto\Data;

final readonly class User extends Data
{
public function __construct(
public string $firstName,
public string $lastName,
public string $email,
public UserType $type = UserType::DEFAULT,
public UserConfigData $config,
) {}
}
```

On the other hand, if we want to specify, for example, a default value for UserType depending
on the provided email address, or if you want to provide a default value for complex data such as
`UserConfigData` which is another DTO, there is no way to do it using plain PHP,
that's where `DefaultsTo` attribute comes in.

```php
use Nuxtifyts\PhpDto\Data;
use Nuxtifyts\PhpDto\Attributes\Property\DefaultsTo;

final readonly class User extends Data
{
public function __construct(
public string $firstName,
public string $lastName,
public string $email,
#[DefaultsTo(UserType::DEFAULT)]
public UserType $type,
#[DefaultsTo(UserConfigDataFallbackResolver::class)]
public UserConfigData $config,
) {}
}
```

The `DefaultsTo` attribute provides the ability to specify default values for complex types,
such as DateTimes and DTOs.

For more details checkout the [DefaultValues](https://github.com/nuxtifyts/php-dto/blob/main/docs/DefaultValues.md)
guide.

In this example, the `UserConfigDataFallbackResolver` would look like this:

```php
use Nuxtifyts\PhpDto\Contexts\PropertyContext;
use Nuxtifyts\PhpDto\FallbackResolver\FallbackResolver;

class UserConfigDataFallbackResolver implements FallbackResolver
{
/**
* @param array<string, mixed> $rawData
*/
public static function resolve(array $rawData, PropertyContext $property) : mixed{
$email = $rawData['email'] ?? null;

return match(true) {
str_contains($email, 'example.com') => new UserConfigData(/** Admin configs */),
default => new UserConfigData(/** User configs */)
}
}
}
```

>! When using `DefaultsTo` attribute, priority is given to the attribute instead of the parameter's default value.

If ever needed to create a new instance of a DTO with complex default value,
using the constructor is no longer possible, instead, you can make use of the
`create` function provided by the DTO class.

Using the same example above, we can create a new instance of `User` with the default value for `config`:

```php
$user = User::create(
firstName: 'John',
lastName: 'Doe',
email: 'johndoe@example.com'
);
```
50 changes: 50 additions & 0 deletions docs/PropertyAttributes.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ Property Attributes
In order to provide more functionality to your DTOs, you can use the following attributes:
- [Computed](#Computed) - To define a property that is computed from other properties.
- [Aliases](#Aliases) - To define aliases for a property.
- [DefaultsTo](#DefaultsTo) - To define a default value for a property using a fallback resolver.
- [CipherTarget](#CipherTarget) - To define a property that should be encrypted/decrypted.

Computed
Expand Down Expand Up @@ -109,3 +110,52 @@ public function __construct(
) {}
```

DefaultsTo
-

Sometimes, we may need to specify that a property has a default value,
we can achieve that using plain PHP for some property types but not all of them.

```php
use Nuxtifyts\PhpDto\Data;

final readonly class User extends Data
{
public function __construct(
public string $firstName,
public string $lastName,
public string $email,
public UserType $type = UserType::DEFAULT,
public UserConfigData $config,
) {}
}
```

On the other hand, if we want to specify, for example, a default value for UserType depending
on the provided email address, or if you want to provide a default value for complex data such as
`UserConfigData` which is another DTO, there is no way to do it using plain PHP,
that's where `DefaultsTo` attribute comes in.

```php
use Nuxtifyts\PhpDto\Data;
use Nuxtifyts\PhpDto\Attributes\Property\DefaultsTo;

final readonly class User extends Data
{
public function __construct(
public string $firstName,
public string $lastName,
public string $email,
#[DefaultsTo(UserType::DEFAULT)]
public UserType $type,
#[DefaultsTo(UserConfigDataFallbackResolver::class)]
public UserConfigData $config,
) {}
}
```

The `DefaultsTo` attribute provides the ability to specify default values for complex types,
such as DateTimes and DTOs.

For more details checkout the [DefaultValues](https://github.com/nuxtifyts/php-dto/blob/main/docs/DefaultValues.md)
guide.
40 changes: 40 additions & 0 deletions src/Attributes/Property/DefaultsTo.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<?php

namespace Nuxtifyts\PhpDto\Attributes\Property;

use Attribute;
use BackedEnum;
use Nuxtifyts\PhpDto\Exceptions\FallbackResolverException;
use Nuxtifyts\PhpDto\FallbackResolver\FallbackResolver;
use ReflectionClass;

#[Attribute(Attribute::TARGET_PROPERTY)]
class DefaultsTo
{
/** @var array<string, ReflectionClass<object>> */
protected static array $_resolverReflections = [];

/** @var ?class-string<FallbackResolver> */
protected(set) ?string $fallbackResolverClass = null;

/**
* @param BackedEnum|array<array-key, mixed>|int|string|float|bool|null $value
*
* @throws FallbackResolverException
*/
public function __construct(
protected(set) BackedEnum|array|int|string|float|bool|null $value
) {
if (is_string($value) && class_exists($value)) {
/** @var ReflectionClass<object> $reflection */
$reflection = self::$_resolverReflections[$value] ??= new ReflectionClass($value);

if (!$reflection->implementsInterface(FallbackResolver::class)) {
throw FallbackResolverException::unableToFindResolverClass($value);
} else {
/** @var class-string<FallbackResolver> $value */
$this->fallbackResolverClass = $value;
}
}
}
}
47 changes: 38 additions & 9 deletions src/Concerns/BaseData.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,11 @@
namespace Nuxtifyts\PhpDto\Concerns;

use Nuxtifyts\PhpDto\Contexts\ClassContext;
use Nuxtifyts\PhpDto\Exceptions\DataCreationException;
use Nuxtifyts\PhpDto\Exceptions\DeserializeException;
use Nuxtifyts\PhpDto\Exceptions\SerializeException;
use Nuxtifyts\PhpDto\Pipelines\DeserializePipeline\DecipherDataPipe;
use Nuxtifyts\PhpDto\Pipelines\DeserializePipeline\DeserializePipeline;
use Nuxtifyts\PhpDto\Pipelines\DeserializePipeline\DeserializePipelinePassable;
use Nuxtifyts\PhpDto\Pipelines\DeserializePipeline\RefineDataPipe;
use Nuxtifyts\PhpDto\Pipelines\DeserializePipeline\ResolveValuesFromAliasesPipe;
use Nuxtifyts\PhpDto\Support\Pipeline;
use Nuxtifyts\PhpDto\Support\Traits\HasNormalizers;
use ReflectionClass;
use Throwable;
Expand All @@ -18,6 +16,40 @@ trait BaseData
{
use HasNormalizers;

final public static function create(mixed ...$args): static
{
if (array_any(
array_keys($args),
static fn (string|int $arg) => is_numeric($arg)
)) {
throw DataCreationException::invalidProperty();
}

try {
$value = static::normalizeValue($args, static::class);

if ($value === false) {
throw new DeserializeException(
code: DeserializeException::INVALID_VALUE_ERROR_CODE
);
}

/** @var ClassContext<static> $context */
$context = ClassContext::getInstance(new ReflectionClass(static::class));

$data = DeserializePipeline::createFromArray()
->sendThenReturn(new DeserializePipelinePassable(
classContext: $context,
data: $value
))
->data;

return static::instanceWithConstructorCallFrom($context, $data);
} catch (Throwable $e) {
throw DataCreationException::unableToCreateInstance(static::class, $e);
}
}

/**
* @throws DeserializeException
*/
Expand All @@ -26,7 +58,7 @@ final public static function from(mixed $value): static
try {
$value = static::normalizeValue($value, static::class);

if (empty($value)) {
if ($value === false) {
throw new DeserializeException(
code: DeserializeException::INVALID_VALUE_ERROR_CODE
);
Expand All @@ -35,10 +67,7 @@ final public static function from(mixed $value): static
/** @var ClassContext<static> $context */
$context = ClassContext::getInstance(new ReflectionClass(static::class));

$data = new Pipeline(DeserializePipelinePassable::class)
->through(ResolveValuesFromAliasesPipe::class)
->through(RefineDataPipe::class)
->through(DecipherDataPipe::class)
$data = DeserializePipeline::hydrateFromArray()
->sendThenReturn(new DeserializePipelinePassable(
classContext: $context,
data: $value
Expand Down
14 changes: 14 additions & 0 deletions src/Contexts/PropertyContext.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
use Nuxtifyts\PhpDto\Attributes\Property\Aliases;
use Nuxtifyts\PhpDto\Attributes\Property\CipherTarget;
use Nuxtifyts\PhpDto\Attributes\Property\Computed;
use Nuxtifyts\PhpDto\Attributes\Property\DefaultsTo;
use Nuxtifyts\PhpDto\Attributes\Property\WithRefiner;
use Nuxtifyts\PhpDto\DataCiphers\CipherConfig;
use Nuxtifyts\PhpDto\DataRefiners\DataRefiner;
Expand All @@ -13,6 +14,7 @@
use Nuxtifyts\PhpDto\Exceptions\SerializeException;
use Nuxtifyts\PhpDto\Exceptions\UnknownTypeException;
use Nuxtifyts\PhpDto\Exceptions\UnsupportedTypeException;
use Nuxtifyts\PhpDto\FallbackResolver\FallbackConfig;
use Nuxtifyts\PhpDto\Serializers\Serializer;
use Nuxtifyts\PhpDto\Support\Traits\HasSerializers;
use Nuxtifyts\PhpDto\Support\Traits\HasTypes;
Expand Down Expand Up @@ -42,6 +44,8 @@ class PropertyContext

private(set) ?CipherConfig $cipherConfig = null;

private(set) ?FallbackConfig $fallbackConfig = null;

/** @var list<DataRefiner> */
private(set) array $dataRefiners = [];

Expand Down Expand Up @@ -112,6 +116,16 @@ private function syncPropertyAttributes(): void
encoded: $instance->encoded
);
}

if ($defaultsToAttribute = $this->reflection->getAttributes(DefaultsTo::class)[0] ?? null) {
/** @var ReflectionAttribute<DefaultsTo> $defaultsToAttribute */
$instance = $defaultsToAttribute->newInstance();

$this->fallbackConfig = new FallbackConfig(
value: $instance->value,
resolverClass: $instance->fallbackResolverClass
);
}
}

public function getValue(object $object): mixed
Expand Down
6 changes: 6 additions & 0 deletions src/Contracts/BaseData.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,18 @@

namespace Nuxtifyts\PhpDto\Contracts;

use Nuxtifyts\PhpDto\Exceptions\DataCreationException;
use Nuxtifyts\PhpDto\Exceptions\DeserializeException;
use Nuxtifyts\PhpDto\Exceptions\SerializeException;
use JsonSerializable;

interface BaseData extends JsonSerializable
{
/**
* @throws DataCreationException
*/
public static function create(mixed ...$args): static;

/**
* @return array<string, mixed>
*
Expand Down
31 changes: 31 additions & 0 deletions src/Exceptions/DataCreationException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?php

namespace Nuxtifyts\PhpDto\Exceptions;

use Exception;
use Throwable;

class DataCreationException extends Exception
{
protected const int UNABLE_TO_CREATE_INSTANCE = 0;
protected const int INVALID_PROPERTY = 1;

public static function unableToCreateInstance(
string $class,
?Throwable $previous = null
): self {
return new self(
message: "Unable to create instance of class {$class}",
code: self::UNABLE_TO_CREATE_INSTANCE,
previous: $previous
);
}

public static function invalidProperty(): self
{
return new self(
message: 'Invalid property passed to create method',
code: self::INVALID_PROPERTY
);
}
}
27 changes: 27 additions & 0 deletions src/Exceptions/FallbackResolverException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<?php

namespace Nuxtifyts\PhpDto\Exceptions;

use Exception;

class FallbackResolverException extends Exception
{
protected const int UNABLE_TO_FIND_RESOLVER_CLASS = 0;
protected const int UNABLE_TO_RESOLVE_DEFAULT_VALUE = 1;

public static function unableToFindResolverClass(string $resolverClass): self
{
return new self(
"Unable to find resolver class: {$resolverClass}",
self::UNABLE_TO_FIND_RESOLVER_CLASS
);
}

public static function unableToResolveDefaultValue(): self
{
return new self(
'Unable to resolve default value',
self::UNABLE_TO_RESOLVE_DEFAULT_VALUE
);
}
}
Loading
Loading