Skip to content

Commit

Permalink
Adding support for TypeCasting alias
Browse files Browse the repository at this point in the history
  • Loading branch information
nyamsprod committed Dec 5, 2023
1 parent 8d55c66 commit 11324fb
Show file tree
Hide file tree
Showing 5 changed files with 257 additions and 42 deletions.
157 changes: 136 additions & 21 deletions src/Serializer/CallbackCasting.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,15 +32,20 @@ final class CallbackCasting implements TypeCasting
/** @var array<string, Closure(?string, bool, mixed...): mixed> */
private static array $casters = [];

/** @var array<string, array<string, Closure(?string, bool, mixed...): mixed>> */
private static array $aliases = [];

private string $type;
private readonly bool $isNullable;
/** @var Closure(?string, bool, mixed...): mixed */
private Closure $callback;
private array $options;
private string $message;

public function __construct(ReflectionProperty|ReflectionParameter $reflectionProperty)
{
public function __construct(
ReflectionProperty|ReflectionParameter $reflectionProperty,
private readonly ?string $alias = null
) {
[$this->type, $this->isNullable] = self::resolve($reflectionProperty);

$this->message = match (true) {
Expand All @@ -56,15 +61,28 @@ public function __construct(ReflectionProperty|ReflectionParameter $reflectionPr
*/
public function setOptions(string $type = null, mixed ...$options): void
{
if (Type::Mixed->value === $this->type && null !== $type) {
$this->type = $type;
}
if (null === $this->alias) {
if (Type::Mixed->value === $this->type && null !== $type) {
$this->type = $type;
}

if (array_key_exists($this->type, self::$casters)) {
$this->callback = self::$casters[$this->type];
$this->options = $options;

return;
}

if (!array_key_exists($this->type, self::$casters)) {
throw new MappingFailed($this->message);
}

$this->callback = self::$casters[$this->type];
if (Type::Mixed->value === $this->type) {
$this->type = self::aliases()[$this->alias];
}

/** @var Closure $callback */
$callback = self::$aliases[$this->type][$this->alias];
$this->callback = $callback;
$this->options = $options;
}

Expand Down Expand Up @@ -94,18 +112,52 @@ public function toVariable(?string $value): mixed
/**
* @param Closure(?string, bool, mixed...): TValue $callback
*/
public static function register(string $type, Closure $callback): void
public static function register(string $type, Closure $callback, string $alias = null): void
{
self::$casters[$type] = match (true) {
if (null === $alias) {
self::$casters[$type] = match (true) {
class_exists($type),
interface_exists($type),
Type::tryFrom($type) instanceof Type => $callback,
default => throw new MappingFailed('The `'.$type.'` could not be register.'),
};

return;
}

if (1 !== preg_match('/^@\w+$/', $alias)) {
throw new MappingFailed("The alias `$alias` is invalid. It must start with an `@` character and contain alphanumeric (letters, numbers, regardless of case) plus underscore (_).");
}

foreach (self::$aliases as $aliases) {
foreach ($aliases as $registeredAlias => $__) {
if ($alias === $registeredAlias) {
throw new MappingFailed("The alias `$alias` is already registered. Please choose another name.");
}
}
}

self::$aliases[$type][$alias] = match (true) {
class_exists($type),
interface_exists($type),
Type::tryFrom($type) instanceof Type => $callback,
default => throw new MappingFailed('The `'.$type.'` could not be register.'),
};
}

public static function unregister(string $type): bool
public static function unregister(string $type, string $alias = null): bool
{
if (null !== $alias) {
$callback = self::$aliases[$type][$alias] ?? null;
if (null === $callback) {
return false;
}

unset(self::$aliases[$type][$alias]);

return true;
}

if (!array_key_exists($type, self::$casters)) {
return false;
}
Expand All @@ -115,15 +167,62 @@ public static function unregister(string $type): bool
return true;
}

public static function unregisterAll(): void
public static function unregisterAliases(string $type): bool
{
if (!array_key_exists($type, self::$aliases)) {
return false;
}

unset(self::$aliases[$type]);

return true;
}

public static function unregisterAll(string $type = null): void
{
if (null !== $type) {
unset(self::$aliases[$type], self::$casters[$type]);

return;
}

self::$casters = [];
self::$aliases = [];
}

public static function supportsAlias(string $alias): bool
{
return array_key_exists($alias, self::aliases());
}

/**
* @return array<string, string>
*/
public static function aliases(): array
{
$res = [];
foreach (self::$aliases as $registeredType => $aliases) {
foreach ($aliases as $registeredAlias => $__) {
$res[$registeredAlias] = $registeredType;
}
}

return $res;
}

public static function supports(ReflectionParameter|ReflectionProperty $reflectionProperty): bool
public static function supports(ReflectionParameter|ReflectionProperty $reflectionProperty, string $alias = null): bool
{
foreach (self::getTypes($reflectionProperty->getType()) as $type) {
if (array_key_exists($type->getName(), self::$casters)) {
foreach (self::getTypes($reflectionProperty->getType()) as $propertyType) {
$type = $propertyType->getName();
if (null === $alias) {
if (array_key_exists($type, self::$casters)) {
return true;
}

continue;
}

if ((self::aliases()[$alias] ?? null) === $type || (Type::Mixed->value === $type && self::supportsAlias($alias))) {
return true;
}
}
Expand All @@ -138,22 +237,38 @@ public static function supports(ReflectionParameter|ReflectionProperty $reflecti
*/
private static function resolve(ReflectionParameter|ReflectionProperty $reflectionProperty): array
{
$types = self::getTypes($reflectionProperty->getType());

$type = null;
$isNullable = false;
foreach (self::getTypes($reflectionProperty->getType()) as $foundType) {
$hasMixed = false;
foreach ($types as $foundType) {
if (!$isNullable && $foundType->allowsNull()) {
$isNullable = true;
}

if (null === $type && array_key_exists($foundType->getName(), self::$casters)) {
$type = $foundType;
if (null === $type) {
if (
array_key_exists($foundType->getName(), self::$casters)
|| array_key_exists($foundType->getName(), self::$aliases)
) {
$type = $foundType;
}

if (true !== $hasMixed && Type::Mixed->value === $foundType->getName()) {
$hasMixed = true;
}
}
}

return $type instanceof ReflectionNamedType ? [$type->getName(), $isNullable] : throw new MappingFailed(match (true) {
$reflectionProperty instanceof ReflectionParameter => 'The method `'.$reflectionProperty->getDeclaringClass()?->getName().'::'.$reflectionProperty->getDeclaringFunction()->getName().'` argument `'.$reflectionProperty->getName().'` must be typed with a supported type.',
$reflectionProperty instanceof ReflectionProperty => 'The property `'.$reflectionProperty->getDeclaringClass()->getName().'::'.$reflectionProperty->getName().'` must be typed with a supported type.',
});
return match (true) {
$type instanceof ReflectionNamedType => [$type->getName(), $isNullable],
$hasMixed => [Type::Mixed->value, true],
default => throw new MappingFailed(match (true) {
$reflectionProperty instanceof ReflectionParameter => 'The method `'.$reflectionProperty->getDeclaringClass()?->getName().'::'.$reflectionProperty->getDeclaringFunction()->getName().'` argument `'.$reflectionProperty->getName().'` must be typed with a supported type.',
$reflectionProperty instanceof ReflectionProperty => 'The property `'.$reflectionProperty->getDeclaringClass()->getName().'::'.$reflectionProperty->getName().'` must be typed with a supported type.',
}),
};
}

/**
Expand Down
75 changes: 62 additions & 13 deletions src/Serializer/Denormalizer.php
Original file line number Diff line number Diff line change
Expand Up @@ -68,21 +68,39 @@ public static function disallowEmptyStringAsNull(): void
/**
* @throws MappingFailed
*/
public static function registerType(string $type, Closure $callback): void
public static function registerType(string $type, Closure $callback, string $alias = null): void
{
CallbackCasting::register($type, $callback);
CallbackCasting::register($type, $callback, $alias);
}

public static function unregisterType(string $type): bool
public static function unregisterType(string $type, string $alias = null): bool
{
return CallbackCasting::unregister($type);
return CallbackCasting::unregister($type, $alias);
}

public static function unregisterTypeAliases(string $type): void
{
CallbackCasting::unregisterAliases($type);
}

public static function unregisterAllTypes(): void
{
CallbackCasting::unregisterAll();
}

/**
* @return array<string, string>
*/
public static function aliases(): array
{
return CallbackCasting::aliases();
}

public static function supportsAlias(string $alias): bool
{
return CallbackCasting::supportsAlias($alias);
}

/**
* @param class-string $className
* @param array<?string> $record
Expand Down Expand Up @@ -312,11 +330,7 @@ private function autoDiscoverPropertySetter(ReflectionMethod|ReflectionProperty
*/
private function findPropertySetter(MapCell $cell, ReflectionMethod|ReflectionProperty $accessor, array $propertyNames): PropertySetter
{
/** @var ?class-string<TypeCasting> $typeCaster */
$typeCaster = $cell->cast;
if (null !== $typeCaster && (!class_exists($typeCaster) || !(new ReflectionClass($typeCaster))->implementsInterface(TypeCasting::class))) {
throw MappingFailed::dueToInvalidTypeCastingClass($typeCaster);
}
$typeCaster = $this->resolveTypeCaster($cell, $accessor);

$offset = $cell->column ?? match (true) {
$accessor instanceof ReflectionMethod => $this->getMethodFirstArgument($accessor)->getName(),
Expand Down Expand Up @@ -365,8 +379,6 @@ private function getMethodFirstArgument(ReflectionMethod $reflectionMethod): Ref
}

/**
* @param class-string<TypeCasting> $typeCaster
*
* @throws MappingFailed
*/
private function getTypeCasting(
Expand All @@ -375,12 +387,22 @@ private function getTypeCasting(
array $options
): TypeCasting {
try {
if (str_starts_with($typeCaster, CallbackCasting::class.'@')) {
$cast = new CallbackCasting($reflectionProperty, substr($typeCaster, strlen(CallbackCasting::class)));
$cast->setOptions(...$options);

return $cast;
}

/** @var TypeCasting $cast */
$cast = new $typeCaster($reflectionProperty);
$cast->setOptions(...$options);

return $cast;
} catch (MappingFailed $exception) {
throw $exception;
} catch (Throwable $exception) {
throw $exception instanceof MappingFailed ? $exception : MappingFailed::dueToInvalidCastingArguments($exception);
throw MappingFailed::dueToInvalidCastingArguments($exception);
}
}

Expand All @@ -389,7 +411,7 @@ private function getTypeCasting(
*/
private function resolveTypeCasting(ReflectionProperty|ReflectionParameter $reflectionProperty, array $options = []): TypeCasting
{
$castResolver = function (ReflectionProperty|ReflectionParameter $reflectionProperty, $options): TypeCasting {
$castResolver = function (ReflectionProperty|ReflectionParameter $reflectionProperty, $options): CallbackCasting {
$cast = new CallbackCasting($reflectionProperty);
$cast->setOptions(...$options);

Expand All @@ -407,4 +429,31 @@ private function resolveTypeCasting(ReflectionProperty|ReflectionParameter $refl
throw MappingFailed::dueToInvalidCastingArguments($exception);
}
}

public function resolveTypeCaster(MapCell $cell, ReflectionMethod|ReflectionProperty $accessor): ?string
{
/** @var ?class-string<TypeCasting> $typeCaster */
$typeCaster = $cell->cast;
if (null === $typeCaster) {
return null;
}

if (class_exists($typeCaster)) {
if (!(new ReflectionClass($typeCaster))->implementsInterface(TypeCasting::class)) {
throw MappingFailed::dueToInvalidTypeCastingClass($typeCaster);
}

return $typeCaster;
}

if ($accessor instanceof ReflectionMethod) {
$accessor = $accessor->getParameters()[0];
}

if (!CallbackCasting::supports($accessor, $typeCaster)) {
throw MappingFailed::dueToInvalidTypeCastingClass($typeCaster);
}

return CallbackCasting::class.$typeCaster;
}
}
Loading

0 comments on commit 11324fb

Please sign in to comment.