This commit is contained in:
2022-02-05 07:43:27 +00:00
committed by Gitea
parent db19655b60
commit 944c4cb564
34 changed files with 1936 additions and 8 deletions
+98
View File
@@ -0,0 +1,98 @@
<?php
namespace Spatie\DataTransferObject;
use ArrayAccess;
class Arr
{
public static function only($array, $keys): array
{
return array_intersect_key($array, array_flip((array) $keys));
}
public static function except($array, $keys): array
{
return static::forget($array, $keys);
}
public static function forget($array, $keys): array
{
$keys = (array) $keys;
if (count($keys) === 0) {
return $array;
}
foreach ($keys as $key) {
// If the exact key exists in the top-level, remove it
if (static::exists($array, $key)) {
unset($array[$key]);
continue;
}
// Check if the key is using dot-notation
if (! str_contains($key, '.')) {
continue;
}
// If we are dealing with dot-notation, recursively handle i
$parts = explode('.', $key);
$key = array_shift($parts);
if (static::exists($array, $key) && static::accessible($array[$key])) {
$array[$key] = static::forget($array[$key], implode('.', $parts));
if (count($array[$key]) === 0) {
unset($array[$key]);
}
}
}
return $array;
}
public static function get($array, $key, $default = null)
{
if (! static::accessible($array)) {
return $default;
}
if (is_null($key)) {
return $array;
}
if (static::exists($array, $key)) {
return $array[$key];
}
if (strpos($key, '.') === false) {
return $array[$key] ?? $default;
}
foreach (explode('.', $key) as $segment) {
if (static::accessible($array) && static::exists($array, $segment)) {
$array = $array[$segment];
} else {
return $default;
}
}
return $array;
}
public static function accessible($value)
{
return is_array($value) || $value instanceof ArrayAccess;
}
public static function exists($array, $key): bool
{
if ($array instanceof ArrayAccess) {
return $array->offsetExists($key);
}
return array_key_exists($key, $array);
}
}
@@ -0,0 +1,24 @@
<?php
namespace Spatie\DataTransferObject\Attributes;
use Attribute;
use Spatie\DataTransferObject\Caster;
use Spatie\DataTransferObject\Exceptions\InvalidCasterClass;
#[Attribute(Attribute::TARGET_CLASS | Attribute::TARGET_PROPERTY)]
class CastWith
{
public array $args;
public function __construct(
public string $casterClass,
mixed ...$args
) {
if (! is_subclass_of($this->casterClass, Caster::class)) {
throw new InvalidCasterClass($this->casterClass);
}
$this->args = $args;
}
}
@@ -0,0 +1,53 @@
<?php
namespace Spatie\DataTransferObject\Attributes;
use Attribute;
use JetBrains\PhpStorm\Immutable;
use ReflectionNamedType;
use ReflectionProperty;
use ReflectionUnionType;
use Spatie\DataTransferObject\Caster;
#[Attribute(Attribute::TARGET_CLASS | Attribute::IS_REPEATABLE)]
class DefaultCast
{
public function __construct(
#[Immutable]
private string $targetClass,
#[Immutable]
private string $casterClass,
) {
}
public function accepts(ReflectionProperty $property): bool
{
$type = $property->getType();
/** @var \ReflectionNamedType[]|null $types */
$types = match ($type::class) {
ReflectionNamedType::class => [$type],
ReflectionUnionType::class => $type->getTypes(),
default => null,
};
if (! $types) {
return false;
}
foreach ($types as $type) {
if ($type->getName() !== $this->targetClass) {
continue;
}
return true;
}
return false;
}
public function resolveCaster(): Caster
{
return new $this->casterClass();
}
}
@@ -0,0 +1,14 @@
<?php
namespace Spatie\DataTransferObject\Attributes;
use Attribute;
#[Attribute(Attribute::TARGET_CLASS | Attribute::TARGET_PROPERTY)]
class MapFrom
{
public function __construct(
public string | int $name,
) {
}
}
@@ -0,0 +1,14 @@
<?php
namespace Spatie\DataTransferObject\Attributes;
use Attribute;
#[Attribute(Attribute::TARGET_CLASS | Attribute::TARGET_PROPERTY)]
class MapTo
{
public function __construct(
public string $name,
) {
}
}
@@ -0,0 +1,10 @@
<?php
namespace Spatie\DataTransferObject\Attributes;
use Attribute;
#[Attribute(Attribute::TARGET_CLASS)]
class Strict
{
}
+8
View File
@@ -0,0 +1,8 @@
<?php
namespace Spatie\DataTransferObject;
interface Caster
{
public function cast(mixed $value): mixed;
}
@@ -0,0 +1,70 @@
<?php
namespace Spatie\DataTransferObject\Casters;
use ArrayAccess;
use LogicException;
use Spatie\DataTransferObject\Caster;
use Traversable;
class ArrayCaster implements Caster
{
public function __construct(
private array $types,
private string $itemType,
) {
}
public function cast(mixed $value): array | ArrayAccess
{
foreach ($this->types as $type) {
if ($type == 'array') {
return $this->mapInto(
destination: [],
items: $value
);
}
if (is_subclass_of($type, ArrayAccess::class)) {
return $this->mapInto(
destination: new $type(),
items: $value
);
}
}
throw new LogicException(
"Caster [ArrayCaster] may only be used to cast arrays or objects that implement ArrayAccess."
);
}
private function mapInto(array | ArrayAccess $destination, mixed $items): array | ArrayAccess
{
if ($destination instanceof ArrayAccess && ! is_subclass_of($destination, Traversable::class)) {
throw new LogicException(
"Caster [ArrayCaster] may only be used to cast ArrayAccess objects that are traversable."
);
}
foreach ($items as $key => $item) {
$destination[$key] = $this->castItem($item);
}
return $destination;
}
private function castItem(mixed $data)
{
if ($data instanceof $this->itemType) {
return $data;
}
if (is_array($data)) {
return new $this->itemType(...$data);
}
throw new LogicException(
"Caster [ArrayCaster] each item must be an array or an instance of the specified item type [{$this->itemType}]."
);
}
}
@@ -0,0 +1,25 @@
<?php
namespace Spatie\DataTransferObject\Casters;
use Spatie\DataTransferObject\Caster;
use Spatie\DataTransferObject\DataTransferObject;
class DataTransferObjectCaster implements Caster
{
public function __construct(
private array $classNames
) {
}
public function cast(mixed $value): DataTransferObject
{
foreach ($this->classNames as $className) {
if ($value instanceof $className) {
return $value;
}
}
return new $this->classNames[0](...$value);
}
}
@@ -0,0 +1,125 @@
<?php
namespace Spatie\DataTransferObject;
use ReflectionClass;
use ReflectionProperty;
use Spatie\DataTransferObject\Attributes\CastWith;
use Spatie\DataTransferObject\Attributes\MapTo;
use Spatie\DataTransferObject\Casters\DataTransferObjectCaster;
use Spatie\DataTransferObject\Exceptions\UnknownProperties;
use Spatie\DataTransferObject\Reflection\DataTransferObjectClass;
#[CastWith(DataTransferObjectCaster::class)]
abstract class DataTransferObject
{
protected array $exceptKeys = [];
protected array $onlyKeys = [];
public function __construct(...$args)
{
if (is_array($args[0] ?? null)) {
$args = $args[0];
}
$class = new DataTransferObjectClass($this);
foreach ($class->getProperties() as $property) {
$property->setValue(Arr::get($args, $property->name) ?? $this->{$property->name} ?? null);
$args = Arr::forget($args, $property->name);
}
if ($class->isStrict() && count($args)) {
throw UnknownProperties::new(static::class, array_keys($args));
}
$class->validate();
}
public static function arrayOf(array $arrayOfParameters): array
{
return array_map(
fn (mixed $parameters) => new static($parameters),
$arrayOfParameters
);
}
public function all(): array
{
$data = [];
$class = new ReflectionClass(static::class);
$properties = $class->getProperties(ReflectionProperty::IS_PUBLIC);
foreach ($properties as $property) {
if ($property->isStatic()) {
continue;
}
$mapToAttribute = $property->getAttributes(MapTo::class);
$name = count($mapToAttribute) ? $mapToAttribute[0]->newInstance()->name : $property->getName();
$data[$name] = $property->getValue($this);
}
return $data;
}
public function only(string ...$keys): static
{
$dataTransferObject = clone $this;
$dataTransferObject->onlyKeys = [...$this->onlyKeys, ...$keys];
return $dataTransferObject;
}
public function except(string ...$keys): static
{
$dataTransferObject = clone $this;
$dataTransferObject->exceptKeys = [...$this->exceptKeys, ...$keys];
return $dataTransferObject;
}
public function clone(...$args): static
{
return new static(...array_merge($this->toArray(), $args));
}
public function toArray(): array
{
if (count($this->onlyKeys)) {
$array = Arr::only($this->all(), $this->onlyKeys);
} else {
$array = Arr::except($this->all(), $this->exceptKeys);
}
$array = $this->parseArray($array);
return $array;
}
protected function parseArray(array $array): array
{
foreach ($array as $key => $value) {
if ($value instanceof DataTransferObject) {
$array[$key] = $value->toArray();
continue;
}
if (! is_array($value)) {
continue;
}
$array[$key] = $this->parseArray($value);
}
return $array;
}
}
@@ -0,0 +1,18 @@
<?php
namespace Spatie\DataTransferObject\Exceptions;
use Exception;
use Spatie\DataTransferObject\Caster;
class InvalidCasterClass extends Exception
{
public function __construct(string $className)
{
$expected = Caster::class;
parent::__construct(
"Class `{$className}` doesn't implement {$expected} and can't be used as a caster"
);
}
}
@@ -0,0 +1,15 @@
<?php
namespace Spatie\DataTransferObject\Exceptions;
use Exception;
class UnknownProperties extends Exception
{
public static function new(string $dtoClass, array $fields): self
{
$properties = json_encode($fields);
return new self("Unknown properties provided to `{$dtoClass}`: {$properties}");
}
}
@@ -0,0 +1,27 @@
<?php
namespace Spatie\DataTransferObject\Exceptions;
use Exception;
use Spatie\DataTransferObject\DataTransferObject;
class ValidationException extends Exception
{
public function __construct(
public DataTransferObject $dataTransferObject,
public array $validationErrors,
) {
$className = $dataTransferObject::class;
$messages = [];
foreach ($validationErrors as $fieldName => $errorsForField) {
/** @var \Spatie\DataTransferObject\Validation\ValidationResult $errorForField */
foreach ($errorsForField as $errorForField) {
$messages[] = "\t - `{$className}->{$fieldName}`: {$errorForField->message}";
}
}
parent::__construct("Validation errors:" . PHP_EOL . implode(PHP_EOL, $messages));
}
}
@@ -0,0 +1,84 @@
<?php
namespace Spatie\DataTransferObject\Reflection;
use ReflectionClass;
use ReflectionProperty;
use Spatie\DataTransferObject\Attributes\Strict;
use Spatie\DataTransferObject\DataTransferObject;
use Spatie\DataTransferObject\Exceptions\ValidationException;
class DataTransferObjectClass
{
private ReflectionClass $reflectionClass;
private DataTransferObject $dataTransferObject;
private bool $isStrict;
public function __construct(DataTransferObject $dataTransferObject)
{
$this->reflectionClass = new ReflectionClass($dataTransferObject);
$this->dataTransferObject = $dataTransferObject;
}
/**
* @return \Spatie\DataTransferObject\Reflection\DataTransferObjectProperty[]
*/
public function getProperties(): array
{
$publicProperties = array_filter(
$this->reflectionClass->getProperties(ReflectionProperty::IS_PUBLIC),
fn (ReflectionProperty $property) => ! $property->isStatic()
);
return array_map(
fn (ReflectionProperty $property) => new DataTransferObjectProperty(
$this->dataTransferObject,
$property
),
$publicProperties
);
}
public function validate(): void
{
$validationErrors = [];
foreach ($this->getProperties() as $property) {
$validators = $property->getValidators();
foreach ($validators as $validator) {
$result = $validator->validate($property->getValue());
if ($result->isValid) {
continue;
}
$validationErrors[$property->name][] = $result;
}
}
if (count($validationErrors)) {
throw new ValidationException($this->dataTransferObject, $validationErrors);
}
}
public function isStrict(): bool
{
if (! isset($this->isStrict)) {
$attribute = null;
$reflectionClass = $this->reflectionClass;
while ($attribute === null && $reflectionClass !== false) {
$attribute = $reflectionClass->getAttributes(Strict::class)[0] ?? null;
$reflectionClass = $reflectionClass->getParentClass();
}
$this->isStrict = $attribute !== null;
}
return $this->isStrict;
}
}
@@ -0,0 +1,182 @@
<?php
namespace Spatie\DataTransferObject\Reflection;
use JetBrains\PhpStorm\Immutable;
use ReflectionAttribute;
use ReflectionClass;
use ReflectionNamedType;
use ReflectionProperty;
use ReflectionType;
use ReflectionUnionType;
use Spatie\DataTransferObject\Attributes\CastWith;
use Spatie\DataTransferObject\Attributes\DefaultCast;
use Spatie\DataTransferObject\Attributes\MapFrom;
use Spatie\DataTransferObject\Caster;
use Spatie\DataTransferObject\DataTransferObject;
use Spatie\DataTransferObject\Validator;
class DataTransferObjectProperty
{
#[Immutable]
public string $name;
private DataTransferObject $dataTransferObject;
private ReflectionProperty $reflectionProperty;
private ?Caster $caster;
public function __construct(
DataTransferObject $dataTransferObject,
ReflectionProperty $reflectionProperty
) {
$this->dataTransferObject = $dataTransferObject;
$this->reflectionProperty = $reflectionProperty;
$this->name = $this->resolveMappedProperty();
$this->caster = $this->resolveCaster();
}
public function setValue(mixed $value): void
{
if ($this->caster && $value !== null) {
$value = $this->caster->cast($value);
}
$this->reflectionProperty->setValue($this->dataTransferObject, $value);
}
/**
* @return \Spatie\DataTransferObject\Validator[]
*/
public function getValidators(): array
{
$attributes = $this->reflectionProperty->getAttributes(
Validator::class,
ReflectionAttribute::IS_INSTANCEOF
);
return array_map(
fn (ReflectionAttribute $attribute) => $attribute->newInstance(),
$attributes
);
}
public function getValue(): mixed
{
return $this->reflectionProperty->getValue($this->dataTransferObject);
}
private function resolveCaster(): ?Caster
{
$attributes = $this->reflectionProperty->getAttributes(CastWith::class);
if (! count($attributes)) {
$attributes = $this->resolveCasterFromType();
}
if (! count($attributes)) {
return $this->resolveCasterFromDefaults();
}
/** @var \Spatie\DataTransferObject\Attributes\CastWith $attribute */
$attribute = $attributes[0]->newInstance();
return new $attribute->casterClass(
array_map(fn ($type) => $this->resolveTypeName($type), $this->extractTypes()),
...$attribute->args
);
}
private function resolveCasterFromType(): array
{
foreach ($this->extractTypes() as $type) {
$name = $this->resolveTypeName($type);
if (! class_exists($name)) {
continue;
}
$reflectionClass = new ReflectionClass($name);
do {
$attributes = $reflectionClass->getAttributes(CastWith::class);
$reflectionClass = $reflectionClass->getParentClass();
} while (! count($attributes) && $reflectionClass);
if (count($attributes) > 0) {
return $attributes;
}
}
return [];
}
private function resolveCasterFromDefaults(): ?Caster
{
$defaultCastAttributes = [];
$class = $this->reflectionProperty->getDeclaringClass();
do {
array_push($defaultCastAttributes, ...$class->getAttributes(DefaultCast::class));
$class = $class->getParentClass();
} while ($class !== false);
if (! count($defaultCastAttributes)) {
return null;
}
foreach ($defaultCastAttributes as $defaultCastAttribute) {
/** @var \Spatie\DataTransferObject\Attributes\DefaultCast $defaultCast */
$defaultCast = $defaultCastAttribute->newInstance();
if ($defaultCast->accepts($this->reflectionProperty)) {
return $defaultCast->resolveCaster();
}
}
return null;
}
private function resolveMappedProperty(): string | int
{
$attributes = $this->reflectionProperty->getAttributes(MapFrom::class);
if (! count($attributes)) {
return $this->reflectionProperty->name;
}
return $attributes[0]->newInstance()->name;
}
/**
* @return ReflectionNamedType[]
*/
private function extractTypes(): array
{
$type = $this->reflectionProperty->getType();
if (! $type) {
return [];
}
return match ($type::class) {
ReflectionNamedType::class => [$type],
ReflectionUnionType::class => $type->getTypes(),
};
}
private function resolveTypeName(ReflectionType $type): string
{
return match ($type->getName()) {
'self' => $this->dataTransferObject::class,
'parent' => get_parent_class($this->dataTransferObject),
default => $type->getName(),
};
}
}
@@ -0,0 +1,31 @@
<?php
namespace Spatie\DataTransferObject\Validation;
use JetBrains\PhpStorm\Immutable;
class ValidationResult
{
public function __construct(
#[Immutable]
public bool $isValid,
#[Immutable]
public ?string $message = null
) {
}
public static function valid(): self
{
return new self(
isValid: true,
);
}
public static function invalid(string $message): self
{
return new self(
isValid: false,
message: $message,
);
}
}
+10
View File
@@ -0,0 +1,10 @@
<?php
namespace Spatie\DataTransferObject;
use Spatie\DataTransferObject\Validation\ValidationResult;
interface Validator
{
public function validate(mixed $value): ValidationResult;
}