返回

API日期参数处理: Symfony DateTime查询参数解析最佳实践

php

URL 查询参数中的日期处理

一个常见的需求是在 API 端点接收日期类型的查询参数。 例如,/orders/list-all?since=2024-10-21,期望将 2024-10-21 解析成日期对象(DateTimeImmutableDateTimeInterface)。但是直接将参数绑定到 DateTimeInterface 可能不会按预期工作。本篇文章会探讨这一问题并提供解决方案。

问题根源

通过观察 DateTimeValueResolver 的源代码,可以发现核心问题在于 $request->attributes 不包含查询参数的信息。DateTimeValueResolver 默认会从请求属性中尝试读取参数。但请求的查询参数通常位于 $request->query,而不是 $request->attributes。这是参数绑定失败的关键所在。

解决方案 1: 手动解析

最直接的方法是在控制器中手动获取和解析查询参数。这样做灵活性更高,并能让你完全掌控解析过程。

步骤:

  1. 从请求中获取 since 查询参数。
  2. 检查参数是否存在且不为空。
  3. 尝试使用指定的格式将字符串解析为日期对象。
  4. 处理任何可能出现的解析错误。

代码示例:

use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use \DateTimeImmutable;
use \DateTimeInterface;

class OrderController
{
    public function listOrders(Request $request): Response
    {
      $since = $request->query->get('since');

      if (!empty($since)) {
          try {
              $sinceDate = DateTimeImmutable::createFromFormat('Y-m-d', $since);
              // 使用 $sinceDate 进行业务逻辑处理
              return new Response("Orders since: " . $sinceDate->format('Y-m-d H:i:s'));
          } catch (\Exception $e) {
            //处理日期解析失败的情况
            return new Response("Invalid date format", 400);
          }
        }

      // 当 since 参数为空的情况
      return new Response("No since parameter provided.");
    }
}

这种方法简单直接,并提供了足够的控制力。但是代码可能会显得较为冗余,如果你的控制器有很多类似的日期参数需要处理,将会造成代码重复。

解决方案 2: 使用 MapQueryParameter 和自定义转换器

MapQueryParameter 组件提供了一种声明式的方式处理查询参数。你可以创建自定义的 ValueResolver 来实现 DateTimeDateTimeImmutable 对象的解析,以提高代码的可读性和复用性。

步骤:

  1. 创建自定义的 ValueResolver ,负责日期解析逻辑。这个解析器会实现 Symfony\Component\HttpKernel\Controller\ValueResolverInterface
  2. 在控制器的参数中使用 MapQueryParameter 标注。MapQueryParameter 能将参数传递给自定义解析器,它需要制定 resolver 选项来制定自定义的解析器服务名称。
  3. 配置你的 service.yaml。

自定义解析器示例:

// src/Controller/Resolver/DateTimeQueryResolver.php

namespace App\Controller\Resolver;

use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Controller\ValueResolverInterface;
use Symfony\Component\HttpKernel\Controller\ArgumentMetadata;
use \DateTimeImmutable;
use \DateTimeInterface;


class DateTimeQueryResolver implements ValueResolverInterface
{
    public function supports(Request $request, ArgumentMetadata $argument): bool
    {
        return $argument->getType() === DateTimeInterface::class &&
            $request->query->has($argument->getName());
    }


    public function resolve(Request $request, ArgumentMetadata $argument): iterable
    {
       $dateString = $request->query->get($argument->getName());

       if (empty($dateString)) {
            return [null]; // 参数为空
        }
        try {
            yield  DateTimeImmutable::createFromFormat('Y-m-d', $dateString); // 日期格式必须符合指定格式
        } catch (\Exception $e) {
             // 日期解析失败,可以返回 null 或者抛出异常,根据需求自行处理
            yield null;
        }
    }
}

控制器示例:

// src/Controller/OrderController.php
namespace App\Controller;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Attribute\MapQueryParameter;
use App\Controller\Resolver\DateTimeQueryResolver;
use \DateTimeInterface;


class OrderController
{
    public function listOrders(
      #[MapQueryParameter(resolver: DateTimeQueryResolver::class)]
        ?DateTimeInterface $since = null
    ): Response {
         if($since) {
             return new Response('Orders since:' .$since->format('Y-m-d H:i:s'));
        }
        return new Response("No since parameter provided.");
    }
}

services.yaml示例:

  App\Controller\Resolver\DateTimeQueryResolver:
      tags:
          - { name: 'controller.argument_value_resolver'}

这种方式提供了更优雅的处理日期查询参数的方式,并且代码更为结构化。这种方式减少了控制器的代码量,增强了可重用性。并且将解析逻辑抽象到专门的解析器类中,遵循了单一职责原则,并有助于构建可测试代码。

额外安全建议

无论哪种方法,都需要格外注意输入验证。验证日期的格式和有效性,避免非预期的数据进入应用,导致程序出现错误或被滥用。例如,可以配置明确的日期格式(如'Y-m-d')并在 createFromFormat 中使用,当传入错误格式时,程序不会产生预期外的结果,并通过捕获异常来处理异常数据。

通过理解问题,选用合适的解决方案, 可以有效地解决参数映射的问题。 你需要根据应用的规模和要求选择合适的方法。 手动解析适用于简单的场景,自定义解析器则能提供更高的灵活性和可重用性。记住,良好的错误处理和输入验证对于应用的安全和稳定至关重要。