brick/app

网络应用框架

0.8.1 2021-01-19 15:18 UTC

This package is auto-updated.

Last update: 2024-09-02 23:57:15 UTC


README

这是一个网络应用框架。

Build Status Coverage Status Latest Stable Version License

安装

此库可以通过 Composer 安装。

composer require brick/app

要求

此库需要 PHP 8.0 或更高版本。

项目状态 & 发布流程

此库仍在开发中。

当前版本号格式为 0.x.y。当引入非破坏性更改(添加新方法、优化现有代码等)时,y 会增加。

当引入破坏性更改时,总是启动新的 0.x 版本周期。

因此,将您的项目锁定到给定的版本周期(如 0.6.*)是安全的。

如果您需要升级到较新的版本周期,请查看 发布历史,以获取每个后续 0.x.0 版本引入的更改列表。

概述

设置

我们假设您已经使用 Composer 安装了此库。

让我们创建一个包含最简单应用的 index.php 文件

use Brick\App\Application;

require 'vendor/autoload.php';

$application = Application::create();
$application->run();

如果您在浏览器中运行此文件,应该会得到一个包含 HttpNotFoundException 详细信息的 404 页面。这是完全正常的,我们的应用是空的。

在向我们的应用添加更多内容之前,让我们创建一个 .htaccess 文件,告诉 Apache 将所有未针对现有文件的请求重定向到我们的 index.php 文件

RewriteEngine on
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^.*$ /index.php [L]

现在,如果您在浏览器中打开任何路径,您应该会得到类似的异常页面。我们在这里创建的被称为 前端控制器,这是一个非常实用的模式,可以确保所有请求都通过同一个入口进入您的应用。

创建控制器

控制器是一段代码,为传入的 Request 返回一个 Response。这是您放置所有粘合逻辑以与模型协同工作、与数据库交互以及生成 HTML 内容的地方。

控制器可以是任何 callable,但通常是具有对应不同页面或动作的多个方法的类。

让我们创建一个简单的控制器类

namespace MyApp\Controller;

use Brick\Http\Response;

class IndexController
{
    public function indexAction()
    {
        return new Response('This is the index page.');
    }

    public function helloAction()
    {
        return new Response('Hello, world');
    }
}

控制器类不需要扩展任何特定的类,对控制器方法的要求是它必须返回一个 Response 对象。通过使用创建 Response 的插件,甚至可以减轻这个要求。

添加路由

下一步是指导我们的应用为给定的请求调用哪个控制器。将请求映射到控制器的对象称为 Route

应用附带一些路由,覆盖了最常见的用例。如果您有更复杂的要求,可以轻松编写自己的路由。

让我们添加一个将路径目录映射到控制器的开箱即用路由

use Brick\App\Route\SimpleRoute;

$route = new SimpleRoute([
    '/' => MyApp\Controller\IndexController::class,
]);

$application->addRoute($route);

在浏览器中打开 /,您应该会得到索引页面的消息。在浏览器中打开 /hello,您应该会得到 "Hello, world" 消息。

获取请求数据

返回信息很好,但大多数时候您需要先从当前请求中获取数据。获取对 Request 对象的访问权限非常简单,只需将其作为参数添加到方法中即可

public function helloAction(Request $request)
{
    return new Response('Hello, ' . $request->getQuery('name'));
}

现在如果您在浏览器中打开 /hello?name=John,您应该会看到一个 "Hello, John" 消息。

添加插件

应用程序已经可以执行一些有趣的事情,但仍然相当简单。幸运的是,有一个很好的方法可以扩展它,增加额外的功能:插件

应用程序自带一些有用的插件。让我们看看其中一个:RequestParamPlugin。这个插件允许您自动将请求参数映射到您的控制器参数,带有属性。

让我们将这个插件添加到我们的应用程序中

use Brick\App\Plugin\RequestParamPlugin;

$plugin = new RequestParamPlugin();
$application->addPlugin($plugin);

就这样。让我们再次更新我们的 helloAction() 以使用这个新功能

namespace MyApp\Controller;

use Brick\App\Controller\Attribute\QueryParam;
use Brick\Http\Response;

class Index
{
    #[QueryParam('name')]
    public function helloAction(string $name)
    {
        return new Response('Hello, ' . $name);
    }
}

如果您在浏览器中打开 /hello?name=Bob,您应该会看到 "Hello, Bob"。我们不再需要直接与请求对象交互。请求变量现在自动注入到我们的控制器参数中。魔法。

编写自己的插件

您可以使用插件无限扩展应用程序。编写自己的插件很容易,就像我们将通过以下示例看到的那样。让我们想象一下,我们想要创建一个插件,在控制器被调用之前开始一个 PDO 事务,并在控制器返回后自动提交。

首先,让我们看看 Plugin 接口

interface Plugin
{
    public function register(EventDispatcher $dispatcher) : void;
}

只需要实现一个方法。这个方法允许您在应用程序的事件调度器中注册您的插件,即告诉应用程序它想要接收哪些事件。以下是应用程序发出的事件的概述。

IncomingRequestEvent

当应用程序收到请求时,会发出此事件。

此事件包含 Request 对象。

RouteMatchedEvent

在路由器返回匹配项之后,会发出此事件。如果没有找到匹配项,请求处理将被中断,并发出带有 HttpNotFoundExceptionExceptionCaughtEvent

此事件包含 RequestRouteMatch 对象。

ControllerReadyEvent

当控制器准备好被调用时,会发出此事件。如果控制器是一个类方法,类将被实例化,并且此控制器实例将可用于事件。

此事件包含 RequestRouteMatch 对象,以及控制器实例(如果控制器是一个类方法)。

NonResponseResultEvent

如果控制器不返回 Response 对象,则发出此事件。此事件为插件提供了将任意控制器结果转换为 Response 对象的机会。例如,它可以用作将控制器返回值 JSON 编码并包装成带有适当 Content-Type 头的 Response 对象。

此事件包含 RequestRouteMatch 对象,控制器实例(如果控制器是一个类方法),以及控制器的返回值。

ControllerInvocatedEvent

在控制器调用之后,无论是否抛出异常,都会发出此事件。

此事件包含 RequestRouteMatch 对象,以及控制器实例(如果控制器是一个类方法)。

ResponseReceivedEvent

在控制器响应被接收之后,会发出此事件。如果在控制器方法调用期间捕获到 HttpException,则将其转换为 Response,并发出此事件。其他异常会中断应用程序流程,不会触发此事件。

此事件包含 RequestResponseRouteMatch 对象,以及控制器实例(如果控制器是一个类方法)。

ExceptionCaughtEvent

如果捕获到异常,则会发出此事件。如果异常不是 HttpException,则首先将其包装在 HttpInternalServerErrorException 中,以便此事件始终接收一个 HttpException。创建默认响应以显示异常的详细信息。

此事件为修改默认响应以向客户端显示自定义错误消息提供了机会。

此事件包含HttpExceptionRequestResponse对象。

我们可以看到,在控制器被调用前后立即调用的两个事件是

  • ControllerReadyEvent
  • ControllerInvocatedEvent

我们只需要将这些事件映射到执行相应工作的函数上。让我们来做这件事

use Brick\App\Event\ControllerReadyEvent;
use Brick\App\Event\ControllerInvocatedEvent;
use Brick\App\Plugin;
use Brick\Event\EventDispatcher;

class TransactionPlugin implements Plugin
{
    private $pdo;

    public function __construct(\PDO $pdo)
    {
        $this->pdo = $pdo;
    }

    public function register(EventDispatcher $dispatcher) : void
    {
        $dispatcher->addListener(ControllerReadyEvent::class, function() {
            $this->pdo->beginTransaction();
        });

        $dispatcher->addListener(ControllerInvocatedEvent::class, function() {
            $this->pdo->commit();
        });
    }
}

就像做派一样简单!让我们将我们的插件添加到我们的应用程序中

$pdo = new PDO(/* insert parameters to connect to your database */);
$plugin = new TransactionPlugin($pdo);
$application->addPlugin($plugin);

我们刚刚实现了一个插件,该插件在我们的应用程序的所有控制器中都可以使用,而且用时很短。当然,这种实现仍然很简单,但它做到了它所说的,并且是更高级功能的一个好起点。

会话

该框架提供了一个功能强大的PHP原生会话的替代方案,允许同步且非阻塞地对单个会话条目进行读写,并支持可插拔的存储机制。

原生PHP会话有什么问题?

原生会话存储在一个数据块中,并且整个PHP脚本期间会话文件都会被锁定。因此,对单个会话的所有请求都会被序列化:如果同时收到针对同一会话的多个请求,它们将排队并依次处理。这在传统的页面到页面的浏览情况下就足够好了,但在使用AJAX发出可能并发HTTP调用的网页时可能会导致瓶颈。

Brick\App的会话管理器工作方式不同

  • 会话中的每个键值对都独立存储
  • 只有当明确请求时才会加载每个键值对
  • 可以使用has()get()set()remove()来读取或写入每个键值对,而无需锁定
  • 当需要锁定时,可以使用synchronize()方法读取和写入键值对
    $session->synchronize('session-key', function($currentValue) {
        // ...
        return $newValue;
    });

只锁定给定的键,并且函数返回后立即释放锁

安装会话插件

要将会话存储在文件系统中,与传统的PHP会话一起,只需使用

use Brick\App\Session\CookieSession;
use Brick\App\Plugin\SessionPlugin;

$session = new CookieSession();
$app->addPlugin(new SessionPlugin($session));

您可以选择提供一个自定义存储适配器,并使用new CookieSession($storage)代替。已提供文件系统适配器和数据库(PDO)适配器;您也可以通过实现SessionStorage来编写自己的适配器。

使用会话

如果您在应用程序中使用依赖注入,可以轻松地将Session对象传递到您的控制器中。只需在应用程序中注册容器,并指导它解析会话

use Brick\DI\Container;
use Brick\App\Application;
use Brick\App\Session\CookieSession;
use Brick\App\Session\Session;
use Brick\App\Plugin\SessionPlugin;

// Create a DI container, and use it with our app
$container = Container::create();
$app = Application::create($container);

// Create a session, add the session plugin to our app
$session = new CookieSession();
$app->addPlugin(new SessionPlugin($session));

// Instruct the DI container to resolve the Session object 
$container->set(Session::class, $session);

现在应用程序可以在您的控制器函数中自动解析您的会话

public function indexAction(Request $request, Session $session)
{
    $userId = $session->get('user-id');
    // ...
}