PHP Fractal - Viết API's JSON đẹp hơn

Chắc hẳn các bạn cũng đã nhiều lần viết API cho ứng dụng di động rồi phải không, đa số chúng ta đều đang dump data và trả về response trực tiếp. Nó sẽ gặp một chút khó khăn và khó quản lý khi chúng ta muốn trả về những dữ liệu tuỳ chỉnh hoặc chỉ đơn giản là client muốn trả về như thế.

Có một giải pháp là Fractal. Nó cho phép chúng ta tạo ra một lớp chuyển đổi mới cho các models trước khi trả về chúng như là một response. Cách làm này rất linh hoạt và dễ dàng tích hợp vào bất kỳ ứng dụng hoặc framework nào.

Cài đặt

Chúng ta sẽ sử dụng Laravel 5.4 để xây dựng một ví dụ và tích hợp các gói Fractal với nó, trước hết tạo ra một ứng dụng Laravel mới sử dụng trình cài đặt hoặc thông qua Composer.

laravel new demo

hoặc

composer create-project laravel/laravel demo

Sau đó, bên trong thư mục, require Fractal package.

composer require league/fractal

Database của chúng ta bao gồm bảng users và bảng roles. Mỗi user có 1 role, và mỗi role có một danh sách các permissions.

// app/User.php

class User extends Authenticatable
{
    protected $fillable = [
        'name',
        'email',
        'password',
        'role_id',
    ];

    protected $hidden = [
        'password', 'remember_token',
    ];

    /**
     * @return \Illuminate\Database\Eloquent\Relations\BelongsTo
     */
    public function role()
    {
        return $this->belongsTo(Role::class);
    }
}
// app/Role.php

class Role extends Model
{
    protected $fillable = [
        'name',
        'slug',
        'permissions'
    ];

    /**
     * @return \Illuminate\Database\Eloquent\Relations\HasMany
     */
    public function users()
    {
        return $this->hasMany(User::class);
    }
}

Tạo các Tranformers

Chúng ta sẽ tạo ra một tranformer cho mỗi models. Class UserTransformer trông như thế này:

// app/Transformers/UserTransformer.php

namespace App\Transformers;

use App\User;
use League\Fractal\TransformerAbstract;

class UserTransformer extends TransformerAbstract
{
    public function transform(User $user)
    {
        return [
            'name' => $user->name,
            'email' => $user->email
        ];
    }
}

Đó là tất cả những gì cần để tạo ra một transfomer! Nó chỉ biến đổi dữ liệu trong một cách mà có thể được quản lý bởi các developer, và không dùng tới ORM hay một repository.

Chúng ta extends TransformerAbstract class và định nghĩa tranform method sẽ được gọi với một User instance. Làm điều tương tự như vậy với RoleTransformer class.

namespace App\Transformers;

use App\Role;
use League\Fractal\TransformerAbstract;

class RoleTransformer extends TransformerAbstract
{
    public function transform(Role $role)
    {
        return [
            'name' => $role->name,
            'slug' => $role->slug,
            'permissions' => $role->permissions
        ];
    }
}

Tạo các Controllers

Các controllers nên chuyển đổi các dữ liệu trước khi gửi nó lại cho người sử dụng. Chúng ta sẽ làm việc với UsersController class và chỉ định nghĩa indexshow action cho lần này.

// app/Http/Controllers/UsersController.php

class UsersController extends Controller
{
    /**
     * @var Manager
     */
    private $fractal;

    /**
     * @var UserTransformer
     */
    private $userTransformer;

    function __construct(Manager $fractal, UserTransformer $userTransformer)
    {
        $this->fractal = $fractal;
        $this->userTransformer = $userTransformer;
    }

    public function index(Request $request)
    {
        $users = User::all(); // Get users from DB
        $users = new Collection($users, $this->userTransformer); // Create a resource collection transformer
        $users = $this->fractal->createData($users); // Transform data

        return $users->toArray(); // Get transformed array of data
    }
}

index action sẽ truy vấn tất cả các users từ database, tạo ra một resource collection với list các users và tranformer, và sau đó thực hiện quá trình chuyển đổi.

{
  "data": [
    {
      "name": "Nyasia Keeling",
      "email": "[email protected]"
    },
    {
      "name": "Laron Olson",
      "email": "[email protected]"
    },
    {
      "name": "Prof. Fanny Dach III",
      "email": "[email protected]"
    },
    {
      "name": "Athena Olson Sr.",
      "email": "[email protected]"
    }
    // ...
  ]
}

Tất nhiên, chúng ta sẽ không trả về tất cả các users cùng một lúc, và nên sử dụng một paginator cho việc này.

Phân trang

Laravel có xu hướng làm cho mọi thứ đơn giản. Chúng ta có thể thực hiện phân trang như thế này:

$users = User::paginate(10);

Nhưng để làm công việc này với Fractal, chúng ta có thể cần thêm một chút code để chuyển đổi dữ liệu trước khi gọi các paginator.

// app/Http/Controllers/UsersController.php

class UsersController extends Controller
{
    // ...

    public function index(Request $request)
    {
        $usersPaginator = User::paginate(10);

        $users = new Collection($usersPaginator->items(), $this->userTransformer);
        $users->setPaginator(new IlluminatePaginatorAdapter($usersPaginator));

        $users = $this->fractal->createData($users); // Transform data

        return $users->toArray(); // Get transformed array of data
    }
}

Bước đầu tiên là để đánh số trang dữ liệu từ model. Tiếp theo, tạo ra một resource collection như trước đây, và sau đó thiết lập paginator vào collection.

Fractal cung cấp một paginator adapter cho Laravel để chuyển đổi các LengthAwarePaginator class, và nó cũng có cho cả Symfony và Zend.

{
    "data": [
        {
            "name": "Nyasia Keeling",
            "email": "[email protected]"
        },
        {
            "name": "Laron Olson",
            "email": "[email protected]"
        },
        // ...
    ],
    "meta": {
        "pagination": {
            "total": 50,
            "count": 10,
            "per_page": 10,
            "current_page": 1,
            "total_pages": 5,
            "links": {
                "next": "http://demo.vaprobash.dev/users?page=2"
            }
        }
    }

}

Chú ý rằng nó thêm trường bổ sung cho pagination. Bạn có thể đọc thêm về pagination trong tài liệu.

Include Sub-Resources

Bây giờ chúng ta đã trở nên quen thuộc hơn với Fractal, đây là thời gian để tìm hiểu xem làm thế nào để trả về response include sub-resources khi nó được request từ người sử dụng.

Chúng ta có thể request thêm resources kèm theo response như sau http://demo.dev/users?include=role . Tranformer có thể tự động phát hiện những gì được request và phân tích tham số include.

// app/Transformers/UserTransformer.php

class UserTransformer extends TransformerAbstract
{
    protected $availableIncludes = [
        'role'
    ];

    public function transform(User $user)
    {
        return [
            'name' => $user->name,
            'email' => $user->email
        ];
    }

    public function includeRole(User $user)
    {
        return $this->item($user->role, App::make(RoleTransformer::class));
    }
}

Thuộc tính $availableIncludes nói với tranformer rằng chúng ta có thể cần phải include một số dữ liệu thêm với response. Nó sẽ gọi includeRole method nếu include query parameter là role.

// app/Http/Controllers/UsersController.php

class UsersController extends Controller
{
    // ...

    public function index(Request $request)
    {
        $usersPaginator = User::paginate(10);

        $users = new Collection($usersPaginator->items(), $this->userTransformer);
        $users->setPaginator(new IlluminatePaginatorAdapter($usersPaginator));

        $this->fractal->parseIncludes($request->get('include', '')); // parse includes
        $users = $this->fractal->createData($users); // Transform data

        return $users->toArray(); // Get transformed array of data
    }
}

Dòng $ this->fractal->parseIncludes chịu trách nhiệm phân tích include query parameter. Nếu chúng ta request dách của người sử dụng chúng ta sẽ thấy một cái gì đó như thế này:

{
    "data": [
        {
            "name": "Nyasia Keeling",
            "email": "[email protected]",
            "role": {
                "data": {
                    "name": "User",
                    "slug": "user",
                    "permissions": [ ]
                }
            }
        },
        {
            "name": "Laron Olson",
            "email": "[email protected]",
            "role": {
                "data": {
                    "name": "User",
                    "slug": "user",
                    "permissions": [ ]
                }
            }
        },
        // ...
    ],
    "meta": {
        "pagination": {
            "total": 50,
            "count": 10,
            "per_page": 10,
            "current_page": 1,
            "total_pages": 5,
            "links": {
                "next": "http://demo.vaprobash.dev/users?page=2"
            }
        }
    }
}

Nếu các users có một danh sách các roles, chúng ta có thể thay đổi tranformer như thế này:

// app/Transformers/UserTransformer.php

class UserTransformer extends TransformerAbstract
{
    protected $availableIncludes = [
        'roles'
    ];

    public function transform(User $user)
    {
        return [
            'name' => $user->name,
            'email' => $user->email
        ];
    }

    public function includeRoles(User $user)
    {
        return $this->collection($user->roles, App::make(RoleTransformer::class));
    }
}

Khi đó muốn kèm theo sub-resources, chúng ta có thể tác tới nest relations bằng cách sử dụng dấu chấm(.). Hãy tưởng tượng rằng 1 role có một danh sách các permissions được lưu trữ trong một bảng riêng biệt và chúng ta muốn một danh sách các users với role và các permissions của chúng. Có thể làm như thế này include=role.permissions.

Đôi khi, chúng ta cần phải include một số relations cần thiết theo mặc định, như địa chỉ chẳng hạn. Có thể làm điều này bằng cách sử dụng $defaultIncludes property trong tranformer.

class UserTransformer extends TransformerAbstract
{
    // ...

    protected $defaultIncludes = [
        'address'
    ];

    // ...
}

Một trong những điều tôi thực sự yêu thích về Fractal package là khả năng truyền thêm các parameters vào include parameter . Một ví dụ điển hình trong tài liệu hướng dẫn là order by. Chúng ta có thể áp dụng nó như trong ví dụ này:

// app/Transformers/RoleTransformer.php

use App\Role;
use Illuminate\Support\Facades\App;
use League\Fractal\ParamBag;
use League\Fractal\TransformerAbstract;

class RoleTransformer extends TransformerAbstract
{
    protected $availableIncludes = [
        'users'
    ];

    public function transform(Role $role)
    {
        return [
            'name' => $role->name,
            'slug' => $role->slug,
            'permissions' => $role->permissions
        ];
    }

    public function includeUsers(Role $role, ParamBag $paramBag)
    {
        list($orderCol, $orderBy) = $paramBag->get('order') ?: ['created_at', 'desc'];

        $users = $role->users()->orderBy($orderCol, $orderBy)->get();

        return $this->collection($users, App::make(UserTransformer::class));
    }
}

Phần quan trọng ở đây là list($orderCol, $orderBy) = $paramBag->get('order') ?: ['created_at', 'desc']; sẽ lấy về order parameter từ users include query parameter sau đó sử dụng trong query builder.

Bây giờ chúng ta có thể order dach sách các users bằng cách truyền vào các tham số (/roles?include=users:order(name|asc)). Bạn có thể đọc thêm về include resources trong tài liệu.

Nhưng, những gì nếu người dùng không có bất kỳ role? Nó sẽ xảy ra một lỗi bởi vì nó đang cần dữ liệu hợp lệ thay vì null. Hãy xóa các quan hệ từ các response thay vì hiển thị nó với một giá trị null.

// app/Transformers/UserTransformer.php

class UserTransformer extends TransformerAbstract
{
    protected $availableIncludes = [
        'roles'
    ];

    public function transform(User $user)
    {
        return [
            'name' => $user->name,
            'email' => $user->email
        ];
    }

    public function includeRoles(User $user)
    {
        if (!$user->role) {
            return null;
        }

        return $this->collection($user->roles, App::make(RoleTransformer::class));
    }
}

Eager Loading

Bởi vì Eloquent sẽ lazy load các models khi truy cập tới chúng, chúng ta có thể gặp phải n + 1 các vấn đề. Điều này có thể được giải quyết bằng eager loading relations để tối ưu hóa các truy vấn.

class UsersController extends Controller
{

    // ...

    public function index(Request $request)
    {
        $this->fractal->parseIncludes($request->get('include', '')); // parse includes

        $usersQueryBuilder = User::query();
        $usersQueryBuilder = $this->eagerLoadIncludes($request, $usersQueryBuilder);
        $usersPaginator = $usersQueryBuilder->paginate(10);

        $users = new Collection($usersPaginator->items(), $this->userTransformer);
        $users->setPaginator(new IlluminatePaginatorAdapter($usersPaginator));
        $users = $this->fractal->createData($users); // Transform data

        return $users->toArray(); // Get transformed array of data
    }

    protected function eagerLoadIncludes(Request $request, Builder $query)
    {
        $requestedIncludes = $this->fractal->getRequestedIncludes();

        if (in_array('role', $requestedIncludes)) {
            $query->with('role');
        }

        return $query;
    }
}

Bằng cách này, chúng ta sẽ không có bất kỳ truy vấn thêm khi truy cập vào các model relations.

Tham khảo


All Rights Reserved