tobento/app-http

应用 http 支持。


README

应用 http,路由,中间件和会话支持。

目录

入门

使用此命令添加运行此应用 http 项目的最新版本。

composer require tobento/app-http

要求

  • PHP 8.0 或更高版本

文档

应用

如果您正在使用骨架,请查看 App 骨架

您还可以查看 App 以了解更多关于应用的一般信息。

Http 引导

http 引导执行以下操作

  • PSR-7 实现
  • PSR-17 实现
  • 安装和加载 http 配置文件
  • 基本和当前 URI 实现
  • http 错误处理实现
  • 发出响应
use Tobento\App\AppFactory;

// Create the app
$app = (new AppFactory())->createApp();

// Adding boots
$app->boot(\Tobento\App\Http\Boot\Http::class);

// Run the app
$app->run();

Http 配置

查看 app/config/http.php 以更改所需值。

请求和响应

您可以通过应用访问 PSR-7 和 PSR-17 接口

use Tobento\App\AppFactory;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ResponseFactoryInterface;
use Psr\Http\Message\StreamFactoryInterface;
use Psr\Http\Message\UploadedFileFactoryInterface;
use Psr\Http\Message\UriFactoryInterface;
use Psr\Http\Message\UriInterface;
use Tobento\Service\Uri\BaseUriInterface;
use Tobento\Service\Uri\CurrentUriInterface;
use Tobento\Service\Uri\PreviousUriInterface;

// Create the app
$app = (new AppFactory())->createApp();

// Adding boots
$app->boot(\Tobento\App\Http\Boot\Http::class);
$app->booting();

// PSR-7
$request = $app->get(ServerRequestInterface::class);
$response = $app->get(ResponseInterface::class);

// returns UriInterface
$baseUri = $app->get(BaseUriInterface::class);
$currentUri = $app->get(CurrentUriInterface::class);
$previousUri = $app->get(PreviousUriInterface::class);
// Session Boot is needed, otherwise it is always same as base uri.

// PSR-17
$responseFactory = $app->get(ResponseFactoryInterface::class);
$streamFactory = $app->get(StreamFactoryInterface::class);
$uploadedFileFactory = $app->get(UploadedFileFactoryInterface::class);
$uriFactory = $app->get(UriFactoryInterface::class);

// Run the app
$app->run();

查看 Uri 服务 了解更多关于基本和当前 URI 的信息。

交换 PSR-7 和 PSR-17 实现

您可以将 PSR-7 和 PSR-17 实现交换为任何替代方案。
查看 App - 定制 了解更多信息。

请求者和响应者引导

请求者和响应者引导执行以下操作

use Tobento\App\AppFactory;
use Tobento\Service\Requester\RequesterInterface;
use Tobento\Service\Responser\ResponserInterface;

// Create the app
$app = (new AppFactory())->createApp();

// You may add the session boot to enable
// flash messages and flash input data.
$app->boot(\Tobento\App\Http\Boot\Session::class);

$app->boot(\Tobento\App\Http\Boot\RequesterResponser::class);
$app->booting();

$requester = $app->get(RequesterInterface::class);
$responser = $app->get(ResponserInterface);

// Run the app
$app->run();

查看 Requester 服务 了解更多信息。

查看 Responser 服务 了解更多信息。

响应者消息

消息 将在您安装了 App 消息 - 翻译消息 时被翻译。

添加的中间件

中间件引导

中间件引导执行以下操作

  • PSR-15 HTTP 处理器(中间件)实现
  • 分配中间件
use Tobento\App\AppFactory;

// Create the app
$app = (new AppFactory())->createApp();

// Adding boots
$app->boot(\Tobento\App\Http\Boot\Middleware::class);
$app->booting();

// add middleware aliases using app macro:
$app->middlewareAliases([
    'alias' => FooMiddleware::class,
]);

// add middleware group using app macro:
$app->middlewareGroup(name: 'api', middlewares: [
    Middleware::class,
]);

// add middleware using app macro:
$app->middleware(BarMiddleware::class);

// Run the app
$app->run();

查看 中间件服务 了解更多关于中间件实现的信息。

通过配置添加中间件

您可以在配置文件 app/config/middleware.php 中配置中间件,这些中间件将应用于所有路由和请求

return [
    // ...
    'middlewares' => [
        // priority => middleware
        
        // via fully qualified class name:
        8000 => \Tobento\App\Http\Middleware\SecurePolicyHeaders::class,
        
        // with build-in parameters:
        7900 => [AnotherMiddleware::class, 'name' => 'Sam'],
        
        // by alias:
        7800 => 'aliasedMiddleware',
        
        // by group name:
        7800 => 'groupedMiddlewares',
        
        // by class instance:
        7700 => new SomeMiddleware(),
    ],
];

通过引导添加中间件

您可能创建一个引导来添加中间件

use Tobento\App\Boot;
use Tobento\App\Http\Boot\Middleware;

class MyMiddlewareBoot extends Boot
{
    public const BOOT = [
        // you may ensure the middleware boot.
        Middleware::class,
    ];
    
    public function boot(Middleware $middleware)
    {
        $middleware->add(MyMiddleware::class);
    }
}

中间件别名

use Tobento\App\Boot;
use Tobento\App\Http\Boot\Middleware;

class MyMiddlewareBoot extends Boot
{
    public const BOOT = [
        // you may ensure the middleware boot.
        Middleware::class,
    ];
    
    public function boot(Middleware $middleware)
    {
        $middleware->addAliases([
            'alias' => MyMiddleware::class,
        ]);
        
        // add by alias:
        $middleware->add('alias');
    }
}

通过配置添加别名

您可以在配置文件 app/config/middleware.php 中配置中间件别名。

return [
    // ...
    'aliases' => [
        'alias' => MyMiddleware::class,
    ],
];

中间件组

use Tobento\App\Boot;
use Tobento\App\Http\Boot\Middleware;

class MyMiddlewareBoot extends Boot
{
    public const BOOT = [
        // you may ensure the middleware boot.
        Middleware::class,
    ];
    
    public function boot(Middleware $middleware)
    {
        $middleware->addGroup(name: 'api', middlewares: [
            Middleware::class,
            // with build-in parameters:
            [AnotherMiddleware::class, 'name' => 'Sam'],
            // by alias:
            'aliasedMiddleware',
            // by class instance:
            new SomeMiddleware(),
        ]);
        
        // add by group:
        $middleware->add('api');
    }
}

通过配置添加分组

您可以在配置文件 app/config/middleware.php 中配置中间件分组。

return [
    // ...
    'groups' => [
        'name' => [
            SomeMiddleware::class,
        ],
    ],
];

可用中间件

前一个 URI 会话中间件

Tobento\App\Http\Middleware\PreviousUriSession::class 中间件由 会话启动 自动添加,用于在会话中存储uri历史记录。

获取上一个Uri

$previousUri = $app->get(PreviousUriInterface::class);

从上一个Uri历史记录中排除

您可以通过在响应中添加 X-Exclude-Previous-Uri 标头来从历史记录中排除某个uri。

$response = $response->withHeader('X-Exclude-Prev-Url', '1');

安全策略头中间件

此中间件将向响应添加以下安全策略标头

  • Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
  • X-Frame-Options: DENY
  • X-Content-Type-Options: nosniff
  • Referrer-Policy: same-origin
  • Content-Security-Policy: base-uri 'none'; default-src 'self'; script-src 'nonce-***' 'self'; object-src 'none'; style-src 'nonce-***' 'self';

app/config/middleware.php 文件中

'middlewares' => [
    8000 => \Tobento\App\Http\Middleware\SecurePolicyHeaders::class,
],

使用内联脚本和样式

中间件将为 script-srcstyle-src 添加带有nonce的 Content-Security-Policy 标头。因此,要允许内联脚本或样式,您必须在html中添加nonce。

如果您使用的是 App View 包,您可以从视图中检索nonce。

<style nonce="<?= $view->esc($view->get('cspNonce', '')) ?>">
    ...
</style>

<script nonce="<?= $view->esc($view->get('cspNonce', '')) ?>">
    ...
</script>

路由引导

路由启动执行以下操作

  • 启动http和中间件启动
  • RouterInterface 实现
  • 添加路由宏
  • 为路由异常添加http错误处理器
use Tobento\App\AppFactory;
use Tobento\Service\Routing\RouterInterface;

// Create the app
$app = (new AppFactory())->createApp();

// Adding boots
$app->boot(\Tobento\App\Http\Boot\Routing::class);
$app->booting();

// using interface:
$router = $app->get(RouterInterface::class);
$router->get('blog', function() {
    return ['page' => 'blog'];
});

// using macros:
$app->route('GET', 'foo', function() {
    return ['page' => 'foo'];
});

// Run the app
$app->run();

查看 路由服务 了解更多关于路由的信息。

通过引导进行路由

您可能创建一个用于定义路由的启动器

use Tobento\App\Boot;
use Tobento\Service\Routing\RouterInterface;
use Tobento\Service\Routing\RouteGroupInterface;

class RoutesBoot extends Boot
{
    public const BOOT = [
        // you may ensure the routing boot.
        \Tobento\App\Http\Boot\Routing::class,
    ];
    
    public function boot(RouterInterface $router)
    {
        // Add routes on the router
        $router->get('blog', [Controller::class, 'method']);
        
        // Add routes with the provided app macros
        $this->app->route('GET', 'blog', [Controller::class, 'method']);
        
        $this->app->routeGroup('admin', function(RouteGroupInterface $group) {

            $group->get('blog/{id}', function($id) {
                // do something
            });
        });
        
        $this->app->routeResource('products', ProductsController::class);
        
        $this->app->routeMatched('blog.edit', function() {
            // do something after the route has been matched.
        });
        
        $url = $this->app->routeUrl('blog.edit', ['id' => 5])->get();
    }
}

然后,将您的路由启动器添加到应用程序中

use Tobento\App\AppFactory;

// Create the app
$app = (new AppFactory())->createApp();

// Adding boots
$app->boot(RoutesBoot::class);

// Run the app
$app->run();

域名路由

您可以在 app/config/http.php 文件中指定路由的域名。

查看 路由服务 - 域名路由 部分以了解更多关于域名路由的信息。

路由处理器

您可以为路由处理添加路由处理程序。

以下是添加处理程序的一些好处

  • 您可以对响应进行缓存
  • 在特定请求中,您可能抛出异常,稍后由错误处理器捕获(例如验证)
  • 等等

首先,创建一个路由处理程序

use Tobento\App\AppInterface;
use Tobento\Service\Routing\RouteInterface;
use Tobento\Service\Routing\RouteHandlerInterface;
use Tobento\App\Http\Routing\DeclaredHandlerParameters;
use Tobento\App\Http\Routing\ArgumentsHandlerParameters;
use Psr\Http\Message\ServerRequestInterface;

final class CustomRouteHandler implements RouteHandlerInterface
{
    public function __construct(
        private AppInterface $app
    ) {}
    
    /**
     * Handles the route.
     *
     * @param RouteInterface $route
     * @param null|ServerRequestInterface $request
     * @return mixed The return value of the handler called.
     */
    public function handle(RouteInterface $route, null|ServerRequestInterface $request = null): mixed
    {
        // You may interfere with the arguments for the route handler to be called:
        $arguments = $route->getParameter('_arguments');
        
        var_dump($arguments instanceof ArgumentsHandlerParameters);
        // bool(true)
        
        // You may use the declared route handler parameters:
        $declared = $route->getParameter('_declared');
        
        var_dump($declared instanceof DeclaredHandlerParameters);
        // bool(true)
        
        // Let further handlers handle it:
        return [$route, $request];
        
        // Or prevent further handlers from being called
        // and returning the route handler result:
        $result = $this->app->call($route->getHandler(), $arguments->getParameters());
        
        return [$route, $request, $result];
    }
}

最后,添加路由处理程序

use Tobento\App\Http\Routing\RouteHandlerInterface;

// Create the app
$app = (new AppFactory())->createApp();

// Adding boots:
$app->boot(\Tobento\App\Http\Boot\Routing::class);

// Add route handler using the app on method:
$app->on(RouteHandlerInterface::class, function(RouteHandlerInterface $handler) {
    $handler->addHandler(CustomRouteHandler::class);
})->priority(1500);

// Run the app
$app->run();

唯一添加的处理程序是具有 1000 优先级的 Tobento\App\Http\Routing\RequestRouteHandler::class

路由列表命令

如果您已安装 App Console,您可以通过提供应用程序定义的所有路由的概览来运行 route:list 命令。

php ap route:list

仅显示具有附加信息的特定名称的特定路由

php ap route:list --name=blog.show

会话引导

会话启动执行以下操作

use Tobento\App\AppFactory;
use Tobento\Service\Session\SessionInterface;
use Psr\Http\Message\ServerRequestInterface;

// Create the app
$app = (new AppFactory())->createApp();

// Adding boots
$app->boot(\Tobento\App\Http\Boot\Middleware::class);
$app->boot(\Tobento\App\Http\Boot\Routing::class);
$app->boot(\Tobento\App\Http\Boot\Session::class);
$app->booting();

$app->route('GET', 'foo', function(SessionInterface $session) {
    $session->set('key', 'value');
    return ['page' => 'foo'];
});

// or you may get the session from the request attributes:
$app->route('GET', 'bar', function(ServerRequestInterface $request) {
    $session = $request->getAttribute(SessionInterface::class);
    $session->set('key', 'value');
    return ['page' => 'bar'];
});

// Run the app
$app->run();

查看 会话服务 了解更多关于会话的一般信息。

会话配置

查看 app/config/session.php 修改所需的值。

会话生命周期

会话由会话中间件启动和保存,通过会话中间件与会话数据的交互之后可用。

会话错误处理

您可以为会话中间件引起的异常添加错误处理器。

use Tobento\App\Boot;
use Tobento\App\Http\HttpErrorHandlersInterface;
use Tobento\Service\Session\SessionStartException;
use Tobento\Service\Session\SessionExpiredException;
use Tobento\Service\Session\SessionValidationException;
use Tobento\Service\Session\SessionSaveException;
use Throwable;

class HttpErrorHandlerBoot extends Boot
{
    public const BOOT = [
        // you may ensure the http boot.
        \Tobento\App\Http\Boot\Http::class,
    ];
    
    public function boot()
    {
        $this->app->on(HttpErrorHandlersInterface::class, function(HttpErrorHandlersInterface $handlers) {

            $handlers->add(function(Throwable $t) {
                
                if ($t instanceof SessionStartException) {
                    // You may do something if starting session fails.
                } elseif ($t instanceof SessionExpiredException) {
                    // This is already handled by the session middleware,
                    // so you might check it out.
                } elseif ($t instanceof SessionValidationException) {
                    // You may do something if session validation fails. 
                } elseif ($t instanceof SessionSaveException) {
                    // You may do something if saving session fails. 
                }
                
                return $t;
            })->priority(2000); // you might add a priority.
        });
    }
}

您可以使用 错误处理器 - 处理其他异常 替代处理这些异常。

查看 可抛出处理器 了解更多关于处理器的一般信息。

Cookie 引导

cookies启动执行以下操作

  • 基于cookies配置文件实现 cookie接口
  • 根据cookies配置文件添加中间件
use Tobento\App\AppFactory;
use Tobento\Service\Cookie\CookiesFactoryInterface;
use Tobento\Service\Cookie\CookieFactoryInterface;
use Tobento\Service\Cookie\CookieValuesFactoryInterface;
use Tobento\Service\Cookie\CookiesProcessorInterface;

// Create the app
$app = (new AppFactory())->createApp();

// Adding boots
$app->boot(\Tobento\App\Http\Boot\Cookies::class);
$app->booting();

// The following interfaces are available after booting:
$cookiesFactory = $app->get(CookiesFactoryInterface::class);
$cookieFactory = $app->get(CookieFactoryInterface::class);
$cookieValuesFactory = $app->get(CookieValuesFactoryInterface::class);
$cookiesProcessor = $app->get(CookiesProcessorInterface::class);

// Run the app
$app->run();

查看Cookie 服务以了解更多信息。

Cookie 配置

查看app/config/cookies.php以更改所需的值。

Cookie 使用

读取和写入 cookies

use Tobento\App\AppFactory;
use Tobento\Service\Cookie\CookieValuesInterface;
use Tobento\Service\Cookie\CookiesInterface;
use Psr\Http\Message\ServerRequestInterface;

// Create the app
$app = (new AppFactory())->createApp();

// Adding boots
$app->boot(\Tobento\App\Http\Boot\Routing::class);
$app->boot(\Tobento\App\Http\Boot\Cookies::class);
$app->booting();

$app->route('GET', 'bar', function(ServerRequestInterface $request) {

    // read cookies:
    $cookieValues = $request->getAttribute(CookieValuesInterface::class);
    
    $value = $cookieValues->get('foo');
    
    // or
    var_dump($request->getCookieParams());
    
    // write cookies:
    $cookies = $request->getAttribute(CookiesInterface::class);
    
    $cookies->add('name', 'value');
    
    return ['page' => 'bar'];
});

// Run the app
$app->run();

查看Cookie 值以了解更多信息。

查看Cookies以了解更多信息。

Cookie 加密

首先安装 app-encryption 包

composer require tobento/app-encryption

然后,如果您想加密和解密所有 cookies 值,只需启动 Encryption::class。这就完成了。

// ...

$app->boot(\Tobento\App\Encryption\Boot\Encryption::class);
$app->boot(\Tobento\App\Http\Boot\Cookies::class);

// ...

白名单 cookie

要在启动后启用白名单(禁用加密)cookie,请使用 CookiesProcessorInterface::class

use Tobento\Service\Cookie\CookiesProcessorInterface;

$cookiesProcessor = $app->get(CookiesProcessorInterface::class);

$cookiesProcessor->whitelistCookie(name: 'name');

// or
$cookiesProcessor->whitelistCookie(name: 'name[foo]');
$cookiesProcessor->whitelistCookie(name: 'name[bar]');

配置

加密和解密是通过在 app/config/cookies.php 文件中指定的中间件处理实现的 CookiesProcessor::class 来完成的。

use Tobento\Service\Cookie;
use Tobento\Service\Encryption;
use Psr\Container\ContainerInterface;

return [

    'middlewares' => [
        Cookie\Middleware\Cookies::class,
    ],

    'interfaces' => [

        //...

        Cookie\CookiesProcessorInterface::class => Cookie\CookiesProcessor::class,

        // or you may use a specified encrypter only for cookies:
        Cookie\CookiesProcessorInterface::class => static function(ContainerInterface $c): Cookie\CookiesProcessorInterface {

            $encrypter = null;

            if (
                $c->has(Encryption\EncryptersInterface::class)
                && $c->get(Encryption\EncryptersInterface::class)->has('cookies')
            ) {
                $encrypter = $c->get(Encryption\EncryptersInterface::class)->get('cookies');
            }

            return new Cookie\CookiesProcessor(
                encrypter: $encrypter,
                whitelistedCookies: [],
            );
        },
    ],

    //...
];

您可以查看App 加密以了解更多信息。

错误处理器引导

默认情况下,错误处理器将以 json 或纯文本格式渲染异常。如果您想渲染支持 html 和 xml 格式的异常视图,请查看渲染异常视图部分。

// ...
$app->boot(\Tobento\App\Http\Boot\ErrorHandler::class);
// ...

它处理以下异常以及Http 异常

Http 异常

您可以从控制器和中间件抛出几个 HTTP 异常,这些异常将由默认错误处理器处理。

示例

use Tobento\App\Http\Exception\HttpException;
use Tobento\App\Http\Exception\NotFoundException;

class SomeController
{
    public function index()
    {
        throw new HttpException(statusCode: 404);
        
        // or:
        throw new NotFoundException();
    }
}

渲染异常视图

为了以 html 或 xml 格式渲染异常,必须在应用中提供 ViewInterface::class。您可以安装App View 包,或者仅实现 ViewInterface::class

composer require tobento/service-view
use Tobento\Service\View;
use Tobento\Service\Dir\Dirs;
use Tobento\Service\Dir\Dir;

// ...
$app->set(View\ViewInterface::class, function() {
    return new View\View(
        new View\PhpRenderer(
            new Dirs(
                new Dir('home/private/views/'),
            )
        ),
        new View\Data(),
        new View\Assets('home/public/src/', 'https://www.example.com/src/')
    );
});
// ...

如果存在,它将渲染以下视图

处理其他异常

您可以通过扩展错误处理器来处理其他异常

如果您已安装App Translation并使用*作为资源名称,则消息将被翻译。查看翻译资源翻译文件资源以了解更多信息。

use Tobento\App\Http\Boot\ErrorHandler;
use Tobento\Service\Requester\RequesterInterface;
use Tobento\Service\Responser\ResponserInterface;
use Psr\Http\Message\ResponseInterface;
use Throwable;

class CustomErrorHandler extends ErrorHandler
{
    public function handleThrowable(Throwable $t): Throwable|ResponseInterface
    {
        $requester = $this->app->get(RequesterInterface::class);
        
        if ($t instanceof SomeException) {
            return $requester->wantsJson()
                ? $this->renderJson(code: 404)
                : $this->renderView(code: 404);
            
            // or with custom message:
            return $requester->wantsJson()
                ? $this->renderJson(code: 404, message: 'Custom')
                : $this->renderView(code: 404, message: 'Custom');
                
            // or with custom message and message parameters:
            return $requester->wantsJson()
                ? $this->renderJson(code: 404, message: 'Custom :value', parameters: [':value' => 'foo'])
                : $this->renderView(code: 404, message: 'Custom :value', parameters: [':value' => 'foo']);   
        }
        
        // using the responser:
        if ($t instanceof SomeOtherException) {
            $responser = $this->app->get(ResponserInterface::class);
            
            return $responser->json(
                data: ['key' => 'value'],
                code: 200,
            );
        }        
        
        return parent::handleThrowable($t);
    }
}

然后启动您自定义的错误处理器而不是默认的错误处理器

// ...
$app->boot(CustomErrorHandler::class);
// ...

优先错误处理器

您可以创建一个错误处理器并使用 HANDLER_PRIORITY 常量来定义优先级。

默认优先级为 1500,优先级越高,处理越早。

use Tobento\App\Http\Boot\ErrorHandler;
use Tobento\Service\Requester\RequesterInterface;
use Tobento\Service\Responser\ResponserInterface;
use Psr\Http\Message\ResponseInterface;
use Throwable;

class PrioritizedErrorHandler extends ErrorHandler
{
    protected const HANDLER_PRIORITY = 5000;
    
    public function handleThrowable(Throwable $t): Throwable|ResponseInterface
    {
        $requester = $this->app->get(RequesterInterface::class);
        
        if ($t instanceof SomeException) {
            return $requester->wantsJson()
                ? $this->renderJson(code: 404)
                : $this->renderView(code: 404);
        }
        
        // using the responser:
        if ($t instanceof SomeOtherException) {
            $responser = $this->app->get(ResponserInterface::class);
            
            return $responser->json(
                data: ['key' => 'value'],
                code: 200,
            );
        }        
        
        // return throwable to let other handler handle it:
        return $t;
    }
}

然后启动您错误处理器

// ...
$app->boot(PrioritizedErrorHandler::class);

$app->boot(DefaultErrorHandler::class);

// you could boot it after the default,
// it gets called first if priority is higher as default:
$app->boot(PrioritizedErrorHandler::class);
// ...

致谢