jasny/controller

Slim和其他微框架的控制器

v2.0.0 2022-10-09 16:43 UTC

This package is auto-updated.

Last update: 2024-09-12 23:46:58 UTC


README

PHP Scrutinizer Code Quality Code Coverage Packagist Stable Version Packagist License

PSR-7控制器,用于Slim Framework和其他微框架。

控制器负责处理HTTP请求,操作模型并启动视图。

控制器中的代码可以读取为每个动作的高级描述。控制器不应包含实现细节。这些属于模型、视图或服务库。

安装

使用composer安装

composer require jasny\controller

设置

Jasny\Controller可以用作每个控制器的基类。它允许您以友好的方式与PSR-7服务器请求和响应交互。

class MyController extends Jasny\Controller\Controller
{
    public function hello(string $name, #[QueryParam] string $others = ''): void
    {
        $this->output("Hello $name" . ($others ? " and $others" : ""), 'text');
    }
}

访问https://example.com/hello/Arnold&others=friends将输出Hello Arnold and friends

动作定义为控制器的公共方法。

控制器通过实现__invoke方法成为一个可调用的对象。调用方法接受PSR-7服务器请求和响应对象,并将返回一个修改后的响应对象。当您编写控制器时,所有这些都是抽象化的。

路由器通常处理请求并选择正确的控制器对象来调用。路由器还负责从URL路径中提取参数,并可能选择控制器中要调用的方法。

Slim框架

Slim是一个与PSR-7一起工作的PHP微框架。要使用此库与Slim一起使用,请使用提供的中间件。

use Jasny\Controller\Middleware\Slim as ControllerMiddleware;
use Slim\Factory\AppFactory;

$app = AppFactory::create();

$app->add(new ControllerMiddleware());
$app->addRoutingMiddleware();

$app->get('/hello/{name}', ['MyController', 'hello']);

可选地,中间件可以通过将true传递给中间件构造函数,将控制器中的错误响应转换为Slim HTTP错误。

use Jasny\Controller\Middleware\Slim as ControllerMiddleware;
use Slim\Factory\AppFactory;

$app = AppFactory::create();

$app->add(new ControllerMiddleware(true));
$app->addRoutingMiddleware();
$app->addErrorMiddleware(true, true, true);

Relay + SwitchRoute

SwitchRoute是一个基于生成代码的超快路由器。该路由器需要一个PSR-15请求处理器来与PSR-7服务器请求一起工作,如Relay

默认情况下,路由动作被转换为PSR-15处理器将调用的方法。对于此库,应调用__invoke。调用方法将负责在控制器中调用正确的方法。

$stud = fn($str) => strtr(ucwords($str, '-'), ['-' => '']);

$invoker = new Invoker(fn (?string $controller, ?string $action) => [
    $controller !== null ? $stud($controller) . 'Controller' : $stud($action) . 'Action',
    '__invoke'
]);

有关更多信息,请参阅SwitchRoute

输出

当使用PSR-7时,您不应使用echo,因为它使得编写测试更加困难。相反,请使用控制器的output方法,该方法将写入响应体流对象。

$this->output('Hello world');

可以传递第二个参数,该参数设置Content-Type标题。您可以传递如'text/html'这样的MIME类型。或者,您可以使用常见的文件扩展名,如'txt'。控制器使用ralouphie/mimey库来获取MIME类型。

class MyController extends Jasny\Controller\Controller
{
    /**
     * Output a random number between 0 and 100 as HTML
     */
    public function random()
    {
        $number = rand(0, 100);
        $this->output("<h1>$number</h1>", 'html');
    }
}

JSON

可以使用json方法序列化和输出数据作为JSON。

class MyController extends Jasny\Controller\Controller
{
    /**
     * Output 5 random numbers between 0 and 100 as JSON
     */
    public function random()
    {
        $numbers = array_map(fn() => rand(0, 100), range(1, 5));
        $this->json($numbers);
    }
}

响应状态

要设置响应状态,您可以使用status()方法。此方法可以接受响应状态作为整数或作为指定状态代码和短语的字符串。

class MyController extends Jasny\Controller\Controller
{
    public function process(string $size)
    {
        if (!in_array($size, ['XS', 'S', 'M', 'L', 'XL'])) {
            return $this
                ->status("400 Bad Request")
                ->output("Invalid size: $size");
        }

        // Create something ...
        
        return $this
            ->status(201)
            ->header("Location: http://www.example.com/foo/something")
            ->json($something);
    }
}

或者最好使用辅助方法来设置特定的响应状态。一些方法可以可选地接受与该状态相关的参数。

class MyController extends Jasny\Controller\Controller
{
    public function process(string $size)
    {
        if (!in_array($size, ['XS', 'S', 'M', 'L', 'XL'])) {
            return $this->badRequest()->output("Invalid size: $size");
        }

        // Create something ...
        
        return $this
            ->created("http://www.example.com/foo/something")
            ->json($something);
    }
}

以下方法可用于设置输出状态

  • 一些方法接受$message参数。这将设置输出。
  • 如果方法接受$code参数,则可以指定状态代码。
  • back() 方法将重定向到来源页面,但仅当来源页面与当前 URL 的域名相同时才生效。

有时检查响应已设置的状态码很有用。这可以通过 getStatusCode() 方法完成。此外,还有检查状态类型的方法。

响应头

您可以使用 setResponseHeader() 方法设置响应头。

class MyController extends Jasny\Controller\Controller
{
    public function process()
    {
        $this->header("Content-Language", "nl");
        // ...
    }
}

默认情况下,响应头会被覆盖。在某些情况下,您可能想要有重复的头。在这种情况下,将第三个参数设置为 true,例如 header($header, $value, true)

$this->header("Cache-Control", "no-cache"); // overwrite header
$this->header("Cache-Control", "no-store", true); // add header

输入

使用 PSR-7,您不应该使用超级全局变量 $_GET$_POST$_COOKIE$_SERVER。相反,这些值可以通过服务器请求对象获取。这是通过 PHP 属性 实现的。

控制器将方法参数映射到参数。默认情况下,参数映射到路径参数。

参数

路径参数

路由器可以从请求 URL 中提取参数。在以下示例中,URL 路径 /hello/world,路径参数 name 将具有值 "world"

$app->get('/hello/{name}', ['MyController', 'hello']);

name 参数将被作为参数传递给 hello 方法。

class MyController extends Jasny\Controller\Controller
{
    public function hello(string $name)
    {
        $this->output("Hello $name");
    }
}

单个请求参数

控制器将 PSR-7 请求参数作为参数传递。这通过一个属性指定。

  • QueryParam
  • BodyParam
  • Cookie
  • UploadedFile
  • Header

如果将参数名称用作参数名称

  • 对于 QueryParam,下划线被替换为短横线。例如:$foo_bar 将转换为查询参数 foo-bar
  • 对于 Header,单词被大写,下划线变成短横线。例如:$foo_bar 转换为头 Foo-Bar

所有请求参数

要获取特定类型的所有请求参数,以下属性可用。

  • Query
  • Body
  • Cookies
  • UploadedFiles
  • Headers

对于 Body 属性,参数类型应该是数组或字符串。如果传递数组,则参数将是解析后的正文。如果是字符串,它将是原始正文。

PSR-7 请求属性

中间件可以设置 PSR-7 请求的属性。这些请求属性可以通过使用 Attr 属性作为参数使用。

参数名称

对于单个参数,参数名称将用作参数名称。或者,在定义属性时可以指定名称。

use Jasny\Controller\Controller;
use Jasny\Controller\Parameter\PathParam;
use Jasny\Controller\Parameter\QueryParam;

class MyController extends Controller
{
    public function hello(#[PathParam] string $name, #[QueryParam('and')] string $other = '')
    {
        $this->output("Hello $name" . ($other ? " and $other" : ""));
    }
}

注意:#[PathParam] 可以省略,因为它默认行为。

参数类型

在定义属性时,可以将类型指定为第二个参数。默认情况下,类型由参数类型确定。

use Jasny\Controller\Controller;
use Jasny\Controller\Parameter\BodyParam;

class MyController extends Controller
{
    public function send(#[BodyParam(type: 'email')] string $emailAddress)
    {
        // ...
    }
}

参数属性使用 filter_var 函数来清理输入。以下过滤器已定义

对于其他类型(如 string),不应用任何过滤器。

use Jasny\Controller\Controller;
use Jasny\Controller\Parameter\PostParam;

class MyController extends Controller
{
    public function message(#[PostParam(type: 'email')] array $email)
    {
        // ...
    }
}

要添加自定义类型,请向 SingleParameter::$types 添加过滤器

use Jasny\Controller\Parameter\SingleParameter;

SingleParameter::$types['slug'] = [FILTER_VALIDATE_REGEXP, '/^[a-z\-]+$/'];

内容协商

内容协商允许控制器根据 Accept 请求头提供不同的输出。它可以用来选择内容类型(在 JSON 和 XML 之间切换)、内容语言、编码和字符集。

negotiateCharset() 将修改已设置的 Content-Type 头。否则,它将仅返回所选字符集。

协商方法接受一个优先级列表或优先级作为参数。它设置响应头并返回所选选项。

class MyController extends Jasny\Controller\Controller
{
    public function hello()
    {
        $language = $this->negotiateLanguage(['en', 'de', 'fr', 'nl;q=0.6']);
        
        switch ($language) {
            case 'en':
                return $this->output('Good morning');
            case 'de':
                return $this->output('Guten Morgen');
            case 'fr':
                return $this->output('Bonjour');
            case 'nl':
                return $this->output('Goedemorgen');
            default:
                return $this
                    ->notAcceptable()
                    ->output("This content isn't available in your language");
        }
    }
}

有关更多信息,请参阅 willdurand/negotiation 库的文档。

钩子

除了操作方法之外,控制器还会调用 before()after() 方法。

在 ...

在执行动作方法之前会调用 before() 方法。如果它返回响应,则动作方法不会被调用。

class MyController extends Jasny\Controller\Controller
{
    protected function before()
    {
        if ($this->auth->getUser()->getCredits() <= 0) {
            return $this->paymentRequired()->output("Sorry, you're out of credits");
        }
    }

    // ...
}

与其使用 before(),不如考虑使用守卫。

之后

after() 方法在动作之后被调用,无论动作响应类型如何。

class MyController extends Jasny\Controller\Controller
{
    // ...
    
    protected function after()
    {
        $this->header('X-Available-Credits', $this->auth->getUser()->getCredits());
    }
}

守卫

守卫是PHP 属性,在调用控制器方法之前被调用。守卫类似于中间件,但功能更为有限。使用守卫的目的是检查控制器动作是否可以执行。如果守卫返回响应,则该响应会被发送,控制器上的方法将不会被调用。

class MyController extends Jasny\Controller\Controller
{
    #[MustBeLoggedIn]
    public function send()
    {
        // ...
    }
}

守卫类应该实现 process 方法。守卫类具有与控制器类相同的方法。`process` 方法可以有输入参数。

use Jasny\Controller\Guard;
use Jasny\Controller\Parameter\Attr;

#[\Attribute]
class MustBeLoggedIn extends Guard
{
    public function process(#[Attr] User? $sessionUser)
    {
        if ($sessionUser === null) {
            return $this->forbidden()->output("Not logged in");
        }
    }
}

执行顺序

守卫可以定义在控制器类或动作方法上。执行顺序为

  • 类守卫
  • before()
  • 方法守卫
  • 动作
  • after()

依赖注入

守卫是属性,它们通过PHP 反射实例化。在声明守卫时可以指定参数。

#[MinimalCredits(value: 20)]
class MyController extends \Jasny\Controller\Controller
{
    // ...
}

这使得使用依赖注入将服务(如数据库连接)提供给守卫变得困难。

一些 DI 容器库,如PHP-DI,能够向已实例化的对象注入服务。要利用这一点,请重写 Guardian 类并将其注册到容器中。

use Jasny\Controller\Guardian;
use Jasny\Controller\Guard;
use DI\Container;

return [
    Guardian::class => function (Container $container) {
        return new class ($container) extends Guardian {
            public function __construct(private Container $container) {}
            
            public function instantiate(\ReflectionAttribute $attribute): Guard {
                $guard = $attribute->newInstance();
                $this->container->injectOn($guard);
                
                return $guard;
            }
        } 
    }
];

守卫类可以使用 #[Inject] 属性或 @Inject 注解。

use Jasny\Controller\Guard;
use DI\Attribute\Inject;

class MyGuard extends Guard
{
    #[Inject]
    private DBConnection $db;
    
    // ...
}

确保通过依赖注入将 Guardian 服务注入到控制器中。

use Jasny\Controller\Controller;
use Jasny\Controller\Guardian;

class MyController extends Controller
{
    public function __construct(
        protected Guardian $guardian
    ) {}
}