Skip to content

Commit 7ff39e6

Browse files
committed
Add Cloneable Data feature with documentation and tests
Introduced the Cloneable Data feature to allow `Data` objects to support cloning with updated values using the `with` method. Added documentation, unit tests, examples, and updated related components to reflect this enhancement.
1 parent d92c841 commit 7ff39e6

File tree

10 files changed

+201
-9
lines changed

10 files changed

+201
-9
lines changed

docs/CloneableData.md

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
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+
```

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/CloneableData.php

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,10 @@ public function with(mixed ...$args): static
2020
try {
2121
$value = static::normalizeValue($args, static::class);
2222

23+
if ($value === false) {
24+
throw DataCreationException::invalidParamsPassed(static::class);
25+
}
26+
2327
/** @var ClassContext<static> $context */
2428
$context = ClassContext::getInstance(new ReflectionClass(static::class));
2529

@@ -46,7 +50,7 @@ protected function cloneInstanceWithConstructorCall(ClassContext $context, array
4650
$propertyContext = $context->properties[$paramName] ?? null;
4751

4852
if (!$propertyContext) {
49-
DataCreationException::invalidProperty();
53+
throw DataCreationException::invalidProperty();
5054
}
5155

5256
$args[$paramName] = array_key_exists($propertyContext->propertyName, $value)

src/Exceptions/DataCreationException.php

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ class DataCreationException extends Exception
1111
protected const int INVALID_PROPERTY = 1;
1212
protected const int UNABLE_TO_CREATE_EMPTY_INSTANCE = 2;
1313
protected const int UNABLE_TO_CLONE_INSTANCE_WITH_NEW_DATA = 3;
14+
protected const int INVALID_PARAMS_PASSED = 4;
1415

1516
public static function unableToCreateInstance(
1617
string $class,
@@ -52,4 +53,15 @@ public static function unableToCloneInstanceWithNewData(
5253
previous: $previous
5354
);
5455
}
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+
}
5567
}

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,

tests/Unit/Concerns/CloneableDataTest.php

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,19 @@ public function will_throw_an_exception_if_invalid_value_for_property_is_passed(
4040
$person->with(firstName: new DateTimeImmutable());
4141
}
4242

43+
/**
44+
* @throws Throwable
45+
*/
46+
#[Test]
47+
public function will_throw_an_exception_if_invalid_arguments_are_passed_using_with_function(): void
48+
{
49+
$person = new PersonData(firstName: 'John', lastName: 'Doe');
50+
51+
self::expectException(DataCreationException::class);
52+
53+
$person->with('{firstName: "Jane"');
54+
}
55+
4356
/**
4457
* @throws Throwable
4558
*/
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
<?php
2+
3+
namespace Nuxtifyts\PhpDto\Tests\Unit\Documentation;
4+
5+
use Nuxtifyts\PhpDto\Tests\Dummies\DocsDummies\TodoData;
6+
use Nuxtifyts\PhpDto\Tests\Dummies\PersonData;
7+
use Nuxtifyts\PhpDto\Tests\Unit\UnitCase;
8+
use PHPUnit\Framework\Attributes\UsesClass;
9+
use PHPUnit\Framework\Attributes\Test;
10+
use Nuxtifyts\PhpDto\Tests\Dummies\Enums\Todo\Status;
11+
use DateTimeImmutable;
12+
use Throwable;
13+
14+
#[UsesClass(TodoData::class)]
15+
#[UsesClass(PersonData::class)]
16+
final class CloneableDataExampleTest extends UnitCase
17+
{
18+
/**
19+
* @throws Throwable
20+
*/
21+
#[Test]
22+
public function test_it_can_clone_data_as_example_in_documentation(): void
23+
{
24+
$emptyTodo = TodoData::empty();
25+
26+
self::assertEquals([
27+
'title' => '',
28+
'content' => '',
29+
'status' => Status::BACKLOG->value,
30+
'dueDate' => null
31+
], $emptyTodo->toArray());
32+
33+
$todo = $emptyTodo->with(
34+
title: 'Learn PHP DTO',
35+
content: 'Learn how to use PHP DTO',
36+
status: Status::IN_PROGRESS
37+
);
38+
39+
self::assertEquals([
40+
'title' => 'Learn PHP DTO',
41+
'content' => 'Learn how to use PHP DTO',
42+
'status' => Status::IN_PROGRESS->value,
43+
'dueDate' => null
44+
], $todo->toArray());
45+
46+
$dueDate = new DateTimeImmutable('2021-10-10');
47+
$todoWithDueDate = $todo->with(dueDate: $dueDate);
48+
49+
self::assertEquals(
50+
$dueDate->format('Y-m-d H:i'),
51+
$todoWithDueDate->dueDate?->format('Y-m-d H:i')
52+
);
53+
}
54+
55+
/**
56+
* @throws Throwable
57+
*/
58+
#[Test]
59+
public function test_is_can_clone_dts_with_computed_properties(): void
60+
{
61+
$person = new PersonData(firstName: 'John', lastName: 'Doe');
62+
63+
self::assertEquals('John Doe', $person->fullName);
64+
65+
$personWithFirstName = $person->with(firstName: 'Jane');
66+
67+
self::assertEquals('Jane Doe', $personWithFirstName->fullName);
68+
}
69+
}

tests/Unit/Documentation/NormalizersExampleTest.php

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,13 @@
66
use Nuxtifyts\PhpDto\Tests\Unit\UnitCase;
77
use PHPUnit\Framework\Attributes\Test;
88
use PHPUnit\Framework\Attributes\UsesClass;
9-
use Nuxtifyts\PhpDto\Tests\Dummies\DocsDummies\Todo;
9+
use Nuxtifyts\PhpDto\Tests\Dummies\DocsDummies\TodoData;
1010
use Nuxtifyts\PhpDto\Tests\Dummies\DocsDummies\NonData\Goal;
1111
use DateTimeImmutable;
1212
use DateTimeInterface;
1313
use Throwable;
1414

15-
#[UsesClass(Todo::class)]
15+
#[UsesClass(TodoData::class)]
1616
#[UsesClass(Goal::class)]
1717
final class NormalizersExampleTest extends UnitCase
1818
{
@@ -30,7 +30,7 @@ public function will_be_able_to_normalize_instance_of_goal_class_to_todo_class()
3030
dueDate: $now
3131
);
3232

33-
$todo = Todo::from($goal);
33+
$todo = TodoData::from($goal);
3434

3535
self::assertEquals(
3636
[

tests/Unit/Documentation/QuickStartExampleTest.php

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
namespace Nuxtifyts\PhpDto\Tests\Unit\Documentation;
44

5-
use Nuxtifyts\PhpDto\Tests\Dummies\DocsDummies\Todo;
5+
use Nuxtifyts\PhpDto\Tests\Dummies\DocsDummies\TodoData;
66
use Nuxtifyts\PhpDto\Tests\Unit\UnitCase;
77
use PHPUnit\Framework\Attributes\Test;
88
use PHPUnit\Framework\Attributes\UsesClass;
@@ -11,7 +11,7 @@
1111
use DateTimeInterface;
1212
use Throwable;
1313

14-
#[UsesClass(Todo::class)]
14+
#[UsesClass(TodoData::class)]
1515
final class QuickStartExampleTest extends UnitCase
1616
{
1717
/**
@@ -29,14 +29,14 @@ public function will_perform_serialize_and_deserialize_on_data_transfer_objects_
2929
'dueDate' => $now->format(DateTimeInterface::ATOM)
3030
];
3131

32-
$todo = new Todo(
32+
$todo = new TodoData(
3333
title: 'Learn PHP DTO',
3434
content: 'Learn how to use PHP DTO',
3535
status: Status::READY,
3636
dueDate: $now
3737
);
3838

39-
$todoFrom = Todo::from([
39+
$todoFrom = TodoData::from([
4040
'title' => 'Learn PHP DTO',
4141
'content' => 'Learn how to use PHP DTO',
4242
'status' => 'ready',

0 commit comments

Comments
 (0)