Cronjob đơn giản trong Laravel 5.4

1. Vấn đề


Hiện nay tại công ty, khi làm việc mình thường gặp phải các task như:

  • Đặt lịch mỗi ngày hệ thống đều thống kê số lượng đơn hàng nhập, xuất rồi gửi lại email cho các sếp vào 7h sáng hàng ngày
  • Hệ thống tự động thống kê hoạt động kiểm kho rồi gửi email cho quản lý và nhân viên kho vào 11h đêm
  • Hoặc đơn giản là thống kê số yêu cầu nhận được từ bên hệ thống bán hàng gửi sang rồi cứ 2h sáng lại gửi request post lên api để Dev kiểm tra hệ thống gửi nhận có bị mất yêu cầu nào không Điểm chung của tất cả các task này là hệ thống đều cần tự động chạy một chức năng cố định vào một thời điểm cố định nào đó được định sẵn (như trên ví dụ là 2h, 7h, 11h hàng ngày, …)

2. Giải pháp


Laravel cung cấp một bộ lệnh đặt lịch của Laravel cho phép bạn định nghĩa các lệnh đặt lịch một cách liền mạch và rõ ràng, công việc này ở đây mình gọi chung là tạo cronjob. Việc đặt lịch các cronjob được định nghĩa bên trong file app/Console/Kernel.php và trong hàm schedule. Ở đây mình sẽ làm một ví dụ đơn giản để các bạn hiểu được các làm 1 cronjob là như thế nào : Thống kê số yêu cầu nhận được từ hệ thống bán hàng trong ngày hôm trước và gửi lên api mỗi 2h sáng (dữ liệu gửi lên định dạng json, phương thức gửi là POST)

3. Thực hiện


Trong file app/Console/Commands tạo file mới tên là MessagesFromSystem.php

<?php

namespace App\Console\Commands;

use Illuminate\Console\Command;

class MessagesFromSystem extends Command
{
    public function handle()
    {
        //
    }
}

Trong nội dung file có sẵn hàm handle, đây là nơi định nghĩa và thực thi các hành động khi được hệ thống gọi đến Tạo 2 biến $signature$description, trong đó biến $signature là định nghĩa tên của cronjob mà hệ thống sẽ sử dụng tên này để gọi và $description là mô tả hành động của cronjob

protected $signature = 'api:number-message-from-systems';

protected $description = 'Add data to api about number message from systems';

Trong task này có 2 hành động chính, thứ nhất là cần lấy dữ liệu số yêu cầu nhận được từ hệ thống bán hàng trong ngày hôm trước (1) và sau đó gửi tất cả dữ liệu đó lên api theo phương thức POST (2), ở đây mục đích của chúng ta là tìm hiểu về cách tạo cronjob trong laravel nên mình sẽ đi nhanh 2 phần này. Định dạng gửi request lên sever bên team api yêu cầu là:

{
  "service_name": "teko.lo",
  "sent": [
    {
      "routing_key": "lo.bl.create",
      "quantity": 0
    }
  ],
  "received": [
    {
      "routing_key": "wh.export.create",
      "quantity": 0
    }
  ]
}

Định dạng trả ra là kiểu json, vì vậy việc cần làm là tạo ra 1 mảng có đầy đủ các thông tin service_name, sent, received rồi sau đó dùng hàm json_encode() để trả về dữ liệu kiểu json. Các thông tin về request gửi nhận đã lưu sẵn ở 2 bảng 'log_publish_queues' và 'log_consumer_queues' .

public function checksumMessageQueue()
{
    $checksumData = [];

    $checksumData['service_name'] = self::SERVICE_NAME;

    $logPublishQueues = DB::table('log_publish_queues')
        ->select('routing_key', DB::raw('COUNT(1) as quantity'))
        ->whereDate('created_at', Carbon::yesterday()->toDateString())
        ->groupBy('routing_key')
        ->get();

    foreach ($logPublishQueues as $logPublishQueue) {
        $checksumData['sent'][] = [
            'routing_key' => $logPublishQueue->routing_key,
            'quantity' => $logPublishQueue->quantity
        ];
    }

    $logConsumerQueues = DB::table('log_consumer_queues')
        ->select('routing_key', DB::raw('COUNT(1) as quantity'))
        ->whereDate('created_at', Carbon::yesterday()->toDateString())
        ->groupBy('routing_key')
        ->get();

    foreach ($logConsumerQueues as $logConsumerQueue) {$checksumData['received'][] = [
            'routing_key' => $logConsumerQueue->routing_key,
            'quantity' => $logConsumerQueue->quantity
        ];
    }

    return $checksumData;
}

Đã có dữ liệu trả về, giờ làm tiếp việc thứ 2 gửi tất cả dữ liệu lên api theo phương thức POST, tại đây mình sử dụng thư viện GuzzleHttp để gửi request, chi tiết thư viện và cách sử dụng các bạn có thể xem tại http://docs.guzzlephp.org/en/stable/ . Đoạn code để gửi request POST lên api, mình giả dụ đường link api là http://apicuaban.com/post

$client->request('POST', 'http://apicuaban.com/post ', [
    'json' => ['foo' => 'bar']
]);

Vì vậy đoạn code tại function handle() sẽ là

public function handle()
{
    $client = new Client();

    try {
        $client->request('POST', self::URL, [
            'json' => $this->checksumMessageQueue()
        ]);

        \Log::info('Sent message Queue Checksum Service APIs success at ' . Carbon::now() . '. Data : ' . json_encode($this->checksumMessageQueue()));
    } catch (RequestException $e) {
        \Log::info('Error sent message Queue Checksum Service APIs at ' . Carbon::now() . '. Error message ' . $e->getMessage() . '. Data : ' . json_encode($this->checksumMessageQueue()));
    }
}

Trong đoạn code trên URL là biến static mình tạo ở bên trên lưu địa chỉ api, ngoài ra mình còn cẩn thận log lại vào Laravel.log phòng trường hợp bên api không nhận được request thì còn có cái mà nói chuyện với họ. Đã xong phần logic, giờ chỉ cần config thời gian chạy của cronjob nữa là thành công. Mở file app/Console/Kernel.php thêm vào trong $commands

protected $commands = [
    MessagesFromSystem::class,
];

Và trong hàm schedule thêm vào đoạn code

protected function schedule(Schedule $schedule)
{
    $schedule->command('api:numberMessageFromSystems')
        ->dailyAt('02:00');
}

Nội dung 2 đoạn code này đơn giản là thêm class MessagesFromSystem để hàm schedule có thể nhận diện và tìm kiếm cronjob tên là 'api:numberMessageFromSystems' , và đoạn ->dailyAt('02:00') là định nghĩa cronjob chạy tự động lúc 2h sáng hàng ngày. Chi tiết các cách định nghĩa khác (1 tiếng 1 lần, Chạy hàng ngày lúc nửa đêm, Cứ mỗi quý chạy một lần…) các bạn xem tại đây Cảm ơn các bạn đã theo dõi bài viết của mình, có thiếu sót gì các bạn góp ý dưới phần comment giúp mình nhé, à mình bonus thêm toàn bộ đoạn code vừa viết để các bạn tiện theo dõi

<?php

namespace App\Console\Commands;

use DB;
use Carbon\Carbon;
use GuzzleHttp\Client;
use Illuminate\Console\Command;
use GuzzleHttp\Exception\RequestException;

class MessagesFromSystem extends Command
{
    const SERVICE_NAME = 'teko.wh;

    const URL = 'https://teko-msgqueue.com/api/message_log/';

    protected $signature = 'api:numberMessageFromSystems';

    protected $description = 'Add data to api about number message from systems';

    public function checksumMessageQueue()
    {
        $checksumData = [];

        $checksumData['service_name'] = self::SERVICE_NAME;

        $logPublishQueues = DB::table('log_publish_queues')
            ->select('routing_key', DB::raw('COUNT(1) as quantity'))
            ->whereDate('created_at', Carbon::yesterday()->toDateString())
            ->groupBy('routing_key')
            ->get();

        foreach ($logPublishQueues as $logPublishQueue) {
            $checksumData['sent'][] = [
                'routing_key' => $logPublishQueue->routing_key,
                'quantity' => $logPublishQueue->quantity
            ];
        }

        $logConsumerQueues = DB::table('log_consumer_queues')
            ->select('routing_key', DB::raw('COUNT(1) as quantity'))
            ->whereDate('created_at', Carbon::yesterday()->toDateString())
            ->groupBy('routing_key')
            ->get();

        foreach ($logConsumerQueues as $logConsumerQueue) {$checksumData['received'][] = [
                'routing_key' => $logConsumerQueue->routing_key,
                'quantity' => $logConsumerQueue->quantity
            ];
        }

        return $checksumData;
    }

    public function handle()
    {
        $client = new Client();

        try {
            $client->request('POST', self::URL, [
                'json' => $this->checksumMessageQueue()
            ]);

            \Log::info('Sent message Queue Checksum Service APIs success at ' . Carbon::now() . '. Data : ' . json_encode($this->checksumMessageQueue()));
        } catch (RequestException $e) {
            \Log::info('Error sent message Queue Checksum Service APIs at ' . Carbon::now() . '. Error message ' . $e->getMessage() . '. Data : ' . json_encode($this->checksumMessageQueue()));
        }
    }
}

<?php

namespace App\Console;

use App\Console\Commands\MessagesFromSystem;
use Illuminate\Console\Scheduling\Schedule;
use Illuminate\Foundation\Console\Kernel as ConsoleKernel;

class Kernel extends ConsoleKernel
{
    /**
     * The Artisan commands provided by your application.
     *
     * @var array
     */
    protected $commands = [
        MessagesFromSystem::class,
    ];

    /**
     * Define the application's command schedule.
     *
     * @param  \Illuminate\Console\Scheduling\Schedule  $schedule
     * @return void
     */
    protected function schedule(Schedule $schedule)
    {
        $schedule->command('api:numberMessageFromSystems')
            ->dailyAt('02:00');
    }

    /**
     * Register the Closure based commands for the application.
     *
     * @return void
     */
    protected function commands()
    {
        require base_path('routes/console.php');
    }
}