Forum dạng SPA với Laravel và Vue.js – Phần 3: Xây dựng API

Trong các phần trước chúng ta đã xây dựng xong phần khung cho Fontend, bài viết này sẽ tập trung vào phần Backend của ứng dụng. Mô hình của một ứng dụng đơn trang SPA có dạng như sau:

Mô hình ứng dụng đa tầng sử dụng Laravel + Vuejs

Khi đó giữa Fontend và Backend sẽ nói chuyện với nhau qua các API, đây là cách chia sẻ dữ liệu trong một ứng dụng đa tầng. Đến đây thì bạn biết tại sao bài viết này chỉ tập trung xây dựng API rồi chứ.

Chúng ta điểm qua các công việc cần thực hiện trong phần này:

  • Thiết lập cấu hình database trong Laravel
  • Tạo bảng dữ liệu, quan hệ giữa các bảng và đưa vào dữ liệu mẫu với Laravel migrate và seeding, Laravel Eloquent ORM.
  • Viết API thao tác dữ liệu với Category, Topic và Comment.

1. Thiết lập cấu hình database trong Laravel

Laravel cho phép làm việc với nhiều các Hệ cơ sở dữ liệu khác nhau như mySQL, SQL server, SQLite… trong dự án SPA-Forum chúng ta sẽ cùng làm việc với mySQL do đây là một Hệ CSDL được dùng nhiều nhất cho mã nguồn mở hiện nay. Mở file .env và đưa vào các đoạn thiết lập cấu hình kết nối đến mySQL như sau:

DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=spa-forum
DB_USERNAME=root
DB_PASSWORD=''

Tạo ra một cơ sở dữ liệu tên là spa-forum trong mySQL, phần DB_PASSWORD bạn đưa vào mật khẩu của user root, trong máy cá nhân của tôi thì mật khẩu của root là không có gì. Như vậy, chúng ta đã thiết lập xong phần cấu hình để kết nối đến database.

2. Tạo bảng các đối tượng trong ứng dụng SPA-Forum bằng Laravel Migrate

Trong các diễn đàn, chúng ta thấy có các đối tượng như sau: Người dùng (User), Danh mục (Category), Chủ đề (Topic) và Bình luận (Comment), thực hiện thiết kế CSDL như sau:

Thiết kế CSDL cho SPA forum

Thực hiện tạo ra các Model và file migrate với Category, Topic và Comment:

c:\xampp\htdocs\spa-forum>php artisan make:model Category -m
Model created successfully.
Created Migration: 2017_05_12_050811_create_categories_table

c:\xampp\htdocs\spa-forum>php artisan make:model Topic -m
Model created successfully.
Created Migration: 2017_05_12_050917_create_topics_table

c:\xampp\htdocs\spa-forum>php artisan make:model Comment -m
Model created successfully.
Created Migration: 2017_05_12_050935_create_comments_table

Riêng với User thì Laravel đã tạo sẵn migrate cho tính năng Laravel Authentication do đó không cần tạo model User. Thêm nội dung tạo các trường trong các bảng liên quan ở các file migrate tương ứng:

2017_05_12_050811_create_categories_table:

public function up()
{
    Schema::create('categories', function (Blueprint $table) {
        $table->increments('id');
        $table->string('name')->unique();
        $table->string('description');
        $table->timestamps();
    });
}

2017_05_12_050917_create_topics_table:

public function up()
{
    Schema::create('topics', function (Blueprint $table) {
        $table->increments('id');
        $table->integer('category_id')->unsigned();
        $table->string('title');
        $table->text('body');
        $table->integer('views')->default(0);
        $table->timestamps();

        $table->foreign('category_id')
            ->references('id')->on('categories')
            ->onDelete('cascade');
    });
}

2017_05_12_050935_create_comments_table:

public function up()
{
    Schema::create('comments', function (Blueprint $table) {
        $table->increments('id');
        $table->integer('topic_id')->unsigned();
        $table->integer('user_id')->unsigned();
        $table->text('body');
        $table->timestamps();

        $table->foreign('topic_id')
            ->references('id')->on('topics')
            ->onDelete('cascade');

        $table->foreign('user_id')
            ->references('id')->on('users')
            ->onDelete('cascade');
    });
}

Chú ý, tên các file migrate này khi bạn chạy lệnh tạo Model và file migrate có thể khác với trong bài do tên file migrate chứa cả yếu tố thời gian hiện tại khi thực hiện lệnh artisan make:model. Sau khi đã chuẩn bị xong, chúng ta thực hiện artisan migrate để tạo các bảng trong cơ sở dữ liệu:

c:\xampp\htdocs\spa-forum>php artisan migrate
Migration table created successfully.
Migrating: 2014_10_12_000000_create_users_table
Migrated:  2014_10_12_000000_create_users_table
Migrating: 2014_10_12_100000_create_password_resets_table
Migrated:  2014_10_12_100000_create_password_resets_table
Migrating: 2017_05_12_050811_create_categories_table
Migrated:  2017_05_12_050811_create_categories_table
Migrating: 2017_05_12_050917_create_topics_table
Migrated:  2017_05_12_050917_create_topics_table
Migrating: 2017_05_12_050935_create_comments_table
Migrated:  2017_05_12_050935_create_comments_table

Nếu bạn gặp lỗi max key length is 767 bytes như sau:

c:\xampp\htdocs\spa-forum>php artisan migrate
Migration table created successfully.


  [Illuminate\Database\QueryException]
  SQLSTATE[42000]: Syntax error or access violation: 1071 Specified key was t
  oo long; max key length is 767 bytes (SQL: alter table `users` add unique `
  users_email_unique`(`email`))



  [PDOException]
  SQLSTATE[42000]: Syntax error or access violation: 1071 Specified key was t
  oo long; max key length is 767 bytes

Thì do Laravel 5.4 thay đổi character set sang utf8mb4, để fix thêm vào phương thức boot() của AppServiceProvider.php mặc định độ dài chuỗi như sau:

use Illuminate\Support\Facades\Schema;

public function boot()
{
    Schema::defaultStringLength(191);
}

Mở database spa-forum chúng ta thấy các bảng đã được tạo ra như mong muốn.

Cơ sở dữ liệu cho SPA Forum

3. Thiết lập quan hệ giữa các đối tượng trong Laravel

Gợi ý: Xem Xử lý quan hệ trong CSDL với Laravel Eloquent ORM trước khi thực hiện, bạn sẽ hiểu hơn cách thức thiết lập các quan hệ một – một, một – nhiều, nhiều -nhiều.

Trong thiết kế CSDL ở đầu bài viết chúng ta có các quan hệ như sau:

3.1 Quan hệ một – nhiều giữa Category và Topic

Thêm phương thức topic vào model Category

public function topics()
{
    return $this->hasMany(Topic::class);
}

Thêm phương thức category vào model Topic:

public function category()
{
    return $this->belongsTo(Category::class);
}

3.2 Quan hệ một nhiều giữa Topic và Comment, User và Comment

Thêm phương thức comment vào model Topic:

public function comments()
{
    return $this->hasMany(Comment::class);
}

public function user()
{
    return $this->belongsTo(User::class);
}

và phương thức topic vào model Comment:

public function user()
{
    return $this->belongsTo(User::class);
}

public function topic()
{
    return $this->belongsTo(Topic::class);
}

4. Đưa dữ liệu mẫu vào CSDL bằng Laravel Seeding

Trong bước tiếp theo này, chúng ta sẽ tạo ra dữ liệu mẫu cho các bảng liên quan, với Laravel việc này thật đơn giản vì đã có Laravel Seeding. Thực hiện định nghĩa việc tạo ra model bằng Faker trong database/factories/ModelFactory.php như sau:

$factory->define(App\User::class, function (Faker\Generator $faker) {
    static $password;

    return [
        'name' => $faker->name,
        'email' => $faker->unique()->safeEmail,
        'password' => $password ?: $password = bcrypt('123456'),
        'remember_token' => str_random(10),
    ];
});

$factory->define(App\Category::class, function (Faker\Generator $faker) {
    return [
        'name' => implode(' ', $faker->words(2)),
        'description' => $faker->sentence(),
    ];
});

$factory->define(App\Topic::class, function (Faker\Generator $faker) {
    return [
        'category_id' => null,
        'user_id' => 1,
        'title' => $faker->sentence,
        'body' => $faker->paragraph(7),
        'views' => $faker->numberBetween(0, 10000),
        'created_at' => $faker->datetimeBetween('-5 months'),
    ];
});

$factory->define(App\Comment::class, function (Faker\Generator $faker) {
    return [
        'topic_id' => 1,
        'user_id' => 1,
        'body' => $faker->sentence,
    ];
});

Tiếp theo, tạo một seeder bằng câu lệnh artisan make:seed, chúng ta chỉ cần tạo dữ liệu mẫu cho Category và Topic do đó thực hiện lệnh như sau:

c:\xampp\htdocs\spa-forum>php artisan make:seed CategoriesTopicsSeeder
Seeder created successfully.

Nó sẽ tạo ra file CategoriesTopicsSeeder.php trong thư mục database/seeds, thay đổi nội dung phương thức run() trong đó như sau:

public function run()
{
    Topic::truncate();
    Category::truncate();

    factory(Category::class, 5)->create()->each(function($c) {
        $c->topics()->saveMany(
            factory(Topic::class, 5)->create([
                'category_id' => $c->id
            ])
        );
    });
}

Đoạn mã này khi được chạy nó sẽ truncate toàn bộ các bản ghi đang có trong hai bảng Categories và Topics, sau đó nó thực hiện tạo ra 5 bản ghi trong Categories và tương ứng với mỗi bản ghi này nó tạo ra 5 bản ghi liên quan đến category_id trong Topics, tức là bảng Topics sẽ có 25 bản ghi mẫu.

Cuối cùng, chúng ta đăng ký seeder này trong file database/seeds/DatabaseSeeder.php để sử dụng:

public function run()
{
    factory(App\User::class)->create(['name' => 'Nguyễn Văn A', 'email' => 'test@allaravel.com']);
    $this->call(CategoriesTopicsSeeder::class);
}

Khi chạy, seeder này cũng tạo ra một user test@allaravel.com để sử dụng với mật khẩu mặc định là “123456”. Ok, vậy là đã chuẩn bị xong, chúng ta thực hiện chạy sinh dữ liệu mẫu bằng câu lệnh artisan db:seed.

c:\xampp\htdocs\spa-forum>php artisan db:seed
Seeding: CategoriesTopicsSeeder

Kiểm tra lại các bảng trong cơ sở dữ liệu chúng ta đã thấy có dữ liệu mẫu được đưa vào.

5. Xây dựng API thao tác dữ liệu với Category, Topic và Comment

Khi xây dựng các API, chúng ta muốn xác định xem sẽ gửi dữ liệu gì đến trình duyệt khi yêu cầu, việc này có thể thiết lập dễ dàng thông qua thuộc tính $visible trong Model. Một điều tuyệt vời nữa là Laravel cho phép thêm các thuộc tính vào model mà các thuộc tính này không liên quan đến các cột trong bảng trên cơ sở dữ liệu, chúng ta thực hiện thông qua thuộc tính $appends.

Bắt đầu với Model Topic, chúng ta sẽ thêm thuộc tính time vào Model này, time sẽ chứa giá trị của created_at, time sẽ được dùng filter trong Vue để ra được dạng có thể đọc được. Khai báo thuộc tính $appends trong Model Topic như sau:

protected $appends = ['time'];

và định nghĩa accessor cho thuộc tính này:

public function getTimeAttribute()
{
    return $this->created_at->timestamp;
}

Cuối cùng, chúng ta xác định các trường sẽ hiển thị trong JSON reponse khi gọi các API thông qua thuộc tính $visible:

protected $visible = ['id', 'title', 'body', 'views', 'time', 'category_id'];

Như vậy, Model Topic (file app\Topic.php) sẽ có nội dung sau khi tổng hợp lại như sau:

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class Topic extends Model
{
    protected $visible = ['id', 'title', 'body', 'views', 'time', 'category_id'];
    protected $appends = ['time'];

    public function getTimeAttribute()
    {
        return $this->created_at->timestamp;
    }

    public function category()
    {
        return $this->belongsTo(Category::class);
    }

    public function user()
    {
        return $this->belongsTo(User::class);
    }

    public function comments()
    {
        return $this->hasMany(Comment::class);
    }
}

Với Model Category chúng ta cũng thực hiện thêm trường numberOfTopics chứa số lượng chủ đề trong danh mục và cấu hình các trường sẽ hiển thị trong JSON response như sau:

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class Category extends Model
{
    protected $visible = ['id', 'name', 'description', 'numberOfTopics'];
    protected $appends = ['numberOfTopics'];

    public function topics()
    {
        return $this->hasMany(Topic::class);
    }

    public function getNumberOfTopicsAttribute()
    {
        return $this->topics->count();
    }
}

Thiết lập các route cho API

Laravel từ phiên bản 5.3 đã tách biệt các route cho web và cho api ra các file khác nhau, các route cho api sẽ được đưa vào routes/api.php. Chúng ta thêm vào các api xử lý category vào như sau:

Route::get('categories', 'CategoryController@index');
Route::get('categories/{id}/topics', 'CategoryController@topics');

Tiếp đến, chúng ta tạo Controller để thực hiện xử lý dữ liệu trước khi response dạng JSON. Sử dụng câu lệnh artisan make:controller để tạo CategoryController như sau:

c:\xampp\htdocs\spa-forum>php artisan make:controller CategoryController
Controller created successfully.

Thêm các phương thức index() và topics() vào CategoryController này (file app\Http\Controllers\CategoryController.php):

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;

use App\Http\Requests;
use App\Category;

class CategoryController extends Controller
{
    public function index()
    {
        return Category::all();
    }

    public function topics($categoryId)
    {
        $page = request()->input('page');

        \Illuminate\Pagination\Paginator::currentPageResolver(function () use ($page) {
            return $page;
        });

        $topics = Category::findOrFail($categoryId)
            ->topics()
            ->orderBy('created_at', 'desc')
            ->paginate(5);

        if ($topics->isEmpty()) {
            return response('The provided page exceeds the available number of pages', 404);
        }

        return $topics;

    }
}

OK, vậy là chúng ta đã thực hiện xong api xử lý category, chúng ta thử thực hiện một yêu cầu api http://spa-forum.dev/api/categories xem thế nào.

JSON response qua API trong SPA Forum

Ok, như vậy chúng đã đã tạo ra các api xử lý việc lấy dữ liệu về category như sau:

  • http://spa-forum.dev/api/categories: liệt kê tất cả các danh mục trong hệ thống diễn đàn
  • http://spa-forum.dev/api/categories/{id}/topics: liệt kê các topic của một danh mục trong diễn đàn.

Bước tiếp theo

Mục tiêu của loạt bài viết Xây dựng Forum dạng ứng dụng đơn trang SPA bằng Laravel + Vue.js là để giúp bạn đọc hiểu được một số vấn đề:

  • Cách thức xây dựng một ứng dụng Laravel.
  • Cách tích hợp framework khác vào Laravel.

Các bài viết không thể tường tận từng chi tiết mà chỉ đưa cho bạn các khái niệm và cách suy nghĩ logic để giải quyết bài toán lập trình, cuối loạt bài viết này sẽ có source code và trang demo giúp bạn đọc tìm hiểu kỹ càng thích hợp hơn.

Trong bài viết này, chúng ta đã xây dựng xong các API, bước tiếp theo là xây dựng các giao diện sử dụng các API được viết này hiển thị dữ liệu cho người dùng cuối. Chúng ta sẽ sử dụng vue-router và axios để thực hiện các công việc này cùng với Vue.js.

 

5 thoughts on “Forum dạng SPA với Laravel và Vue.js – Phần 3: Xây dựng API

  1. Làm sao đề khi xóa cái migrate ấy thì khi chạy lại câu lệnh:
    php artisan make:model Comment -m
    nó lại báo Model already exists!

    1. Nếu Model đã tồn tại thì bạn xóa file Model đó đi, ở đây là file Comment.php nằm trong thư mục app.

  2. Bài viết rất hay và chi tiết! Mình cũng đang tập tành tìm hiểu về laravel và vuejs. Cám ơn ad rất nhiều!

  3. khi chạy artisan db:seed để tạo dữ liệu mẫu nó báo lỗi “SQLSTATE[42000]: Syntax error or access violation: 1701 Cannot truncate a table referenced in a foreign key constraint (`spaforum`.`comments`, CONSTRAINT `comments_topic_id_foreign` FOREIGN KEY (`topic_id`) REFERENCES `spaforum`.`topics` (`id`)) (SQL: truncate `topics`)”

Add Comment