Skip to content

Commit dcc6127

Browse files
authored
Merge pull request #7 from nuxtifyts/feature/empty-dtos
Added empty data contract
2 parents a3ca6fb + 9745cdb commit dcc6127

20 files changed

+595
-55
lines changed

composer.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,6 @@
2929
},
3030
"scripts": {
3131
"ci-test": "XDEBUG_MODE=coverage vendor/bin/phpunit --testsuite=ci --configuration phpunit.xml",
32-
"phpstan": "vendor/bin/phpstan analyse --configuration phpstan.neon"
32+
"phpstan": "vendor/bin/phpstan analyse --configuration phpstan.neon --memory-limit=256M"
3333
}
3434
}

docs/EmptyData.md

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
Empty Data
2+
=
3+
4+
Sometimes we may need to create a fresh instance of a DTO without any data,
5+
and by default `Data` classes have the ability to create an `"empty"` instance:
6+
7+
```php
8+
use Nuxtifyts\PhpDto\Data;
9+
use DateTimeImmutable;
10+
11+
final reaconly class Todo extends Data
12+
{
13+
public function __construct(
14+
public string $title,
15+
public string $content,
16+
public Status $status,
17+
public ?DateTimeImmutable $dueDate
18+
) {}
19+
}
20+
```
21+
22+
The `Status` enum is defined as follows:
23+
24+
```php
25+
enum Status: string
26+
{
27+
case DEFAULT = 'default';
28+
case DONE = 'done';
29+
case CANCELED = 'canceled';
30+
}
31+
```
32+
33+
By calling the `empty()` method, we can create a new instance of the `Todo` class with all properties set to `null`:
34+
35+
```php
36+
$emptyTodo = Todo::empty();
37+
```
38+
39+
The `$emptyTodo` variable will contain the following data:
40+
41+
```
42+
[
43+
'title' => '',
44+
'comtent' => '',
45+
'status' => Status::DEFAULT,
46+
'dueDate' => null
47+
]
48+
```
49+
50+
This is useful when we want to gradually fill in the data of a DTO instance,
51+
here is a list of the empty values for each type:
52+
53+
- `NULL`: `null` (Null takes priority over everything)
54+
- `STRING`: `''`
55+
- `INT`: `0`
56+
- `FLOAT`: `0.0`
57+
- `BOOLEAN`: `false`
58+
- `ARRAY`: `[]` (Any type of array will default to an empty one)
59+
- `DATETIME`: New instance of DateTime/DateTimeImmutable
60+
- `BACKEDENUM`: First case of the enum

docs/Quickstart.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,3 +80,4 @@ can be found here:
8080
- [Normalizers](https://github.com/nuxtifyts/php-dto/blob/main/docs/Normalizers.md)
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)
83+
- [Empty Data](https://github.com/nuxtifyts/php-dto/blob/main/docs/EmptyData.md)

src/Concerns/BaseData.php

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@ trait BaseData
1616
{
1717
use HasNormalizers;
1818

19+
/**
20+
* @throws DataCreationException
21+
*/
1922
final public static function create(mixed ...$args): static
2023
{
2124
if (array_any(
@@ -124,13 +127,7 @@ protected static function instanceWithConstructorCallFrom(ClassContext $context,
124127
$args[$paramName] = $propertyContext->deserializeFrom($value);
125128
}
126129

127-
$instance = $context->newInstanceWithConstructorCall(...$args);
128-
129-
if (!$instance instanceof static) {
130-
throw new DeserializeException('Could not create instance of ' . static::class);
131-
}
132-
133-
return $instance;
130+
return $context->newInstanceWithConstructorCall(...$args);
134131
}
135132

136133
/**
@@ -159,4 +156,17 @@ final public function jsonSerialize(): array
159156
throw new SerializeException($e->getMessage(), $e->getCode(), $e);
160157
}
161158
}
159+
160+
/**
161+
* @throws SerializeException
162+
*/
163+
final public function toArray(): array
164+
{
165+
return $this->jsonSerialize();
166+
}
167+
168+
final public function toJson(): false|string
169+
{
170+
return json_encode($this);
171+
}
162172
}

src/Concerns/EmptyData.php

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\Concerns;
4+
5+
use Nuxtifyts\PhpDto\Contexts\ClassContext;
6+
use Nuxtifyts\PhpDto\Exceptions\DataCreationException;
7+
use ReflectionClass;
8+
use Throwable;
9+
10+
trait EmptyData
11+
{
12+
/**
13+
* @throws DataCreationException
14+
*/
15+
public static function empty(): static
16+
{
17+
try {
18+
/** @var ClassContext<static> $classContext */
19+
$classContext = ClassContext::getInstance(new ReflectionClass(static::class));
20+
21+
return $classContext->emptyValue();
22+
} catch (Throwable $t) {
23+
throw DataCreationException::unableToCreateEmptyInstance(static::class, $t);
24+
}
25+
}
26+
}

src/Contexts/ClassContext.php

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,15 @@
22

33
namespace Nuxtifyts\PhpDto\Contexts;
44

5+
use Nuxtifyts\PhpDto\Data;
6+
use Nuxtifyts\PhpDto\Exceptions\DataCreationException;
57
use Nuxtifyts\PhpDto\Exceptions\UnsupportedTypeException;
6-
use ReflectionClass;
78
use ReflectionException;
89
use ReflectionParameter;
10+
use ReflectionClass;
911

1012
/**
11-
* @template T of object
13+
* @template T of Data
1214
*/
1315
class ClassContext
1416
{
@@ -101,9 +103,36 @@ public function newInstanceWithoutConstructor(): mixed
101103

102104
/**
103105
* @throws ReflectionException
106+
*
107+
* @return T
104108
*/
105109
public function newInstanceWithConstructorCall(mixed ...$args): mixed
106110
{
107111
return $this->reflection->newInstance(...$args);
108112
}
113+
114+
/**
115+
* @return T
116+
*
117+
* @throws ReflectionException
118+
* @throws UnsupportedTypeException
119+
* @throws DataCreationException
120+
*/
121+
public function emptyValue(): mixed
122+
{
123+
/** @var array<string, mixed> $args */
124+
$args = [];
125+
126+
foreach ($this->constructorParams as $paramName) {
127+
$propertyContext = $this->properties[$paramName] ?? null;
128+
129+
if (!$propertyContext) {
130+
throw DataCreationException::invalidProperty();
131+
}
132+
133+
$args[$paramName] = $propertyContext->emptyValue();
134+
}
135+
136+
return $this->newInstanceWithConstructorCall(...$args);
137+
}
109138
}

src/Contexts/PropertyContext.php

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,15 @@
22

33
namespace Nuxtifyts\PhpDto\Contexts;
44

5+
use BackedEnum;
6+
use Nuxtifyts\PhpDto\Exceptions\DataCreationException;
7+
use UnitEnum;
58
use Nuxtifyts\PhpDto\Attributes\Property\Aliases;
69
use Nuxtifyts\PhpDto\Attributes\Property\CipherTarget;
710
use Nuxtifyts\PhpDto\Attributes\Property\Computed;
811
use Nuxtifyts\PhpDto\Attributes\Property\DefaultsTo;
912
use Nuxtifyts\PhpDto\Attributes\Property\WithRefiner;
13+
use Nuxtifyts\PhpDto\Data;
1014
use Nuxtifyts\PhpDto\DataCiphers\CipherConfig;
1115
use Nuxtifyts\PhpDto\DataRefiners\DataRefiner;
1216
use Nuxtifyts\PhpDto\Enums\Property\Type;
@@ -18,8 +22,12 @@
1822
use Nuxtifyts\PhpDto\Serializers\Serializer;
1923
use Nuxtifyts\PhpDto\Support\Traits\HasSerializers;
2024
use Nuxtifyts\PhpDto\Support\Traits\HasTypes;
25+
use DateTimeInterface;
26+
use ReflectionEnum;
27+
use ReflectionException;
2128
use ReflectionProperty;
2229
use ReflectionAttribute;
30+
use ReflectionClass;
2331
use Exception;
2432

2533
class PropertyContext
@@ -225,4 +233,61 @@ public function serializeFrom(object $object): array
225233
throw new SerializeException('Could not serialize value for property: ' . $this->propertyName);
226234
}
227235
}
236+
237+
/**
238+
* @throws UnsupportedTypeException
239+
* @throws ReflectionException
240+
* @throws DataCreationException
241+
*/
242+
public function emptyValue(): mixed
243+
{
244+
if ($this->isNullable) {
245+
return null;
246+
}
247+
248+
if (! $typeContext = $this->typeContexts[0] ?? null) {
249+
throw UnsupportedTypeException::emptyType();
250+
}
251+
252+
switch (true) {
253+
case $typeContext->type === Type::STRING:
254+
return '';
255+
256+
case $typeContext->type === Type::INT:
257+
return 0;
258+
259+
case $typeContext->type === Type::FLOAT:
260+
return 0.0;
261+
262+
case $typeContext->type === Type::BOOLEAN:
263+
return false;
264+
265+
case $typeContext->type === Type::ARRAY:
266+
return [];
267+
268+
case $typeContext->type === Type::DATA:
269+
/** @var null|ReflectionClass<Data> $reflection */
270+
$reflection = $typeContext->reflection;
271+
272+
return !$reflection
273+
? throw UnsupportedTypeException::invalidReflection()
274+
: ClassContext::getInstance($reflection)->emptyValue();
275+
276+
case $typeContext->type === Type::BACKED_ENUM:
277+
/** @var null|ReflectionEnum<UnitEnum|BackedEnum> $reflection */
278+
$reflection = $typeContext->reflection;
279+
280+
return $reflection instanceof ReflectionEnum && $reflection->isBacked()
281+
? $reflection->getCases()[0]->getValue()
282+
: throw UnsupportedTypeException::invalidReflection();
283+
284+
default:
285+
/** @var null|DateTimeInterface $dateTime */
286+
$dateTime = $typeContext->reflection?->newInstance();
287+
288+
return $dateTime instanceof DateTimeInterface
289+
? $dateTime
290+
: throw UnsupportedTypeException::invalidReflection();
291+
}
292+
}
228293
}

src/Contexts/TypeContext.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ public static function getInstances(PropertyContext $property): array
109109
);
110110
break;
111111
default:
112-
throw UnsupportedTypeException::from($type);
112+
throw UnsupportedTypeException::unknownType($type);
113113
}
114114
}
115115

src/Contracts/BaseData.php

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,20 @@ public static function create(mixed ...$args): static;
2121
*/
2222
public function jsonSerialize(): array;
2323

24+
/**
25+
* @return array<string, mixed>
26+
*
27+
* @throws SerializeException
28+
*/
29+
public function toArray(): array;
30+
31+
/**
32+
* @return false|string
33+
*
34+
* @throws SerializeException
35+
*/
36+
public function toJson(): false|string;
37+
2438
/**
2539
* @throws DeserializeException
2640
*/

src/Contracts/EmptyData.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 EmptyData
8+
{
9+
/**
10+
* @throws DataCreationException
11+
*/
12+
public static function empty(): static;
13+
}

src/Data.php

Lines changed: 6 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -3,25 +3,14 @@
33
namespace Nuxtifyts\PhpDto;
44

55
use Nuxtifyts\PhpDto\Contracts\BaseData as BaseDataContract;
6+
use Nuxtifyts\PhpDto\Contracts\EmptyData as EmptyDataContract;
67
use Nuxtifyts\PhpDto\Concerns\BaseData;
7-
use Nuxtifyts\PhpDto\Exceptions\SerializeException;
8+
use Nuxtifyts\PhpDto\Concerns\EmptyData;
89

9-
abstract readonly class Data implements BaseDataContract
10+
abstract readonly class Data implements
11+
BaseDataContract,
12+
EmptyDataContract
1013
{
1114
use BaseData;
12-
13-
/**
14-
* @return array<string, mixed>
15-
*
16-
* @throws SerializeException
17-
*/
18-
final public function toArray(): array
19-
{
20-
return $this->jsonSerialize();
21-
}
22-
23-
final public function toJson(): false|string
24-
{
25-
return json_encode($this);
26-
}
15+
use EmptyData;
2716
}

src/Exceptions/DataCreationException.php

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ class DataCreationException extends Exception
99
{
1010
protected const int UNABLE_TO_CREATE_INSTANCE = 0;
1111
protected const int INVALID_PROPERTY = 1;
12+
protected const int UNABLE_TO_CREATE_EMPTY_INSTANCE = 2;
1213

1314
public static function unableToCreateInstance(
1415
string $class,
@@ -28,4 +29,15 @@ public static function invalidProperty(): self
2829
code: self::INVALID_PROPERTY
2930
);
3031
}
32+
33+
public static function unableToCreateEmptyInstance(
34+
string $class,
35+
?Throwable $previous = null
36+
): self {
37+
return new self(
38+
message: "Unable to create empty instance of class {$class}",
39+
code: self::UNABLE_TO_CREATE_EMPTY_INSTANCE,
40+
previous: $previous
41+
);
42+
}
3143
}

0 commit comments

Comments
 (0)