На мой взгляд, язык php всегда был довольно хорошим решением для создания сложного бекэнда веб-приложений, а в девяностые и нулевые приобрел такую огромную популярность (именно огромную, сопоставимую с IE для веб-серфига того времени) в первую очередь благодаря легкости, скорости разработки и поддержки кода. Но те времена прошли. Сегодня считается, что приложения на php стали монструозны, долго и сложно запускаемы, способны работать только с подтягиванием множества зависимостей в директорию /vendor...

Зачастую все именно так, но я хочу попробовать вам показать, что может быть иначе. Попробуем сделать простое API и приложить не больше усилий чем при использовании Node.js или Go.

Под катом мой пример того, как можно быстро и без лишней головной боли сделать API на php.

Я решил использовать понравившийся мне по работе в нескольких проектах фреймворк Slim (https://www.slimframework.com/), потому что имею опыт написания/поддержки с нуля и роутера, и ORM, а также поддержки самописных CMS, и это точно не про "быстро и легко".

Для решения на фреймворке Slim не понадобится устанавливать дополнительных расширений на php. Кроме, конечно же, самого сервера (Apache 2, Nginx или другого на наш вкус), сервера баз данных и composer, но этот набор обычно уже есть и на серверах, и тем более на хостинге. Теперь начинаем кухарить (cooking app):

mkdir my-slim-api && cd my-slim-api
composer init

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

{ 
  "name": "zhukmax/slim", 
  "type": "project", 
  "license": "MIT", 
  "autoload": { 
    "psr-4": { 
      "Zhukmax\\Slim\\": "src/" 
    } 
  }, 
  "require": {}
}

Устанавливаем зависимости, сам Slim, а также реализацию интерфейсов http-сообщений (PSR-7) и интерфейсов фабрик http-сообщений (PSR-17):

composer require slim/slim:"4.*"
composer require slim/psr7

Теперь мы можем создать простое API в одном index-файле, возвращающее, как сейчас "модно-молодежно", json строку в теле ответа:

<?php 
require __DIR__ . '/vendor/autoload.php'; 
 
use Psr\Http\Message\ResponseInterface as Response; 
use Psr\Http\Message\ServerRequestInterface as Request; 
use Slim\Factory\AppFactory; 
 
$app = AppFactory::create(); 
 
$app->get('/', function (Request $request, Response $response, $args) {
  $data = array('name' => 'Max', 'role' => 'web developer');
  $response->getBody()->write(json_encode($data));

  return $response->withHeader('Content-Type', 'application/json'); 
}); 
 
$app->run();

При этом укажем в composer.json информацию, что нам требуется расширение php-json (и если расширение не установлено – установим)

"require": {
  "slim/slim": "4.*",
  "slim/psr7": "^1.4",
  "ext-json": "*"
}

Работа с данными из MySQL

В Slim придется сначала установить стороннее решение для удобной работы с базой, так как сам фреймворк поставляется без ORM. Можно использовать и решение от Laravel Eloquent и популярную Doctrine, которая также подойдет и для работы с NoSQL базами. Для нашей статьи выберем Idiorm за простоту использования.

composer require j4mie/idiorm

Настраиваем подключение и запрашиваем данные:

<?php

ORM::configure([
  // Обратите внимание, что localhost не работает по умолчанию,
  // прежде чем паниковать попробуйте 127.0.0.1

  'connection_string' => 'mysql:host=127.0.0.1;dbname=mydb', 
  'username' => 'root', 
  'password' => '124' 
]);
 
$app->get('/users', function (Request $request, Response $response, $args) { 
  $users = ORM::forTable("users")->find_array();
  $response->getBody()->write(json_encode($users));

  return $response;
});

Теперь, если запустить приложение и проверить url, мы получим список всех записей в таблице users в виде json-строки.

Создать, обновить, получить и удалить

Все должно работать отлично, и теперь можно создать методы для получения одного пользователя, добавления нового пользователя, изменения и удаления. Для этого используем GET, POST, PUT и DELETE http-запросы и их обработку.

<?php

$app->post('/users', function (Request $request, Response $response, $args) { 
  $parsedBody = $request->getParsedBody(); 
 
  $user = ORM::forTable("users")->create(); 
  $user->name = $parsedBody['name'] ?? ''; 
 
  if ($user->save()) { 
    $successRes = $response->withStatus(201); 
    $successRes->getBody()->write(json_encode([ 
      "message" => "Success" 
    ])); 
 
	  return $successRes; 
  } else { 
    $errorRes = $response->withStatus(501); 
    $errorRes->getBody()->write(json_encode([ 
      "message" => "Error" 
    ]));
    
    return $errorRes; 
  } 
});

Мы, конечно же, хотим, чтобы наш код был готов к ошибкам, и поэтому пишем их обработку в теле функции. Кроме того, что мы добавили обработку ошибки сохранения нового пользователя в таблице, еще можно сделать проверку соответствия полей определенным шаблонам, например, их обязательность. Но для этого лучше подходят модели, которые могут немного по-разному создаваться в зависимости от выбранного ORM. В Idiorm нет стандарта оформления класса моделей, да и описание фильтров и шаблонов немного за рамками данной статьи, так что оставим это за скобками. Мы же хотим быстро сделать рабочее API, а доводить его до ума можно годами, как показывает практика.

Контроллеры и actions

Хранить код всех запросов и их обработки в одном index.php файле – затея не из лучших, поэтому давайте воспользуемся возможностью фреймворка по созданию контроллеров. В обработчике маршрута оставим только описание пути и ссылку на нужный нам метод в классе-контроллере.

$app->get('/users/{id}', [UserController::class, "getOne"]);

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

<?php

class UserController 
{ 
  public function getOne(Request $request, Response $response, $args): Response 
  { 
    $id = (int)$args['id'] ?? 0; 
    $user = ORM::forTable("table1")->findOne($id); 
 
    if (!$user) { 
      return self::errorResponse($response, 404, "Error text"); 
    } 
 
    $response->getBody()->write(json_encode([ 
      "id" => $user->id, 
      "name" => $user->name 
    ])); 
 
    return $response; 
  }
 
  private static function errorResponse(Response $response, int $code, string $text): Response 
  { 
    $errorRes = $response->withStatus($code); 
    $errorRes->getBody()->write(json_encode([ 
      "message" => $text 
    ])); 
 
    return $errorRes; 
  }
}

Итог

В первую очередь, я удовлетворил свой собственный интерес. Мне хотелось быстро набросать на php простое API, как я много раз делал на Express, когда создавал приложения на Angular и требовалось что-то поднять на backend. И я считаю, что описанный вариант более чем достоин для использования. Он предполагает возможность развиваться, увеличивать количество строк кода. При этом может быть поддерживаемым и недорогим решением, так как достаточно прост в освоении даже не очень опытным web-разработчиком.

Так как помещать весь код в статью довольно неудобно и она сильно увеличится, я сделал на github репозиторий (https://github.com/ZhukMax/slim-api), в котором можно посмотреть весь получившийся код.