Tạo lệnh sort migration với Artisan Console Laravel (phần đầu)

Tạo lệnh sort migration với Artisan Console Laravel (phần đầu)

Nội dung bài viết


​Khi làm việc với Laravel framework, điều mình "khổ tâm" nhất đó chính là việc sắp xếp thứ tự các migration khi muốn thay đổi lại cấu trúc database, hoặc chỉ xớn xác không theo thứ tự khi khởi tạo chúng. Không biết các bạn có cảm thấy khó chịu như mình không khi phải xem xét các chuỗi prefix ngày tháng năm gì đó để sắp xếp vị trí của các migration. Mình thì rất lười mấy việc như thế này nên nghĩ tại sao không tự viết một lệnh có thể quản lý, sắp xếp vị trí của các migration thông qua terminal mà không phải đụng đến bất kỳ thao tác tay nào. Ok, hãy cùng theo dõi quá trình tạo lệnh sort migration với Artisan Console Laravel của mình nhé!

1. Ý tưởng

Cốt lõi chúng ta sẽ sử dụng lớp Illuminate\Console\Command trong Laravel để viết, các bạn có thể tham khảo tại Artisan Console để biết thêm chi tiết.

Ok, tiếp theo mình sẽ trình bày ý tưởng thực hiện chương trình sort migration này. Đầu tiên các bạn hãy quan sát hình bên dưới:

Quy trình chuyển định dạng tên migration cho việc sort miratgion trong Artisan Console Laravel

Vấn đề thiết yếu trước khi chúng ta thực hiện việc sắp xếp các migration chính là đưa prefiex của nó về định dạng chung để dễ dàng xử lý. Như hình bên trên thì mình sẽ đổi tên các migration trong project từ prefix có dạng là Y_m_d_Hisa về định dạng Y_m_d_position, với position sẽ là chuỗi số có 6 ký tự chạy từ 000001 đến 999999 tương ứng với vị trí của chính migration đó. Như vậy chúng đã giải quyết được vấn đề đầu tiên của việc thực hiện sort migration, đó chính là đã đưa cấu trúc tên phức tạp của nó về định dạng có thể quản lý.

Vấn đề cần giải quyết tiếp theo đó chính là việc update lại database khi vị trí migration bị thay đổi, nếu không thì các thao tác lệnh với migration lần tiếp theo sẽ xảy ra lỗi không mong muốn. Theo cá nhân mình thì không cần phải update database trong quá trình sort migration vì việc này xử lý rất "cồng kềnh" và mất khá nhiều thời gian. Mình sẽ giải quyết vấn đề này bằng cách reset migration database trước mỗi lần sort migration và hiển thị thông báo cho người dùng nhớ migrate lại database khi quá trình sắp xếp đã hoàn tất. Việc này tuy không tự động lắm nhưng nó cũng phần nào làm nhẹ nhàng đi quy trình xử lý thay vì phải tự động migrate database sau mồi lần sắp xếp, chưa nói đến việc trong dự án có các file seed nữa. Vì vậy theo mình hướng giải quyết này sẽ tốt hơn.

Cơ bản đã xong các vấn đề thiết yếu, việc cần làm tiếp theo chỉ là viết các tính năng sắp xếp cho chương trình sort migration. Cụ thể việc xây dựng và hướng giải thuật các tính năng như thế nào, các bạn hãy cùng mình đi qua phần tiếp theo.

Xem thêm: Tự viết lệnh make view trong Artisan Laravel

2. Tiến hành thực hiện

Trước tiên các bạn chuẩn bị cho mình một fresh project Laravel để chúng ta có thể bắt đầu ngay và luôn nhé!

2.1. Tạo command và khai báo

Việc này thì vô cùng đơn giản, bạn chỉ cần mở terminal lên và chạy lệnh:

php artisan make:command SortMigration

Tèn ten, một file app/Console/Commands/SortMigration.php lập tức sẽ được tạo trong source code của chúng ta. Yub, và đây là fresh code khi mới vừa tạo file:

<?php

namespace App\Console\Commands;

use Illuminate\Console\Command;

class SortMigration extends Command
{
    /**
     * The name and signature of the console command.
     *
     * @var string
     */
    protected $signature = 'command:name';

    /**
     * The console command description.
     *
     * @var string
     */
    protected $description = 'Command description';

    /**
     * Create a new command instance.
     *
     * @return void
     */
    public function __construct()
    {
        parent::__construct();
    }

    /**
     * Execute the console command.
     *
     * @return mixed
     */
    public function handle()
    {
        //
    }
}

Đầu tiên chúng ta cần đăng ký cú pháp command thông qua $signature. Dưới đây là cú pháp command cho toàn bộ tính năng sort migration của mình:

protected $signature = 'migrate:sort 
                            {migration? : The name of target migration that you want to sort} 
                            {--top : Push a migration to top} 
                            {--bottom : Push a migration to bottom} 
                            {--exchange= : Swapping the position of two migrations} 
                            {--above= : Put one migration onto another} 
                            {--bellow= : Put one migration under another}';

Ở đây chúng ta sẽ có 1 tham số và 5 tùy chọn:

  • Tham số migration: chính là tên migration mà chúng ta muốn sắp xếp, mình tạm gọi đây là target migration
  • Tùy chọn top: đẩy migration đã khai báo ở tham số lên vị trí đầu tiên
  • Tùy chọn bottom: đẩy migration đã khai báo ở tham số xuống vị trí dưới cùng
  • Tùy chọn exchange: ở tùy chọn này nhận giá trị là một migration, lệnh này giúp ta có thể hoán đổi vị trí giữa hai migration với nhau
  • Tùy chọn above: tùy chọn này nhận giá trị là một migration, giúp đưa target migration lên phía trên migration đã khai báo ở tùy chọn above
  • Tùy chọn bellow: tùy chọn này nhận giá trị là một migration, giúp đưa target migration xuống bên dưới migration đã khai báo ở tùy chọn bellow
  • Ngoài ra ta có còn cú pháp migrate:sort để xem danh sách migration hiển thị theo thứ tự đã sắp xếp

Đây là toàn bộ các chức năng trong chương trình của chúng ta. Quy tắc chung khi nhập tham số migration và giá trị tại cái tùy chọn trong command, ta chỉ nhập tên chính của migration đó. Chẳng hạn một migration có tên file đầy đủ là 2020_01_01_000001_create_users_table.php, thì tên migration cần nhập trên command chỉ cần create_users_table là đủ. Hiểu đơn giản hơn thì tên migration này sẽ trùng với tên dùng để khởi tạo migration trong lệnh php artisan make:migration name_migration. Từ giờ mình sẽ gọi tên migration này là "primary name migration" để các bạn dễ phân biệt. 

Về phần mô tả command thì mình chỉ viết nhiêu đây thôi:

protected $description = 'Sorting the migrations.';

Tiếp theo mình cần các bạn khai báo các biến và hằng bên dưới cho class SortMigration:

protected $migrationPath;

const MAX_LEN_POSITION_NUMBER = 6;
const POSITION_MIGRATION_PATTERN = '/[0-9]{4}\_[0-9]{2}\_[0-9]{2}\_([0-9]{6})\_.*\.php/';
const NAME_MIGRATION_PATTERN = '/[0-9]{4}\_[0-9]{2}\_[0-9]{2}\_[0-9]{6}\_(.*)\.php/';
const PREFIX_MIGRATION_PATTERN = '/([0-9]{4}\_[0-9]{2}\_[0-9]{2}\_[0-9]{6})\_.*/';

$migrationPath sẽ chứa đường dẫn thư mục database/migrations, việc này cần thiết cho các phương thức thao tác với file migration. Ngoài ra mình còn sử dụng thêm 4 hằng cho chương trình này:

  • MAX_LEN_POSITION_NUMBER: hằng này chứa số ký tự tối đa của chuỗi positon migration. Mình sẽ để là 6 ký tự cho khớp với định dạng thời gian His mặc định của Laravel
  • POSITION_MIGRATION_PATTERN: hằng này chứa chuỗi pattern dùng để lấy position của một migration
  • NAME_MIGRATION_PATTERN: hằng này chứa chuỗi pattern dùng để lấy tên của một migration
  • PREFIX_MIGRATION_PATTERN: hằng này chứa chuỗi pattern dùng để lấy prefix của một migration

Trong mỗi pattern, mình có sử dụng cặp () để có thể nhóm chuỗi mà mình muốn lấy.

Tại method __construct(), các bạn append dòng code này vào:

$this->migrationPath = database_path('migrations');

Như đã nói ở trên, chúng ta sẽ gán đường dẫn thư mục của database/migrations thông qua helper database_path() cho $migrationPath.

Các bạn hãy lập luồng xử lý cho phương thức handle() giúp mình như bên dưới:

public function handle()
{
    // Nhận tham số migration
    $migration = $this->argument('migration');

    // Định dạng migration đồng thời hiển thị danh sách migration theo thứ tự

    // Hiển thị lỗi nếu như tên migration không tồn tại

    // Phương thức đẩy một migration lên vị trí đầu tiên

    // Phương thức đẩy một migration xuống vị trí cuối cùng

    // Phương thức hoán đổi vị trí giữa hai migration

    // Phương thức đặt một migration phía trên một migration khác

    // Phương thức đặt một migration bên dưới một migration khác 

    // Phương thức hiển thị danh sách migration theo thứ tự

    // Hiển thị lỗi nếu như không nhập phương thức sắp xếp cho migration
}

Việc này giúp mình dễ dàng ghép nối giữa các mục nội dung trong bài lại với nhau một cách dễ hiểu, đồng thời giúp các bạn ráp code dễ dàng hơn.

Cuối cùng các bạn hãy copy đoạn inject các lớp hỗ trợ cho chương trình trong quá trình hoạt động vào nhé!

use Illuminate\Console\Command;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Str;

Mình dùng facade File để thao tác các migration dưới vai trò là một file. Lớp hỗ trợ Str giúp chúng ta trong quá trình valid, match tên migration thông qua các pattern đã thông báo ở trên.

Đến đây thì việc cho công đoạn này đã hoàn tất, bây giờ chúng ta sẽ bắt tay xây dựng các phương thức xử lý thông tin migration cho chương trình này.

2.2. Viết các phương thức lấy thông tin của migration

Ở mục này chúng ta sẽ định nghĩa các phương thức làm việc với thông tin của migration. Các method này sẽ hỗ trợ cho việc sắp xếp các migration một cách dễ dàng.

2.2.1. Viết phương thức lấy danh sách migration trong source files

Cái này viết khá đơn giản, bạn chỉ cần sử dụng phương thức allFiles() trong lớp File với tham số truyền vào là $migrationPath. Các bạn copy đoạn code này và khai báo trong lớp SortMigration nhé!

protected function getMigrations()
{
    return File::allFiles($this->migrationPath);
}

Bạn có thể thử kiểm tra bằng cách chạy die dump trong phương thức handle() và chạy lệnh php artisan migrate:sort để xem kết quả.

dd($this->getMigrations());

Kết quả phương thức lấy danh sách migration trong source files sort migration Artisan Console Laravel

Quan sát hình các bạn có thể thấy được là nó sẽ trả về một mảng chứa các object Symfony\Component\Finder\SplFileInfo. Với lớp này ta có thể lấy thông tin file qua các method có sẵn, các bạn có thể tham khảo thêm về SplFileInfo để biết thêm chi tiết.

Sau khi test xong, bạn hãy bỏ dòng code die dump vừa rồi đi nhé!

2.2.2. Viết phương thức lấy tên file migration

Với phương thức này ta chỉ cần tham số $migration duy nhất là primary name migration hoặc cũng có thể là một tên file migration đầy đủ, tùy theo ngữ cảnh sử dụng. Phương thức sẽ trả về cho chúng ta tên file đầy đủ của migration đó.

protected function getFileNameMigration($migration)
{
    // Duyệt qua từng migration trong source files
    foreach ($this->getMigrations() as $migrationItem) {
        // Lấy tên file migration
        $fileNameMigration = $migrationItem->getFileName();

        // So sánh nếu $migration trùng khớp với tên file của migration
        // So sánh giữa $migration và kết quả của match pattern NAME_MIGRATION_PATTERN trả về
        // Nếu một trong hai phép so sánh đúng thì trả về tên file của migration và dừng tìm kiếm
        if (
            $fileNameMigration == $migration ||
            Str::of($fileNameMigration)->match(self::NAME_MIGRATION_PATTERN) == $migration
        ) return $fileNameMigration;
    }
        
    return null;
}

Cách hoạt động của nó rất đơn giản, mình chỉ việc duyệt qua từng file migration bằng phương thức getMigrations() đã định nghĩa. Tên file migration sẽ được lấy thông qua method getFileName() mà lớp SplFileInfo cung cấp. Sau đó mình chỉ việc thực hiện hai phép so sánh:

  1. So sánh $migration và tên file migration (trường hợp này do $migration đã là tên file migration rồi)
  2. So sánh $migration đầu vào và chuỗi được bóc tách từ method match() với pattern NAME_MIGRATION_PATTERN.

Nếu một trong hai phép so sánh đúng thì chỉ cần trả về tên file migration tương ứng và kết thúc vòng lặp, ngược lại nếu không có file migration nào tương ứng với $migration thì sẽ trả về NULL. Và method này cũng dùng để kiểm tra xem một migration nhập từ command có tồn tại hay không.

Nếu các bạn thắc mắc về cú pháp Str::of($fileNameMigration)->match(self::NAME_MIGRATION_PATTERN) thì có thể tham khảo tại đây.

Ok, giờ thử die dump method getFileNameMigration() như sau:

$migration = $this->argument('migration');

dd($this->getFileNameMigration($migration));

Sau đó thì chạy lệnh:

Kết quả phương thức lấy tên file migration sort migration Artisan Console Laravel

Thử một case khác, lần này ta sẽ nhập tham số primary name migration không tồn tại trong source files.

Kết quả phương thức lấy tên file migration sort migration Artisan Console Laravel

Những kết quả đầy mong đợi phải không nào, bỏ dòng die dump đi và cùng mình qua phần tiếp theo thôi.

2.2.3. Viết phương thức lấy prefix của migration

Phương thức này bạn chỉ việc sử dụng match() trong lớp Str thông qua pattern PREFIX_MIGRATION_PATTERN mà ta đã định nghĩa sẵn.

protected function getPrefixMigration($migration)
{
    return (string) Str::of($this->getFileNameMigration($migration))->match(self::PREFIX_MIGRATION_PATTERN);
}

Như các bạn có thể thấy tham số trong method of(), mình có sử dụng thêm method getFileNameMigration() để lấy được tên file của migration đầu vào.

Thử die dump method này như sau:

$migration = $this->argument('migration');

dd($this->getPrefixMigration($migration));

Và đây là kết quả chúng ta thu được, kết quả hoàn toàn trùng khớp với prefix của file migration trong source files.

Kết quả phương thức lấy prefix migration sort migration Artisan Console Laravel

2.2.4. Viết phương thức lấy primary name của migration

Phương thức này cũng tương tự như trên, mình sẽ match thông qua NAME_MIGRATION_PATTERN. Các bạn copy đoạn code bên dưới:

protected function getNameMigration($migration)
{
    return (string) Str::of($this->getFileNameMigration($migration))->match(self::NAME_MIGRATION_PATTERN); 
}

Phần test mình sẽ để các bạn tự kiểm thực nhé!

2.2.5. Viết phương thức lấy vị trí của migration

Mình sẽ sử dụng pattern còn lại là POSTION_MIGRATION_PATTERN để lấy vị trí của migration.

protected function getPositionMigration($migration)
{
    return (int)(string) Str::of($this->getFileNameMigration($migration))->match(self::POSITION_MIGRATION_PATTERN); 
}

Phần test các bạn cũng tự mình kiểm thực nhé!

Ok, chúng ta đã kết thúc phần viết các phương thức lấy thông tin của migration. Qua các phần tiếp theo độ khó sẽ tăng thêm một xíu đấy, các bạn có thể thư giãn một chút rồi chúng ta tiếp tục.

2.3. Viết phương thức đổi vị trí của một migration

Giải thuật của phương thức này chính là việc bóc tách phần prefix cũ của migration và thay thế bằng prefix mới do code chúng ta định dạng. Vì thế mình sẽ viết một phương thức với tên là createFileNameSortedMigration() để xử lý việc này.

protected function createFileNameSortedMigration($migration, $pos)
{
    // Tạo position
    while (strlen($pos) < self::MAX_LEN_POSITION_NUMBER) {
        $pos = '0' . $pos;
    }

    // Tạo prefix mới
    $newPrefixMigration = date('Y_m_d')."_{$pos}";

    // Thay thế prefix mới
    return str_replace(
        $this->getPrefixMigration($migration), 
        $newPrefixMigration, 
        $this->getFileNameMigration($migration)
    );
}

Nhìn lướt qua ta có thể thấy nó không hề phức tạp. Chúng ta cần hai tham số $migration tương ứng với primary name của migration và $pos tương ứng với vị trí mà chúng ta muốn thay thế.

Vì position sẽ là chuỗi số 6 ký tự từ 000000 đến 999999 nên cần tạo một chuỗi số đủ 6 ký tự thông qua vòng lặp while với điều kiện strlen($pos) < self::MAX_LEN_POSITION_NUMBER, tham số $pos chính là vị trí mà ta muốn đặt. Bên dưới là một số ví dụ:

  • $pos = 1, chuỗi position sẽ là 000001
  • $post = 20, chuỗi position sẽ là 000020
  • ...

Việc tạo prefix mới thì mình không nói nữa, đây chỉ là phép ghép chuỗi đơn giản, định dạng Y_m_d mình sẽ lấy ngày tháng năm hiện tại khi chạy code, các bạn có thể thay đổi theo ý muốn của mình miễn sao giữ đúng định dạng date Y_m_d này là được. Còn về hàm str_replace() thì tham số tìm kiếm sẽ là prefix hiện tại của migration được lấy từ method getPrefixMigration(), tham số thay thế sẽ là chuỗi prefix mình vừa mới tạo và chuỗi subject sẽ là tên file của migration đó lấy từ getFileNameMigration(). Kết quả trả về chính là tên file migration cùng với prefix mới tương ứng với vị trí mà chúng ta truyền vào.

Tốt rồi, tiếp theo mình sẽ hướng dẫn các bạn viết method đổi vị trí của một migration. Bên dưới là đoạn code mà mình đã viết để làm việc này:

protected function sortMigration($migration, $pos)
{
    $newFileNameMigration = $this->createFileNameSortedMigration($migration, $pos);

    File::move(
        $this->migrationPath.DIRECTORY_SEPARATOR.$this->getFileNameMigration($migration),
        $this->migrationPath.DIRECTORY_SEPARATOR.$newFileNameMigration
    );
}

Yub, các tham số của phương thức sortMigration() này cũng có $migration$pos. Các bạn có thể thấy ngay tại dòng đầu tiên trong method, mình đã khởi tạo ngay tên file migration mới với position truyền vào method createFileNameSortedMigration() ta vừa mới viết ban nãy. Việc còn lại đơn giản chỉ là đổi tên file migration thông qua method File::move() mà thôi.

Như vậy ta đã có thể đổi vị trí của một migration rồi đấy, test ngay và luôn nào. Lần nay mình sẽ truyền tham số trực tiếp vào method luôn:

dd($this->sortMigration('create_users_table', 1));

Đây là kết quả của chúng ta sau khi chạy dòng lệnh php artisan migrate:sort

ket-qua-cua-phuong-thuc-sort-migration-artisan-console-laravel

Bắt đầu cảm thấy mọi chuyện đơn giản hơn nhiều rồi đúng không nào. Những phương thức tiếp theo của chúng ta sẽ xoay quanh sử dụng method sortMigration() này.

2.4. Viết phương thức định dạng vị trí migration

Phương thức này sẽ thực hiện thao tác format các migration trong khoảng mà ta quy định có trong project về định dạng chung như mình đã nói ở phần ý tưởng. Mình sẽ đặt tên cho nó là resetSortedMigrations(), phương thức này sẽ có 3 tham số đầu vào là $start, $from, và $end để quy định khoảng migration mà ta muốn format.

protected function resetSortedMigrations($start, $from, $to = null)
{
    // Lấy danh sách migration trong source files
    $migrations = $this->getMigrations();

    // Xử lý tùy chọn tham số $to, nếu NULL thì sẽ có giá trị là số lượng migration có trong source files
    $to = $to ?? count($migrations);

    // Dùng vòng lặp for chạy từ $from đến $to với bước nhảy là $pos
    for ($pos = $from; $pos <= $to; $pos++) {

        // Lấy primary name migration từ tên file migration
        $migration = $this->getNameMigration($migrations[$pos - 1]->getFileName());

        // Thực hiện đặt vị trí tương ứng giá trị $start cho migration
        $this->sortMigration($migration, $start++);
    }
}

Tham số $start chính là vị trí bắt đầu cho quá trình format và sẽ tăng dần qua mỗi vòng lặp, tham số $from chính là vị trí file migration bắt đầu và $end là vị trí file migration kết thúc. 

Chẳng hạn trong source files có các file migration sau:

2020_01_01_000001_create_a_table.php
2020_01_01_000002_create_b_table.php
2020_01_01_000003_create_c_table.php
2020_01_01_000004_create_d_table.php
2020_01_01_000005_create_e_table.php

Nếu mình mình chạy method resetSortedMigration() với $start = 6, $from = 2$end = 4 thì các migration của chúng ta sẽ như thế này:

2020_01_01_000001_create_a_table.php
2020_01_01_000006_create_b_table.php
2020_01_01_000007_create_c_table.php
2020_01_01_000008_create_d_table.php
2020_01_01_000005_create_e_table.php

Các file có vị trí bắt đầu từ 2 đến 4 đã được format lại vị trí bắt đầu từ 6 và cứ thế tăng dần. Ở đây mình không hiển thị theo sắp xếp file như trong thư mục để các bạn dễ quan sát, thực tế thì các file migration có positon từ 000006 đến 000008 phải nằm ở dưới file migration có position là 000005.

Còn chỗ lấy primary name của migration:

$migration = $this->getNameMigration($migrations[$pos - 1]->getFileName());

Tại sao key mình truyền vào không phải là $pos mà lại là $pos - 1, lý do là vì position của mình bắt đầu là 1, còn mảng bắt đầu là 0 nên mình phải trừ đi 1.

Ok, giờ chúng ta test xem method hoạt động ổn chưa nhé. Mình sẽ thử format toàn bộ file migration có trong source files như sau:

dd($this->resetSortedMigrations($start = 1, $from = 1));

Mình sẽ đặt tên biến tham số như các parameter hint để các bạn dễ quan sát. Ở đây mình không truyền tham số $end, tức nó sẽ chạy cho đến file migration cuối cùng. 

Và đây là kết quả sau khi chạy lệnh php artisan migrate:sort:

Kết quả phương thức định dạng vị trí migration sort migration Aritsan Console Laravel

Các migration đã đưa về định dạng theo ý tưởng ban đầu đề ra. Đây là nền tảng để xây dựng các tính năng sắp xếp về sau.

3. Còn nữa

Phù, mình sẽ xin tạm dừng tay tại đây, bài đã quá dài rồi, sẽ có một phần của bài này nữa nên các bạn yên tâm nhé! Ở phần cuối, chúng ta sẽ xây dựng các tính năng sắp xếp dựa trên các method nền tảng đã định nghĩa ở trong bài này. Hãy theo dõi Flipper để đón xem bài viết tiếp theo của mình, xin cảm ơn và hẹn gặp lại.

To be continue...

Xem tiếp: Tạo lệnh sort migration với Artisan Console Laravel (phần cuối)


Ủng hộ chúng tôi

Bình luận