+4

Sử dụng NestJS Microservices với Laravel.

Mở đầu

Trong bài viết này, chúng ta sẽ cùng thiết lập giao tiếp giữa framework PHP Laravelmicroservice NestJS. Nhưng ngay cả khi bạn không sử dụng Laravel (hoặc PHP), các ý tưởng và logic tương tự cũng áp dụng cho việc kết nối NestJS với một framework hoặc language khác (Java, Python, Ruby,...).

Để hiểu cách giao tiếp này hoạt động, chúng ta sẽ xây dựng một Laravel client cơ bản gọi đến các microservice NestJS. Hãy bắt đầu nào !!!

Các bước thực hiện

Để có thể xây dựng được microservice NestJS từ Laravel, chúng ta cần thực hiện các bước sau:

  1. Cài đặt Redis
  2. Thiết lập một microservice NestJS cơ bản
  3. Hiểu cách microservice NestJS giao tiếp
  4. Xây dựng một client cho Laravel gọi microservice

Tiến hành thôi 💪

1. Cài đặt Redis

Đối với ví dụ này, chúng ta sẽ sử dụng Redis làm kênh giao tiếp cho microservice, vì nó rất quen thuộc với chúng ta. Thêm vào đó, nó giúp chúng ta dễ dàng theo dõi các giao tiếp; chỉ cần chạy redis-cli monitor từ terminal.

Để cài đặt redis, hãy chạy các lệnh sau trong terminal:

MacOS:

brew install redis

Ubuntu:

sudo apt -y install redis-server

Nếu Redis đã được cài đặt thành công, một máy chủ sẽ hoạt động trên port 6379. Để kiểm tra, hãy chạy redis-cli từ terminal của bạn. Nếu hiển thị như sau thì có nghĩa là Redis đã hoạt động.

$ redis-cli
127.0.0.1:6379> quit

Vậy là xong bước 1 rồi. Chúng ta cùng đến bước tiếp theo nhé

2. Setup NestJS Microservice

Hãy đảm bảo máy tính của bạn đã cài Node nhé.

Tiếp theo, chúng ta hãy tạo một project NestJS. Mở một terminal và thực hiện các lệnh sau để tạo một dự án mới và cài đặt các phụ thuộc cần thiết:

$ nest new PROJECT_NAME
$ cd PROJECT_NAME
$ yarn add @nestjs/microservices redis

Sau khi hoàn tất, mở trình file main.ts và thêm đoạn code dưới đây để tiến hành kết nối với redis:

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { Transport } from '@nestjs/microservices';

async function bootstrap() {
  const app = await NestFactory.createMicroservice(AppModule, {
    transport: Transport.REDIS,
    options: {
      url: 'redis://localhost:6379',
    },
  });
  await app.listenAsync();
}
bootstrap();

Tiếp theo là xây dựng function cơ bản để Laravel gọi đến trong file app.controller.ts:

import { Controller } from '@nestjs/common';
import { MessagePattern } from '@nestjs/microservices';
import { from, Observable } from 'rxjs';

@Controller()
export class AppController {
  @MessagePattern({ cmd: 'greeting' })
  getHello(name: string): string {
    return `Hello ${name}!`;
  }

  @MessagePattern({ cmd: 'observable' })
  getObservable(): Observable<number> {
    return from([1, 2, 3]);
  }
}

Trong controller, bạn có thể thấy chúng ta có hai lệnh có sẵn. Lệnh đầu tiên là lệnh greeting sẽ trả về"Hello <name>!". Lệnh thứ hai, observable, trả về một observable đơn giản phát ra các số 1, 2, 3 theo thứ tự và sau đó hoàn tất. Điều này sẽ được sử dụng để minh họa sự khác biệt trong cách xử lý observables.

Mình sẽ giải thích chi tiết hơn ở phần sau nhé!

3. Hiểu cách microservice NestJS giao tiếp

Trước khi chúng ta xây dựng 1 client trong Laravel, hãy cùng xem xét cách microservice NestJS giao tiếp.

Đối với mỗi lệnh, Nest sẽ thiết lập hai kênh một chiều trong Redis: một để lắng nghe yêu cầu và cái còn lại để gửi phản hồi. Phía dưới, microservice giao tiếp bằng cách chấp nhận các đối tượng đã được tuần tự hóa (tức là request payload) qua một channel(kênh) và sau đó trả lại response payload (cũng là một đối tượng đã được tuần tự hóa) qua channel còn lại.

Do đó, để xây dựng một client, chúng ta phải biết quy tắc đặt tên cho các channel này và cách mà các payload được cấu trúc.

Quy tắc đặt tên kênh Xác định tên channelrất đơn giản: chỉ cần thêm _ack hoặc _res vào mẫu chuỗi JSON.

Đối với mỗi phương thức dùng decorated @MessagePattern(<pattern>), NestJS sẽ subscribe vào một pub/sub channel của Redis có tên \<pattern\>_ackvà sẽ trả response trên channel \<pattern\>_res. Ví dụ, đối với lệnh greeting, NestJS sẽ đăng ký vào channel '{ "cmd": "greeting" }_ack' và trả response trên channel '{ "cmd": "greeting" }_res'.

Payload Schema

Để thực hiện một request đến microservice, bạn cần gửi mộtobject với các key sau qua tên channel:

  • id: một UUID được sử dụng để theo dõi phản hồi cho một yêu cầu cụ thể.
  • pattern: pattern của tin nhắn được xác định trong microservice NestJS.
  • data: tham số được truyền từ khách hàng.

Ví dụ, để thực hiện lệnh greeting trong microservice mẫu, bạn có thể gửi một object như sau qua channel '{ "cmd": "greeting" }_ack':

{
  "id": "68950cb0-397b-43b3-bf03-9c5f8aa619fa",
  "pattern": "{ \"cmd\": \"greeting\" }",
  "data": "Son"
}

Response bạn nhận được sẽ có schema như sau:

  • id: sẽ trùng khớp với UUID của đối tượng yêu cầu.
  • response: giá trị trả về.
  • err: thông báo lỗi; null nếu không có lỗi.
  • isDisposed: khi có mặt và true, xác định object là truyền tải cuối cùng trong response; nó chỉ có mặt trong response object cuối cùng.
{
  "id": "68950cb0-397b-43b3-bf03-9c5f8aa619fa",
  "response": "Hello John!",
  "err": null,
  "isDisposed": true
}

Single Message

Vì lệnh greeting không trả về một observable, chỉ có một response object được trả về.

image.png

Streaming mesage Tuy nhiên, nếu chúng ta thực hiện một request đến lệnh observable, chúng ta sẽ nhận được nhiều response object. Response object cuối cùng được gửi sẽ được đánh dấu với key isDisposed để xác định nó là cuối cùng.

4. Laravel setup

Chúng ta có bốn bước để hoàn thành việc Laravel hoạt động với NestJS:

  1. Tạo một dự án Laravel mới.
  2. Cấu hình client Redis.
  3. Tạo dịch vụ NestJS và liên kết nó với container.
  4. Xác minh rằng dịch vụ hoạt động đúng.

1. Tạo một dự án Laravel mới Nhập các lệnh sau trong terminal của bạn để tạo dự án Laravel mới và cài đặt các phụ thuộc:

$ laravel new PROJECT_NAME
$ cd PROJECT_NAME
$ composer require predis/predis
  1. Cập nhật Cấu hình Redis Để thực hiện các thay đổi cần thiết trong cấu hình Redis, mở config/database.php và thực hiện các thay đổi sau:

Lưu ý: Ngoài driver predit ra, bạn cũng có thể dùng driver phpredis cũng có thể hoạt động. Mình dùng predis vì nó không yêu cầu một extension PECL phải được cài đặt.

...
'redis' => [
  // 'client' => 'phpredis',
  'client' => 'predis',
  'default' => [
    'host' => env('REDIS_HOST', '127.0.0.1'),
    'password' => env('REDIS_PASSWORD', null),
    'port' => env('REDIS_PORT', 6379),
    'database' => 0,
  ],
  'pubsub' => [
    'host' => env('REDIS_HOST', '127.0.0.1'),
    'password' => env('REDIS_PASSWORD', null),
    'port' => env('REDIS_PORT', 6379),
    'database' => 0,
    'prefix' => '', // Disable prefix
  ],
],
...

Đặt client Redis thành predis và tắt tiền tố cho tên Redis bằng cách đặt tiền tố thành chuỗi rỗng. Chúng ta sẽ thêm một kết nối pubsub để xử lý phản hồi.

3. Tạo client cho NestJS microservice

Bây giờ, hãy tạo NestJsService. Trong thư mục app/Services, tạo một file mới có tên NestJsService.php. Đặt nội dung sau vào file đó:

<?php
namespace App\Services;
use Illuminate\Redis\RedisManager;
use Illuminate\Support\Str;
use Predis\PubSub\Consumer;
use Illuminate\Support\Collection;
class NestJsService
{
  /** @var RedisManager $redis */
  protected $redis;
  function __construct(RedisManager $redis)
  {
    $this->redis = $redis;
  }
  /*---------------------------------------------------------------------*
      PUBLIC METHODS
    *---------------------------------------------------------------------*/
  public function send($pattern, $data = null)
  {
    // Build the payload object from the params
    $payload = $this->newPayload($pattern, $data);
    // Make a call to NestJS with the payload &
    // return the response.
    return $this->callNestMicroservice($payload);
  }
  /*---------------------------------------------------------------------*
      INTERNAL METHODS
    *---------------------------------------------------------------------*/
  /**
  * Create new UUID
  *
  * @return string
  */
  protected function newUuid()
  {
    return Str::uuid()->toString();
  }
  /**
  * Create new collection
  *
  * @return Collection
  */
  protected function newCollection()
  {
    return collect();
  }
  /**
  * Create new payload array
  *
  * @param string $pattern
  * @param mixed $data
  * @return array
  */
  protected function newPayload($pattern, $data) {
    return [
      'id' => $this->newUuid(),
      'pattern' => json_encode($pattern),
      'data' => $data,
    ];
  }
  /**
  * Make request to microservice
  *
  * @param array $payload
  * @return Collection
  */
  protected function callNestMicroservice($payload)
  {
    $uuid = $payload['id'];
    $pattern = $payload['pattern'];
    // Subscribe to the response channel
    /** @var Consumer $loop */
    $loop = $this->redis->connection('pubsub')
            ->pubSubLoop(['subscribe' => "{$pattern}_res"]);
    // Send payload across the request channel
    $this->redis->connection('default')
          ->publish("{$pattern}_ack", json_encode($payload));
    // Create a collection to store response(s); there could be multiple!
    // (e.g., if NestJS returns an observable)
    $result = $this->newCollection();
    // Loop through the response object(s), pushing the returned vals into
    // the collection.  If isDisposed is true, break out of the loop.
    foreach ($loop as $msg) {
      if ($msg->kind === 'message') {
        $res = json_decode($msg->payload);
        if ($res->id === $uuid) {
          $result->push($res->response);
          if (property_exists($res, 'isDisposed') && $res->isDisposed) {
            $loop->stop();
          }
        }
      }
    }
    return $result; // return the collection
  }
}

Để sử dụng, chúng ta chỉ cần khởi tạo class NestjsService và dùng phương thức send() là xong:

$nestService = new NestJsService();
$result = $nestService->send(['cmd' => 'greeting'], 'Đăng CQ');

Kết nối Dịch vụ với Container của Laravel

Tiếp theo, chúng ta cần liên kết service này với container của Laravel. Trong file app/Providers/AppServiceProvider.php, thêm code sau vào phương thức register():

<?php
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
class AppServiceProvider extends ServiceProvider
{
    /**
     * Register any application services.
     *
     * @return void
     */
    public function register()
    {
        // Bind the NestJsService to the container
        $this->app->singleton(\App\Services\NestJsService::class);
    }
}

4. Tạo 1 Endpoint để Test

Vì đây chỉ là test nên mình tránh tạo controllers và chèn code trực tiếp vào routers. Mở** routes/api.php** code như sau:

<?php
use Illuminate\Http\Request;
use App\Services\NestJsService;

Route::post('/greeting', function (Request $request, NestJsService $nestService) {
    $name = $request->get('name');
    $nestResponse = $nestService->send(['cmd' => 'greeting'], $name);
    return $nestResponse->first();
});
Route::get('/observable', function (NestJsService $nestService) {
    $nestResponse = $nestService->send(['cmd' => 'observable']);
    return $nestResponse->sum();
});

Vậy là chúng ta đã hoàn thành và sẵn sàng để test rồi.

Gặt hái thành quả

Hãy khởi động các ứng dụng NestJSLaravel và thử truy cập các endpoint này để kiểm tra xem mọi thứ có hoạt động ổn không. Điều hướng đến thư mục dự án NestJS của bạn và khởi động server:

$ cd full_path_to_nestjs_project
$ yarn start:dev

Sau đó, đi đến thư mục dự án Laravel và khởi động server ở đó.

$ cd full_path_to_laravel_project
$ php artisan serve

Tiếp theo, gửi một request phương thức POST đến endpoint greeting. Bạn có thể sử dụng một GUI như Postman hoặc Insomnia, hoặc một công cụ CLI đơn giản như curl hoặc httpie. Ở đây mình dùng httpie

http --form POST http://127.0.0.1:8000/api/greeting name="Dang CQ"

HTTP/1.1 200 OK
Cache-Control: no-cache, private
Connection: close
Content-Type: text/html; charset=UTF-8
Date: Sun, 27 Sep 2024 23:21:55 +0000
Date: Sun, 27 Sep 2024 23:21:55 GMT
Host: 127.0.0.1:8000
X-Powered-By: PHP/7.3.1
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 59

Hello Dang CQ!

Bây giờ hãy thử endpoint gọi lệnh observable.

http http://127.0.0.1:8000/api/observable

HTTP/1.1 200 OK
Cache-Control: no-cache, private
Connection: close
Content-Type: text/html; charset=UTF-8
Date: Sun, 15 Sep 2019 23:22:18 +0000
Date: Sun, 15 Sep 2019 23:22:18 GMT
Host: 127.0.0.1:8000
X-Powered-By: PHP/7.3.1
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 57

6

Luồng của endpoint observeble

  • Định nghĩa Route: Sử dụng Route::get('/observable', ...) nghĩa là endpoint này sẽ phản hồi với các yêu cầu GET tới URL /observable.

  • Gọi NestJS Service: Trong hàm xử lý của route, phương thức send() của NestJsService được gọi thông qua biến $nestService. Phương thức send() nhận hai tham số:

    • Tham số đầu tiên là một mảng xác định pattern của thông điệp,['cmd' => 'observable'], giúp NestJS nhận diện và gọi đến hàm xử lý getObservable.
    • Không có tham số thứ hai, nghĩa là không có dữ liệu bổ sung nào được gửi đi.
  • Xử lý Response: Phương thức send() trả về kết quả là một collection chứa các số được phát ra từ getObservable của microservice NestJS. Vì getObservable trả về một observable (dòng dữ liệu liên tiếp), $nestResponse sẽ là một collection chứa nhiều số (ví dụ: [1, 2, 3]).

  • Tổng hợp Response: Dòng $nestResponse->sum() sẽ tính tổng tất cả các giá trị trong collection này, nên endpoint này sẽ trả về tổng của các số mà observable phát ra—trong ví dụ này là 6 (1 + 2 + 3).

Lời kết

Bài viết này cung cấp một phương pháp để tích hợp Laravel với microservice NestJS, sử dụng Redis cho giao tiếp.

Ta thấy

  • NestJS Microservices có thể được gọi từ bất kỳ framework nào.
  • Vì NestJS được cấu hình để lắng nghe/nhận dữ liệu từ Redis, chúng ta cần đảm bảo nắm rõ quy tắc đặt tên từ Laravel.

Cách tiếp cận này có thể giúp chúng ta sử dụng NestJS để bổ sung các dịch vụ mới cho các dự án đã có sẵn.

Cảm ơn các bạn đã đọc bài viết của mình. Hẹn gặp lại 🤚🤚🤚


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí