在 Swoole 上运行 Symfony

在 Swoole 上运行 Symfony

Chris Yue No Comment
Posts

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.0PHP 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.

微信赞赏码

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

发表评论

+ 39 = 41