Skip to content

Added empty data contract #7

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 9 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
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,6 @@
},
"scripts": {
"ci-test": "XDEBUG_MODE=coverage vendor/bin/phpunit --testsuite=ci --configuration phpunit.xml",
"phpstan": "vendor/bin/phpstan analyse --configuration phpstan.neon"
"phpstan": "vendor/bin/phpstan analyse --configuration phpstan.neon --memory-limit=256M"
}
}
60 changes: 60 additions & 0 deletions docs/EmptyData.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
Empty Data
=

Sometimes we may need to create a fresh instance of a DTO without any data,
and by default `Data` classes have the ability to create an `"empty"` instance:

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

final reaconly class Todo 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 DONE = 'done';
case CANCELED = 'canceled';
}
```

By calling the `empty()` method, we can create a new instance of the `Todo` class with all properties set to `null`:

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

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

```
[
'title' => '',
'comtent' => '',
'status' => Status::DEFAULT,
'dueDate' => null
]
```

This is useful when we want to gradually fill in the data of a DTO instance,
here is a list of the empty values for each type:

- `NULL`: `null` (Null takes priority over everything)
- `STRING`: `''`
- `INT`: `0`
- `FLOAT`: `0.0`
- `BOOLEAN`: `false`
- `ARRAY`: `[]` (Any type of array will default to an empty one)
- `DATETIME`: New instance of DateTime/DateTimeImmutable
- `BACKEDENUM`: First case of the enum
1 change: 1 addition & 0 deletions docs/Quickstart.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,3 +80,4 @@ can be found here:
- [Normalizers](https://github.com/nuxtifyts/php-dto/blob/main/docs/Normalizers.md)
- [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)
24 changes: 17 additions & 7 deletions src/Concerns/BaseData.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ trait BaseData
{
use HasNormalizers;

/**
* @throws DataCreationException
*/
final public static function create(mixed ...$args): static
{
if (array_any(
Expand Down Expand Up @@ -124,13 +127,7 @@ protected static function instanceWithConstructorCallFrom(ClassContext $context,
$args[$paramName] = $propertyContext->deserializeFrom($value);
}

$instance = $context->newInstanceWithConstructorCall(...$args);

if (!$instance instanceof static) {
throw new DeserializeException('Could not create instance of ' . static::class);
}

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

/**
Expand Down Expand Up @@ -159,4 +156,17 @@ final public function jsonSerialize(): array
throw new SerializeException($e->getMessage(), $e->getCode(), $e);
}
}

/**
* @throws SerializeException
*/
final public function toArray(): array
{
return $this->jsonSerialize();
}

final public function toJson(): false|string
{
return json_encode($this);
}
}
26 changes: 26 additions & 0 deletions src/Concerns/EmptyData.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?php

namespace Nuxtifyts\PhpDto\Concerns;

use Nuxtifyts\PhpDto\Contexts\ClassContext;
use Nuxtifyts\PhpDto\Exceptions\DataCreationException;
use ReflectionClass;
use Throwable;

trait EmptyData
{
/**
* @throws DataCreationException
*/
public static function empty(): static
{
try {
/** @var ClassContext<static> $classContext */
$classContext = ClassContext::getInstance(new ReflectionClass(static::class));

return $classContext->emptyValue();
} catch (Throwable $t) {
throw DataCreationException::unableToCreateEmptyInstance(static::class, $t);
}
}
}
33 changes: 31 additions & 2 deletions src/Contexts/ClassContext.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,15 @@

namespace Nuxtifyts\PhpDto\Contexts;

use Nuxtifyts\PhpDto\Data;
use Nuxtifyts\PhpDto\Exceptions\DataCreationException;
use Nuxtifyts\PhpDto\Exceptions\UnsupportedTypeException;
use ReflectionClass;
use ReflectionException;
use ReflectionParameter;
use ReflectionClass;

/**
* @template T of object
* @template T of Data
*/
class ClassContext
{
Expand Down Expand Up @@ -101,9 +103,36 @@ public function newInstanceWithoutConstructor(): mixed

/**
* @throws ReflectionException
*
* @return T
*/
public function newInstanceWithConstructorCall(mixed ...$args): mixed
{
return $this->reflection->newInstance(...$args);
}

/**
* @return T
*
* @throws ReflectionException
* @throws UnsupportedTypeException
* @throws DataCreationException
*/
public function emptyValue(): mixed
{
/** @var array<string, mixed> $args */
$args = [];

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

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

$args[$paramName] = $propertyContext->emptyValue();
}

return $this->newInstanceWithConstructorCall(...$args);
}
}
65 changes: 65 additions & 0 deletions src/Contexts/PropertyContext.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,15 @@

namespace Nuxtifyts\PhpDto\Contexts;

use BackedEnum;
use Nuxtifyts\PhpDto\Exceptions\DataCreationException;
use UnitEnum;
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\Data;
use Nuxtifyts\PhpDto\DataCiphers\CipherConfig;
use Nuxtifyts\PhpDto\DataRefiners\DataRefiner;
use Nuxtifyts\PhpDto\Enums\Property\Type;
Expand All @@ -18,8 +22,12 @@
use Nuxtifyts\PhpDto\Serializers\Serializer;
use Nuxtifyts\PhpDto\Support\Traits\HasSerializers;
use Nuxtifyts\PhpDto\Support\Traits\HasTypes;
use DateTimeInterface;
use ReflectionEnum;
use ReflectionException;
use ReflectionProperty;
use ReflectionAttribute;
use ReflectionClass;
use Exception;

class PropertyContext
Expand Down Expand Up @@ -225,4 +233,61 @@ public function serializeFrom(object $object): array
throw new SerializeException('Could not serialize value for property: ' . $this->propertyName);
}
}

/**
* @throws UnsupportedTypeException
* @throws ReflectionException
* @throws DataCreationException
*/
public function emptyValue(): mixed
{
if ($this->isNullable) {
return null;
}

if (! $typeContext = $this->typeContexts[0] ?? null) {
throw UnsupportedTypeException::emptyType();
}

switch (true) {
case $typeContext->type === Type::STRING:
return '';

case $typeContext->type === Type::INT:
return 0;

case $typeContext->type === Type::FLOAT:
return 0.0;

case $typeContext->type === Type::BOOLEAN:
return false;

case $typeContext->type === Type::ARRAY:
return [];

case $typeContext->type === Type::DATA:
/** @var null|ReflectionClass<Data> $reflection */
$reflection = $typeContext->reflection;

return !$reflection
? throw UnsupportedTypeException::invalidReflection()
: ClassContext::getInstance($reflection)->emptyValue();

case $typeContext->type === Type::BACKED_ENUM:
/** @var null|ReflectionEnum<UnitEnum|BackedEnum> $reflection */
$reflection = $typeContext->reflection;

return $reflection instanceof ReflectionEnum && $reflection->isBacked()
? $reflection->getCases()[0]->getValue()
: throw UnsupportedTypeException::invalidReflection();

default:
/** @var null|DateTimeInterface $dateTime */
$dateTime = $typeContext->reflection?->newInstance();

return $dateTime instanceof DateTimeInterface
? $dateTime
: throw UnsupportedTypeException::invalidReflection();
}
}
}
2 changes: 1 addition & 1 deletion src/Contexts/TypeContext.php
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ public static function getInstances(PropertyContext $property): array
);
break;
default:
throw UnsupportedTypeException::from($type);
throw UnsupportedTypeException::unknownType($type);
}
}

Expand Down
14 changes: 14 additions & 0 deletions src/Contracts/BaseData.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,20 @@ public static function create(mixed ...$args): static;
*/
public function jsonSerialize(): array;

/**
* @return array<string, mixed>
*
* @throws SerializeException
*/
public function toArray(): array;

/**
* @return false|string
*
* @throws SerializeException
*/
public function toJson(): false|string;

/**
* @throws DeserializeException
*/
Expand Down
13 changes: 13 additions & 0 deletions src/Contracts/EmptyData.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 EmptyData
{
/**
* @throws DataCreationException
*/
public static function empty(): static;
}
23 changes: 6 additions & 17 deletions src/Data.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,25 +3,14 @@
namespace Nuxtifyts\PhpDto;

use Nuxtifyts\PhpDto\Contracts\BaseData as BaseDataContract;
use Nuxtifyts\PhpDto\Contracts\EmptyData as EmptyDataContract;
use Nuxtifyts\PhpDto\Concerns\BaseData;
use Nuxtifyts\PhpDto\Exceptions\SerializeException;
use Nuxtifyts\PhpDto\Concerns\EmptyData;

abstract readonly class Data implements BaseDataContract
abstract readonly class Data implements
BaseDataContract,
EmptyDataContract
{
use BaseData;

/**
* @return array<string, mixed>
*
* @throws SerializeException
*/
final public function toArray(): array
{
return $this->jsonSerialize();
}

final public function toJson(): false|string
{
return json_encode($this);
}
use EmptyData;
}
12 changes: 12 additions & 0 deletions src/Exceptions/DataCreationException.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ 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;

public static function unableToCreateInstance(
string $class,
Expand All @@ -28,4 +29,15 @@ public static function invalidProperty(): self
code: self::INVALID_PROPERTY
);
}

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