Skip to content

Commit

Permalink
Custom casters (#2)
Browse files Browse the repository at this point in the history
* Update project structure

* Foundation for writing custom casters

* Custom casters logic

* Anonymous casters

* Update readme + refactor callable casters
  • Loading branch information
antoninmasek authored Jun 5, 2022
1 parent 778df23 commit 3c890f6
Show file tree
Hide file tree
Showing 13 changed files with 323 additions and 15 deletions.
65 changes: 64 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,14 +53,77 @@ object. At least the parent. Nested object does not have to extend anything.
For each of your DTO's properties you can use either a camelCase or snake_case approach to set their values which ever
suites your preference, in the example below we have the propeties `first_name` and `last_name` set on the DTO here.


```php
$person = Human::make()
->firstName('John')
->lastName('Doe')
->kids(3);
```

### Casters

For cases, where the type of property isn't built in PHP, or it needs a special care than just try to fill properties
by name it is possible to write a caster.

#### Caster class

The first way to define a caster is to create a class, that extends `AntoninMasek\SimpleHydrator\Casters\Caster`. You only need to implement the `cast` method which is supplied
with `$value` parameter that contains the raw data from the input array, that should be used to hydrate this class.

As an example take a look at simple `DateTime` caster:

```php
class DateTimeCaster extends Caster
{
public function cast(mixed $value): ?DateTime
{
if (is_null($value)) {
return null;
}

return new DateTime($value);
}
}
```

It expects the `$value` to be a string in valid date format. For example `1969-07-20` and returns a `DateTime` object with this date.

#### Anonymous caster
If you don't want to create a caster class you can create anonymous caster by supplying a closure instead of a caster class.

```php
Caster::registerCaster(DateTime::class, function ($value) {
if (is_null($value)) {
return null;
}

return new DateTime($value);
});
```

#### Registering casters
You can register casters in two ways. First is to specify the mapping between all classes and their respective casters:
```php
Caster::setCasters([
YourObject::class => YourObjectCaster::class,
YourSecondObject::class => AnotherCaster::class,
]);
```

Or just specify one caster at a time:
```php
Caster::registerCaster(YourObject::class, YourObjectCaster::class);
```

To clear all caster you may use:
```php
Caster::clearCasters();
```

#### Overwriting default casters
If any of the default casters in the package does not suit your needs you can easily overwrite it. All you need to do is register your caster for the specific class.
Registered casters have higher priority and default casters in the package are used if no mapping for the specific class is supplied.

## Testing

```bash
Expand Down
81 changes: 81 additions & 0 deletions src/Casters/Caster.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
<?php

namespace AntoninMasek\SimpleHydrator\Casters;

use AntoninMasek\SimpleHydrator\Exceptions\InvalidCasterException;
use AntoninMasek\SimpleHydrator\Exceptions\UnknownCasterException;
use ReflectionProperty;

abstract class Caster
{
private const CASTERS_NAMESPACE = 'AntoninMasek\SimpleHydrator\Casters';
private const CASTERS_SUFFIX = 'Caster';

private static array $casters = [];

public function __construct(protected ReflectionProperty $property)
{
}

public static function setCasters(array $map): array
{
return self::$casters = $map;
}

public static function clearCasters(): array
{
return self::$casters = [];
}

public static function registerCaster(string $className, string|callable $caster): array
{
return self::setCasters(array_merge(self::$casters, [
$className => $caster,
]));
}

/**
* @throws InvalidCasterException
* @throws UnknownCasterException
*/
public static function make(ReflectionProperty $property): Caster
{
$propertyClassName = $property->getType()->getName();

$casterClassNameOrCallable = ! array_key_exists($propertyClassName, self::$casters)
? self::CASTERS_NAMESPACE . "\\$propertyClassName" . self::CASTERS_SUFFIX
: self::$casters[$propertyClassName];

if (is_callable($casterClassNameOrCallable)) {
return self::handleCallableCaster($casterClassNameOrCallable);
}

if (! class_exists($casterClassNameOrCallable)) {
throw new UnknownCasterException($casterClassNameOrCallable);
}

$caster = new $casterClassNameOrCallable($property);

if (! ($caster instanceof Caster)) {
throw new InvalidCasterException();
}

return $caster;
}

private static function handleCallableCaster($callable): Caster
{
return new class($callable) extends Caster {
public function __construct(private mixed $callable)
{
}

public function cast(mixed $value): mixed
{
return ($this->callable)($value);
}
};
}

abstract public function cast(mixed $value): mixed;
}
17 changes: 17 additions & 0 deletions src/Casters/DateTimeCaster.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?php

namespace AntoninMasek\SimpleHydrator\Casters;

use DateTime;

class DateTimeCaster extends Caster
{
public function cast(mixed $value): ?DateTime
{
if (is_null($value)) {
return null;
}

return new DateTime($value);
}
}
3 changes: 2 additions & 1 deletion src/DataObject.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace AntoninMasek\SimpleHydrator;

use AntoninMasek\SimpleHydrator\Support\Str;
use ReflectionObject;
use ReflectionProperty;

Expand All @@ -25,7 +26,7 @@ public function __call($method, $arguments): self
foreach ($properties as $property) {
$name = $property->getName();

if (Helper::camel($name) === Helper::camel($method)) {
if (Str::camel($name) === Str::camel($method)) {
$property->setValue($this, ...$arguments);
}
}
Expand Down
13 changes: 13 additions & 0 deletions src/Exceptions/CasterException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?php

namespace AntoninMasek\SimpleHydrator\Exceptions;

use Exception;

class CasterException extends Exception
{
public static function invalidValue(string $className, mixed $value): static
{
return new static("Array expected. Got $value. Cannot tell how to build $className from $value. To solve this you can write your own caster. To find out how, take a look at 'Casters' section in the readme.");
}
}
14 changes: 14 additions & 0 deletions src/Exceptions/InvalidCasterException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?php

namespace AntoninMasek\SimpleHydrator\Exceptions;

use AntoninMasek\SimpleHydrator\Casters\Caster;
use Exception;

class InvalidCasterException extends Exception
{
public function __construct()
{
parent::__construct('All casters have to extend ' . Caster::class);
}
}
13 changes: 13 additions & 0 deletions src/Exceptions/UnknownCasterException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?php

namespace AntoninMasek\SimpleHydrator\Exceptions;

use Exception;

class UnknownCasterException extends Exception
{
public function __construct(string $className)
{
parent::__construct("Unknown caster $className");
}
}
29 changes: 18 additions & 11 deletions src/Hydrator.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@

namespace AntoninMasek\SimpleHydrator;

use DateTime;
use AntoninMasek\SimpleHydrator\Casters\Caster;
use AntoninMasek\SimpleHydrator\Exceptions\CasterException;
use AntoninMasek\SimpleHydrator\Exceptions\UnknownCasterException;
use ReflectionObject;
use ReflectionProperty;

Expand All @@ -14,25 +16,30 @@ public static function hydrate(string $className, array $data = null): ?object
return null;
}

$reflectionClass = new ReflectionObject($dto = new $className());
$reflectionClass = new ReflectionObject($dto = new $className());
$publicProperties = $reflectionClass->getProperties(ReflectionProperty::IS_PUBLIC);

foreach ($publicProperties as $property) {
$value = array_key_exists($property->getName(), $data)
? $data[$property->getName()]
: null;

if (! $property->getType()->isBuiltin()) {
$value = match ($property->getType()->getName()) {
DateTime::class => $value ? new DateTime($value) : null,
default => self::hydrate($property->getType()->getName(), $value),
};
if ($property->getType()->isBuiltin()) {
$property->setValue($dto, $value);
continue;
}

$property->setValue(
$dto,
$value,
);
try {
$value = Caster::make($property)->cast($value);
} catch (UnknownCasterException) {
if (! is_null($value) && ! is_array($value)) {
throw CasterException::invalidValue($property->getType()->getName(), $value);
}

$value = self::hydrate($property->getType()->getName(), $value);
}

$property->setValue($dto, $value);
}

return $dto;
Expand Down
4 changes: 2 additions & 2 deletions src/Helper.php → src/Support/Str.php
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
<?php

namespace AntoninMasek\SimpleHydrator;
namespace AntoninMasek\SimpleHydrator\Support;

final class Helper
final class Str
{
public static function camel($value): ?string
{
Expand Down
19 changes: 19 additions & 0 deletions tests/Casters/TestingCaster.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?php

namespace AntoninMasek\SimpleHydrator\Tests\Casters;

use AntoninMasek\SimpleHydrator\Casters\Caster;
use AntoninMasek\SimpleHydrator\Tests\Models\ClassThatNeedsCustomCaster;
use DateTime;

class TestingCaster extends Caster
{
public function cast(mixed $value): ClassThatNeedsCustomCaster
{
$class = new ClassThatNeedsCustomCaster();

$class->value = floatval((new DateTime())->format('n')) + $value;

return $class;
}
}
1 change: 1 addition & 0 deletions tests/Models/Car.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,5 @@ class Car
{
public string $type;
public string $brand;
public ?ClassThatNeedsCustomCaster $customCaster;
}
12 changes: 12 additions & 0 deletions tests/Models/ClassThatNeedsCustomCaster.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?php

namespace AntoninMasek\SimpleHydrator\Tests\Models;

class ClassThatNeedsCustomCaster
{
public float $value;

public function __construct()
{
}
}
Loading

0 comments on commit 3c890f6

Please sign in to comment.