Laravel RESTful API

Прежде всего разберемся, что же такое RESTful API.
REST stands for REpresentational State Transfer и представляет собой архитектурное решение для сетевого взаимодействия между приложениями.
HTTP Экшены.

В API RESTful мы используем HTTP-экшены как действия, а конечными точками являются ресурсы, на которые воздействуют. Мы будем использовать HTTP-экшены по их семантическому значению:

POST: создать
GET: получить
PUT: обновить
DELETE: удалить

CRUD

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() and down() — методы будут выполняться при миграции и ее откате;
  • $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

Если попытаетесь запросить несуществующий ресурс, то вы получите:

laravel 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 ) , который вернет правильный ответ после регистрации пользователя. Он также доступен из коробки, но до сих пор нуждается в доработке, чтобы вернул ответ, который нам нужен.

laravel регистрация пользователя

Контроллер использует Трейт 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);

И все.

Вот что мы получим обращаясь к конечной точке


$ 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

И получим результат подобный этому:

Laravel middelware управление доступом

Это связано с тем, что нам нужно отредактировать текущий 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

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *