Skip to content

Instantly share code, notes, and snippets.

@theofidry
Created February 19, 2025 13:57
Show Gist options
  • Save theofidry/9f4b0e9753069c87bfdcf5950bda811c to your computer and use it in GitHub Desktop.
Save theofidry/9f4b0e9753069c87bfdcf5950bda811c to your computer and use it in GitHub Desktop.
<?php
use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata;
interface ValueResolverInterface {
public function resolve(mixed $source, ArgumentMetadata $metadata): iterable;
}
interface ValueResolverExtractor {
public function extract(mixed $source, ArgumentMetadata $metadata): mixed;
}
class DateTimeValueResolver implements ValueResolverInterface {
public function __construct(
private ValueResolverExtractor $extractor,
private readonly ?ClockInterface $clock = null,
)
{
}
public function resolve(mixed $source, ArgumentMetadata $metadata): iterable
{
$value = $this->extractor->extract($source, $metadata);
$class = \DateTimeInterface::class === $argument->getType() ? \DateTimeImmutable::class : $argument->getType();
if (!$value) {
if ($argument->isNullable()) {
return [null];
}
if (!$this->clock) {
return [new $class()];
}
$value = $this->clock->now();
}
if ($value instanceof \DateTimeInterface) {
return [$value instanceof $class ? $value : $class::createFromInterface($value)];
}
$format = null;
if ($attributes = $argument->getAttributes(MapDateTime::class, ArgumentMetadata::IS_INSTANCEOF)) {
$attribute = $attributes[0];
$format = $attribute->format;
}
if (null !== $format) {
$date = $class::createFromFormat($format, $value, $this->clock?->now()->getTimeZone());
if (($class::getLastErrors() ?: ['warning_count' => 0])['warning_count']) {
$date = false;
}
} else {
if (false !== filter_var($value, \FILTER_VALIDATE_INT, ['options' => ['min_range' => 0]])) {
$value = '@'.$value;
}
try {
$date = new $class($value, $this->clock?->now()->getTimeZone());
} catch (\Exception) {
$date = false;
}
}
if (!$date) {
throw new NotFoundHttpException(sprintf('Invalid date given for parameter "%s".', $argument->getName()));
}
return [$date];
}
}
class ValueExtractorAggregate implements ValueResolverExtractor {
public function __construct(private ValueResolverExtractor ...$extractors)
{
}
public function extract(mixed $source, ArgumentMetadata $metadata): mixed
{
foreach ($this->extractors as $extractor) {
$result = $extractor->extract($source, $metadata);
if ($result !== -1) {
return $result;
}
}
throw new Error();
}
}
class HttpValueResolverAdaptor implements ValueResolverExtractor {
public function __construct(private HttpValueResolverExtractor $httpExtractor)
{
}
public function extract(mixed $source, ArgumentMetadata $metadata): mixed
{
return $source instanceof Request
? $this->httpExtractor->extract($source, $metadata)
: -1; // TODO: use another special value
}
}
interface HttpValueResolverExtractor {
public function extract(Request $request, ArgumentMetadata $metadata): mixed;
}
final class DateTimeValueExtractor implements HttpValueResolverExtractor
{
public function __construct(
private readonly ?ClockInterface $clock = null,
) {
}
public function extract(Request $request, ArgumentMetadata $metadata): array
{
if (!is_a($argument->getType(), \DateTimeInterface::class, true) || !$request->attributes->has($argument->getName())) {
return [];
}
return $request->attributes->get($argument->getName());
}
}
@chalasr
Copy link

chalasr commented Feb 20, 2025

Alternative approach inspired from our discussion, closer to current behavior so easier to make in BC-wise. The new interface can even be implemented by legacy resolvers classes so we'll just have to remove the deprecated interface and methods implementations at the end.

<?php

final class SourceValue { 
    public function __construct(private mixed $value) { 
        public function getValue(): mixed {
            return $this->value;
        }
    }; 
}

interface ValueResolverInterface {
    public function resolveArgument(array $metadata, SourceValue $value): iterable;
}

interface HttpValueResolverInterface extends ValueResolverInterface {    
    public function extractSourceValue(ArgumentMetadata $argument, Request $request): SourceValue;
}

class BakedEnumResolver implements ValueResolverInterface {
        public function resolve(array $metadata, SourceValue $value): iterable {
             return BackedEnum::tryFrom($value->value);
        }
}

final class HttpBakedEnumResolver implements HttpValueResolverInterface  {
    public function __construct(private BakedEnumValueResolver $bakedEnumResolver) {}

    public function resolveArgument(ArgumentMetadata $metadata, SourceValue $value): iterable
    {
        try {
            return $this->bakedEnumResolver->resolve($metadata, $value);
        } catch (InvalidSourceValue $e) {
            throw new HttpException($e->getMessage());
        }
    }

    public function extractSourceValue(ArgumentMetadata $argument, Request $request): SourceValue;
    {
        if (!$request->attributes->has($argument->getName()) {
             return new MissingSourceValue(SourceValue);
        }

        return new SourceValue($request->attributes->get($argument->getName());
    }
} 

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment