Skip to content

Added cloneable data contract and trait #8

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 28, 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
119 changes: 119 additions & 0 deletions docs/CloneableData.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
Cloneable Data
=

Sometimes we may want to alter the data of a `Data` object (Partially or completely).
And since `Data` objects are immutable by default, we can't change the data directly.

To solve this, we can use the `with` function that will return a new instance of the `Data` object with the new data.
Let take the `TodoData` class as an example:

```php
use Nuxtifyts\PhpDto\Data;
use DateTimeImmutable;

final readonly class TodoData extends Data
{
public function __construct(
public string $title,
public string $content,
public Status $status,
public ?DateTimeImmutable $dueDate
) {}
}
```

The `Status` enum is defined as follows:

```php
enum Status: string
{
case DEFAULT = 'default';
case IN_PROGRESS = 'in_progress';
case DONE = 'done';
}
```

Using `with` function, we can easily create new instances of the `TodoData` class with the new data:

```php
$emptyTodo = Todo::empty();

// ...

$todo = $emptyTodo->with(
title: 'Learn PHP DTO',
content: 'Learn how to use PHP DTO',
status: Status::IN_PROGRESS
);

// ...

$todoWithDueDate = $todo->with(
dueDate: new DateTimeImmutable('2025-01-06')
);
```

> We are using the `empty` method
> from [Empty Data](https://github.com/nuxtifyts/php-dto/blob/main/docs/EmptyData.md)
> here

> `emptyTodo`, `todo` and `todoWithDueDate` are all different instances.

Computed properties
-

When cloning a `Data` object, computed properties are automatically updated with the new data.

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

final readonly class PersonData extends Data
{
#[Computed]
public string $fullName;

public function __construct(
public string $firstName,
public string $lastName
) {}
}
```

For example:

```php
$johnDoe = new PersonData(firstName: 'John', lastName: 'Doe');

$janeDoe = $johnDoe->with(firstName: 'Jane');

$janeDoe->fullName; // 'Jane Doe'
```

Normalizers
-

When cloning a `Data` object, normalizers that are typically used when hydrating a `Data` object
using `from` method are also used.

This will allow the ability to pass `json` data, `ArrayAccess` or `stdClass` objects for example to the `with` method.
If a custom normalizer is implemented for the `Data` class, it can be used as well.

```php
$johnDoe = new PersonDaa('John', 'Doe');

$janeDoe = $johnDoe->with('{"firstName": "Jane"}');

$janeDoe->fullName; // 'Jane Doe'
```

Using an `stdClass` object:

```php
$object = new stdClass();
$object->firstName = 'Jake';

$jakeDoe = $janeDoe->with($object);

$jakeDoe->fullName; // 'Jake Doe'
```
2 changes: 2 additions & 0 deletions docs/EmptyData.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ By calling the `empty()` method, we can create a new instance of the `Todo` clas
$emptyTodo = Todo::empty();
```

> This is really useful with [Cloneable Data](https://github.com/nuxtifyts/php-dto/blob/main/docs/CloneableData.md)

The `$emptyTodo` variable will contain the following data:

```
Expand Down
1 change: 1 addition & 0 deletions docs/Quickstart.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,3 +81,4 @@ can be found here:
- [Property Attributes](https://github.com/nuxtifyts/php-dto/blob/main/docs/PropertyAttributes.md)
- [Data Refiners](https://github.com/nuxtifyts/php-dto/blob/main/docs/DataRefiners.md)
- [Empty Data](https://github.com/nuxtifyts/php-dto/blob/main/docs/EmptyData.md)
- [Cloneable Data](https://github.com/nuxtifyts/php-dto/blob/main/docs/CloneableData.md)
14 changes: 3 additions & 11 deletions src/Concerns/BaseData.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,20 +21,12 @@ trait BaseData
*/
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);
$value = static::normalizeValue($args, static::class)
?: static::normalizeValue($args[0] ?? [], static::class);

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

/** @var ClassContext<static> $context */
Expand Down
88 changes: 88 additions & 0 deletions src/Concerns/CloneableData.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
<?php

namespace Nuxtifyts\PhpDto\Concerns;

use Nuxtifyts\PhpDto\Contexts\ClassContext;
use Nuxtifyts\PhpDto\Exceptions\DataCreationException;
use Nuxtifyts\PhpDto\Support\Traits\HasNormalizers;
use ReflectionClass;
use Throwable;

trait CloneableData
{
use HasNormalizers;

/**
* @throws DataCreationException
*/
public function with(mixed ...$args): static
{
try {
if (empty($args)) {
throw DataCreationException::invalidParamsPassed(static::class);
}

$value = static::normalizeValue($args, static::class)
?: static::normalizeValue($args[0], static::class);

if ($value === false) {
throw DataCreationException::invalidParamsPassed(static::class);
}

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

return $context->hasComputedProperties
? $this->cloneInstanceWithConstructorCall($context, $value)
: $this->cloneInstanceWithoutConstructorCall($context, $value);
} catch (Throwable $t) {
throw DataCreationException::unableToCloneInstanceWithNewData($t);
}
}

/**
* @param ClassContext<static> $context
* @param array<string, mixed> $value
*
* @throws Throwable
*/
protected function cloneInstanceWithConstructorCall(ClassContext $context, array $value): static
{
/** @var array<string, mixed> $args */
$args = [];

foreach ($context->constructorParams as $paramName) {
$propertyContext = $context->properties[$paramName] ?? null;

if (!$propertyContext) {
throw DataCreationException::invalidProperty();
}

$args[$paramName] = array_key_exists($propertyContext->propertyName, $value)
? $value[$paramName]
: $this->{$propertyContext->propertyName};
}

return $context->newInstanceWithConstructorCall(...$args);
}

/**
* @param ClassContext<static> $context
* @param array<string, mixed> $value
*
* @throws Throwable
*/
protected function cloneInstanceWithoutConstructorCall(ClassContext $context, array $value): static
{
$instance = $context->newInstanceWithoutConstructor();

foreach ($context->properties as $propertyContext) {
$instance->{$propertyContext->propertyName} =
array_key_exists($propertyContext->propertyName, $value)
? $value[$propertyContext->propertyName]
: $this->{$propertyContext->propertyName};
}

return $instance;
}
}
13 changes: 13 additions & 0 deletions src/Contracts/CloneableData.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?php

namespace Nuxtifyts\PhpDto\Contracts;

use Nuxtifyts\PhpDto\Exceptions\DataCreationException;

interface CloneableData
{
/**
* @throws DataCreationException
*/
public function with(mixed ...$args): static;
}
6 changes: 5 additions & 1 deletion src/Data.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,17 @@

use Nuxtifyts\PhpDto\Contracts\BaseData as BaseDataContract;
use Nuxtifyts\PhpDto\Contracts\EmptyData as EmptyDataContract;
use Nuxtifyts\PhpDto\Contracts\CloneableData as CloneableDataContract;
use Nuxtifyts\PhpDto\Concerns\BaseData;
use Nuxtifyts\PhpDto\Concerns\EmptyData;
use Nuxtifyts\PhpDto\Concerns\CloneableData;

abstract readonly class Data implements
BaseDataContract,
EmptyDataContract
EmptyDataContract,
CloneableDataContract
{
use BaseData;
use EmptyData;
use CloneableData;
}
24 changes: 24 additions & 0 deletions src/Exceptions/DataCreationException.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ class DataCreationException extends Exception
protected const int UNABLE_TO_CREATE_INSTANCE = 0;
protected const int INVALID_PROPERTY = 1;
protected const int UNABLE_TO_CREATE_EMPTY_INSTANCE = 2;
protected const int UNABLE_TO_CLONE_INSTANCE_WITH_NEW_DATA = 3;
protected const int INVALID_PARAMS_PASSED = 4;

public static function unableToCreateInstance(
string $class,
Expand Down Expand Up @@ -40,4 +42,26 @@ public static function unableToCreateEmptyInstance(
previous: $previous
);
}

public static function unableToCloneInstanceWithNewData(
string $class,
?Throwable $previous = null
): self {
return new self(
message: "Unable to clone instance of class {$class} with new data",
code: self::UNABLE_TO_CLONE_INSTANCE_WITH_NEW_DATA,
previous: $previous
);
}

public static function invalidParamsPassed(
string $class,
?Throwable $previous = null
): self {
return new self(
message: "Invalid params passed to create method of class {$class}",
code: self::INVALID_PARAMS_PASSED,
previous: $previous
);
}
}
26 changes: 26 additions & 0 deletions tests/Dummies/DataCiphers/UselessDataCipher.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?php

namespace Nuxtifyts\PhpDto\Tests\Dummies\DataCiphers;

use Nuxtifyts\PhpDto\DataCiphers\DataCipher;
use Nuxtifyts\PhpDto\Exceptions\DataCipherException;

class UselessDataCipher implements DataCipher
{

/**
* @throws DataCipherException
*/
public static function cipher(mixed $data, string $secret, bool $encode = false): never
{
throw DataCipherException::failedToCipherData();
}

/**
* @throws DataCipherException
*/
public static function decipher(string $data, string $secret, bool $decode = false): never
{
throw DataCipherException::failedToDecipherData();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
use Nuxtifyts\PhpDto\Tests\Dummies\Enums\Todo\Status;
use Nuxtifyts\PhpDto\Tests\Dummies\Normalizers\GoalTodoNormalizer;

final readonly class Todo extends Data
final readonly class TodoData extends Data
{
public function __construct(
public string $title,
Expand Down
Loading
Loading