Как создать RESTful API Laravel

REST stands for REpresentational State Transfer и представляет собой архитектурное решение для сетевого взаимодействия между приложениями.
HTTP Action.
В API RESTful мы используем HTTP-action как действия, а конечными точками являются ресурсы, на которые воздействуют. Мы будем использовать HTTP-action по их семантическому значению:
POST: создать
GET: получить
PUT: обновить
DELETE: удалить
CreateReadUpdateDelete (CRUD) — эта аббревиатура частенько может попадаться в статьях.
Update Action: PUT vs. POST
Много копий сломано в спорах API RESTful — о том, лучше ли обновлять с помощью POST, PATCH, или PUT.
В этой статье мы будем использовать PUT для действия обновления, так как согласно HTTP RFC, PUT означает создание / обновление ресурса в определенном месте.
Краткое отступление: те, кто любит читать, может и далее впитывать знания полнотекстово.
Для киноманов — предоставляю видео тематично статье, но не по содержанию.
Ресурсы
Ресурсы станут целями действий, в нашем случае «Статьи и пользователи», и у них есть свои конечные точки:
/articles
/users
В этом уроке о api laravel ресурсы будут иметь представление 1:1 для наших моделей данных, но это не является обязательным. Вы можете иметь ресурсы, представленные более чем в одной модели данных (или вообще не представленные в базе данных). В конце концов, вы решаете, как спроектировать ресурсы и модели таким образом, который наиболее подходит вашему приложению.
Пропустим момент установки Laravel 5.4, а заодно и как выбрать хостинг, и перейдем прямо к сути.
Создадим модель Article.
Запустим терминал CTRL+ALT+T
перейдем в каталог со свежеустановленным Laravel
cd /path/to/my/project
и введем команду
php artisan make:model Article -m
The опция -m кратко от —migration скажет Artisan создать модель Article и создать миграцию для нее.
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class CreateArticlesTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('articles', function (Blueprint $table) {
$table->increments('id');
$table->timestamps();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('articles');
}
}
Немного подробнее:
- The
up()
anddown()
— методы будут выполняться при миграции и ее откате; $table->increments('id')
установит автоинкремент для поляid
;$table->timestamps()
установит timestamps для полей —created_at
иupdated_at
, и можно не беспокоиться о дефолтных значениях, Laravel их внесет по мере необходимости.- И наконец,
Schema::dropIfExists()
удалит таблицу, если такая существует.
На основе вышесказанного давайте добавим в метод up()
следующие строки:
public function up()
{
Schema::create('articles', function (Blueprint $table) {
$table->increments('id');
$table->string('title');
$table->text('body');
$table->timestamps();
});
}
string()
метод создаст VARCHAR
столбец, а text()
создаст TEXT
.
После этого:
php artisan migrate
Laravel, из коробки поставляется с двумя миграциями create_users_table
и create_password_resets_table
.
Мы не будем использовать миграцию password_resets
table, but having the users
.
Вернемся к нашей модели и добавим атрибуты $fillable
чтобы мы могли их использовать в методах Article::create
and Article::update
нашей модели:
class Article extends Model
{
protected $fillable = ['title', 'body'];
}
Поля с атрибутом
$fillable
доступны для массового заполнения.
Database Seeding
Не будем останавливаться на создании сидирования(автозаполнения) таблиц данными, об этом подробнее можно прочесть здесь.
Routes and Controllers (маршруты и контроллеры)
Давайте создадим конечные точки нашего приложения: создание, получение списка, получение единичной сущности(сингл), обновление, и удаление. В routes/api.php
, напишем:
Use App\Article;
Route::get('articles', function() {
// If the Content-Type and Accept headers are set to 'application/json',
// this will return a JSON structure. This will be cleaned up later.
return Article::all();
});
Route::get('articles/{id}', function($id) {
return Article::find($id);
});
Route::post('articles', function(Request $request) {
return Article::create($request->all);
});
Route::put('articles/{id}', function(Request $request, $id) {
$article = Article::findOrFail($id);
$article->update($request->all());
return $article;
});
Route::delete('articles/{id}', function($id) {
Article::find($id)->delete();
return 204;
})
Маршруты внутри api.php
будут иметь префикс / api /
, и API Middleware
будет автоматически применяться к ним.
Давайте переместим наш код в Controller (Контроллер):
$ php artisan make:controller ArticleController
ArticleController.php:
use App\Article;
class ArticleController extends Controller
{
public function index()
{
return Article::all();
}
public function show($id)
{
return Article::find($id);
}
public function store(Request $request)
{
return Article::create($request->all());
}
public function update(Request $request, $id)
{
$article = Article::findOrFail($id);
$article->update($request->all());
return $article;
}
public function delete(Request $request, $id)
{
$article = Article::findOrFail($id);
$article->delete();
return 204;
}
}
Изменим routes/api.php:
Route::get('articles', 'ArticleController@index');
Route::get('articles/{id}', 'ArticleController@show');
Route::post('articles', 'ArticleController@store');
Route::put('articles/{id}', 'ArticleController@update');
Route::delete('articles/{id}', 'ArticleController@delete');
Мы можем улучшить наши конечные точки, используя неявные привязки маршрутов.
Route::get('articles', 'ArticleController@index');
Route::get('articles/{article}', 'ArticleController@show');
Route::post('articles', 'ArticleController@store');
Route::put('articles/{article}', 'ArticleController@update');
Route::delete('articles/{article}', 'ArticleController@delete');
class ArticleController extends Controller
{
public function index()
{
return Article::all();
}
public function show(Article $article)
{
return $article;
}
public function store(Request $request)
{
$article = Article::create($request->all());
return response()->json($article, 201);
}
public function update(Request $request, Article $article)
{
$article->update($request->all());
return response()->json($article, 200);
}
public function delete(Article $article)
{
$article->delete();
return response()->json(null, 204);
}
}
Примечание к кодам HTTP статусов и Формату ответа(Responce)
Мы также добавили response()->json()
к вызову наших конечных точек. Это позволит нам возвращать явно JSON data а также HTTP код, который будет обрабатываться клиентом.
Общий список кодов:
200
: OK. Стандартный код успешного ответа.201
: Объект создан. Полезен при работе с хранилищем(магазином).204
: Отсутствует контент. Когда действие выполнено успешно, но не возвращен контент.206
: Частичный контент. Используется когда вы возвращаете контент постранично(пагинация).400
: Bad request. Стандартная опция для запросов которые не прошли валидацию.401
: Unauthorized. Пользователь не прошел авторизацию.403
: Forbidden. Пользователь авторизован, но у него не хватает прав для выполнения запроса.404
: Not found. возвращается Laravel автоматически когда запрошенный ресурс не найден.500
: Internal server error. В идеале, вы не должны возвращать такой ответ, но когда, что то неожиданно сбоит, то пользователь получит такой ответ.503
: Service unavailable. Довольно понятно, но тоже, код который не будет явно возращен приложением.
Отправка корректного 404
Если попытаетесь запросить несуществующий ресурс, то вы получите:
Мы можем исправить это отредактировав исключение нашего класса, расположенного в app/Exceptions/Handler.php
, которое вернет JSON:
public function render($request, Exception $exception)
{
// This will replace our 404 response with
// a JSON response.
if ($exception instanceof ModelNotFoundException) {
return response()->json([
'error' => 'Resource not found'
], 404);
}
return parent::render($request, $exception);
}
Результатом будет:
{
data: "Resource not found"
}
Если вы используете Laravel для обслуживания других страниц, вы должны отредактировать код для работы с Accept
header, в противном случае ошибки 404 из регулярных запросов будут возвращены с таким же успехом.
public function render($request, Exception $exception)
{
// This will replace our 404 response with
// a JSON response.
if ($exception instanceof ModelNotFoundException &&
$request->wantsJson())
{
return response()->json([
'data' => 'Resource not found'
], 404);
}
return parent::render($request, $exception);
}
В этом случае, API запросам будет нужен header Accept: application/json
.
Аутентификация
Существует много способов аутентификации в Laravel, но статья не об этом, поэтому будем использовать упрощенную.
Для начала нужно добавить поле api_token
в таблицу users
:
$ php artisan make:migration --table=users adds_api_token_to_users_table
Не забудем о миграции:
public function up()
{
Schema::table('users', function (Blueprint $table) {
$table->string('api_token', 60)->unique()->nullable();
});
}
public function down()
{
Schema::table('users', function (Blueprint $table) {
$table->dropColumn(['api_token']);
});
}
Создание Регистрации пользователя
Будем использовать RegisterController
(в папке Auth
) , который вернет правильный ответ после регистрации пользователя. Он также доступен из коробки, но до сих пор нуждается в доработке, чтобы вернул ответ, который нам нужен.
Контроллер использует Трейт RegistersUsers
реализующий регистрацию
public function register(Request $request)
{
// Here the request is validated. The validator method is located
// inside the RegisterController, and makes sure the name, email
// password and password_confirmation fields are required.
$this->validator($request->all())->validate();
// A Registered event is created and will trigger any relevant
// observers, such as sending a confirmation email or any
// code that needs to be run as soon as the user is created.
event(new Registered($user = $this->create($request->all())));
// After the user is created, he's logged in.
$this->guard()->login($user);
// And finally this is the hook that we want. If there is no
// registered() method or it returns null, redirect him to
// some other URL. In our case, we just need to implement
// that method to return the correct response.
return $this->registered($request, $user)
?: redirect($this->redirectPath());
}
Мы просто включим метод registered()
в наш RegisterController
. Метод принимает $request
и $user
, и это как раз то что нам нужно. Вот так метод выглядит внутри контроллера:
protected function registered(Request $request, $user)
{
$user->generateToken();
return response()->json(['data' => $user->toArray()], 201);
}
Мы можем подключить его в файле routes
Route::post(register, 'Auth\RegisterController@register);
В приведенном выше разделе мы использовали метод модели User для генерации токена. Это полезно, так что у нас есть только один способ генерации токенов. Добавьте в свою модель пользователя следующий метод:
class User extends Authenticatable
{
...
public function generateToken()
{
$this->api_token = str_random(60);
$this->save();
return $this->api_token;
}
}
И все.
Вот что мы получим обращаясь к конечной точке
$ curl -X POST http://localhost:8000/api/register \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
-d '{"name": "John", "email": "john.doe@toptal.com", "password": "toptal123", "password_confirmation": "toptal123"}'
{
"data": {
"api_token":"0syHnl0Y9jOIfszq11EC2CBQwCfObmvscrZYo5o2ilZPnohvndH797nDNyAT",
"created_at": "2017-06-20 21:17:15",
"email": "john.doe@toptal.com",
"id": 51,
"name": "John",
"updated_at": "2017-06-20 21:17:15"
}
}
Создание Login
Также как и в случае с регистрацией, мы можем отредактировать LoginController
(в папке Auth
) для поддержки API аутентификации. Метод login
в трейте AuthenticatesUsers
может быть переопределен для поддержки нашей API:
public function login(Request $request)
{
$this->validateLogin($request);
if ($this->attemptLogin($request)) {
$user = $this->guard()->user();
$user->generateToken();
return response()->json([
'data' => $user->toArray(),
]);
}
return $this->sendFailedLoginResponse($request);
}
И также подключим его в routes
Route::post('login', 'Auth\LoginController@login');
Теперь, предположим, что пользователи у нас существуют
$ curl -X POST localhost:8000/api/login \
-H "Accept: application/json" \
-H "Content-type: application/json" \
-d "{\"email\": \"admin@test.com\", \"password\": \"toptal\" }"
{
"data": {
"id":1,
"name":"Administrator",
"email":"admin@test.com",
"created_at":"2017-04-25 01:05:34",
"updated_at":"2017-04-25 02:50:40",
"api_token":"Jll7q0BSijLOrzaOSm5Dr5hW9cJRZAJKOzvDlxjKCXepwAeZ7JR6YP5zQqnw"
}
}
Logging Out
В соответствии с нашей текущей стратегией, если токен ошибочен или отсутствует, пользователь должен получить не прошедший валидацию ответ. Поэтому для мы отправим токен, и он будет удален в базе данных.
routes/api.php:
Auth\LoginController.php:
public function logout(Request $request)
{
$user = Auth::guard('api')->user();
if ($user) {
$user->api_token = null;
$user->save();
}
return response()->json(['data' => 'User logged out.'], 200);
Используя эту стратегию, любой токен, который есть у пользователя будет недействительным, и API откажет в доступе (используя middlewares, как описано в следующем разделе). Для этого необходимо согласование с клиентской частью(front-end), чтобы пользователь оставался залогиненым, и не имеющим доступа к какому-либо контенту.
Использование Middlewares(Посредников) для запрета доступа.
С созданием api_token
, мы можем переключать middleware в файле route:
Route::middleware('auth:api')
->get('/user', function (Request $request) {
return $request->user();
});
Мы можем управлять доступом текущего пользователя используя метод $request->user()
иил посредством фасада Auth
Auth::guard('api')->user(); // instance of the logged user
Auth::guard('api')->check(); // if a user is authenticated
Auth::guard('api')->id(); // the id of the authenticated user
И получим результат подобный этому:
Это связано с тем, что нам нужно отредактировать текущий unauthenticated
метод в нашем Handler class. Текущая версия возвратит JSON только если запрос будет иметь заголовок Accept: application/json
давайте изменим это:
protected function unauthenticated($request, AuthenticationException $exception)
{
return response()->json(['error' => 'Unauthenticated'], 401);
}
Изменив это, можно изменить наши конечные точки статей, обернув их auth:api
middleware. Это можно сделать посредством route groups:
Route::group(['middleware' => 'auth:api'], function() {
Route::get('articles', 'ArticleController@index');
Route::get('articles/{article}', 'ArticleController@show');
Route::post('articles', 'ArticleController@store');
Route::put('articles/{article}', 'ArticleController@update');
Route::delete('articles/{article}', 'ArticleController@delete');
});
Таким образом нам не нужно прописывать middleware для каждого маршрута(Route).
В следующей статье Laravel RESTful API. Тестирование я опишу как тестировать наши конечные точки(endpoints).
Оригинал статьи Laravel API Tutorial: How to Build and Test a RESTful API
5 комментариев
После попытки логина выдает ошибку в концоли..
curl -X POST http://work.back/api/login -H «Accept: application/json» -H «Content-type: application/json» -d «{\»email\»: \»john.doe@toptal.com\», \»password\»: \»toptal123\» }»
Ошибка.
{
«message»: «Class App\\Http\\Controllers\\Auth\\Request does not exist»,
«exception»: «ReflectionException»,
«file»: «/home/batis/project/worktimebackend/vendor/laravel/framework/src/Illuminate/Routing/RouteSignatureParameters.php»,
«line»: 25,
Прошу прощения.
Статья переведена. Тут скорее вопрос автору.
Но, как говориться спасибо за сигнал.
Обкатаю на своем сервере этот код. Интересно.
Добавить
use Illuminate\Http\Request;
в LoginController
добавь сверху после namespace это:
use Illuminate\Http\Request;
Странный маневр помещать логику в роуты, а потом переписывать ее в контроллеры.