solventt/slim-route-strategy

为Slim微框架的路由调用策略

1.0.0 2021-11-25 13:03 UTC

README

目录

  1. 需求
  2. 安装
  3. 灵活控制器签名
  4. 特性
  5. 解析DTO
  6. 用例
  7. 编写自定义规则

该软件包是Slim微框架的路由调用策略的实现。它允许您灵活地设置控制器参数的解析。关于调用策略,您可以在Slim文档中阅读。

需求

  • PHP 7.4+ 或 8.0+
  • Slim微框架版本 3+ 或 4+
  • 任何DI容器。但如果它没有自动装配,则TypeHintContainerRuleMakeDtoRule将不会影响控制器参数的解析。

安装

// php 7.4+
composer require solventt/slim-route-strategy ^0.1

// php 8.0+
composer require solventt/slim-route-strategy ^1.0

灵活控制器签名

默认情况下,Slim控制器有一个严格的签名:$request$response$args

因此,即使其中一个参数不需要,也不能省略任何一个。这被称为RequestResponse策略。

但使用此软件包

  • 您可以指定您需要的任何参数,甚至可以指定一个空的控制器签名
  • 参数的顺序无关紧要
  • 服务将通过类型提示进行注入
  • 除了路由占位符之外,您还可以在控制器参数中接收请求属性和数据传输对象(而不是POST/PUT/PATCH数组)
  • 传入的$id参数将是整数类型而不是默认的字符串类型(可选)
  • 您可以添加自己的参数解析功能,例如,您可以通过接收一些实体(用户)而不是$id参数

特性

自定义规则聚合器路由策略由以下规则组成

1) IdIntegerTypeRule(可选)- 如果存在,将路由参数“id”的字符串类型转换为整数类型。在声明strict_types=1时特别方便。

$app->get('/profile/{id:\d+}', [ProfileController::class, 'show']);

...

public function show(int $id): Response
{
   // incoming $id has integer type instead of default's string
}

注意:控制器参数名称和路由占位符的名称必须是id


2) FlexibleSignatureRule - 尝试将路由参数的关联数组映射到控制器参数名称。

假设有一个控制器方法

public function show($request, $response, $id) {}

并且有路由参数

[
   'request' => 'value_1', 
   'response' => 'value_2', 
   'id' = '1'
]

那么控制器方法将接收到以下参数值

public function show($request, $response, $id)
{
  echo $request;   // 'value_1'
  echo $response;  // 'value_2'
  echo $id;        // '1' - string, because the IdIntegerTypeRule is off
}

注意:控制器请求/响应参数的名称必须是requestresponse


3) TypeHintContainerRule - 使用DI容器注入类型提示的控制器参数。但联合类型将被忽略。

public function show(Twig $twig, self $surrentClass) 
{
   // The Twig and declaring class instances will be automatically resolved
}

4) NullTypeRule - 如果控制器参数没有默认值,则检查“null”参数类型是否存在,并在成功的情况下将其用于解析。

public function show(?string $name, ?int $count = 5) 
{
   var_dump($name);   // null
   echo $count;       // 5
}

5) MakeDtoRule - 请参阅下一节。

默认情况下,只有FlexibleSignatureRuleTypeHintContainerRuleNullTypeRule是启用的。

您还可以添加自己的规则。

解析DTO

MakeDtoRule将POST|PUT|PATCH请求的数据数组转换为数据传输对象(DTO)

public function update(Dto $dto, int $id)
{
   // do something with $dto
}

默认情况下,将创建一个内置的Dto类,并用请求数据填充。但您可以使用工厂定义自己的DTO类和自己的数据处理逻辑,以填充对象。

示例

DI容器的定义

return [
    'dtoFactories' => [
         
         // key - is a parameter name of a controller method
         // value - a corresponding DTO factory class
         
         'dto' => UserUpdateDtoFactory::class 
    ]
];

工厂逻辑

class UserUpdateDtoFactory
{
   public function __invoke(array $requestData): UserUpdateDto
   {
      $dto = new UserUpdateDto();

      foreach ($requestData as $field => $value) {

         $value = match ($field) {
                'phoneType' => (int) $value,
                'date' => new \DateTime($value),
                'isActive' => (bool) $value,
                 default => $value
            };

         $dto->$field = $value;
      }
      
      return $dto;
   }
}

以及控制器方法

public function update(UserUpdateDto $dto)
{
   // do something with $dto
}

记住

  1. 参数名称必须包含“dto”子字符串。例如:'$userUpdateDto'、'$dto'、'myDto'、'loginDto'等。
  2. 您需要在DI容器定义中指定参数名称作为数组键。数组的值是相应的DTO工厂类。
  3. DI 容器定义必须命名为 'dtoFactories'(见上方示例)。

用例

对于 Slim 版本 ^4.0,index.php

<?php

use DI\Container;
use Slim\Factory\AppFactory;
use SlimRouteStrategy\CustomRulesAggregator;

require __DIR__ . '/vendor/autoload.php';

$container = new Container();

$app = AppFactory::createFromContainer($container);

$strategy = new CustomRulesAggregator($container);

$app->getRouteCollector()->setDefaultInvocationStrategy($strategy);

$app->get('/hello/{name}', function ($response, $name) {
    $response->getBody()->write($name);

    return $response;
});

$app->run();

对于 Slim 版本 ^3.0,index.php

<?php

use Slim\App;
use Slim\Container;
use SlimRouteStrategy\CustomRulesAggregator;

require __DIR__ . '/vendor/autoload.php';

$container = new Container();

$container['foundHandler'] = fn () => new CustomRulesAggregator($container);

$app = new App($container);

$app->get('/hello/{name}', function ($response, $name) {
    $response->getBody()->write($name);

    return $response;
});

$app->run();

关于策略规则

如果您没有向路由策略构造函数提供任何规则,则默认情况下将启用 FlexibleSignatureRuleTypeHintContainerRuleNullTypeRule

例如,您想添加 IdIntegerTypeRuleMakeDtoRule,那么您应该明确定义所有必要的规则

...

$strategyRules = [

    IdIntegerTypeRule::class,
    FlexibleSignatureRule::class,
    MakeDtoRule::class,
    TypeHintContainerRule::class,
    NullTypeRule::class
    
];

$strategy = new CustomRulesAggregator($container, $strategyRules);

...

或者如果您只想添加一个规则

...

$strategy = new CustomRulesAggregator($container, [FlexibleSignatureRule::class]);

...

记住

  • 规则必须指定为存在的类字符串
  • 规则顺序很重要。例如,如果您在 FlexibleSignatureRule 之后定义 IdIntegerTypeRule。那么 IdIntegerTypeRule 将没有效果 - id 的类型将是字符串而不是整数。

上述示例显示了规则的正确顺序.

编写自定义规则

您的自定义规则必须实现 AggregatorRuleInterface

让我们看一个简单的例子。假设您希望控制器方法接收用户实体作为参数。因此路由是

$app->get('/profile/{user:\d+}', [ProfileController::class, 'show']);

控制器方法是

public function show(User $user){}

并且您编写了您的自定义 FindUserEntityRule

class FindUserEntityRule implements AggregatorRuleInterface
{
    public function __construct(private UserRepository $users){}

    /**
     * @param ReflectionParameter[]  $unresolvedParams parameters that have not yet been resolved
     * @param array $routeParams     request/response objects, route placeholders values, request attributes
     * @param array $resolvedParams  parameters resolved by previous rule (indexed by parameter position)
     * @return array                 parameters resolved by this + by previous rule
     */
    public function resolveParameters(array $unresolvedParams,
                                      array $routeParams,
                                      array $resolvedParams): array
    {
        foreach ($unresolvedParams as $position => $parameter) {

            if ($parameter->name === 'user' && $this->hasAppropriateType($parameter)) {
                if (array_key_exists($parameter->name, $routeParams)) {

                    $userId = (int) $routeParams[$parameter->name];
                    $user = $this->users->findOne($userId);

                    $resolvedParams[$position] = $user;
                }
            }
        }

        return $resolvedParams;
    }

    private function hasAppropriateType(ReflectionParameter $parameter): bool
    {
        $type = $parameter->getType();

        return !$type instanceof ReflectionUnionType && $type->getName() === User::class;
    }
}