inb4: копипаста из документации


В гайде упор на быстрое развертывание минимального набора для полноценной разработки API в соответствии с best practice, взятыми из документации Laravel 5.7, собранными в одном месте. Писал для себя и коллег как шпаргалку, надеюсь пригодится кому-нибудь еще.


Предварительная настройка


Ставим фреймворк


composer create-project --prefer-dist laravel/laravel scaffold-api


Удаляем ненужные UI компоненты (vuejs, react)


php artisan preset none


Настраиваем подключение к БД


Переходим в папку, редактируем файл .env:


DB_CONNECTION=mysql
DB_HOST=localhost
DB_PORT=3306
DB_DATABASE=api-authentification
DB_USERNAME=root
DB_PASSWORD=

Приступаем к генерации


Выполняем в консоли
php artisan make:model Game -mrc


Получаем модель, миграцию и контроллер:


Model created successfully.
Factory created successfully.
Created Migration: 2019_02_27_105610_create_games_table
Controller created successfully.

Создаем колонки в таблице БД


Правим миграцию, добавляя в таблицу нужны колонки. Наиболее часто используемые типы:


  • increments('id')
  • string('title')
  • text('description')
  • tinyInteger('complexity')
  • boolean('isActive')
  • softDeletes()

Для необязательных полей не забываем добавлять значение по умолчанию с помощью ->default()


Применяем миграции, выполняя php artisan migrate


Генерируем правила валидации


Выполняем php artisan make:request GameRequest


Открываем App/Http/Requests/GameRequest.php.
В методе authorize() ставим return true, пока мы не добавили авторизацию.
В массиве, который возвращается в методе rules(), описываются правила для всех колонок, которые мы перечисляли в миграции. Доступные правила здесь


Для минимизации кода, мы используем конструкцию switch для разных http-глаголов, вместо того, чтобы делать отдельные StoreGameRequest, UpdateGameRequest и т.д.


public function rules(Request $request)
    {
      $rules = [
          'title' => 'required|string|unique:games,title',
          'description' => '',
          'complexity' => 'required|min:1|max:10',
          'minPlayers' => 'required|min:1|max:10',
          'maxPlayers' => 'required|min:1|max:10',
          'isActive' => 'required|boolean'
      ];

      switch ($this->getMethod())
      {
        case 'POST':
          return $rules;
        case 'PUT':
          return [
            'game_id' => 'required|integer|exists:games,id', //должен существовать. Можно вот так: unique:games,id,' . $this->route('game'),
            'title' => [
              'required',
              Rule::unique('games')->ignore($this->title, 'title') //должен быть уникальным, за исключением себя же
            ]
          ] + $rules; // и берем все остальные правила
        // case 'PATCH':
        case 'DELETE':
          return [
              'game_id' => 'required|integer|exists:games,id'
          ];
      }
    }

Собственные варианты описания ошибок


Если нужны собственные тексты ошибок, переопределяем метод messages(), который возвращает массив с переводами каждого правила:


public function messages()
    {
        return [
            'date.required' => 'A date is required',
            'date.date_format'  => 'A date must be in format: Y-m-d',
            'date.unique'  => 'This date is already taken',
            'date.after_or_equal'  => 'A date must be after or equal today',
            'date.exists'  => 'This date doesn\'t exists',
        ];
    }

Для того, чтобы в правилах валидации были доступны не только параметры, переданные в теле запроса, но и параметры, переданные в URL, переопределяем метод all (который обычно используют в контролере в виде $request->all()):


public function all($keys = null)
    {
      // return $this->all();
      $data = parent::all($keys);
      switch ($this->getMethod())
      {
        // case 'PUT':
        // case 'PATCH':
        case 'DELETE':
          $data['date'] = $this->route('day');
      }
      return $data;
    }

Настраиваем контроллер и описываем бизнес-логику


Открываем Http\Controllers\GameController. Удаляем сгенерированные методы create(), edit(), предназначенные для рендеринга форм (поскольку у нас REST API, они не нужны).


Заменяем стандартный use Illuminate\Http\Request;, на наш use App\Http\Requests\GameRequest;


Далее правим методы:


public function index()
    {
        return Game::all();
    }

public function store(GameRequest $request)
     {
         $day = Game::create($request->validated());
         return $day;
     }

public function show(Game $game)
    {
      return $game = Game::findOrFail($game);
    }

public function update(GameRequest $request, $id)
     {
         $game = Game::findOrFail($id);
         $game->fill($request->except(['game_id']));
         $game->save();
         return response()->json($game);
     }

public function destroy(GameRequest $request, $id)
     {
         $game = Game::findOrFail($id);
         if($game->delete()) return response(null, 204);
     }

Если логики много, то её лучше вынести в отдельный слой Service/Repository


Настраиваем модель


Открываем модель app/Http/Game.php и добавляем свойства:


protected $fillable = ['title', 'description', 'complexity', 'minPlayers', 'maxPlayers', 'isActive'];

protected $hidden = ['created_at', 'updated_at', 'deleted_at'];

Настраиваем middleware


Чтобы наше приложение всегда возвращало json независимо от переданных заголовков, создаем middleware:


php artisan make:middleware ForceJsonResponse

и добавляем в него код:


public function handle($request, Closure $next)
    {
        $request->headers->set('Accept', 'application/json');
        return $next($request);
    }

Регистрируем этот middleware в app/Http/Kernel.php:


...
'api' => [
            'throttle:60,1',
            'bindings',
            \App\Http\Middleware\ForceJsonResponse::class,
        ],

Настраиваем роутинг


Открываем routes/api.php и добавляем:


use Http\Controllers\GameController;

Route::apiResource('/games', 'GameController');

Статичский метод Route::apiResource, в отличие от метода resource, исключает методы edit и create, оставляя только index, show, store, update, destroy.


Этого же можно добиться более очевидной записью:


Route::resource('/games', 'GameController')->only([
    'index', 'show', 'store', 'update', 'destroy'
]);

Теперь, можно посмотреть пути командой php artisan route:list и пользоваться.


REST API готово!


Послесловие

Послесловие


Если нужна авторизация, то подойдет стандартный Laravel Passport.


Настраиваем авторизацию Laravel Passport


composer require laravel/passport
php artisan make:auth
php artisan passport:install
php artisan migrate

Добавляем трейт Laravel\Passport\HasApiTokens в модель App\User и вызов Passport::routesmethod в метод boot app/AuthServiceProvider:


public function boot() {
    $this->registerPolicies();
    Passport::routes();
}

В файле config/auth.php меняем драйвер на passport:


'api' => [
        'driver' => 'passport',
        'provider' => 'users',
    ],

Создаем контроллер для авторизации 'php artisan make:controller Api/AuthController.php`


Добавляем туда код


use App\User;
use Illuminate\Support\Facades\Validator;

public function register (Request $request) {

    $validator = Validator::make($request->all(), [
        'name' => 'required|string|max:255',
        'email' => 'required|string|email|max:255|unique:users',
        'password' => 'required|string|min:6|confirmed',
    ]);

    if ($validator->fails())
    {
        return response(['errors'=>$validator->errors()->all()], 422);
    }

    $request['password']=Hash::make($request['password']);
    $user = User::create($request->toArray());

    $token = $user->createToken('Laravel Password Grant Client')->accessToken;
    $response = ['token' => $token];

    return response($response, 200);

}

public function login (Request $request) {

    $user = User::where('email', $request->email)->first();

    if ($user) {

        if (Hash::check($request->password, $user->password)) {
            $token = $user->createToken('Laravel Password Grant Client')->accessToken;
            $response = ['token' => $token];
            return response($response, 200);
        } else {
            $response = "Password missmatch";
            return response($response, 422);
        }

    } else {
        $response = 'User does not exist';
        return response($response, 422);
    }

}

public function logout (Request $request) {

    $token = $request->user()->token();
    $token->revoke();

    $response = 'You have been succesfully logged out!';
    return response($response, 200);

}

После этого можно пользоваться методами api/register, api/login, api/logout для авторизации, и закрыть доступ к апи. Для этого нужно обернуть роутинг наших REST контроллеров в middleware:


Route::middleware('auth:api')->group(function () {
    ...
    Route::get('/logout', 'Api\AuthController@logout')->name('logout');
});

Послепослесловие

Послепослесловие:


Тут бы еще сделать функциональные тесты и генерацию документации в swagger, но это немного выходит за рамки scaffold-туториала, так что об этом в другой раз


Еще почитать

Комментарии (11)


  1. uonick
    27.02.2019 16:39
    +2

    Вы серьезно пишите бизнес-логику и работу с моделями в контроллере?


    1. parotikov Автор
      27.02.2019 16:47

      Статью не читай@сразу отвечай


      Я написал, что логику нужно в сервис выносить.
      Просто все это по своему делают, тут конкретные советы давать не стал.


      1. uonick
        27.02.2019 16:54

        А лучше бы дать.
        Ведь те, кто на самом деле «не читал», но изучает, скопипастят это в проект.
        Не надо так.


        1. parotikov Автор
          27.02.2019 16:56

          Те, кто не читал — должен прочитать. Это не замена документации, лишь выдержка в виде шпаргалки.
          Архитектура бизнес логики не рассматривается в документации, я решил следовать этому.


          Про сервис слой я сделаю отдельную статью, спасибо за идею.


          1. uonick
            27.02.2019 17:01

            А можно попробовать, например, расширить эту статью циклом: «это было для проверки ответа, читать продолжение...». А там раскрыть тему про репозитории, сервисы, di.


        1. ZurgInq
          27.02.2019 17:06

          Тех, кто копипастит не читая и не думая, уже ничего не спасёт.


    1. ellrion
      28.02.2019 11:32

      тот код что написан в методах контроллера в статье и так короткий. Для чего контроллер делать еще тоньше?


  1. gudster
    28.02.2019 14:46

    Если планируете дальше развивать статью, наверно можно добавить:
    1 после установки добавить вызов:

    php artisan preset none
    Удаляет не нужные UI компоненты (vuejs, react)

    2 Route::resource поменял на вызов apiResource
    Route::apiResource('/games', 'GameController');


    1. parotikov Автор
      28.02.2019 14:46

      Добавил в статью, спасибо.


  1. gudster
    01.03.2019 16:36

    Посредника

    \App\Http\Middleware\ForceJsonResponse::class
    перед bindings лучше добавить иначе ошибки будут html формате


    1. ellrion
      01.03.2019 17:11

      А смысл? Запрашивай джейсон и будет в ответе тебе джейсон. Не понятно желание форса.