Laravel Octane - Những điều cần lưu ý

Laravel Octane - Những điều cần lưu ý

Tuần trước, Laravel Octane đã tung ra phiên bản chính thức v1.0, đánh dấu bước phát triển mới của package rất đáng kỳ vọng này. Và nếu cũng đang cân nhắc áp dụng Octane cho ứng dụng của mình, có một số vấn đề bạn cần hiểu kỹ càng. Những vấn đề đó là gì, mời bạn cùng tìm hiểu với QMAS - Thiết kế web Quảng Ninh nhé.

Laravel Octane là gì?

Laravel Octane là một package mã nguồn mở được viết ra với mục đích giúp tăng tốc nhiều lần cho các ứng dụng Laravel. Laravel Octane yêu cầu PHP 8 trở lên, do đó có thể bạn sẽ cần nâng cấp phiên bản PHP của mình.

Laravel Octane được phát triển trên nền tảng của SwooleRoadRunner - 2 tiện ích xử lý bất đồng bộ dành cho PHP.

Tại sao Laravel Octane giúp tăng tốc ứng dụng Laravel?

Trong bất kỳ một ứng dụng Laravel thông thường nào, mọi request đều khiến ứng dụng phải trải qua tất cả các bước cần thiết như: bootingcontainer binding, khởi tạo các lớp, chạy middleware, gọi các route action và cuối cùng là trả về dữ liệu cho trình duyệt.

Tuy nhiên khi được chạy với Laravel Octane, ứng dụng Laravel của chúng ta sẽ có một sự thay đổi cực lớn: đó là nó sẽ chỉ phải boot một lần duy nhất khi các Octane worker khởi động, tất cả các request sau đó đều sẽ được sử dụng cùng một instance của lần khởi động đầu tiên. 

Để tìm hiểu sâu hơn, chúng ta hãy cùng xem điều gì xảy ra khi Octane khởi động:

$app = require BASE_PATH . '/bootstrap/app.php'

$app->bootstrapWith([
    LoadEnvironmentVariables::class,
    LoadConfiguration::class,
    HandleExceptions::class,
    RegisterFacades::class,
    SetRequestForConsole::class,
    RegisterProviders::class,
    BootProviders::class,
]);

$app->loadDeferredProviders();

foreach ($app->make('config')->get('octane.warm') as $service) {
    $app->make($service);
}

Như chúng ta có thể thấy, một instance của ứng dụng sẽ được tạo ra khi một Octane worker bắt đầu công việc. Instance này sau đó thực thi một số tác vụ như tải các file cấu hình, đăng ký Facade và Service Provider.

Ở thời điểm này, các phương thức register() boot() trong tất cả Service Provider (trừ các Deferred Service Providers) đều được gọi, có nghĩa là tất cả các service này đã được bao lại bởi container, và do đó, container đã có thể biết làm thế nào để resolve các service binding. Ví dụ nếu gọi app('config') ở thời điểm này, container sẽ có thể đưa ra cho chúng ta chính xác configuration repository.

Tiếp theo, $app->loadDeferredProviders() có tác dụng đảm bảo rằng tất cả các Deferred Service Providers cũng sẽ được register()boot(). Bởi vì ứng dụng Laravel Octane của chúng ta sẽ chỉ khởi động một lần duy nhất, do đó các Deferred Service Providers sẽ không có tác dụng gì trong trường hợp này.

Việc Binding trong container sẽ có thể được đăng ký dưới dạng Singleton, các binding đặc biệt này sẽ chỉ được resolve một lần duy nhất trong suốt vòng đời ứng dụng. Instance của lớp được resolve sẽ được lưu trữ trong container cache các instance tương tự sẽ được tái sử dụng trong suốt vòng đời ứng dụng. Việc này sẽ giúp ứng dụng Laravel của chúng ta chạy rất nhanh vì đã không cần phải construct các instance hết lần này tới lần khác mỗi khi chúng ta cần resolve chúng trong container.

⚠️ Những điều cần lưu ý!!!

Khi sử dụng Octane cho ứng dụng Laravel, chúng ta cần đặc biệt để tâm tới các singleton. Bởi vì chúng chỉ được resolve một lần duy nhất, do đó bất kỳ thay đổi nào của các instance này cũng sẽ tồn tại mãi mãi cho tới khi nào Octane server vẫn còn đang hoạt động.

Các instance được resolve bằng cách gọi $app->resolve('singleton') hay $app->make('singleton'). Nên nếu chúng ta resolve bất kỳ singleton nào bên trong các phương thức  register() và boot() của Service Provider, các singleton này vẫn tồn tại. Các singleton được resolve trong khi xử lý các request thì không như vậy, chúng sẽ được khởi tạo trong mỗi request, do đó chúng ta không cần lo lắng về chúng.

Trong trường hợp muốn resolve một singleton nào đó trong quá trình worker khởi động mà không phải trong service provider, chúng ta có thể liệt kê chúng trong file config. Đó là lý do đoạn code này được xuất hiện:

foreach ($app->make('config')->get('octane.warm') as $service) {
    $app->make($service);
}

Octane sẽ loop qua các Service được liệt kê trong config('octane.warm') để resolve chúng giúp chúng ta. Bằng cách này chúng sẽ tồn tại trong bộ nhớ của container cho tới khi nào Octane còn hoạt động.

Octane xử lý Request như thế nào?

Bây giờ chúng ta đã có một instance của ứng dụng với Service Container biết cách để resolve các binding. Và giờ là lúc xử lý các request.

Đây là cách mà Octane xử lý các request:

$server->on('request', function($request) use ($app){
    $sandbox = clone $app;

    Container::setInstance($sandbox);

    $sandbox->make('events')->dispatch(new RequestReceived);

    $response = $sandbox->make(Kernel::class)->handle($request);

    Container::setInstance($app);

    return $response;
});

Chúng ta có thể thấy ở đây, Octane thực hiện sao chép instance nguyên bản của ứng dụng (biến $sandbox) và sử dụng bản sao chép này để xử lý các request. Việc sử dụng instance được sao chép này với mỗi request sẽ cho phép Octane làm mới lại một số service đã bị làm thay đổi trạng thái trong request trước đó nhằm đảm bảo rằng request tiếp theo sẽ được các service xử lý trong trạng thái mới hoàn toàn.

Như đã giải thích trước đó, các singleton tồn tại trong suốt vòng đời của Octane server sẽ giúp ích lớn cho quá trình tăng tốc ứng dụng. Nhưng trong hầu hết các trường hợp, trạng thái một hoặc nhiều service ứng dụng của chúng ta sẽ bị thay đổi trong mỗi request. Ví dụ: cấu hình cho thể bị thay đổi trong một request bởi một câu lệnh như:

app('config')->set('services.aws.key', A_NEW_AWS_KEY);

Với câu lệnh trên, thì từ request tiếp theo, giá trị key cấu hình services.aws.key sẽ trở thành A_NEW_AWS_KEY, điều mà chúng ta không mong muốn.

Việc theo dõi các thay đổi này có thể vô cùng phức tạp, đặc biệt nếu chúng xảy ra bởi các package của các bên thứ 3 không phải do chính chúng ta xây dựng. Vì lý do đó, Laravel cung cấp cho $sandbox một config service riêng biệt được lược bỏ sau mỗi yêu cầu trong khi vẫn giữ cho config service gốc không thay đổi bên trong instance ứng dụng gốc.

// Đây là cách mà Octane giúp cho mỗi request đều nhận được giá trị config nguyên bản
$sandbox->instance('config', clone $sandbox['config']);

Quay trở lại cách mà Octane xử lý các request. Trước khi instance $sandbox được sử dụng để làm việc này, Octane bắn ra một event có tên là RequestReceived. Sự kiện này sẽ được Octane lắng nghe và thực hiện một số bước chuẩn bị để $sandbox có thể xử lý được các request.

Và đây là chi tiết của quá trình chuẩn bị này:

$sandbox->instance('config', clone $sandbox['config']);

$sandbox[Kernel::class]->setApplication($sandbox);

$sandbox['cache']->store('array')->flush();

$sandbox['session']->driver()->flush();
$sandbox['session']->driver()->regenerate();

$sandbox['translator']->setLocale($sandbox['config']['app.locale']);
$sandbox['translator']->setFallback($sandbox['config']['app.fallback_locale']);

$sandbox['auth']->forgetGuards();

$app->instance('request', $request);
$sandbox->instance('request', $request);

Đầu tiên, nó tạo một bản copy cấu hình từ instance $sandbox mới được tạo. Sau đó bản cấu hình mới được copy này sẽ được bind vào trong chính $sandbox. Và 🎉, mọi câu lệnh làm thay đổi giá trị gốc của cấu hình sẽ chỉ có tác dụng trong $sandbox mà thôi.

Tiếp theo, Octane sẽ truyền instance $sandbox tới một vài service. Bằng cách này, khi $this->app được gọi bên trong các service này, instance $sandbox sẽ được trả về thay vì instance gốc của ứng dụng. Thực tế thì Octace thực hiện thao tác này với một vài service, nhưng ở đoạn code trên chúng ta chỉ lấy service Kernel làm ví dụ mà thôi.

Tiếp theo, Octane sẽ xóa các array cache và tái khởi tạo session để chúng không tồn tại giữa các request. Và cũng vì lý do này, nó thiết lập luôn các giá trị cho cấu hình locale.

Sau đó, Octace xóa tất các các instance xác thực người dùng để các instance này sẽ được tạo mới cho mỗi request. Việc này là cực kỳ quan trọng vì các instance này cache lại người dùng đã xác thực bên trong chúng và do đó chúng ta cần reset lại chúng bởi request tiếp theo có thể là từ một người dùng khác.

Và cuối cùng, Octane đồng thời truyền instance của request tới cho cả instance gốc của ứng dụng và $sandbox. Tại sao lại truyền tới instance gốc? Bởi Octane muốn đảm bảo rằng chúng ta vẫn có thể tham chiếu đến instance gốc của ứng dụng trong trường hợp có nhu cầu. Mặc dù việc này hoàn toàn không được khuyến khích, tuy nhiên ở thời điểm hiện tại, Octane buộc phải làm vậy cho tới khi tất cả các ứng dụng và package (của Laravel) đều đã hoàn toàn toàn Octane compatibiliy.

Những điều cần lưu ý!

Bây giờ chúng ta đã biết cách mà Octane giúp tăng tốc ứng dụng và xử lý các incoming request. Và giờ là lúc đi vào những điều cần đặc biệt chú ý khi làm việc với công cụ này.

Truyền instance của ứng dụng tới các service

Nếu một service cần giao tiếp với instance của ứng dụng và bạn truyền nó bên trong constructor, hãy đảm bảo rằng bạn sử dụng instance được truyền vào từ tham số trong callback chứ không phải là instance được lấy trực tiếp từ trong Service Provider.

// Không làm như này...
$this->app->bind(Service::class, function () {
    return new Service($this->app);
});

// Mà hãy làm như này...
$this->app->bind(Service::class, function ($app) {
    return new Service($app);
});

Lý do cho việc này là bởi $this->app giữa tham chiếu từ instance của ứng dụng gốc. Mà theo như cách mà Octane xử lý request ở bên trên chúng ta đã tìm hiểu, thì instance của ứng dụng mà chúng ta cần lấy ra chính là $sandbox.

Hoặc một cách khác nữa là sử dụng helper app(), Container::getInstance(). Chúng sẽ luôn giúp chúng ta lấy tham chiếu của $sandbox.

Truyền instance của ứng dụng đến các singleton

Không truyền instance của ứng dụng đến các singleton, thay vào đó hãy truyền một callback trả về một instance của $sandbox.

// Không làm như này...
$this->app->singleton(Service::class, function ($app) {
    return new Service($app);
});

// Mà hãy làm như này...
$this->app->singleton(Service::class, function ($app) {
    return new Service(fn () => Container::getInstance());
});

Các singleton sẽ tồn tại giữa các request, điều này có nghĩa rằng instance của ứng dụng gốc được truyền vào khi singleton được resolve lần đầu tiên sẽ được sử dụng khi service được gọi trong mỗi request.

Truyền instance của request vào các singleton

Tương tự bên trên:

// Không làm như này...
$this->app->singleton(Service::class, function ($app) {
    return new Service($app['request']);
});

// Mà hãy làm như này...
$this->app->singleton(Service::class, function ($app) {
    return new Service(fn () => Container::getInstance()['request']); // hoặc sử dụng helper request()
});

Truyền instance của config repository vào singleton

Cũng tương tự:

// Không làm như này...
$this->app->singleton(Service::class, function ($app) {
    return new Service($app['config']);
});

// Mà hãy làm như này...
$this->app->singleton(Service::class, function ($app) {
    return new Service(fn() => Container::getInstance()['config']);
});

Persisting Singleton

Chỉ các singleton được resolve trong quá trình khởi động ứng dụng mới tồn tại giữa các request. Các singleton được resolve trong quá trình xử lý request sẽ được đăng ký trong $sandbox container, và nó sẽ bị hủy sau mỗi request.

Để các singleton tồn tại giữa các request, chúng ta có thể hoặc resolve chúng trong các Service Provider, hoặc thêm vào mảng warm trong file cấu hình Octane:

'warm' => [
    ...Octane::defaultServicesToWarm(),
    Service::class
],

Mặt khác, nếu bạn có một package đăng ký và resolve một singleton bên trong Service Provider và bạn muốn xóa instance này trước mỗi request, hãy thêm nó vào mảng flush trong file cấu hình Octane:

'flush' => [
    Service::class
],

Octane sẽ đảm bảo việc xóa các singleton này khỏi container mỗi khi request hoàn tất.

Kết

Như vậy là chúng ta đã điểm qua một số lưu ý cũng như cách xử lý khi áp dụng Octane cho ứng dụng Laravel của mình. Nếu bạn đã sử dụng Octane và có kinh nghiệm nào hay hơn nữa thì mời bạn cùng bình luận và chúng ta cùng trao đổi nhé.

Công ty TNHH Giải pháp Website & Ứng dụng phần mềm Quang Minh

🚩 Địa chỉ
Số 81 Võ Huy Tâm, Phường Cẩm Trung, Thành phố Cẩm Phả, Tỉnh Quảng Ninh
📞 Điện thoại
(0862) 814-787
💌 Email
[email protected]
🌐 Zalo OA
https://zalo.me/369605269295116980
🌐 Facebook
https://www.facebook.com/qmasdotvn/
🌐 Twitter