不用三方包 給 Laravel 開啓 Swoole

Swoole 是一款優秀的 PHP 擴展,利用其可以實現原生 PHP 很難做到的常駐服務和異步。正好我有個 Laravel 項目可以折騰,就研究了下。

Laravel 項目是基於 composer 的,所以我先帖下我的 composer.json 中的 require 聲明:

我的官方羣點擊此處

{
    "require": {
        "php": "^7.1.3",
        "cybercog/laravel-love": "^5.1",
        "dingo/api": "~v2.0.0-alpha2",
        "doctrine/dbal": "^2.8",
        "fideloper/proxy": "^4.0",
        "guzzlehttp/guzzle": "^6.3",
        "infyomlabs/adminlte-templates": "5.6.x-dev",
        "infyomlabs/laravel-generator": "5.6.x-dev",
        "jeroennoten/laravel-adminlte": "^1.23",
        "laravel/framework": "5.6.*",
        "laravel/tinker": "^1.0",
        "laravelcollective/html": "^5.6.0",
        "lshorz/luocaptcha": "^1.0",
        "overtrue/laravel-lang": "v3.0.08",
        "overtrue/laravel-wechat": "^4.0",
        "predis/predis": "^1.1",
        "spatie/laravel-permission": "^2.17",
        "tymon/jwt-auth": "~1.0.0-rc.2",
        "yajra/laravel-datatables-buttons": "^4.0",
        "yajra/laravel-datatables-oracle": "^8.7"
    }
}

如果我們要開啓 swoole,我們可選的包有這些:

但一般來說,項目中需要常駐容器的服務與每次均需重新構建的服務並不一樣,所以我才劍走偏鋒。

起步

我們需要將 public/index.php 替換成如下

<?php

use Illuminate\Http\Request;
use Illuminate\Http\Response;

define('LARAVEL_START', microtime(true));
require __DIR__ . '/../vendor/autoload.php';
$app = require_once __DIR__ . '/../bootstrap/app.php';

class Laravel
{
    /**
     * Illuminate\Foundation\Application
     *
     * @var \Illuminate\Foundation\Application
     */
    public $app;

    /**
     * App\Http\Kernel
     *
     * @var \App\Http\Kernel
     */
    public $kernel;

    /**
     * App\Http\Requests\Request
     *
     * @var \App\Http\Requests\Request
     */
    public $request;

    /**
     * Illuminate\Http\JsonResponse
     *
     * @var \Illuminate\Http\JsonResponse
     */
    public $response;

    /**
     * 構造
     *
     * @param \Illuminate\Foundation\Application $app
     */
    public function __construct(\Illuminate\Foundation\Application $app)
    {
        $this->app = $app;
    }

    /**
     * RUN
     *
     * @return void
     */
    public function run()
    {
        \Swoole\Runtime::enableCoroutine(true);

        $http = new swoole_http_server('127.0.0.1', '80');

        $http->set([
            'document_root' => public_path('/'),
            'enable_static_handler' => true,
        ]);

        $http->on('request', function ($req, $res) {
            try {
                $kernel = $this->app->make(Illuminate\Contracts\Http\Kernel::class);

                $get = $req->get ?? [];
                $post = $req->post ?? [];
                $input = array_merge($get, $post);
                $cookie = $req->cookie ?? [];
                $files = $req->files ?? [];
                $server = $req->server ?? [];

                $request = Request::create($req->server['request_uri'], $req->server['request_method'], $input, $cookie, $files, $server);

                if (isset($req->header['x-requested-with']) && $req->header['x-requested-with'] == "XMLHttpRequest") {
                    $request->headers->set('X-Requested-With', "XMLHttpRequest", true);
                }
                if (isset($req->header['accept']) && $req->header['accept']) {
                    $request->headers->set('Accept', $req->header['accept'], true);
                }

                $response = $kernel->handle($request);

                $res->status($response->getStatusCode());

                foreach ($response->headers->allPreserveCaseWithoutCookies() as $name => $values) {
                    foreach ($values as $value) {
                        $res->header($name, $value, false);
                    }
                }

                foreach ($response->headers->getCookies() as $cookie) {
                    $res->header('Set-Cookie', $cookie->getName() . strstr($cookie, '='), false);
                }
                dump(time());

                $res->end($response->getContent());
                $this->app->forgetInstance('request');
            } catch (\throwable $e) {
                echo $e->getMessage();
                echo PHP_EOL;
                echo $e->getFile();
                echo PHP_EOL;
                echo $e->getLine();
                echo PHP_EOL;
            }
        });
        $http->start();
    }
}

(new Laravel($app))->run();

運行時發現大多數頁面均沒有問題,只有幾個用了 infyomlabs/laravel-generator 產生的列表頁,AJAX 拉取 JSON 時卻返回了 HTML。

排查

在有問題頁面的 controller 代碼中,找到如下

  /**
     * Display a listing of the Star.
     *
     * @param StarDataTable $starDataTable
     * @return Response
     */
    public function index(StarDataTable $starDataTable)
    {
        return $starDataTable->render('stars.index');
    }

定位 StarDataTable::render() 到了

  /**
     * Process dataTables needed render output.
     *
     * @param string $view
     * @param array $data
     * @param array $mergeData
     * @return mixed
     */
    public function render($view, $data = [], $mergeData = [])
    {
        if ($this->request()->ajax() && $this->request()->wantsJson()) {
            return app()->call([$this, 'ajax']);
        }
        ...
    }

這是判斷 $this->request() 是不是 XHR 請求,且 Accept 請求頭聲明瞭 application/json

而 $this->request() 實現如下

    /**
     * Get DataTables Request instance.
     *
     * @return \Yajra\DataTables\Utilities\Request
     */
    public function request()
    {
        return $this->request ?: $this->request = resolve('datatables.request');
    }

不難看出,如果第一次構建,會走到

$this->request = resolve('datatables.request');

而 resolve 的實現是啥?

if (! function_exists('resolve')) {
    /**
     * Resolve a service from the container.
     *
     * @param  string  $name
     * @return mixed
     */
    function resolve($name)
    {
        return app($name);
    }
}

就是從容器中取出 datatables.request 的過程。

所以我們只需讓每次請求結束,$app 容器忘掉 datatables.request 就好了

改進

增加遺忘 datatables.request

    $res->end($response->getContent());
    $this->app->forgetInstance('request');
    $this->app->forgetInstance('datatables.request');
    $this->app->forgetInstance(\Dingo\Api\Http\Middleware\Request::class);

完整最終版:

<?php

use Illuminate\Http\Request;
use Illuminate\Http\Response;

define('LARAVEL_START', microtime(true));
require __DIR__ . '/../vendor/autoload.php';
$app = require_once __DIR__ . '/../bootstrap/app.php';

class Laravel
{
    /**
     * Illuminate\Foundation\Application
     *
     * @var \Illuminate\Foundation\Application
     */
    public $app;

    /**
     * App\Http\Kernel
     *
     * @var \App\Http\Kernel
     */
    public $kernel;

    /**
     * App\Http\Requests\Request
     *
     * @var \App\Http\Requests\Request
     */
    public $request;

    /**
     * Illuminate\Http\JsonResponse
     *
     * @var \Illuminate\Http\JsonResponse
     */
    public $response;

    /**
     * 構造
     *
     * @param \Illuminate\Foundation\Application $app
     */
    public function __construct(\Illuminate\Foundation\Application $app)
    {
        $this->app = $app;
    }

    /**
     * RUN
     *
     * @return void
     */
    public function run()
    {
        \Swoole\Runtime::enableCoroutine(true);

        $http = new swoole_http_server('127.0.0.1', '80');

        $http->set([
            'document_root' => public_path('/'),
            'enable_static_handler' => true,
        ]);

        $http->on('request', function ($req, $res) {
            try {
                $kernel = $this->app->make(Illuminate\Contracts\Http\Kernel::class);

                $get = $req->get ?? [];
                $post = $req->post ?? [];
                $input = array_merge($get, $post);
                $cookie = $req->cookie ?? [];
                $files = $req->files ?? [];
                $server = $req->server ?? [];

                $request = Request::create($req->server['request_uri'], $req->server['request_method'], $input, $cookie, $files, $server);

                if (isset($req->header['x-requested-with']) && $req->header['x-requested-with'] == "XMLHttpRequest") {
                    $request->headers->set('X-Requested-With', "XMLHttpRequest", true);
                }
                if (isset($req->header['accept']) && $req->header['accept']) {
                    $request->headers->set('Accept', $req->header['accept'], true);
                }

                $response = $kernel->handle($request);

                $res->status($response->getStatusCode());

                foreach ($response->headers->allPreserveCaseWithoutCookies() as $name => $values) {
                    foreach ($values as $value) {
                        $res->header($name, $value, false);
                    }
                }

                foreach ($response->headers->getCookies() as $cookie) {
                    $res->header('Set-Cookie', $cookie->getName() . strstr($cookie, '='), false);
                }
                dump(time());

                $res->end($response->getContent());
                $this->app->forgetInstance('request');
                //$this->app->forgetInstance('session');
                //$this->app->forgetInstance('session.store');
                //$this->app->forgetInstance('cookie');
                $this->app->forgetInstance('datatables.request');
                $this->app->forgetInstance(\Dingo\Api\Http\Middleware\Request::class);
                //$kernel->terminate($request, $response);
            } catch (\throwable $e) {
                echo $e->getMessage();
                echo PHP_EOL;
                echo $e->getFile();
                echo PHP_EOL;
                echo $e->getLine();
                echo PHP_EOL;
            }
        });
        $http->start();
    }
}

(new Laravel($app))->run();

測試

比原生 laravel 確實快不少(這還有 4 句 SQL 查詢) 。

注,此處給出的代碼可以借鑑,但未經長期驗證。且不同項目實際用到的包不同,需要在調試過程中 debug 容器中的服務提供者,和追蹤代碼來調優。

已知問題

  • flash 閃存數據以及表單驗證錯誤的展示有問題
  • PDO 會報 Cannot execute queries while other unbuffered queries are active symfony/symfony...
  • Throttle 的 IP 獲取設定默認會產生問題

更多學習內容請訪問:

騰訊T3-T4標準精品PHP架構師教程目錄大全,只要你看完保證薪資上升一個臺階(持續更新)

以上內容希望幫助到大家,很多PHPer在進階的時候總會遇到一些問題和瓶頸,業務代碼寫多了沒有方向感,不知道該從那裏入手去提升,對此我整理了一些資料,包括但不限於:分佈式架構、高可擴展、高性能、高併發、服務器性能調優、TP6,laravel,YII2,Redis,Swoole、Swoft、Kafka、Mysql優化、shell腳本、Docker、微服務、Nginx等多個知識點高級進階乾貨需要的可以免費分享給大家,需要的可以加入我的官方羣點擊此處

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章