mpscholten/request-parser

v1.5.1 2020-06-19 12:20 UTC

README

Latest Stable Version License Circle CI Total Downloads

一个用于类型安全输入处理的PHP小型库。

问题

假设你有一个列出一些实体的动作。这包括分页、升序或降序排序以及可选的按实体创建时间过滤。这个动作将有一些输入解析,可能看起来像这样

public function index()
{
    $page = $this->request->query->get('page');
    if ($page === null || !is_integer($page)) {
        throw new Exception("Parameter page not found");
    }
    
    $order = $this->request->query->get('order');
    if ($order === null || !in_array($order, ['asc', 'desc'])) {
        throw new Exception("Parameter order not found");
    }
    
    // Optional parameter
    $createdAt = $this->query->query->get('createdAt');
    if (is_string($createdAt)) {
        $createdAt = new DateTime($createdAt);
    } else {
        $createdAt = null;
    }
}

显然,这段代码不易阅读,因为它不太具有描述性。对于所做的事情来说,它也很冗长。而且如果你没有仔细注意,你可能会错过null检查或类型检查。

现在将上面的代码与这个版本进行比较

public function index()
{
    $page = $this->queryParameter('page')->int()->required();
    $order = $this->queryParameter('order')->oneOf(['asc', 'desc'])->required();
    $createdAt = $this->queryParameter('createdAt')->dateTime()->defaultsTo(null);
}

这正是这个库所提供的。它允许你表达“这个动作需要一个整型的页面参数”或“这个动作有一个可选的参数createdAt,其类型为DateTime,如果不存在则设置为默认值”。

示例

如果你想立即查看代码,你只需在示例中玩耍即可。

  1. cd /tmp
  2. git clone git@github.com:mpscholten/request-parser.git
  3. cd request-parser
  4. composer install
  5. cd examples
  6. php -S localhost:8080
  7. 打开它: https://:8080/symfony.php?action=hello

examples目录中还有几个其他PHP文件。为了深入实践,我建议稍微修改一下示例。

入门

通过composer安装

composer require mpscholten/request-parser

集成

  • 如果你使用symfony/http-foundation,请点击这里
  • 如果你使用Psr7 ServerRequestInterface实现,请点击这里
  • 如果你使用其他Request抽象(或者可能是原始的$_GET和朋友们),请查看此示例

Symfony HttpFoundation

以下示例假设你使用symfony Request

class MyController
{
    use \MPScholten\RequestParser\Symfony\ControllerHelperTrait;
    
    public function __construct(Request $request)
    {
        $this->initRequestParser($request);
    }
}

然后你可以像这样使用库

class MyController
{
    use \MPScholten\RequestParser\Symfony\ControllerHelperTrait;
    
    public function __construct(Request $request)
    {
        $this->initRequestParser($request);
    }
    
    public function myAction()
    {
        $someParameter = $this->queryParameter('someParameter')->string()->required();
    }
}

当执行GET /MyController/myAction?someParameter=example时,$someParameter变量将包含字符串"example"

你可能会想知道,如果我们省略了?someParameter部分,比如GET /MyController/myAction会发生什么。在这种情况下,$this->queryParameter('someParameter')->string()->required()将抛出NotFoundException。这个异常可以由你的应用程序处理,以显示错误消息。

请参阅示例

Psr7

以下示例假设你使用Psr7 ServerRequestInterface

class MyController
{
    use \MPScholten\RequestParser\Psr7\ControllerHelperTrait;
    
    public function __construct(ServerRequestInterface $request)
    {
        $this->initRequestParser($request);
    }
}

然后你可以像这样使用库

class MyController
{
    use \MPScholten\RequestParser\Psr7\ControllerHelperTrait;
    
    public function __construct(ServerRequestInterface $request)
    {
        $this->initRequestParser($request);
    }
    
    public function myAction()
    {
        $someParameter = $this->queryParameter('someParameter')->string()->required();
    }
}

当执行GET /MyController/myAction?someParameter=example时,$someParameter变量将包含字符串"example"

你可能会想知道,如果我们省略了?someParameter部分,比如GET /MyController/myAction会发生什么。在这种情况下,$this->queryParameter('someParameter')->string()->required()将抛出NotFoundException。这个异常可以由你的应用程序处理,以显示错误消息。

请参阅示例

可选参数

要使someParameter可选,我们只需将required()替换为defaultsTo($someDefaultValue)

class MyController
{
    use \MPScholten\RequestParser\Symfony\ControllerHelperTrait;
    
    public function __construct(Request $request)
    {
        $this->initRequestParser($request);
    }
    
    public function myAction()
    {
        $someParameter = $this->queryParameter('someParameter')->string()->defaultsTo('no value given');
    }
}

当执行GET /MyController/myAction时,现在$someParameter变量将包含字符串"no value given"。不会抛出异常,因为我们指定了一个默认值。

通常情况下,您首先指定参数名称,然后是类型,最后指定参数是必需的还是有默认值的可选参数。

更多示例,请查看本仓库的 examples/ 目录。它包含几个可运行的示例。

整数、枚举、日期时间和JSON有效载荷

我们通常需要的不仅仅是字符串。 RequestParser 还提供了其他数据类型的方法。

class DashboardController
{
    public function show()
    {
        $dashboardId = $this->queryParameter('id')->int()->required();
        
        // GET /dashboard?name=Hello   =>   $dashboardName == "Hello"
        $dashboardName = $this->queryParameter('name')->string()->required();
        
        // Get /dashboard?name=   => $dashboardName == "default value"
        $dashboardName = $this->queryParameter('name')->string()->defaultsToIfEmpty("default value");
        
        // GET /dashboard?status=private  =>   $dashboardStatus == "private"
        // GET /dashboard?status=public   =>   $dashboardStatus == "public"
        // GET /dashboard?status=invalid  =>   A NotFoundException will be thrown
        $dashboardStatus = $this->queryParameter('status')->oneOf(['private', 'public'])->required();
        
        // GET /dashboard?createdAt=01.01.2016     =>   $dateTime == new DateTime("01.01.2016")
        // GET /dashboard?createdAt=invalid_date   =>   A NotFoundException will be thrown
        $dateTime = $this->queryParameter('createdAt')->dateTime()->required();
        
        // GET /dashboard?config={"a":true}     =>   $json == ['a' => true]
        $json = $this->queryParameter('config')->json()->required();
        
        // GET /dashboard?includeWidgets=true    =>   $includeWidgets == true
        // GET /dashboard?includeWidgets=false   =>   $includeWidgets == false
        // GET /dashboard?includeWidgets=0       =>   $includeWidgets == false
        // GET /dashboard?includeWidgets=abcde   =>   A NotFoundException will be thrown
        $includeWidgets = $this->queryParameter('includeWidgets')->boolean()->required();
        
        // GET /dashboard?includeWidgets=yes   =>   $includeWidgets == true
        // GET /dashboard?includeWidgets=no    =>   $includeWidgets == false
        $includeWidgets = $this->queryParameter('includeWidgets')->yesNoBoolean()->required();
        
        // GET /image?scale=2.5   =>   $scale == 2.5
        $scale = $this->queryParameter('scale')->float()->required();
    }
}

所有这些类型也提供了 defaultsTo 变体。

支持的数据类型
GET请求

$this->queryParameter($name) 告诉控制器我们想要一个查询参数(问号之后的所有内容被称为查询字符串)。当我们处理 GET 请求时,这通常是我们的需求。

POST请求

当我们处理 POST 请求时,我们需要使用 $this->bodyParameter($name) 来访问表单字段或 AJAX 负载数据。

自动完成

该库允许您充分利用您的 IDE 的自动完成功能。例如,在输入 $this->queryParameter('someParameter)-> 之后,您的 IDE 将提供所有可能的输入类型,例如 string()int()。选择一个类型后,例如 string(),您的 IDE 将提供 required()defaultsTo(defaultValue) 来指定参数未设置时的行为。

静态分析

该库支持 IDE 进行静态分析。例如,当有一个参数如 $createdAt = $this->queryParameter('createdAt')->dateTime()->required(); 时,您的 IDE 将知道 $createdAt 是一个 DateTime 对象。这允许您在编辑时检测类型错误,同时也降低了操作的维护成本,因为类型提高了可读性。

该库还降低了意外空值的风险,因为参数总是有一个明确的默认值或为必需的。

错误处理

当一个参数是必需的但未找到或验证失败时,该库将抛出异常。默认异常是 \MPScholten\RequestParser\NotFoundException\MPScholten\RequestParser\InvalidValueException。处理库抛出的错误建议的方式是在您的前端控制器内部捕获它们。

try {
    $controller->$action();
} catch (NotFoundException $e) {
    echo $e->getMessage();
} catch (InvalidValueException $e) {
    echo $e->getMessage();
}

使用自定义异常类

class MyController
{
    use \MPScholten\RequestParser\Symfony\ControllerHelperTrait;
    
    public function __construct(Request $request)
    {
        $exceptionFactory = new ExceptionFactory(CustomNotFoundException::class, CustomInvalidValueException::class));

        $config = new \MPScholten\RequestParser\Config();
        $config->setExceptionFactory($exceptionFactory);

        $this->initRequestParser($request, $config);
    }
}

使用自定义异常消息

覆盖单个消息

如果您需要一次或两次覆盖库抛出的异常消息,您可以通过将异常消息作为 ->required() 的第一个和第二个参数传递来实现。

class DashboardController
{
    public function show()
    {
        $dashboardId = $this->queryParameter('id')->int()->required("The dashboard id has to be a valid number", "No dashboard id given");
    }
}
覆盖所有消息

如果您不希望为所有操作指定自定义异常消息,但又不想使用内置的异常消息,您可以提供自己的异常消息生成器。

class FriendlyExceptionMessageFactory extends \MPScholten\RequestParser\ExceptionMessageFactory
{
    protected function createNotFoundMessage($parameterName)
    {
        return "Looks like $parameterName is missing :)";
    }

    protected function createInvalidValueMessage($parameterName, $parameterValue, $expected)
    {
        return "Whoops :) $parameterName seems to be invalid. We're looking for $expected but you provided '$parameterValue'";
    }
}

class MyController
{
    use \MPScholten\RequestParser\Symfony\ControllerHelperTrait;
    
    public function __construct(Request $request)
    {
        $config = new \MPScholten\RequestParser\Config();
        $config->setExceptionMessageFactory(new FriendlyExceptionMessageFactory());

        $this->initRequestParser($request, $config);
    }
}

查看有关自定义异常的示例

是否已准备好投入生产使用?

当然。这个库最初是在 quintly 开发的,自 2015 年 4 月以来在生产环境中广泛使用。在生产环境中大规模使用意味着非常重视向后兼容性,不破坏现有功能。

测试

composer tests
composer tests-coverage

贡献

请随意发送 pull requests!