Эта статья была рецензирована Вираджем Хатавкаром . Спасибо всем рецензентам SitePoint за то, что сделали контент SitePoint как можно лучше!
Если вы уже создали API ранее, держу пари, что вы привыкли сбрасывать данные напрямую в качестве ответа. Это может быть не вредно, если все сделано правильно, но есть практические альтернативы, которые могут помочь решить эту небольшую проблему.
Одним из доступных решений является Fractal . Это позволяет нам создавать новый слой преобразования для наших моделей, прежде чем возвращать их в качестве ответа. Он очень гибкий и простой в интеграции в любое приложение или фреймворк.
Установка
Мы будем использовать приложение Laravel 5.3, чтобы создать пример и интегрировать с ним пакет Fractal, поэтому давайте создадим новое приложение Laravel с помощью установщика или через Composer.
laravel new demo
или
composer create-project laravel/laravel demo
Затем внутри папки нам требуется пакет Fractal.
composer require league/fractal
Создание базы данных
Наша база данных содержит таблицу users
roles
У каждого пользователя есть роль, и у каждой роли есть список разрешений.
// 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);
}
}
Создание Трансформеров
Мы собираемся создать трансформатор для каждой модели. Наш класс UserTransformer
// 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
];
}
}
Да, это все, что нужно для создания трансформатора! Он просто преобразует данные таким способом, которым может управлять разработчик, а не оставлять их в ORM или хранилище.
Мы расширяем класс TransformerAbstract
transform
User
То же самое относится и к классу RoleTransformer
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
];
}
}
Создание контроллеров
Наши контроллеры должны преобразовать данные перед отправкой их обратно пользователю. Мы будем работать с классом UsersController
index
show
// 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 будет запрашивать всех пользователей из базы данных, создавать коллекцию ресурсов со списком пользователей и преобразователя, а затем выполнять фактический процесс преобразования.
{
"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]"
}
// ...
]
}
Конечно, не имеет смысла возвращать всех пользователей одновременно, и для этого нужно реализовать пагинатор.
пагинация
Laravel стремится сделать вещи простыми. Мы можем выполнить нумерацию страниц следующим образом:
$users = User::paginate(10);
Но чтобы это работало с Fractal, нам может понадобиться добавить немного кода для преобразования наших данных перед вызовом 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
}
}
Первым шагом является разбиение на страницы данных из модели. Затем мы создаем коллекцию ресурсов, как и раньше, и затем устанавливаем paginator для коллекции.
Fractal предоставляет адаптер Paginator для Laravel для преобразования класса LengthAwarePaginator
{
"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"
}
}
}
}
Обратите внимание, что это добавляет дополнительные поля для деталей пагинации. Вы можете прочитать больше о нумерации страниц в документации .
Включая подресурсы
Теперь, когда мы познакомились с Fractal, пришло время узнать, как включать подресурсы (отношения) в ответ, когда он запрашивается пользователем.
Мы можем запросить дополнительные ресурсы, которые будут включены в ответ, например, http://demo.vaprobash.dev/users?include=role
Наш преобразователь может автоматически определить, что запрашивается, и проанализировать параметр 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));
}
}
Свойство $availableIncludes
Он будет вызывать метод includeRole
include
// 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
}
}
$this->fractal->parseIncludes
Если мы запросим список пользователей, мы должны увидеть что-то вроде этого:
{
"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"
}
}
}
}
Если бы у каждого пользователя был список ролей, мы могли бы изменить преобразователь так:
// 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));
}
}
При включении подресурса мы можем вкладывать отношения, используя точечную запись . Допустим, у каждой роли есть список разрешений, хранящихся в отдельной таблице, и мы хотели перечислить пользователей с их ролью и разрешениями. Мы можем сделать это следующим образом include=role.permissions
Иногда мы должны включить некоторые необходимые отношения по умолчанию, например, адресное отношение. Мы можем сделать это, используя свойство $defaultIncludes
class UserTransformer extends TransformerAbstract
{
// ...
protected $defaultIncludes = [
'address'
];
// ...
}
Одна из вещей, которые мне действительно нравятся в пакете Fractal — это возможность передавать параметры для включения параметров. Хорошим примером из документации является order by
Мы можем применить его к нашему примеру следующим образом:
// 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));
}
}
Важной частью здесь является list($orderCol, $orderBy) = $paramBag->get('order') ?: ['created_at', 'desc'];
, он попытается получить параметр порядка от включаемых пользователей и применяет его к построителю запросов.
Теперь мы можем заказать наш включенный список пользователей, передав параметры ( /roles?include=users:order(name|asc)
Вы можете прочитать больше о включении ресурсов в документацию .
Но что, если у пользователя не было прикрепленных ролей? Он остановится с ошибкой, потому что ожидал действительные данные вместо нуля. Давайте заставим его удалить отношение из ответа вместо того, чтобы показывать его с нулевым значением.
// 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));
}
}
Нетерпеливая загрузка
Поскольку Eloquent будет лениво загружать модели при доступе к ним, мы можем столкнуться с проблемой n + 1 . Эту проблему можно решить, сразу загрузив отношения для оптимизации запроса.
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;
}
}
Таким образом, у нас не будет никаких дополнительных запросов при доступе к модельным отношениям.
Вывод
Я наткнулся на Fractal, когда читал API-интерфейсы Building, которые вы не будете ненавидеть, от Phil Sturgeon. Это было отличное и информативное чтение, которое я всем сердцем рекомендую.
Используете ли вы трансформаторы при создании вашего API? У вас есть какой-либо предпочтительный пакет, который выполняет ту же работу, или вы просто json_encode
Дайте нам знать в комментариях ниже!