Swoole 的官方网站定义其为『PHP的异步、并行、高性能网络通信引擎』,而对于类似 Symfony 以及 laravel 等每次请求都需要初始化大量代码的巨型框架来说,swoole 可以让初始化框架代码的过程限定在启动服务器时,而非每次请求都初始化,从而极大提升框架的运行效能。关于 swoole 以及类似的 ReactPHP 库让框架运行效率提升多少倍的话题,网上已经有太多,这里不多说。本篇只说目前网上讨论还比较少的话题:如何将 Symfony 跑在 Swoole 上。
前段时间创建了基于 Symfony Flex 的示例项目,这次就基于此项目,让其跑在 swoole 上。
Swoole 官网上提供的代码,已经可以看出 swoole http server 的基本用法:
$serv = new Swoole\Http\Server("127.0.0.1", 9502);
$serv->on('Request', function($request, $response) {
var_dump($request->get);
var_dump($request->post);
var_dump($request->cookie);
var_dump($request->files);
var_dump($request->header);
var_dump($request->server);
$response->cookie("User", "Swoole");
$response->header("X-Server", "Swoole");
$response->end("<h1>Hello Swoole!</h1>");
});
$serv->start();
通过对 swoole http server 的事件监听器中的 $request
和 $response
变量,我们可以获取各种请求信息和发出各种响应信息,是不是有 Node 那味儿了?
对比 Symfony 的前端控制器代码 web/index.php
<?php
...
$kernel = new Kernel(getenv('APP_ENV'), getenv('APP_DEBUG'));
$request = Request::createFromGlobals();
$response = $kernel->handle($request);
$response->send();
$kernel->terminate($request, $response);
其实也是将 $request
通过框架内核的 $kernel->handle
方法,转变成 $response
对象并发送给客户端。
但因 swoole 提供的 $request
对象接口跟 Symfony 的 $request
并不一致,Symfony 的框架内核与 swoole 的 $request
接口无法兼容,此时只能考虑将 swoole 的 $request
对象转换成 Symfony 的请求对象。
不过还好,转换工作不是那么的复杂,我们通过定义转化方法就能解决这个问题:
function toSfRequest($swRequest) {
$query = $swRequest->get ?? [];
$request = $swRequest->post ?? [];
$cookie = $swRequest->cookie ?? [];
$files = $swRequest->files ?? [];
$content = $swRequest->rawContent() ?: null;
$server = array_change_key_case($swRequest->server, CASE_UPPER);
foreach ($swRequest->header as $key => $val) {
$server[sprintf('HTTP_%s', strtoupper(str_replace('-', '_', $key)))] = $val;
}
return new Request($query, $request, [], $cookie, $files, $server);
}
为了写起来方便,这里使用了 PHP7 新的『语法糖』 $foo ?? $bar
,等价于 isset($foo) ? $foo : $bar
,关于更多 PHP7 新语法,可见我之前关于 PHP 7.0 和 PHP 7.1 的文章。
与请求对象的情况类似,响应的用法也并不一样。我们也需要通过稍稍修改,让 Symfony 的响应对象,为 swoole 的响应对象提供数据:
$swResponse->status($sfResponse->getStatusCode());
foreach ($sfResponse->headers->allPreserveCase() as $key => $vals) {
foreach ($vals as $val) {
$swResponse->header($key, $val);
}
}
$swResponse->end($sfResponse->getContent());
因为 swoole 完全成了一个 http server,有一些静态文件还是需要处理,原理跟 nginx 类似,如果按照请求的路径能找到本地文件,那么就把本地文件内容原封不动发出去,当然,还要给头信息里配上合适的 mime_type:
$static = __DIR__.$swRequest->server['path_info'];
if (file_exists($static)) {
$ext = pathinfo($static, PATHINFO_EXTENSION);
$swResponse->header('Content-Type', sprintf('text/%s', $ext));
$swResponse->end(file_get_contents($static));
}
此代码略显简陋,判断 mime_type 最好是使用 mime_content_type
方法。不过用此方法需要对 PHP 进行一些配置,这里懒得说了。另外还有一些第三方库也是可以专门用来判断 mime_type 的,但跟这次主题不符也不多说了。
简单修改后的完整代码如下,注意初始化 Symfony 内核的那段代码一定要放在 swoole 事件处理代码的上面,否则使用 swoole 就没多大意义了
<?php
use App\Kernel;
use Symfony\Component\Dotenv\Dotenv;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Debug\Debug;
use Swoole\Http\Server;
require __DIR__.'/../vendor/autoload.php';
// The check is to ensure we don't use .env in production
if (!getenv('APP_ENV')) {
(new Dotenv())->load(__DIR__.'/../.env');
}
if (getenv('APP_DEBUG')) {
// WARNING: You should setup permissions the proper way!
// REMOVE the following PHP line and read
// https://symfony.com/doc/current/book/installation.html#checking-symfony-application-configuration-and-setup
umask(0000);
Debug::enable();
}
// 创建 HTTP 服务前就将内核初始化好
$kernel = new Kernel(getenv('APP_ENV'), getenv('APP_DEBUG'));
// swoole server
$serv = new Server("localhost", 8000);
$serv->on('Request', function($swRequest, $swResponse) use ($kernel) {
// 每次请求都只会执行这里面的代码,不用再初始化框架内核,运行性能大大提高!
$static = __DIR__.$swRequest->server['path_info'];
if (file_exists($static)) {
$ext = pathinfo($static, PATHINFO_EXTENSION);
$swResponse->header('Content-Type', sprintf('text/%s', $ext));
$swResponse->end(file_get_contents($static));
return;
}
$sfRequest = toSfRequest($swRequest);
$sfResponse = $kernel->handle($sfRequest);
// Request::setTrustedProxies(['0.0.0.0/0'], Request::HEADER_FORWARDED);
$swResponse->status($sfResponse->getStatusCode());
foreach ($sfResponse->headers->allPreserveCase() as $key => $vals) {
foreach ($vals as $val) {
$swResponse->header($key, $val);
}
}
$swResponse->end($sfResponse->getContent());
$kernel->terminate($sfRequest, $sfResponse);
});
$serv->start();
function toSfRequest($swRequest) {
$query = $swRequest->get ?? [];
$request = $swRequest->post ?? [];
$cookie = $swRequest->cookie ?? [];
$files = $swRequest->files ?? [];
$content = $swRequest->rawContent() ?: null;
$server = array_change_key_case($swRequest->server, CASE_UPPER);
foreach ($swRequest->header as $key => $val) {
$server[sprintf('HTTP_%s', strtoupper(str_replace('-', '_', $key)))] = $val;
}
return new Request($query, $request, [], $cookie, $files, $server);
}
将此代码保存为 web/server.php
,然后运行
php web/server.php
然后打开浏览器访问 http://localhost:8000/admin
,可以发现 Symfony Flex 的演示项目在 swoole 上也能跑了。
为什么说修改只是『简单的修改』,是因为类似对 session 的处理,还没有涉及到。swoole 是一个看上去挺有前途的项目,但目前是否可以上生产使用还是一个问题,因为它跟传统的 PHP 网站开发的方式还有很多不同,还不知到底有多少坑。
另外我特别想吐槽一点,swoole 的作者似乎对标准化并没有放在心上,导致 swoole http server 要用在别的框架上还比较困难。其实 PSR7 标准已经出来很久了,Symfony 也发布了 Symfony Request/Response 和 PSR7 标准之间相互转化的 bridge,swoole 上也提了相关的 issue 但也是很长时间并没有得到推进。如果 swoole http server 能拥抱 PSR7 标准,我想它将在 PHP 世界里更有市场。
在 Swoole 上运行 Symfony by Chris Yue is licensed under a Creative Commons Attribution-NonCommercial-NoDerivatives 4.0 International License.

写作累,服务器还越来越贵
求分担,祝愿好人一生平安
天使打赏人