brick / app
网络应用框架
Requires
- php: ^8.0
- brick/di: ~0.4.0
- brick/event: ~0.1.0
- brick/html: ~0.1.0
- brick/http: ~0.3.1
- brick/reflection: ~0.4.0
Requires (Dev)
- ext-pdo: *
- brick/date-time: 0.1.*
- brick/form: 0.1.*
- brick/geo: 0.2.*
- doctrine/orm: 2.*
- php-coveralls/php-coveralls: ^2.4
- phpunit/phpunit: ^9.0
Suggests
- brick/date-time: To use the DateTimeObjectPacker
- brick/form: To use FormView and FormTableView
- brick/geo: To use the GeometryObjectPacker
- doctrine/orm: To use the DoctrineObjectPacker
README
这是一个网络应用框架。
安装
此库可以通过 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
在路由器返回匹配项之后,会发出此事件。如果没有找到匹配项,请求处理将被中断,并发出带有 HttpNotFoundException
的 ExceptionCaughtEvent
。
此事件包含 Request
和 RouteMatch
对象。
ControllerReadyEvent
当控制器准备好被调用时,会发出此事件。如果控制器是一个类方法,类将被实例化,并且此控制器实例将可用于事件。
此事件包含 Request
和 RouteMatch
对象,以及控制器实例(如果控制器是一个类方法)。
NonResponseResultEvent
如果控制器不返回 Response
对象,则发出此事件。此事件为插件提供了将任意控制器结果转换为 Response
对象的机会。例如,它可以用作将控制器返回值 JSON 编码并包装成带有适当 Content-Type 头的 Response
对象。
此事件包含 Request
和 RouteMatch
对象,控制器实例(如果控制器是一个类方法),以及控制器的返回值。
ControllerInvocatedEvent
在控制器调用之后,无论是否抛出异常,都会发出此事件。
此事件包含 Request
和 RouteMatch
对象,以及控制器实例(如果控制器是一个类方法)。
ResponseReceivedEvent
在控制器响应被接收之后,会发出此事件。如果在控制器方法调用期间捕获到 HttpException
,则将其转换为 Response
,并发出此事件。其他异常会中断应用程序流程,不会触发此事件。
此事件包含 Request
、Response
和 RouteMatch
对象,以及控制器实例(如果控制器是一个类方法)。
ExceptionCaughtEvent
如果捕获到异常,则会发出此事件。如果异常不是 HttpException
,则首先将其包装在 HttpInternalServerErrorException
中,以便此事件始终接收一个 HttpException
。创建默认响应以显示异常的详细信息。
此事件为修改默认响应以向客户端显示自定义错误消息提供了机会。
此事件包含HttpException
、Request
和Response
对象。
我们可以看到,在控制器被调用前后立即调用的两个事件是
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'); // ... }