Создание REST API для устаревших проектов PHP

Опубликовано: 2022-03-11

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

Создание REST API для устаревших проектов PHP

Создание REST API для устаревших проектов PHP
Твитнуть

В этой статье мы рассмотрим некоторые распространенные проблемы, возникающие при попытке реализовать REST API с нуля, несколько способов решения этих проблем и общую стратегию создания пользовательских API-серверов на основе PHP для устаревших проектов.

Хотя статья основана на PHP 5.3 и более поздних версиях, основные концепции действительны для всех версий PHP после версии 5.0 и даже могут быть применены к проектам, отличным от PHP. Здесь мы не будем рассказывать, что такое REST API в целом, поэтому, если вы не знакомы с ним, обязательно сначала прочитайте о нем.

Чтобы вам было легче следовать, вот список некоторых терминов, используемых в этой статье, и их значения:

  • Сервер API: основное приложение REST, обслуживающее API, в данном случае написанное на PHP.
  • Конечная точка API: внутренний «метод», с которым взаимодействует клиент для выполнения действия и получения результатов.
  • URL-адрес конечной точки API: URL-адрес, по которому серверная система доступна для всего мира.
  • Токен API: уникальный идентификатор, передаваемый через HTTP-заголовки или файлы cookie, по которому можно идентифицировать пользователя.
  • Приложение: клиентское приложение, которое будет взаимодействовать с приложением REST через конечные точки API. В этой статье мы будем предполагать, что это веб-приложение (для настольных компьютеров или мобильных устройств), поэтому оно написано на JavaScript.

Начальные шаги

Паттерны пути

Одна из самых первых вещей, которые нам нужно решить, — это то, по какому URL-адресу будут доступны конечные точки API. Есть 2 популярных способа:

  • Создайте новый поддомен, например, api.example.com.
  • Создайте путь, например, example.com/api.

На первый взгляд может показаться, что первый вариант более популярен и привлекателен. Однако в действительности, если вы создаете API для конкретного проекта, может быть более подходящим выбрать второй вариант.

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

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

Использование файлов cookie можно считать практикой «не RESTful», поскольку запросы REST должны быть без сохранения состояния. В этом случае мы можем пойти на компромисс и передать значение токена в файле cookie вместо того, чтобы передавать его через собственный заголовок. По сути, мы используем файлы cookie только как способ передать значение токена вместо сеанса_id напрямую. Этот подход можно считать безгражданским, но мы можем оставить его на ваше усмотрение.

URL-адреса конечных точек API также могут быть версионными. Кроме того, они могут включать ожидаемый формат ответа в качестве расширения пути. Хотя это не критично, особенно на ранних этапах разработки API, в долгосрочной перспективе эти детали, безусловно, могут окупиться. Особенно, когда вам нужно реализовать новые функции. Проверка того, какую версию ожидает клиент, и предоставление необходимого формата для обратной совместимости может быть лучшим решением.

Структура URL-адреса конечной точки API может выглядеть следующим образом:

 example.com/api/${version_code}/${actual_request_path}.${format}

И, реальный пример:

 example.com/api/v1.0/records.json

Маршрутизация

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

Вы можете думать о «www/api/Apis/Users.php» как об отдельном «контроллере» для конкретной конечной точки API. Было бы здорово повторно использовать реализации из существующей кодовой базы, например повторно использовать модели, которые уже реализованы в проекте, для связи с базой данных.

Наконец, не забудьте указать все входящие запросы с «/api/*» на «/api/index.php». Это можно сделать, изменив конфигурацию вашего веб-сервера.

Класс API

Версия и формат

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

  • v1.0 будет означать первую версию.
  • v1.1 первая версия с небольшими изменениями.
  • v2.0 будет совершенно новой версией.

Формат может быть любым, что нужно вашему клиенту, включая, помимо прочего, JSON, XML и даже CSV. Предоставляя его через URL-адрес в виде расширения файла, URL-адрес конечной точки API обеспечивает удобочитаемость, и потребителю API становится несложно узнать, какой формат они могут ожидать:

  • «/api/v1.0/records.json» вернет массив записей JSON.
  • «/api/v1.0/records.xml» вернет XML-файл записей.

Стоит отметить, что вам также потребуется отправить правильный заголовок Content-Type в ответ для каждого из этих форматов.

Получив входящий запрос, вы должны в первую очередь проверить, поддерживает ли сервер API запрошенную версию и формат. В вашем основном методе, который обрабатывает входящий запрос, проанализируйте $_SERVER['PATH_INFO'] или $_SERVER['REQUEST_URI'] , чтобы определить, поддерживаются ли запрошенный формат и версия. Затем либо продолжите, либо верните ответ 4xx (например, 406 «Неприемлемо»). Самая важная часть здесь — всегда возвращать то, что ожидает клиент. Альтернативой этому может быть проверка заголовка запроса «Принять» вместо расширения пути URL.

Разрешенные маршруты

Вы можете прозрачно перенаправлять все на свои контроллеры API, но может быть лучше использовать набор разрешенных маршрутов из белого списка. Это немного уменьшит гибкость, но даст очень четкое представление о том, как выглядят URL-адреса конечных точек API, когда вы в следующий раз вернетесь к коду.

 private $public_routes = array( 'system' => array( 'regex' => 'system', ), 'records' => array( 'regex' => 'records(?:/?([0-9]+)?)', ), );

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

 /api/v1.0/system.json /api/v1.0/records.json /api/v1.0/records/7.json

Обработка данных PUT

PHP автоматически обрабатывает входящие данные POST и помещает их в $_POST superglobal. Однако это не относится к запросам PUT. Все данные «закопаны» в php://input . Не забудьте разобрать его в отдельную структуру или массив перед вызовом фактического метода API. Простого parse_str может быть достаточно, но если клиент отправляет составной запрос, может потребоваться дополнительный анализ для обработки границ формы. Типичный вариант использования составных запросов включает загрузку файлов. Обнаружение и обработка составных запросов может быть выполнена следующим образом:

 self::$input = file_get_contents('php://input'); // For PUT/DELETE there is input data instead of request variables if (!empty(self::$input)) { preg_match('/boundary=(.*)$/', $_SERVER['CONTENT_TYPE'], $matches); if (isset($matches[1]) && strpos(self::$input, $matches[1]) !== false) { $this->parse_raw_request(self::$input, self::$input_data); } else { parse_str(self::$input, self::$input_data); } }

Здесь parse_raw_request может быть реализован как:

 /** * Helper method to parse raw requests */ private function parse_raw_request($input, &$a_data) { // grab multipart boundary from content type header preg_match('/boundary=(.*)$/', $_SERVER['CONTENT_TYPE'], $matches); $boundary = $matches[1]; // split content by boundary and get rid of last -- element $a_blocks = preg_split("/-+$boundary/", $input); array_pop($a_blocks); // loop data blocks foreach ($a_blocks as $id => $block) { if (empty($block)) { continue; } // parse uploaded files if (strpos($block, 'application/octet-stream') !== false) { // match "name", then everything after "stream" (optional) except for prepending newlines preg_match("/name=\"([^\"]*)\".*stream[\n|\r]+([^\n\r].*)?$/s", $block, $matches); // parse all other fields } else { // match "name" and optional value in between newline sequences preg_match('/name=\"([^\"]*)\"[\n|\r]+([^\n\r].*)?\r$/s', $block, $matches); } $a_data[$matches[1]] = $matches[2]; } }

При этом мы можем иметь необходимую полезную нагрузку запроса в Api::$input как необработанный ввод и Api::$input_data в виде ассоциативного массива.

Подделка PUT/DELETE

Иногда вы можете столкнуться с ситуацией, когда сервер не поддерживает ничего, кроме стандартных HTTP-методов GET/POST. Распространенным решением этой проблемы является «подделка» PUT/DELETE или любого другого пользовательского метода запроса. Для этого вы можете использовать «волшебный» параметр, такой как «_method». Если вы видите его в своем массиве $_REQUEST , просто предположите, что запрос имеет указанный тип. В современные фреймворки, такие как Laravel, встроена такая функциональность. Это обеспечивает отличную совместимость в случае, если ваш сервер или клиент имеет ограничения (например, человек использует свою рабочую сеть Wi-Fi за корпоративным прокси-сервером, который не разрешает запросы PUT).

Переадресация на конкретный API

Если у вас нет возможности повторно использовать существующие автозагрузчики проекта, вы можете создать свой собственный с помощью функции spl_autoload_register . Определите его на своей странице «api/index.php» и вызовите свой класс API, расположенный в «api/Api.php». Класс API действует как промежуточное ПО и вызывает фактический метод. Например, запрос к «/api/v1.0/records/7.json» должен закончиться вызовом метода GET «Apis/Records.php» с параметром 7. Это обеспечит разделение проблем и предоставит способ сохранить очиститель логики. Конечно, если возможно глубже интегрировать это в используемую вами структуру и повторно использовать ее конкретные контроллеры или маршруты, вы должны рассмотреть и эту возможность.

Пример «api/index.php» с примитивным автозагрузчиком:

 <?php // Let's define very primitive autoloader spl_autoload_register(function($classname){ $classname = str_replace('Api_', 'Apis/', $classname); if (file_exists(__DIR__.'/'.$classname.'.php')) { require __DIR__.'/'.$classname.'.php'; } }); // Our main method to handle request Api::serve();

Это загрузит наш класс API и начнет обслуживать его независимо от основного проекта.

ВАРИАНТЫ Запросы

Когда клиент использует настраиваемый заголовок для пересылки своего уникального токена, браузеру сначала необходимо проверить, поддерживает ли сервер этот заголовок. Вот где появляется запрос OPTIONS. Его цель — убедиться, что все в порядке и безопасно как для клиента, так и для сервера API. Таким образом, запрос OPTIONS может запускаться каждый раз, когда клиент пытается что-либо сделать. Однако, когда клиент использует файлы cookie для учетных данных, это избавляет браузер от необходимости отправлять этот дополнительный запрос OPTIONS.

Если клиент запрашивает POST /users/8.json с файлами cookie, его запрос будет довольно стандартным:

  • Приложение выполняет POST-запрос к /users/8.json.
  • Браузер выполняет запрос и получает ответ.

Но с пользовательской авторизацией или заголовком токена:

  • Приложение выполняет POST-запрос к /users/8.json.
  • Браузер прекращает обработку запроса и вместо этого инициирует запрос OPTIONS.
  • Запрос OPTIONS отправляется в /users/8.json.
  • Браузер получает ответ со списком всех доступных методов и заголовков, определенных API.
  • Браузер продолжает исходный запрос POST, только если настраиваемый заголовок присутствует в списке доступных заголовков.

Однако имейте в виду, что даже при использовании файлов cookie с PUT/DELETE вы все равно можете получить этот дополнительный запрос OPTIONS. Так что будьте готовы ответить на него.

API записей

Базовая структура

Наш пример Records API довольно прост. Он будет содержать все методы запроса и возвращать вывод обратно в тот же основной класс API. Например:

 <?php class Api_Records { public function __construct() { // In here you could initialize some shared logic between this API and rest of the project } /** * Get individual record or records list */ public function get($id = null) { if ($id) { return $this->getRecord(intval($id)); } else { return $this->getRecords(); } } /** * Update record */ public function put($record_id = null) { // In real world there would be call to model with validation and probably token checking // Use Api::$input_data to update return Api::responseOk('OK', array()); } // ...

Таким образом, определение каждого метода HTTP позволит нам легче создавать API в стиле REST.

Форматирование вывода

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

Еще одним преимуществом ключей из белого списка является то, что вы можете написать документацию на их основе и выполнить все проверки типов, гарантируя, например, что user_id всегда будет целым числом, флаг is_banned всегда будет логическим значением true или false, а время даты будет иметь один стандартный формат ответа.

Вывод результатов

Заголовки

Отдельные методы для вывода заголовков обеспечат корректность всего, отправляемого в браузер. Этот метод может использовать преимущества доступа к API через тот же домен, сохраняя при этом возможность получения пользовательского заголовка авторизации. Выбор между тем же или сторонним доменом может происходить с помощью серверных заголовков HTTP_ORIGIN и HTTP_REFERER. Если приложение обнаруживает, что клиент использует x-авторизацию (или любой другой настраиваемый заголовок), оно должно разрешить доступ из всех источников, разрешить настраиваемый заголовок. Таким образом, это может выглядеть так:

 header('Access-Control-Allow-Origin: *'); header('Access-Control-Expose-Headers: x-authorization'); header('Access-Control-Allow-Headers: origin, content-type, accept, x-authorization'); header('X-Authorization: '.YOUR_TOKEN_HERE);

Однако, если клиент использует учетные данные на основе файлов cookie, заголовки могут немного отличаться, разрешая только запрошенные заголовки, связанные с хостом и файлами cookie, для учетных данных:

 header('Access-Control-Allow-Origin: '.$origin); header('Access-Control-Expose-Headers: set-cookie, cookie'); header('Access-Control-Allow-Headers: origin, content-type, accept, set-cookie, cookie'); // Allow cookie credentials because we're on the same domain header('Access-Control-Allow-Credentials: true'); if (strtolower($_SERVER['REQUEST_METHOD']) != 'options') { setcookie(TOKEN_COOKIE_NAME, YOUR_TOKEN_HERE, time()+86400*30, '/', '.'.$_SERVER['HTTP_HOST']); }

Имейте в виду, что запрос OPTIONS не поддерживает файлы cookie, поэтому приложение не отправит их вместе с ним. И, наконец, это позволяет всем нашим требуемым методам HTTP иметь истечение срока действия контроля доступа:

 header('Access-Control-Allow-Methods: POST, GET, OPTIONS, PUT, DELETE'); header('Access-Control-Max-Age: 86400');

Тело

Само тело должно содержать ответ в формате, запрошенном вашим клиентом, со статусом HTTP 2xx в случае успеха, статусом 4xx в случае сбоя из-за клиента и статусом 5xx в случае сбоя из-за сервера. Структура ответа может быть разной, хотя указание полей «статус» и «ответ» также может быть полезным. Например, если клиент пытается зарегистрировать нового пользователя, а имя пользователя уже занято, вы можете отправить ответ с HTTP-статусом 200, но JSON в теле, который выглядит примерно так:

 {“status”: “ERROR”, “response”: ”username already taken”}

… вместо ошибки HTTP 4xx напрямую.

Заключение

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

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

Пришлось недавно внедрить сервер REST API для какого-то устаревшего проекта? Поделитесь с нами своим опытом в разделе комментариев ниже.