Статьи

PHP Fractal — сделайте JSON вашего API красивым, всегда!

Эта статья была рецензирована Вираджем Хатавкаром . Спасибо всем рецензентам SitePoint за то, что сделали контент SitePoint как можно лучше!


Если вы уже создали API ранее, держу пари, что вы привыкли сбрасывать данные напрямую в качестве ответа. Это может быть не вредно, если все сделано правильно, но есть практические альтернативы, которые могут помочь решить эту небольшую проблему.

Одним из доступных решений является Fractal . Это позволяет нам создавать новый слой преобразования для наших моделей, прежде чем возвращать их в качестве ответа. Он очень гибкий и простой в интеграции в любое приложение или фреймворк.

Изображение фракталов с встроенным в них PHP

Установка

Мы будем использовать приложение Laravel 5.3, чтобы создать пример и интегрировать с ним пакет Fractal, поэтому давайте создадим новое приложение Laravel с помощью установщика или через Composer.

laravel new demo

или

 composer create-project laravel/laravel demo

Затем внутри папки нам требуется пакет Fractal.

 composer require league/fractal

Создание базы данных

Наша база данных содержит таблицу usersroles У каждого пользователя есть роль, и у каждой роли есть список разрешений.

 // 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 или хранилище.

Мы расширяем класс TransformerAbstracttransformUser То же самое относится и к классу 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
        ];
    }
}

Создание контроллеров

Наши контроллеры должны преобразовать данные перед отправкой их обратно пользователю. Мы будем работать с классом UsersControllerindexshow

 // 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 Он будет вызывать метод includeRoleinclude

 // 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 Дайте нам знать в комментариях ниже!