Статьи

Создайте REST API с нуля — Введение

Нынешняя интернет-экосистема буквально была захвачена API, и на то есть веские причины. Используя сторонние API в своих продуктах или услугах, вы получаете доступ к множеству полезных функций, таких как службы аутентификации и хранения, которые могут принести пользу как вам, так и вашим пользователям. Предоставляя свой собственный API, ваше приложение становится «частью микса» и будет использоваться способами, о которых вы никогда не думали раньше… если вы делаете это правильно, очевидно.

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

Полный исходный код этого проекта будет доступен в конце части 2.

REST: приятный интерфейс для разработчиков

Прежде всего, API — это пользовательский интерфейс для разработчиков , поэтому он должен быть дружественным, простым, простым в использовании и, конечно, приятным; или иначе это будет еще один кусок цифрового мусора.

Документация, даже в форме простого, но хорошо написанного файла README , является хорошим местом для начала. Минимальная информация, которая нам нужна, — это краткая информация о сфере действия службы, а также список методов и точек доступа.

Хорошее резюме может быть:

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

Тогда хорошей идеей будет составить список всех ресурсов и действий, которые мы собираемся реализовать. Это можно рассматривать как эквивалент каркаса для визуальных приложений. Следуя основным принципам REST, каждый ресурс представлен URL-адресом, где действие — это метод HTTP, используемый для доступа к нему.

Например, GET /api/contacts/12 извлекает контакт с id 12, а PUT /api/contacts/12 обновит этот же контакт.

Полный список методов показан ниже:

 URL HTTP Method Operation /api/contacts GET Returns an array of contacts /api/contacts/:id GET Returns the contact with id of :id /api/contacts POST Adds a new contact and return it with an id attribute added /api/contacts/:id PUT Updates the contact with id of :id /api/contacts/:id PATCH Partially updates the contact with id of :id /api/contacts/:id DELETE Deletes the contact with id of :id /api/contacts/:id/star PUT Adds to favorites the contact with id of :id /api/contacts/:id/star DELETE Removes from favorites the contact with id of :id /api/contacts/:id/notes GET Returns the notes for the contact with id of :id /api/contacts/:id/notes/:nid GET Returns the note with id of :nid for the contact with id of :id /api/contacts/:id/notes POST Adds a new note for the contact with id of :id /api/contacts/:id/notes/:nid PUT Updates the note with id if :nid for the contact with id of :id /api/contacts/:id/notes/:nid PATCH Partially updates the note with id of :nid for the contact with id of :id /api/contacts/:id/notes/:nid DELETE Deletes the note with id of :nid for the contact with id of :id 

Для получения более полной и профессиональной документации вы можете рассмотреть некоторые инструменты, такие как Swagger , apiDoc или служба обнаружения API Google : ваши пользователи будут любить вас!

Инструменты и настройка

Основным инструментом, который я собираюсь использовать для создания нашего API, является Slim Framework . Почему?

[Это] поможет вам быстро написать простые, но мощные веб-приложения и API.

И это правда. Его мощная функция маршрутизации позволяет легко использовать методы, отличные от стандартных GET и POST , обеспечивает встроенную поддержку переопределения методов HTTP (через заголовок HTTP и скрытые поля POST), и его можно подключить с помощью промежуточного программного обеспечения и дополнительных функций, чтобы сделать разработка приложений и API действительно проста.

Наряду с Slim я использую Idiorm для доступа к слою базы данных и Monolog для ведения журнала. Итак, наш файл composer.json будет выглядеть так:

 { "name": "yourname/my-contacts", "description": "Simple RESTful API for contacts management", "license": "MIT", "authors": [ { "name": "Your Name", "email": "[email protected]" } ], "require": { "slim/slim": "*", "slim/extras": "*", "slim/middleware": "*", "monolog/monolog": "*", "j4mie/paris": "*", "flynsarmy/slim-monolog": "*" }, "archive": { "exclude": ["vendor", ".DS_Store", "*.log"] }, "autoload": { "psr-0": { "API": "lib/" } } } 

Пакеты slim/extras и slim/middleware предоставляют полезные функции, такие как анализ типа контента и базовая аутентификация. Наши пользовательские классы находятся в пространстве имен API и находятся внутри каталога lib .

На данный момент наша рабочая структура каталогов будет выглядеть так:

 bootstrap . php composer . json README . md bin / import install lib / API / public / . htaccess index . php share / config / default . php db / logs / sql / data / contacts . sql users . sql tables / contacts . sql notes . sql users . sql ssl / mysitename . crt mysitename . key - bootstrap . php composer . json README . md bin / import install lib / API / public / . htaccess index . php share / config / default . php db / logs / sql / data / contacts . sql users . sql tables / contacts . sql notes . sql users . sql ssl / mysitename . crt mysitename . key 

Фронт-контроллер нашего приложения — public/index.php , где весь трафик, не относящийся к файлам или каталогам, перенаправляется через стандартные правила перезаписи URL.

Затем я поместил весь код инициализации в bootstrap.php который мы увидим позже. Каталог общего share содержит такие данные, как журналы, файлы конфигурации, базы данных SQLite и файлы дампа, а также сертификаты SSL. Каталог bin содержит служебные сценарии, которые создают базу данных и импортируют некоторые данные с использованием предоставленных файлов .sql .

SSL везде

Наш API будет доступен только в режиме HTTPS, без перенаправления. Это упрощает логику аутентификации и предотвращает доступ плохо настроенных клиентов к незашифрованным конечным точкам. Самый простой и логичный способ настроить это — действовать прямо на веб-сервере или через прокси-сервер.

Я использую старый доверенный Apache для этого, и мой файл виртуального хоста выглядит так:

 <Directory "/path/to/MyApp/public"> # Required for mod_rewrite in .htaccess AllowOverride FileInfo Options All -Indexes DirectoryIndex index.php index.shtml index.html <IfModule php5_module> # For Development only! php_flag display_errors On </IfModule> # Enable gzip compression <ifModule filter_module> AddOutputFilterByType DEFLATE application/json </ifModule> Order deny,allow Deny from all Allow from 127.0.0.1 </Directory> <VirtualHost *:80> ServerAdmin [email protected] DocumentRoot "/path/to/MyApp/public" ServerName myapp.dev <IfModule rewrite_module> RewriteEngine on ## Throw a 403 (forbidden) status for non secure requests RewriteCond %{HTTPS} off RewriteRule ^.*$ - [L,R=403] </IfModule> </VirtualHost> <IfModule ssl_module> NameVirtualHost *:443 Listen 443 SSLRandomSeed startup builtin SSLRandomSeed connect builtin <VirtualHost *:443> ServerAdmin [email protected] DocumentRoot "/path/to/MyApp/public" ServerName myapp.dev SSLEngine on SSLCertificateFile /path/to/MyApp/share/ssl/mysitename.crt SSLCertificateKeyFile /path/to/MyApp/share/ssl/mysitename.key SetEnv SLIM_MODE development </VirtualHost> </IfModule> 

Настройки каталога определяются в первую очередь, так что они совпадают как с HTTP, так и с HTTPS-версией нашего сайта. В незащищенной конфигурации хоста я использую mod_rewrite для выдачи ошибки 403 Forbidden для любого незащищенного соединения, затем в защищенном разделе я устанавливаю SSL, используя свои самозаверяющие сертификаты, вместе с переменной SLIM_ENV которая сообщает Slim текущий режим приложения.

Для получения дополнительной информации о том, как создать самозаверяющий сертификат и установить его на свой Apache, см. Эту статью о SSLShopper .

Теперь, когда у нас есть четкие цели, базовая структура каталогов и настройка сервера, давайте запустим composer.phar install и начнем писать некоторый код.

Bootstrap и Фронт-контроллер

Как уже было сказано, файл bootstrap.php отвечает за загрузку настроек нашего приложения и настроек автозагрузчика.

 // Init application mode if (empty($_ENV['SLIM_MODE'])) { $_ENV['SLIM_MODE'] = (getenv('SLIM_MODE')) ? getenv('SLIM_MODE') : 'development'; } // Init and load configuration $config = array(); $configFile = dirname(__FILE__) . '/share/config/' . $_ENV['SLIM_MODE'] . '.php'; if (is_readable($configFile)) { require_once $configFile; } else { require_once dirname(__FILE__) . '/share/config/default.php'; } // Create Application $app = new API\Application($config['app']); 

Сначала я получаю текущую среду. Если файл с именем <EnvName>.php присутствует, он загружается, если не загружен файл конфигурации по умолчанию. Специальные настройки Slim хранятся в массиве $config['app'] и передаются в конструктор нашего приложения, который расширяет базовый объект Slim (необязательно, но рекомендуется).

Например утверждение:

 $config['app']['log.writer'] = new \Flynsarmy\SlimMonolog\Log\MonologWriter(array( 'handlers' => array( new \Monolog\Handler\StreamHandler( realpath(__DIR__ . '/../logs') .'/'.$_ENV['SLIM_MODE'] . '_' .date('Ym-d').'.log' ), ), )); 

настраивает регистратор Monolog, который записывает в файл в app/path/share/logs/EnvName_YYYY-mm-dd.log .

Затем, после некоторых улучшений (вы можете увидеть их в исходном коде), я получаю сгенерированный модуль записи журнала и пытаюсь подключиться к базе данных:

 // Get log writer $log = $app->getLog(); // Init database try { if (!empty($config['db'])) { \ORM::configure($config['db']['dsn']); if (!empty($config['db']['username']) && !empty($config['db']['password'])) { \ORM::configure('username', $config['db']['username']); \ORM::configure('password', $config['db']['password']); } } } catch (\PDOException $e) { $log->error($e->getMessage()); } // Add middleware // Your middleware here... $app->add(new Some\Middleware\Class(...)); 

Наконец, я добавляю необходимое промежуточное программное обеспечение к своему экземпляру приложения. Промежуточное программное обеспечение Slim похоже на слои лука, первое промежуточное программное обеспечение, которое вы добавите, будет самым внутренним слоем, поэтому порядок нашего промежуточного программного обеспечения важен. Я использую следующее промежуточное ПО в нашем API:

  • Кеш (самый внутренний);
  • ContentTypes: анализирует тело в формате JSON у клиента;
  • RateLimit: управляет пользовательскими ограничениями API;
  • JSON: служебное промежуточное программное обеспечение для «лучших ответов JSON» и «кодированных тел JSON»;
  • Аутентификация (внешняя).

Мы напишем все это, за исключением ранее существовавших ContentTypes .

В конце файла начальной загрузки я определил две глобальные переменные $app (экземпляр приложения) и $log (средство записи журнала). Файл загружается нашим фронтальным контроллером index.php где происходит волшебство.

Структура маршрутизации

Slim имеет замечательную функцию под названием Route Groups . С помощью этой функции мы можем определить маршруты наших приложений следующим образом:

 // API group $app->group('/api', function () use ($app, $log) { // Version group $app->group('/v1', function () use ($app, $log) { // GET route $app->get('/contacts/:id', function ($id) use ($app, $log) { }); // PUT route, for updating $app->put('/contacts/:id', function ($id) use ($app, $log) { }); // DELETE route $app->delete('/contacts/:id', function ($id) use ($app, $log) { }); }); }); 

Я создал две вложенные группы, /api и /v1 , чтобы мы могли легко следовать рекомендациям «Управление версиями в URL». Я также создал несколько дополнительных маршрутов для /api/ , которые могут содержать читабельное для пользователя содержимое, и общий URL-адрес корневого URL ( / ), который в реальном мире может содержать общедоступный пользовательский интерфейс для приложения.

Промежуточное ПО JSON

Мой первоначальный подход здесь заключался в использовании промежуточного программного обеспечения для маршрутов (другого типа промежуточного программного обеспечения Slim) внутри группы /v1 для аутентификации и запросов / ответов JSON, но я обнаружил, что более практично и чисто использовать классическое промежуточное программное обеспечение. Как было показано ранее, промежуточное программное обеспечение является экземпляром класса, который наследуется от Slim\Middleware . Метод call() промежуточного программного обеспечения Slim — это место, где происходит действие, оно выполняется автоматически, когда промежуточное программное обеспечение связывается как глобальное, с помощью $app->add() .

 $app->add(new API\Middleware\JSON('/api/v1')); 

Наше промежуточное ПО JSON имеет две передовые практики: «Ответы только JSON» и «Закодированные в JSON тела». Вот как:

 // JSON middleware call public function call() { if (preg_match('|^' . $this->root . '.*|', $this->app->request->getResourceUri())) { // Force response headers to JSON $this->app->response->headers->set( 'Content-Type', 'application/json' ); $method = strtolower($this->app->request->getMethod()); $mediaType = $this->app->request->getMediaType(); if (in_array( $method, array('post', 'put', 'patch') )) { if (empty($mediaType) || $mediaType !== 'application/json') { $this->app->halt(415); } } } $this->next->call(); } 

Мы можем передать корневой путь нашему конструктору промежуточного программного обеспечения. В этом случае я передаю /api/v1 чтобы наше промежуточное ПО применялось только в части API нашего сайта. Если текущий путь совпадает с заголовком типа содержимого ответа, принудительно application/json на application/json , тогда я проверяю метод запроса. Если метод запроса является одним из методов с включенной записью ( PUT , POST , PATCH ), заголовок типа содержимого запроса должен быть application/json , если нет, приложение выходит с кодом состояния HTTP 415 Unsupported Media Type .

Если все правильно, оператор $this->next->call() запускает следующее промежуточное ПО в цепочке.

Аутентификация

Поскольку по умолчанию наше приложение будет работать по протоколу HTTPS, я решил использовать подход «токен поверх базовой аутентификации»: ключ API отправляется в поле имени пользователя базовых заголовков HTTP AUTH (пароль не требуется). Для этого я написал класс промежуточного ПО Slim под названием TokenOverBasicAuth , изменив существующий Slim HttpBasicAuth . Это промежуточное программное обеспечение запускается первым в цепочке, поэтому оно добавляется как последнее и принимает необязательный параметр корневого пути в конструкторе.

 // Auth middleware call public function call() { $req = $this->app->request(); $res = $this->app->response(); if (preg_match('|^' . $this->config['root'] . '.*|', $req->getResourceUri())) { // We just need the user $authToken = $req->headers('PHP_AUTH_USER'); if (!($authToken && $this->verify($authToken))) { $res->status(401); $res->header( 'WWW-Authenticate', sprintf('Basic realm="%s"', $this->config['realm']) ); } } $this->next->call(); } 

Метод ищет токен аутентификации в заголовке запроса PHP_AUTH_USER , если он не существует или недействителен, 401 Forbidden статус и заголовки аутентификации передаются клиенту. Метод verify() защищен, поэтому его можно переопределить дочерними классами; моя версия здесь проста:

 protected function verify($authToken) { $user = \ORM::forTable('users')->where('apikey', $authToken) ->findOne(); if (false !== $user) { $this->app->user = $user->asArray(); return true; } return false; } 

Здесь я просто проверяю наличие ключа API в таблице users , и если я нахожу действительного пользователя, он добавляется в контекст приложения для использования со следующим уровнем (RateLimit). Вы можете изменить или расширить этот класс, чтобы добавить собственную логику аутентификации или использовать модуль OAuth . Для получения дополнительной информации об OAuth см . Статью Джейми Мунро .

Полезные данные об ошибках

Наш API должен показывать полезные сообщения об ошибках в формате расходных материалов, если это возможно, в формате JSON. Нам нужна минимальная полезная нагрузка, которая содержит код ошибки и сообщение. Кроме того, ошибки проверки требуют большей разбивки. С помощью Slim мы можем переопределить как ошибки 404 ошибки сервера с помощью $app->notFound() и $app->error() соответственно.

 $app->notFound(function () use ($app) { $mediaType = $app->request->getMediaType(); $isAPI = (bool) preg_match('|^/api/v.*$|', $app->request->getPath()); if ('application/json' === $mediaType || true === $isAPI) { $app->response->headers->set( 'Content-Type', 'application/json' ); echo json_encode( array( 'code' => 404, 'message' => 'Not found' ) ); } else { echo '<html> <head><title>404 Page Not Found</title></head> <body><h1>404 Page Not Found</h1><p>The page you are looking for could not be found.</p></body></html>'; } }); 

Не найденные ошибки проще: сначала я выбираю медиа-тип запроса, затем флаг $isAPI сообщает мне, находится ли текущий URL-адрес в группе /api/v* . Если клиент запросил URL-адрес API или отправил заголовок типа контента JSON, я возвращаю вывод JSON, иначе я могу отобразить шаблон или просто напечатать некоторый статический HTML, как в этом примере.

Другие ошибки немного сложны, метод $app->error() запускается, когда возникает исключение, и Slim преобразует стандартные ошибки PHP в объектах ErrorException . Нам нужен способ дать клиенту полезную ошибку, не раскрывая слишком много нашего внутреннего механизма, чтобы избежать недостатков безопасности. Я создал два пользовательских исключения для этого приложения: API\Exception и API\Exception\ValidationException , которые доступны для общественности, все другие типы исключений регистрируются и отображаются только в режиме разработки.

 $app->error(function (\Exception $e) use ($app, $log) { $mediaType = $app->request->getMediaType(); $isAPI = (bool) preg_match('|^/api/v.*$|', $app->request->getPath()); // Standard exception data $error = array( 'code' => $e->getCode(), 'message' => $e->getMessage(), 'file' => $e->getFile(), 'line' => $e->getLine(), ); // Graceful error data for production mode if (!in_array( get_class($e), array('API\\Exception', 'API\\Exception\ValidationException') ) && 'production' === $app->config('mode')) { $error['message'] = 'There was an internal error'; unset($error['file'], $error['line']); } // Custom error data (eg Validations) if (method_exists($e, 'getData')) { $errors = $e->getData(); } if (!empty($errors)) { $error['errors'] = $errors; } $log->error($e->getMessage()); if ('application/json' === $mediaType || true === $isAPI) { $app->response->headers->set( 'Content-Type', 'application/json' ); echo json_encode($error); } else { echo '<html> <head><title>Error</title></head> <body><h1>Error: ' . $error['code'] . '</h1><p>' . $error['message'] .'</p></body></html>'; } }); 

Метод $app->error() получает выброшенное исключение в качестве аргумента. По умолчанию я извлекаю все необходимые данные и заполняю массив $error , затем, если я нахожусь в рабочем режиме, я сбрасываю личные данные и переписываю сообщение универсальным. ValidationException класс ValidationException имеет пользовательский метод getData() который возвращает массив ошибок проверки, которые добавляются в окончательную полезную нагрузку. Затем ошибка отображается в JSON или HTML в зависимости от запроса. На стороне API у нас может быть простая ошибка, подобная этой:

 { "code": 123, "message": "An error occurred, a support person is being notified of this" } 

или полная ошибка проверки как это:

 { "code": 0, "message": "Invalid data", "errors": { "contact": [ { "field": "email", "message": "Email address already exists" } ] } } 

Вывод

Теперь у нас есть ядро ​​нашего API. В следующей части мы добавим немного мяса для полноценного функционирования сервиса. В то же время, не стесняйтесь читать статьи, ссылки на которые есть в этой части — они являются сокровищницей полезных принципов разработки API.