Говорите то, что имеете в виду
REST API общается двумя словарями. Метод говорит, чего хочет клиент: GET — прочитать, POST — создать, PUT — заменить, PATCH — изменить часть, DELETE — удалить. Код состояния говорит, как всё прошло: 2xx — успех, 4xx — ошибка клиента, 5xx — ошибка сервера.
Деталь, о которой стоит знать: PUT идемпотентен — отправить его дважды заканчивается тем же, чем один раз. POST — нет: два POST создают два ресурса.
Правильный метод
Маршрутизатор читает глагол из $_SERVER["REQUEST_METHOD"] и направляет каждый в свою ветку. Создание отвечает 201 Created и указывает на новый ресурс заголовком Location; удаление отвечает 204 No Content — успех, о котором нечего сказать.
GET, POST и DELETE на одном ресурсе
Три глагола, три ответа: список возвращается с 200, создание — с 201 и Location, удаление — с 204 и пустым телом.
Вывод: PHP-скрипт
<?php
// По одной ветке на каждый глагол: метод — часть маршрута.
$router = <<<'PHP'
<?php
$method = $_SERVER["REQUEST_METHOD"];
$path = parse_url($_SERVER["REQUEST_URI"], PHP_URL_PATH);
$segments = explode("/", trim($path, "/"));
if ($method === "GET" && $path === "/notes") {
echo "notes: shopping, ideas\n";
} elseif ($method === "POST" && $path === "/notes") {
header("Location: /notes/3");
http_response_code(201);
echo "created\n";
} elseif ($method === "DELETE" && count($segments) === 2 && $segments[0] === "notes") {
http_response_code(204);
} else {
http_response_code(404);
}
PHP;
function httpRequest(string $url, string $method = "GET", array $headers = [], ?string $body = null): array {
$options = ["method" => $method, "ignore_errors" => true];
if ($headers !== []) {
$options["header"] = implode("\r\n", $headers);
}
if ($body !== null) {
$options["content"] = $body;
}
$responseBody = file_get_contents($url, false, stream_context_create(["http" => $options]));
return [(int) explode(" ", $http_response_header[0])[1], $responseBody, $http_response_header];
}
$routerFile = sys_get_temp_dir() . "/pc-methods.php";
file_put_contents($routerFile, $router);
// Свободный порт, затем встроенный веб-сервер PHP как дочерний процесс.
$probe = stream_socket_server("tcp://127.0.0.1:0");
$port = (int) explode(":", stream_socket_get_name($probe, false))[1];
fclose($probe);
$server = proc_open(["php", "-S", "127.0.0.1:$port", $routerFile],
[1 => ["file", "/dev/null", "w"], 2 => ["file", "/dev/null", "w"]], $pipes);
do {
usleep(20000);
$up = @fsockopen("127.0.0.1", $port);
} while ($up === false);
fclose($up);
$base = "http://127.0.0.1:$port";
[$status, $body] = httpRequest("$base/notes");
echo "GET /notes -> $status " . trim($body) . "\n";
[$status, , $headers] = httpRequest("$base/notes", "POST",
["Content-Type: application/json"], '{"text": "call Ben"}');
$location = "";
foreach ($headers as $headerLine) {
if (stripos($headerLine, "Location:") === 0) {
$location = trim(substr($headerLine, 9));
}
}
echo "POST /notes -> $status Location: $location\n";
[$status] = httpRequest("$base/notes/3", "DELETE");
echo "DELETE /notes/3 -> $status (no content)\n";
proc_terminate($server);
proc_close($server);
unlink($routerFile);
Правильный код состояния
Ошибки тоже заслуживают точности: отсутствующий ресурс — это 404 Not Found (с JSON-телом ошибки, чтобы его могли прочитать программы), а неверный глагол на известном пути — 405 Method Not Allowed, с заголовком Allow, перечисляющим глаголы, которые сработали бы. Скрипт-маршрутизатор отвечает на оба случая сам, выбирая состояние через http_response_code().
200, 404 и 405
Существующая заметка возвращает 200 с данными, отсутствующая — 404 с JSON-ошибкой, а DELETE на маршруте только для GET отклоняется с 405 и заголовком Allow: GET.
Вывод: PHP-скрипт
<?php
// 200 — найдено, 404 — отсутствует, 405 — неверный глагол.
$router = <<<'PHP'
<?php
$notes = ["1" => "shopping list"];
$method = $_SERVER["REQUEST_METHOD"];
$segments = explode("/", trim(parse_url($_SERVER["REQUEST_URI"], PHP_URL_PATH), "/"));
$id = $segments[1] ?? "";
if ($method !== "GET") {
// Неверный глагол на известном пути: отказываем и сообщаем, что сработает.
header("Allow: GET");
http_response_code(405);
} elseif (!isset($notes[$id])) {
header("Content-Type: application/json");
http_response_code(404);
echo '{"error": "note not found"}' . "\n";
} else {
header("Content-Type: application/json");
echo json_encode(["id" => $id, "text" => $notes[$id]]) . "\n";
}
PHP;
function httpRequest(string $url, string $method = "GET", array $headers = [], ?string $body = null): array {
$options = ["method" => $method, "ignore_errors" => true];
if ($headers !== []) {
$options["header"] = implode("\r\n", $headers);
}
if ($body !== null) {
$options["content"] = $body;
}
$responseBody = file_get_contents($url, false, stream_context_create(["http" => $options]));
return [(int) explode(" ", $http_response_header[0])[1], $responseBody, $http_response_header];
}
$routerFile = sys_get_temp_dir() . "/pc-status-codes.php";
file_put_contents($routerFile, $router);
// Свободный порт, затем встроенный веб-сервер PHP как дочерний процесс.
$probe = stream_socket_server("tcp://127.0.0.1:0");
$port = (int) explode(":", stream_socket_get_name($probe, false))[1];
fclose($probe);
$server = proc_open(["php", "-S", "127.0.0.1:$port", $routerFile],
[1 => ["file", "/dev/null", "w"], 2 => ["file", "/dev/null", "w"]], $pipes);
do {
usleep(20000);
$up = @fsockopen("127.0.0.1", $port);
} while ($up === false);
fclose($up);
$base = "http://127.0.0.1:$port";
foreach (["/notes/1", "/notes/99"] as $path) {
[$status, $body] = httpRequest("$base$path");
printf("GET %s -> %d %s\n", $path, $status, trim($body));
}
[, , $headers] = httpRequest("$base/notes/1", "DELETE");
[, $statusText] = explode(" ", $headers[0], 2);
$allow = "";
foreach ($headers as $headerLine) {
if (stripos($headerLine, "Allow:") === 0) {
$allow = trim(substr($headerLine, 6));
}
}
echo "DELETE /notes/1 -> $statusText (Allow: $allow)\n";
proc_terminate($server);
proc_close($server);
unlink($routerFile);