Статьи

Создание ресурсов REST с Laravel

В этой части мы начнем работать с интерфейсом REST. Создание REST Api на Laravel не очень сложно. Все, что нам нужно иметь в виду, это то, что мы имеем дело с EmberJS и что мы не хотим писать новый адаптер с нуля. Как обычно, вы можете найти исходный код этой части на github .

Когда начать?

Это сложный вопрос. У Ember свой рабочий процесс и логика. Если мы начнем писать наш REST с учетом этой логики, мы сэкономим некоторое время, у нас будет хорошая архитектура и что-то повторно используемое. Я думаю, что Ember сделал хороший выбор с их REST-архитектурой. Посмотрите, как Ember ожидает данные.

Давайте предположим, что мы хотим получить пользователя. Эмбер ожидает что-то вроде этого:

{
      "user": {
        "firstName": "firstName",
        "lastName": "lastName"
      }
    }

Если мы хотим получить список пользователей, Ember будет ожидать такой json:

 {
      "users": 
      [
          {
            "firstName": "firstPersonsName",
            "lastName": "lastname"
          },
          {
            "firstName": "secondPersonName",
            "lastName": "lastName"
          }
      ]
    }

Первый требует «пользователя», а второй требует «пользователей». Второй — во множественном числе. Эмбер тоже ввела некоторые правила для этого. Если вы сами не указали множественное число, используя:

 Ember.Inflector.inflector.irregular('formula', 'formulae');

EmberJ сделает предположение и запросит «формулы». Иногда хорошо, что сам фреймворк предоставляет такие вещи, но, с другой стороны, вещи могут выйти из-под контроля, если вы забудете эти детали.

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

Если мы немного усложним вещи и установим отношения между объектами, например, мы скажем, что у пользователя есть несколько фотографий. Как бы мы это вывели?

 {
      "user": {
        "id": 1,
        "name": "firstName",
        "lastname": "lastname,
        "photos": [1, 2, 3]
      },
    
      "photos": 
      [
          {
            "id": 1,
            "title": "Lorem Ipsum"
          },
          {
            "id": 2,
            "title": "Lorem Ipsum"
          }
      ]
    }

Это отношение один ко многим. Если мы запросим пользователя, его фотографии тоже будут извлечены. Мы уже установили некоторые отношения в Laravel, вы можете использовать их, если хотите, и использовать их также и в Ember.

Я начал с Ember, чтобы увидеть, как эта структура требует данных. Это проще, если вы знаете, как построить структуру. Проверка и получение данных из базы данных просты, но создать надежный и интеллектуальный интерфейс REST — сложная часть.

Готовимся к ОТДЫХУ

Когда вы что-то разрабатываете, макет может быть очень полезным. Даже если вы программист гуру и ненавидите работать с Photoshop или Gimp, есть хорошие инструменты для создания прототипов. Я использовал бальзамик, и мой прототип на первой странице был таким:

Prototyping with Balsamiq

Давайте начнем строить его. Откройте /app/views/index.php Это служит нашим одностраничным приложением. Мы создали этот файл в первой части этой серии.

 <script type="text/x-handlebars">
    
        <!-- The navigation top-bar -->
        <nav class="top-bar" data-topbar>

            <ul class="title-area">
                <li class="name">
                    <h1><a href="#">Photo Upload</a></h1>
                </li>
            </ul>

            <section class="top-bar-section">

                <!-- Left Nav Section -->
                <ul class="left">
                    <li class="has-dropdown">
                        <a href="#">Categories</a>
                        <ul class="dropdown">
                            <li><a href="#">Category1</a></li>
                            <li><a href="#">Category2</a></li>
                            <li><a href="#">Category3</a></li>
                            <li><a href="#">Category4</a></li>
                        </ul>
                    </li>
                </ul>

            </section>

            <div class="clearfix"></div>
            
        </nav><!-- END Navigation -->
        
        <!-- Content -->
        <div style="margin-top: 50px;">
            <!-- The content will be here -->
        </div><!-- END Content -->

    </script>

Позвольте мне объяснить это. Навигационный тег отвечает за навигацию. Тег navultitle-area Я также добавил раскрывающийся список со списком категорий. Отправляйтесь в Фонд 5 документов, если хотите узнать больше. В большинстве случаев это просто операции копирования / вставки, поэтому не беспокойтесь об этой части.

Также я использовал сеточную систему Foundation для области контента. Это будет заполнено всей информацией и будет изменено во время навигации. Все внутренние обновления будут обрабатываться Ember. Мы собираемся построить только 3 шаблона здесь. Один для пользователей, один для одной фотографии и один для целевой страницы.

Вы заметили, что весь наш код находится внутри тега скрипта? Ember использует руль в качестве языка шаблонов. Тип text/x-handlebars Если вы некоторое время использовали Ember и Handlebars, вы, вероятно, использовали имена шаблонов. Я не указываю их в этом, потому что этот шаблон будет использоваться в качестве контейнера для всего приложения. Если вы не укажете имя, Ember использует его в качестве шаблона приложения.

Ресурсные контроллеры

Разрабатывая это очень простое приложение, я обнаружил, что контроллеры ресурсов пригодятся при разработке REST Apis. В этом суть архитектуры REST — все это ресурс. Для всех ресурсов может применяться HTTP-глагол: GET, POST, DELETE, PUT (обновление). Не все глаголы обязательны.

 php artisan controller:make PhotoController --except=create,edit

Вот как мы создаем контроллер ресурсов через Artisan. Опция --except Нам не нужны методы createedit Метод create Поскольку мы создаем одностраничное приложение, нецелесообразно создавать вид за пределами Ember.

Создайте еще один контроллер ресурсов для категорий. Как видите, в этом контроллере доступны только методы showindex Я думаю, что достаточно показать отдельную категорию и получить все категории.

 php artisan controller:make CategoryController --only=show,index

Другим контроллером является контроллер изображений. Зачем нужен контроллер изображений, если он у нас уже есть? Потому что нам нужна конечная точка для обслуживания изображений. Dropbox хранит наши изображения, но мы не можем получить к ним доступ извне. Если вы хотите сделать папку общедоступной, вы должны заплатить. Это первая причина. Вторая причина в том, что я не хочу, чтобы каждое изображение было публичным. В двух словах, этот контроллер будет захватывать изображение из Dropbox и передавать его клиенту.

 php artisan controller:make ImagesController --only=show

И последнее, но не менее важное, это UserController:

 php artisan controller:make UserController --only=show,index

Маршрут

Теперь, когда у нас есть контроллеры, нам нужно связать эти контроллеры с их связанными маршрутами. Давайте обновим /app/routes.php Сначала сгруппируйте их в пространстве имен url, используя Route::group

 Route::group(array('prefix' => 'api/v1'), function()
    {
        
    
    });

Здесь мы указали префикс, пространство имен. Ко всему, что находится внутри этой группы, можно получить доступ следующим образом:

 example.com/api/v1

Также мы можем указать фильтры внутри этой группы. Например, вы можете добавить фильтр Auth::onceBasic('username') Вы также можете использовать другие аутентификации.

Добавьте три контроллера внутри этой группы. PhotoController, UserController и CategoryController.

 Route::group(array('prefix' => 'api/v1'), function()
    {
        Route::resource('photos', 'PhotoController');
        Route::resource('users', 'UserController');
        Route::resource('categories', 'CategoryController');
    });

Добавьте ImagesController вне этой группы. Я не думаю, что этому контроллеру нужно пространство имен — изображения являются изображениями, и нет смысла давать им пространство имен.

 Route::resource('files', 'ImagesController');

В конце файл /app/routes.php

 Route::get('/', function()
    {
    	return View::make('index');
    });
    
    Route::group(array('prefix' => 'api/v1'), function()
    {
        Route::resource('photos', 'PhotoController');
        Route::resource('users', 'UserController');
        Route::resource('categories', 'CategoryController');
    
    });
    
    Route::resource('files', 'ImagesController');

Обратите внимание, что имена ресурсов множественного числа из-за требования Ember.

Заполнение этих контроллеров

Теперь мы можем начать что-то строить. Я не собираюсь рассказывать все о REST здесь, потому что очень сложно объяснить все вещи — узнать больше в этой серии . Начнем с фотоконтроллера.

Метод index() Здесь мы могли бы сделать несколько страниц, но я не хочу, чтобы все стало слишком сложным. Если будет достаточно интереса к комментариям, мы обновим это приложение в следующей статье.

 public function index()
	{
        try{
            $statusCode = 200;
            $response = [
              'photos'  => []
            ];

            $photos = Photo::all()->take(9);

            foreach($photos as $photo){

                $response['photos'][] = [
                    'id' => $photo->id,
                    'user_id' => $photo->user_id,
                    'url' => $photo->url,
                    'title' => $photo->title,
                    'description' => $photo->description,
                    'category' => $photo->category,
                ];
            }

        }catch (Exception $e){
            $statusCode = 400;
        }finally{
            return Response::json($response, $statusCode);
        }

	}

Позвольте мне объяснить это. Я вставил все внутри trycatchfinally Если что-то пойдет не так, верните другой json с кодом состояния.

 $photos = Photo::all()->take(9);

Это берет 9 фотографий из базы данных. Затем возьмите каждую фотографию и отобразите ее в отформатированном массиве, который позже будет преобразован в формат json.

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

Давайте заполним метод show() Опять же, мы хотим получить всю информацию о фотографии с заданным идентификатором.

 public function show($id) 
	{
        try{
            $photo = Photo::find($id);
            $statusCode = 200;
            $response = [ "photo" => [
                'id' => (int) $id,
                'user_id' => (int) $photo->user_id,
                'title' => $photo->title,
                'url' => $photo->url,
                'category' => (int) $photo->category,
                'description' => $photo->description
            ]];

        }catch(Exception $e){
            $response = [
                "error" => "File doesn`t exists"
            ];
            $statusCode = 404;
        }finally{
            return Response::json($response, $statusCode);
        }

	}

При создании собственного приложения не забудьте добавить проверку ввода пользователя.

Логика для UserController почти такая же. На этот раз мы запросим модель пользователя.

 public function index()
	{
		try{

            $response = [
                'users' => []
            ];
            $statusCode = 200;
            $users = User::all()->take(9);

            foreach($users as $user){

                $response['users'][] = [
                    'id' => $user->id,
                    'username' => $user->username,
                    'lastname' => $user->lastname,
                    'name' => $user->username
                ];


            }


        }catch (Exception $e){
            $statusCode = 404;
        }finally{
            return Response::json($response, $statusCode);
        }
	}

Все практически идентично, меняются только модель и поля. Выходной JSON. Метод show

 public function show($id)
	{
	    try{

            $response = [
                'user' => []
            ];
            $statusCode = 200;
            
            $user = User::find($id);
    
            $response = [
                'id' => $user->id,
                'name' => $user->name,
                'lastname' => $user->lastname,
                'username' => $user->username
            ];
            
        }catch (Exception $e){
            $statusCode = 404;
        }finally{
            return Response::json($response, $statusCode);
        }

	}

Эта функция извлекает пользователя с заданным идентификатором.

Последний контроллер, с которым мы имеем дело, это ImagesController. Логика так же проста, как захват изображений из файловой системы и их обслуживание. Это просто, когда вы сохраняете файлы и извлекаете их из локальной файловой системы или из файловой системы сервера. К сожалению, вы не можете сохранить файлы в Heroku, поэтому вы будете использовать Dropbox и обслуживать эти файлы с этой конечной точки.

Импортируйте клиент Dropbox и адаптер Flysystem. Если наша среда локальна, то мы будем использовать flysystem с локальным адаптером; если среда является производственной, то используйте адаптер Dropbox. Назначьте класс Flysystem в приватную переменную внутри этого контроллера.

 if(App::environment() === "local"){
    
        $this->filesystem = new Filesystem(new Adapter( public_path() . '/images/'));

    }else{

        $client = new Client(Config::get('dropbox.token'), Config::get('dropbox.appName'));
        $this->filesystem = new Filesystem(new Dropbox($client, '/images/'));

    }

Метод showdestroy Используя эту библиотеку, мы добавляем уровень абстракции в наше приложение.

 public function show($name)
	{
        try{
            $file = $this->filesystem->read($name); // Read the file
        }catch (Exception $e){
            return Response::json("{}", 404);       // Return a 404 status code in a error case
        }

        $response = Response::make($file, 200);     // If everything goes ok then return that file and 200 status code

        return $response;

	}

Функция destroy() Просто выберите этот файл, используя метод удаления и передав имя удаляемого файла. Если файл не найден, верните 404.

 public function destroy($name)
	{
		try{
            $this->filesystem->delete($name);
            return Response::json("{}", 200);
        }catch (\Dropbox\Exception $e){
            return Response::json("{}", 404);
        }
	}

В итоге ImageController должен выглядеть примерно так:

 /* /app/controllers/ImagesController.php */
    
    use Dropbox\Client;
    use League\Flysystem\Filesystem;
    use League\Flysystem\Adapter\Local as Adapter;
    use League\Flysystem\Adapter\Dropbox;
    
    
    class ImagesController extends \BaseController {
    
        private $filesystem;
    
        public function __construct(){
    
            if(App::environment() === "local"){
    
                $this->filesystem = new Filesystem(new Adapter( public_path() . '/images/'));
    
            }else{
    
                $client = new Client(Config::get('dropbox.token'), Config::get('dropbox.appName'));
                $this->filesystem = new Filesystem(new Dropbox($client, '/images/'));
    
            }
    
        }
    
    
    	public function show($name)
    	{
            try{
                $file = $this->filesystem->read($name);
            }catch (Exception $e){
                return Response::json("{}", 404);
            }
    
            $response = Response::make($file, 200);
    
            return $response;
    
    	}
    
    
    	public function destroy($name)
    	{
    		try{
                $this->filesystem->delete($name);
                return Response::json("{}", 200);
            }catch (\Dropbox\Exception $e){
                return Response::json("{}", 404);
            }
    	}
    
    
    }

Формат, который мы обслуживаем, — HTML. Хорошо, это немного странно. Мы хотели обслуживать изображения, а не HTML. Однако это не проблема, потому что браузер ищет формат файла и распознает, как использовать этот файл.

Попробуй создать CategoryController. Я оставил это как упражнение для вас.

Тестирование API

Я должен признать, что я влюблен в PhpStorm, и для тестирования API-интерфейсов Rest я использую инструмент под названием Rest Client. Это графический интерфейс, который упрощает тестирование. Вы также можете использовать CURL из терминала, если хотите. Давайте сделаем несколько тестов:

 curl http://localhost:8000/api/v1/users

И вот что возвращается:

Using Curl for testing Rest Apis

С клиентом PhpStorm Rest я получаю тот же результат в JSON.

Testing Rest Apis with Rest Client of PhpStorm

И если я хочу увидеть результаты в лучшем формате, я могу просто нажать значок js в левой части инструмента, и клиент Rest дает мне более хорошее представление.

The Json file from the Rest Client of PhpStorm

Вы также можете проверить другие глаголы, такие как удаление и запись. Идите и протестируйте столько, сколько сможете. Есть и другие клиенты, которые вы можете использовать для тестирования: Rest Console и Postman — два из них. Первый доступен только в Chrome, а второй, Postman, доступен как в Chrome, так и в Firefox. Почтальон кажется проще и удобнее для пользователя. Давай, попробуй.

Вывод

Laravel упрощает нашу работу по созданию REST Apis с контроллерами ресурсов. Мы увидели, как интерфейс должен быть построен с использованием соглашений Ember. Ember выбрал хороший интерфейс, и, следуя этой логике, вы можете легко повторно использовать свой код на других платформах.

В этой части я больше сосредоточился на концепциях и не слишком много писал. Заполнение всех методов и добавление проверки излишне расширило бы этот пост, когда он уже достаточно длинный и в длинной серии. При разработке вы всегда должны проверять ввод. Не забывайте об этом, и тестируйте, тестируйте, тестируйте. Тестирование должно быть твоим лучшим другом.

В последней части этой серии мы соберем все вместе в полнофункциональное живое приложение.