Skip to content

Commit f84d04e

Browse files
authored
Merge pull request #8 from nuxtifyts/feature/cloneable-data
Added cloneable data contract and trait
2 parents 0ea7981 + 9d63718 commit f84d04e

15 files changed

+606
-20
lines changed

docs/CloneableData.md

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
Cloneable Data
2+
=
3+
4+
Sometimes we may want to alter the data of a `Data` object (Partially or completely).
5+
And since `Data` objects are immutable by default, we can't change the data directly.
6+
7+
To solve this, we can use the `with` function that will return a new instance of the `Data` object with the new data.
8+
Let take the `TodoData` class as an example:
9+
10+
```php
11+
use Nuxtifyts\PhpDto\Data;
12+
use DateTimeImmutable;
13+
14+
final readonly class TodoData extends Data
15+
{
16+
public function __construct(
17+
public string $title,
18+
public string $content,
19+
public Status $status,
20+
public ?DateTimeImmutable $dueDate
21+
) {}
22+
}
23+
```
24+
25+
The `Status` enum is defined as follows:
26+
27+
```php
28+
enum Status: string
29+
{
30+
case DEFAULT = 'default';
31+
case IN_PROGRESS = 'in_progress';
32+
case DONE = 'done';
33+
}
34+
```
35+
36+
Using `with` function, we can easily create new instances of the `TodoData` class with the new data:
37+
38+
```php
39+
$emptyTodo = Todo::empty();
40+
41+
// ...
42+
43+
$todo = $emptyTodo->with(
44+
title: 'Learn PHP DTO',
45+
content: 'Learn how to use PHP DTO',
46+
status: Status::IN_PROGRESS
47+
);
48+
49+
// ...
50+
51+
$todoWithDueDate = $todo->with(
52+
dueDate: new DateTimeImmutable('2025-01-06')
53+
);
54+
```
55+
56+
> We are using the `empty` method
57+
> from [Empty Data](https://github.com/nuxtifyts/php-dto/blob/main/docs/EmptyData.md)
58+
> here
59+
60+
> `emptyTodo`, `todo` and `todoWithDueDate` are all different instances.
61+
62+
Computed properties
63+
-
64+
65+
When cloning a `Data` object, computed properties are automatically updated with the new data.
66+
67+
```php
68+
use Nuxtifyts\PhpDto\Data;
69+
use Nuxtifyts\PhpDto\Attributes\Property\Computed;
70+
71+
final readonly class PersonData extends Data
72+
{
73+
#[Computed]
74+
public string $fullName;
75+
76+
public function __construct(
77+
public string $firstName,
78+
public string $lastName
79+
) {}
80+
}
81+
```
82+
83+
For example:
84+
85+
```php
86+
$johnDoe = new PersonData(firstName: 'John', lastName: 'Doe');
87+
88+
$janeDoe = $johnDoe->with(firstName: 'Jane');
89+
90+
$janeDoe->fullName; // 'Jane Doe'
91+
```
92+
93+
Normalizers
94+
-
95+
96+
When cloning a `Data` object, normalizers that are typically used when hydrating a `Data` object
97+
using `from` method are also used.
98+
99+
This will allow the ability to pass `json` data, `ArrayAccess` or `stdClass` objects for example to the `with` method.
100+
If a custom normalizer is implemented for the `Data` class, it can be used as well.
101+
102+
```php
103+
$johnDoe = new PersonDaa('John', 'Doe');
104+
105+
$janeDoe = $johnDoe->with('{"firstName": "Jane"}');
106+
107+
$janeDoe->fullName; // 'Jane Doe'
108+
```
109+
110+
Using an `stdClass` object:
111+
112+
```php
113+
$object = new stdClass();
114+
$object->firstName = 'Jake';
115+
116+
$jakeDoe = $janeDoe->with($object);
117+
118+
$jakeDoe->fullName; // 'Jake Doe'
119+
```

docs/EmptyData.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ By calling the `empty()` method, we can create a new instance of the `Todo` clas
3636
$emptyTodo = Todo::empty();
3737
```
3838

39+
> This is really useful with [Cloneable Data](https://github.com/nuxtifyts/php-dto/blob/main/docs/CloneableData.md)
40+
3941
The `$emptyTodo` variable will contain the following data:
4042

4143
```

docs/Quickstart.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,3 +81,4 @@ can be found here:
8181
- [Property Attributes](https://github.com/nuxtifyts/php-dto/blob/main/docs/PropertyAttributes.md)
8282
- [Data Refiners](https://github.com/nuxtifyts/php-dto/blob/main/docs/DataRefiners.md)
8383
- [Empty Data](https://github.com/nuxtifyts/php-dto/blob/main/docs/EmptyData.md)
84+
- [Cloneable Data](https://github.com/nuxtifyts/php-dto/blob/main/docs/CloneableData.md)

src/Concerns/BaseData.php

Lines changed: 3 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -21,20 +21,12 @@ trait BaseData
2121
*/
2222
final public static function create(mixed ...$args): static
2323
{
24-
if (array_any(
25-
array_keys($args),
26-
static fn (string|int $arg) => is_numeric($arg)
27-
)) {
28-
throw DataCreationException::invalidProperty();
29-
}
30-
3124
try {
32-
$value = static::normalizeValue($args, static::class);
25+
$value = static::normalizeValue($args, static::class)
26+
?: static::normalizeValue($args[0] ?? [], static::class);
3327

3428
if ($value === false) {
35-
throw new DeserializeException(
36-
code: DeserializeException::INVALID_VALUE_ERROR_CODE
37-
);
29+
throw DataCreationException::invalidParamsPassed(static::class);
3830
}
3931

4032
/** @var ClassContext<static> $context */

src/Concerns/CloneableData.php

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
<?php
2+
3+
namespace Nuxtifyts\PhpDto\Concerns;
4+
5+
use Nuxtifyts\PhpDto\Contexts\ClassContext;
6+
use Nuxtifyts\PhpDto\Exceptions\DataCreationException;
7+
use Nuxtifyts\PhpDto\Support\Traits\HasNormalizers;
8+
use ReflectionClass;
9+
use Throwable;
10+
11+
trait CloneableData
12+
{
13+
use HasNormalizers;
14+
15+
/**
16+
* @throws DataCreationException
17+
*/
18+
public function with(mixed ...$args): static
19+
{
20+
try {
21+
if (empty($args)) {
22+
throw DataCreationException::invalidParamsPassed(static::class);
23+
}
24+
25+
$value = static::normalizeValue($args, static::class)
26+
?: static::normalizeValue($args[0], static::class);
27+
28+
if ($value === false) {
29+
throw DataCreationException::invalidParamsPassed(static::class);
30+
}
31+
32+
/** @var ClassContext<static> $context */
33+
$context = ClassContext::getInstance(new ReflectionClass(static::class));
34+
35+
return $context->hasComputedProperties
36+
? $this->cloneInstanceWithConstructorCall($context, $value)
37+
: $this->cloneInstanceWithoutConstructorCall($context, $value);
38+
} catch (Throwable $t) {
39+
throw DataCreationException::unableToCloneInstanceWithNewData($t);
40+
}
41+
}
42+
43+
/**
44+
* @param ClassContext<static> $context
45+
* @param array<string, mixed> $value
46+
*
47+
* @throws Throwable
48+
*/
49+
protected function cloneInstanceWithConstructorCall(ClassContext $context, array $value): static
50+
{
51+
/** @var array<string, mixed> $args */
52+
$args = [];
53+
54+
foreach ($context->constructorParams as $paramName) {
55+
$propertyContext = $context->properties[$paramName] ?? null;
56+
57+
if (!$propertyContext) {
58+
throw DataCreationException::invalidProperty();
59+
}
60+
61+
$args[$paramName] = array_key_exists($propertyContext->propertyName, $value)
62+
? $value[$paramName]
63+
: $this->{$propertyContext->propertyName};
64+
}
65+
66+
return $context->newInstanceWithConstructorCall(...$args);
67+
}
68+
69+
/**
70+
* @param ClassContext<static> $context
71+
* @param array<string, mixed> $value
72+
*
73+
* @throws Throwable
74+
*/
75+
protected function cloneInstanceWithoutConstructorCall(ClassContext $context, array $value): static
76+
{
77+
$instance = $context->newInstanceWithoutConstructor();
78+
79+
foreach ($context->properties as $propertyContext) {
80+
$instance->{$propertyContext->propertyName} =
81+
array_key_exists($propertyContext->propertyName, $value)
82+
? $value[$propertyContext->propertyName]
83+
: $this->{$propertyContext->propertyName};
84+
}
85+
86+
return $instance;
87+
}
88+
}

src/Contracts/CloneableData.php

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<?php
2+
3+
namespace Nuxtifyts\PhpDto\Contracts;
4+
5+
use Nuxtifyts\PhpDto\Exceptions\DataCreationException;
6+
7+
interface CloneableData
8+
{
9+
/**
10+
* @throws DataCreationException
11+
*/
12+
public function with(mixed ...$args): static;
13+
}

src/Data.php

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,17 @@
44

55
use Nuxtifyts\PhpDto\Contracts\BaseData as BaseDataContract;
66
use Nuxtifyts\PhpDto\Contracts\EmptyData as EmptyDataContract;
7+
use Nuxtifyts\PhpDto\Contracts\CloneableData as CloneableDataContract;
78
use Nuxtifyts\PhpDto\Concerns\BaseData;
89
use Nuxtifyts\PhpDto\Concerns\EmptyData;
10+
use Nuxtifyts\PhpDto\Concerns\CloneableData;
911

1012
abstract readonly class Data implements
1113
BaseDataContract,
12-
EmptyDataContract
14+
EmptyDataContract,
15+
CloneableDataContract
1316
{
1417
use BaseData;
1518
use EmptyData;
19+
use CloneableData;
1620
}

src/Exceptions/DataCreationException.php

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ class DataCreationException extends Exception
1010
protected const int UNABLE_TO_CREATE_INSTANCE = 0;
1111
protected const int INVALID_PROPERTY = 1;
1212
protected const int UNABLE_TO_CREATE_EMPTY_INSTANCE = 2;
13+
protected const int UNABLE_TO_CLONE_INSTANCE_WITH_NEW_DATA = 3;
14+
protected const int INVALID_PARAMS_PASSED = 4;
1315

1416
public static function unableToCreateInstance(
1517
string $class,
@@ -40,4 +42,26 @@ public static function unableToCreateEmptyInstance(
4042
previous: $previous
4143
);
4244
}
45+
46+
public static function unableToCloneInstanceWithNewData(
47+
string $class,
48+
?Throwable $previous = null
49+
): self {
50+
return new self(
51+
message: "Unable to clone instance of class {$class} with new data",
52+
code: self::UNABLE_TO_CLONE_INSTANCE_WITH_NEW_DATA,
53+
previous: $previous
54+
);
55+
}
56+
57+
public static function invalidParamsPassed(
58+
string $class,
59+
?Throwable $previous = null
60+
): self {
61+
return new self(
62+
message: "Invalid params passed to create method of class {$class}",
63+
code: self::INVALID_PARAMS_PASSED,
64+
previous: $previous
65+
);
66+
}
4367
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<?php
2+
3+
namespace Nuxtifyts\PhpDto\Tests\Dummies\DataCiphers;
4+
5+
use Nuxtifyts\PhpDto\DataCiphers\DataCipher;
6+
use Nuxtifyts\PhpDto\Exceptions\DataCipherException;
7+
8+
class UselessDataCipher implements DataCipher
9+
{
10+
11+
/**
12+
* @throws DataCipherException
13+
*/
14+
public static function cipher(mixed $data, string $secret, bool $encode = false): never
15+
{
16+
throw DataCipherException::failedToCipherData();
17+
}
18+
19+
/**
20+
* @throws DataCipherException
21+
*/
22+
public static function decipher(string $data, string $secret, bool $decode = false): never
23+
{
24+
throw DataCipherException::failedToDecipherData();
25+
}
26+
}

tests/Dummies/DocsDummies/Todo.php renamed to tests/Dummies/DocsDummies/TodoData.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
use Nuxtifyts\PhpDto\Tests\Dummies\Enums\Todo\Status;
99
use Nuxtifyts\PhpDto\Tests\Dummies\Normalizers\GoalTodoNormalizer;
1010

11-
final readonly class Todo extends Data
11+
final readonly class TodoData extends Data
1212
{
1313
public function __construct(
1414
public string $title,

0 commit comments

Comments
 (0)