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-туториала, так что об этом в другой раз