Как написать rest api на php

В данной статье вы узнаете, как создать простой REST API в PHP.

  • 1. Обзор проекта
  • 1.1 Что такое REST API?
  • 1.2 Зачем нужен REST API?
  • 1.3 Где используется REST API?
  • 2. Файловая структура
  • 3. Настройка базы данных
  • 3.1 Создание таблицы категорий
  • 3.2 Дамп данных для таблицы категорий
  • 3.3 Создание таблицы товаров
  • 3.4 Дамп данных для таблицы товаров
  • 3.5 Подключение к базе данных
  • 4. Получение товаров
  • 4.1 Создание объекта Product
  • 4.2 Создание файла для чтения товаров
  • 4.3 Подключение к базе данных и таблице товаров
  • 4.4 Чтение товаров из базы данных
  • 4.5 Создание метода read()
  • 4.6 Уведомление пользователя о том, что товары не найдены
  • 5. Создание товаров
  • 5.1 Создание файла create.php
  • 5.2 Создание метода create()
  • 6. Получение одного товара
  • 6.1 Создание файла read_one.php
  • 6.2 Создание метода readOne()
  • 7. Обновление товара
  • 7.1 Создание файла update.php
  • 7.2 Создание метода update()
  • 8. Удаление товара
  • 8.1 Создание файла delete.php
  • 8.2 Создание метода delete()
  • 9. Поиск товаров
  • 9.1 Создание файла search.php
  • 9.2 Создание метода search()
  • 10. Пагинация товаров
  • 10.1 Создание файла read_paging.php
  • 10.2 Создание файла core.php
  • 10.3 Создание метода readPaging()
  • 10.4 Создание метода count()
  • 10.5 Получение массива пагинации
  • 11. Получение категорий
  • 11.1 Создание класса Category
  • 11.2 Создание метода readAll()
  • 11.3 Создание файла read.php

1. Обзор проекта

1.1 Что такое REST API?

REST — это концепция (архитектура) для организации взаимодействия между независимыми объектами (приложениями) посредством протокола HTTP. Включает в себя набор принципов (рекомендаций) взаимодействия клиент-серверных приложений. Обычно он представлен в формате JSON.

API — интерфейс взаимодействия с каким-либо объектом (программой, приложением), включающий в себя набор правил, которые позволяют одному приложению общаться с другим. Эти «правила» могут включать в себя операции создания, чтения, обновления и удаления. Примером API может служить всем известная библиотека jQuery.

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

RESP API PHP

1.2 Зачем нужен REST API?

Во многих приложениях REST API необходим, потому что это самый легкий способ создания, чтения, обновления или удаления информации между различными приложениями через Интернет или протокол HTTP. Эта информация представляется пользователю в одно мгновение, особенно если вы используете JavaScript для отображения данных на веб-странице.

1.3 Где используется REST API?

REST API может использоваться любым приложением, которое может подключаться к Интернету. Если данные из приложения могут быть созданы, прочитаны, обновлены или удалены с помощью другого приложения, это обычно означает, что используется REST API.

2. Файловая структура

  • api/
    • config/
      • core.php
      • database.php
    • objects/
      • product.php
      • category.php
    • product/
      • create.php/
      • delete.php/
      • read.php/
      • read_paging.php/
      • read_one.php/
      • update.php/
      • search.php/
    • category/
      • read.php
    • shared/
      • utilities.php

3. Настройка базы данных

Используя PhpMyAdmin, создайте новую базу данных api_db. После этого выполните следующие SQL-запросы, чтобы создать новые таблицы с образцами данных.

3.1 Создание таблицы категорий

CREATE TABLE IF NOT EXISTS `categories` (
    `id` int(11) NOT NULL AUTO_INCREMENT,
    `name` varchar(256) NOT NULL,
    `description` text NOT NULL,
    `created` datetime NOT NULL,
    `modified` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
    PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 AUTO_INCREMENT=19 ;

3.2 Дамп данных для таблицы категорий

INSERT INTO `categories` (`id`, `name`, `description`, `created`, `modified`) VALUES
(1, "Fashion", "Category for anything related to fashion.", "2014-06-01 00:35:07", "2014-05-30 17:34:33"),
(2, "Electronics", "Gadgets, drones and more.", "2014-06-01 00:35:07", "2014-05-30 17:34:33"),
(3, "Motors", "Motor sports and more", "2014-06-01 00:35:07", "2014-05-30 17:34:54"),
(5, "Movies", "Movie products.", "2019-05-20 10:22:05", "2019-08-20 10:30:15"),
(6, "Books", "Kindle books, audio books and more.", "2018-03-14 08:05:25", "2019-05-20 11:29:11"),
(13, "Sports", "Drop into new winter gear.", "2016-01-09 02:24:24", "2016-01-09 01:24:24");

3.3 Создание таблицы товаров

CREATE TABLE IF NOT EXISTS `products` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(32) NOT NULL,
  `description` text NOT NULL,
  `price` decimal(10,0) NOT NULL,
  `category_id` int(11) NOT NULL,
  `created` datetime NOT NULL,
  `modified` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 AUTO_INCREMENT=65 ;

3.4 Дамп данных для таблицы товаров

INSERT INTO `products` (`id`, `name`, `description`, `price`, `category_id`, `created`, `modified`) VALUES
(1, "LG P880 4X HD", "My first awesome phone!", "336", 3, "2014-06-01 01:12:26", "2014-05-31 17:12:26"),
(2, "Google Nexus 4", "The most awesome phone of 2013!", "299", 2, "2014-06-01 01:12:26", "2014-05-31 17:12:26"),
(3, "Samsung Galaxy S4", "How about no?", "600", 3, "2014-06-01 01:12:26", "2014-05-31 17:12:26"),
(6, "Bench Shirt", "The best shirt!", "29", 1, "2014-06-01 01:12:26", "2014-05-31 02:12:21"),
(7, "Lenovo Laptop", "My business partner.", "399", 2, "2014-06-01 01:13:45", "2014-05-31 02:13:39"),
(8, "Samsung Galaxy Tab 10.1", "Good tablet.", "259", 2, "2014-06-01 01:14:13", "2014-05-31 02:14:08"),
(9, "Spalding Watch", "My sports watch.", "199", 1, "2014-06-01 01:18:36", "2014-05-31 02:18:31"),
(10, "Sony Smart Watch", "The coolest smart watch!", "300", 2, "2014-06-06 17:10:01", "2014-06-05 18:09:51"),
(11, "Huawei Y300", "For testing purposes.", "100", 2, "2014-06-06 17:11:04", "2014-06-05 18:10:54"),
(12, "Abercrombie Lake Arnold Shirt", "Perfect as gift!", "60", 1, "2014-06-06 17:12:21", "2014-06-05 18:12:11"),
(13, "Abercrombie Allen Brook Shirt", "Cool red shirt!", "70", 1, "2014-06-06 17:12:59", "2014-06-05 18:12:49"),
(26, "Another product", "Awesome product!", "555", 2, "2014-11-22 19:07:34", "2014-11-21 20:07:34"),
(28, "Wallet", "You can absolutely use this one!", "799", 6, "2014-12-04 21:12:03", "2014-12-03 22:12:03"),
(31, "Amanda Waller Shirt", "New awesome shirt!", "333", 1, "2014-12-13 00:52:54", "2014-12-12 01:52:54"),
(42, "Nike Shoes for Men", "Nike Shoes", "12999", 3, "2015-12-12 06:47:08", "2015-12-12 05:47:08"),
(48, "Bristol Shoes", "Awesome shoes.", "999", 5, "2016-01-08 06:36:37", "2016-01-08 05:36:37"),
(60, "Rolex Watch", "Luxury watch.", "25000", 1, "2016-01-11 15:46:02", "2016-01-11 14:46:02");

3.5 Подключение к базе данных

Приведенный ниже код показывает учетные данные базы данных и метод для получения подключения к базе данных с помощью PDO.

Создайте папку api и откройте её. Создайте папку config и в ней создайте файл database.php со следующим кодом.

<?php

class Database
{
    // укажите свои учетные данные базы данных
    private $host = "localhost";
    private $db_name = "api_db";
    private $username = "root";
    private $password = "";
    public $conn;

    // получаем соединение с БД
    public function getConnection()
    {
        $this->conn = null;

        try {
            $this->conn = new PDO("mysql:host=" . $this->host . ";dbname=" . $this->db_name, $this->username, $this->password);
            $this->conn->exec("set names utf8");
        } catch (PDOException $exception) {
            echo "Ошибка подключения: " . $exception->getMessage();
        }

        return $this->conn;
    }
}

4. Получение товаров

4.1 Создание объекта Product

Код ниже содержит класс с именем Product и несколько свойств. Также показан метод конструктора, который принимает соединение с базой данных.

Мы будем использовать этот класс для чтения данных из базы. Откройте папку api. Создайте папку objects. Откройте папку её и создайте файл product.php. Поместите в него следующий код.

<?php

class Product
{
    // подключение к базе данных и таблице "products"
    private $conn;
    private $table_name = "products";

    // свойства объекта
    public $id;
    public $name;
    public $description;
    public $price;
    public $category_id;
    public $category_name;
    public $created;

    // конструктор для соединения с базой данных
    public function __construct($db)
    {
        $this->conn = $db;
    }

    // здесь будет метод read()
}

4.2 Создание файла для чтения товаров

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

В данном случае наш файл read.php может быть прочитан кем угодно (звездочка * означает все) и вернет данные в формате JSON.

Откройте папку api. Создайте в ней папку product. Откройте её и создайте файл read.php со следующим кодом.

<?php

// необходимые HTTP-заголовки
header("Access-Control-Allow-Origin: *");
header("Content-Type: application/json; charset=UTF-8");

// подключение к базе данных будет здесь

4.3 Подключение к базе данных и таблице товаров

В приведенном ниже коде мы подключаем файлы database.php и product.php. Это файлы, которые мы создали ранее.

Нам нужно использовать метод getConnection() класса Database для получения соединения с базой данных. Мы передаем это подключение классу Product.

Замените комментарий // подключение к базе данных будет здесь в файле read.php следующим кодом.

// подключение базы данных и файл, содержащий объекты
include_once "../config/database.php";
include_once "../objects/product.php";

// получаем соединение с базой данных
$database = new Database();
$db = $database->getConnection();

// инициализируем объект
$product = new Product($db);
 
// чтение товаров будет здесь

4.4 Чтение товаров из базы данных

В приведенном ниже коде мы используем метод read() класса Product для получения данных из базы. Через переменную $num мы проверяем, найдены ли записи.

Если найдены записи, мы перебираем их с помощью цикла while, и добавляем каждую запись в массив $products_arr, устанавливаем код ответа 200 OK и показываем его пользователю в формате JSON.

Замените комментарий // чтение товаров будет здесь в файле read.php следующим кодом.

// запрашиваем товары
$stmt = $product->read();
$num = $stmt->rowCount();

// проверка, найдено ли больше 0 записей
if ($num > 0) {
    // массив товаров
    $products_arr = array();
    $products_arr["records"] = array();

    // получаем содержимое нашей таблицы
    // fetch() быстрее, чем fetchAll()
    while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
        // извлекаем строку
        extract($row);
        $product_item = array(
            "id" => $id,
            "name" => $name,
            "description" => html_entity_decode($description),
            "price" => $price,
            "category_id" => $category_id,
            "category_name" => $category_name
        );
        array_push($products_arr["records"], $product_item);
    }

    // устанавливаем код ответа - 200 OK
    http_response_code(200);

    // выводим данные о товаре в формате JSON
    echo json_encode($products_arr);
}

// "товары не найдены" будет здесь

4.5 Создание метода read()

Мы использовали метод read() в предыдущем разделе, но он пока ещё не существует в классе Product. Нам нужно добавить этот метод. С помощью кода ниже, мы делаем запрос для получения записей из базы данных.

Откройте папку objects. Откройте файл product.php. Поместите следующий код в класс Product перед последней закрывающей фигурной скобкой вместо комментария // здесь будет метод read().

// метод для получения товаров
function read()
{
    // выбираем все записи
    $query = "SELECT
        c.name as category_name, p.id, p.name, p.description, p.price, p.category_id, p.created
    FROM
        " . $this->table_name . " p
        LEFT JOIN
            categories c
                ON p.category_id = c.id
    ORDER BY
        p.created DESC";

    // подготовка запроса
    $stmt = $this->conn->prepare($query);

    // выполняем запрос
    $stmt->execute();
    return $stmt;
}

4.6 Уведомление пользователя о том, что товары не найдены

Если переменная $num имеет нулевое или отрицательное значение, это означает, что из базы данных не возвращено никаких записей. Мы должны сообщить пользователю об этом.

В приведенном ниже коде мы устанавливаем код ответа 404 — Не найдено и сообщение, что Товары не найдены.

Замените комментарий // «товары не найдены» будет здесь в файле read.php следующим кодом.

else {
    // установим код ответа - 404 Не найдено
    http_response_code(404);

    // сообщаем пользователю, что товары не найдены
    echo json_encode(array("message" => "Товары не найдены."), JSON_UNESCAPED_UNICODE);
}

5. Создание товаров

5.1 Создание файла create.php

Откройте папку product и создайте в ней файл create.php со следующим содержимым.

<?php

// необходимые HTTP-заголовки
header("Access-Control-Allow-Origin: *");
header("Content-Type: application/json; charset=UTF-8");
header("Access-Control-Allow-Methods: POST");
header("Access-Control-Max-Age: 3600");
header("Access-Control-Allow-Headers: Content-Type, Access-Control-Allow-Headers, Authorization, X-Requested-With");

// получаем соединение с базой данных
include_once "../config/database.php";

// создание объекта товара
include_once "../objects/product.php";
$database = new Database();
$db = $database->getConnection();
$product = new Product($db);

// получаем отправленные данные
$data = json_decode(file_get_contents("php://input"));

// убеждаемся, что данные не пусты
if (
    !empty($data->name) &&
    !empty($data->price) &&
    !empty($data->description) &&
    !empty($data->category_id)
) {
    // устанавливаем значения свойств товара
    $product->name = $data->name;
    $product->price = $data->price;
    $product->description = $data->description;
    $product->category_id = $data->category_id;
    $product->created = date("Y-m-d H:i:s");

    // создание товара
    if ($product->create()) {
        // установим код ответа - 201 создано
        http_response_code(201);

        // сообщим пользователю
        echo json_encode(array("message" => "Товар был создан."), JSON_UNESCAPED_UNICODE);
    }
    // если не удается создать товар, сообщим пользователю
    else {
        // установим код ответа - 503 сервис недоступен
        http_response_code(503);

        // сообщим пользователю
        echo json_encode(array("message" => "Невозможно создать товар."), JSON_UNESCAPED_UNICODE);
    }
}
// сообщим пользователю что данные неполные
else {
    // установим код ответа - 400 неверный запрос
    http_response_code(400);

    // сообщим пользователю
    echo json_encode(array("message" => "Невозможно создать товар. Данные неполные."), JSON_UNESCAPED_UNICODE);
}

5.2 Создание метода create()

Откройте папку objects. Откройте файл product.php и добавьте следующий код внутри класса Product (objects / product.php).

// метод для создания товаров
function create()
{
    // запрос для вставки (создания) записей
    $query = "INSERT INTO
            " . $this->table_name . "
        SET
            name=:name, price=:price, description=:description, category_id=:category_id, created=:created";

    // подготовка запроса
    $stmt = $this->conn->prepare($query);

    // очистка
    $this->name = htmlspecialchars(strip_tags($this->name));
    $this->price = htmlspecialchars(strip_tags($this->price));
    $this->description = htmlspecialchars(strip_tags($this->description));
    $this->category_id = htmlspecialchars(strip_tags($this->category_id));
    $this->created = htmlspecialchars(strip_tags($this->created));

    // привязка значений
    $stmt->bindParam(":name", $this->name);
    $stmt->bindParam(":price", $this->price);
    $stmt->bindParam(":description", $this->description);
    $stmt->bindParam(":category_id", $this->category_id);
    $stmt->bindParam(":created", $this->created);

    // выполняем запрос
    if ($stmt->execute()) {
        return true;
    }
    return false;
}

6. Получение одного товара

6.1 Создание файла read_one.php

Откройте папку product, создайте в ней файл read_one.php со следующим содержимым.

<?php

// необходимые HTTP-заголовки
header("Access-Control-Allow-Origin: *");
header("Access-Control-Allow-Headers: access");
header("Access-Control-Allow-Methods: GET");
header("Access-Control-Allow-Credentials: true");
header("Content-Type: application/json");

// подключение файла для соединения с базой и файл с объектом
include_once "../config/database.php";
include_once "../objects/product.php";

// получаем соединение с базой данных
$database = new Database();
$db = $database->getConnection();

// подготовка объекта
$product = new Product($db);

// установим свойство ID записи для чтения
$product->id = isset($_GET["id"]) ? $_GET["id"] : die();

// получим детали товара
$product->readOne();

if ($product->name != null) {

    // создание массива
    $product_arr = array(
        "id" =>  $product->id,
        "name" => $product->name,
        "description" => $product->description,
        "price" => $product->price,
        "category_id" => $product->category_id,
        "category_name" => $product->category_name
    );

    // код ответа - 200 OK
    http_response_code(200);

    // вывод в формате json
    echo json_encode($product_arr);
} else {
    // код ответа - 404 Не найдено
    http_response_code(404);

    // сообщим пользователю, что такой товар не существует
    echo json_encode(array("message" => "Товар не существует"), JSON_UNESCAPED_UNICODE);
}

6.2 Создание метода readOne()

Откройте папку objects. Откройте файл product.php и добавьте следующий код внутри класса Product.

// метод для получения конкретного товара по ID
function readOne()
{
    // запрос для чтения одной записи (товара)
    $query = "SELECT
            c.name as category_name, p.id, p.name, p.description, p.price, p.category_id, p.created
        FROM
            " . $this->table_name . " p
            LEFT JOIN
                categories c
                    ON p.category_id = c.id
        WHERE
            p.id = ?
        LIMIT
            0,1";
            
    // подготовка запроса
    $stmt = $this->conn->prepare($query);

    // привязываем id товара, который будет получен
    $stmt->bindParam(1, $this->id);

    // выполняем запрос
    $stmt->execute();

    // получаем извлеченную строку
    $row = $stmt->fetch(PDO::FETCH_ASSOC);

    // установим значения свойств объекта
    $this->name = $row["name"];
    $this->price = $row["price"];
    $this->description = $row["description"];
    $this->category_id = $row["category_id"];
    $this->category_name = $row["category_name"];
}

7. Обновление товара

7.1 Создание файла update.php

Откройте папку product, создайте в ней файл update.php и поместите в него следующий код.

<?php

// HTTP-заголовки
header("Access-Control-Allow-Origin: *");
header("Content-Type: application/json; charset=UTF-8");
header("Access-Control-Allow-Methods: POST");
header("Access-Control-Max-Age: 3600");
header("Access-Control-Allow-Headers: Content-Type, Access-Control-Allow-Headers, Authorization, X-Requested-With");

// подключаем файл для работы с БД и объектом Product
include_once "../config/database.php";
include_once "../objects/product.php";

// получаем соединение с базой данных
$database = new Database();
$db = $database->getConnection();

// подготовка объекта
$product = new Product($db);

// получаем id товара для редактирования
$data = json_decode(file_get_contents("php://input"));

// установим id свойства товара для редактирования
$product->id = $data->id;

// установим значения свойств товара
$product->name = $data->name;
$product->price = $data->price;
$product->description = $data->description;
$product->category_id = $data->category_id;

// обновление товара
if ($product->update()) {
    // установим код ответа - 200 ok
    http_response_code(200);

    // сообщим пользователю
    echo json_encode(array("message" => "Товар был обновлён"), JSON_UNESCAPED_UNICODE);
}
// если не удается обновить товар, сообщим пользователю
else {
    // код ответа - 503 Сервис не доступен
    http_response_code(503);

    // сообщение пользователю
    echo json_encode(array("message" => "Невозможно обновить товар"), JSON_UNESCAPED_UNICODE);
}

7.2 Создание метода update()

В папке objects откройте файл product.php и добавьте новый метод update() внутри класса Product.

// метод для обновления товара
function update()
{
    // запрос для обновления записи (товара)
    $query = "UPDATE
            " . $this->table_name . "
        SET
            name = :name,
            price = :price,
            description = :description,
            category_id = :category_id
        WHERE
            id = :id";

    // подготовка запроса
    $stmt = $this->conn->prepare($query);

    // очистка
    $this->name = htmlspecialchars(strip_tags($this->name));
    $this->price = htmlspecialchars(strip_tags($this->price));
    $this->description = htmlspecialchars(strip_tags($this->description));
    $this->category_id = htmlspecialchars(strip_tags($this->category_id));
    $this->id = htmlspecialchars(strip_tags($this->id));

    // привязываем значения
    $stmt->bindParam(":name", $this->name);
    $stmt->bindParam(":price", $this->price);
    $stmt->bindParam(":description", $this->description);
    $stmt->bindParam(":category_id", $this->category_id);
    $stmt->bindParam(":id", $this->id);

    // выполняем запрос
    if ($stmt->execute()) {
        return true;
    }
    return false;
}

8. Удаление товара

8.1 Создание файла delete.php

Откройте папку product и создайте файл delete.php со следующим содержимым.

<?php

// HTTP-заголовки
header("Access-Control-Allow-Origin: *");
header("Content-Type: application/json; charset=UTF-8");
header("Access-Control-Allow-Methods: POST");
header("Access-Control-Max-Age: 3600");
header("Access-Control-Allow-Headers: Content-Type, Access-Control-Allow-Headers, Authorization, X-Requested-With");

// подключим файл для соединения с базой и объектом Product
include_once "../config/database.php";
include_once "../objects/product.php";

// получаем соединение с БД
$database = new Database();
$db = $database->getConnection();

// подготовка объекта
$product = new Product($db);

// получаем id товара
$data = json_decode(file_get_contents("php://input"));

// установим id товара для удаления
$product->id = $data->id;

// удаление товара
if ($product->delete()) {
    // код ответа - 200 ok
    http_response_code(200);

    // сообщение пользователю
    echo json_encode(array("message" => "Товар был удалён"), JSON_UNESCAPED_UNICODE);
}
// если не удается удалить товар
else {
    // код ответа - 503 Сервис не доступен
    http_response_code(503);

    // сообщим об этом пользователю
    echo json_encode(array("message" => "Не удалось удалить товар"));
}

8.2 Создание метода delete()

В папке objects откройте файл product.php и добавьте новый метод в класс Product.

// метод для удаления товара
function delete()
{
    // запрос для удаления записи (товара)
    $query = "DELETE FROM " . $this->table_name . " WHERE id = ?";

    // подготовка запроса
    $stmt = $this->conn->prepare($query);

    // очистка
    $this->id = htmlspecialchars(strip_tags($this->id));

    // привязываем id записи для удаления
    $stmt->bindParam(1, $this->id);

    // выполняем запрос
    if ($stmt->execute()) {
        return true;
    }
    return false;
}

9. Поиск товаров

Теперь реализуем поиск записей в базе данных, в нашем случае товаров.

9.1 Создание файла search.php

В папке product создайте файл search.php со следующим кодом.

<?php

// HTTP-заголовки
header("Access-Control-Allow-Origin: *");
header("Content-Type: application/json; charset=UTF-8");

// подключение необходимых файлов
include_once "../config/core.php";
include_once "../config/database.php";
include_once "../objects/product.php";

// создание подключения к БД
$database = new Database();
$db = $database->getConnection();

// инициализируем объект
$product = new Product($db);

// получаем ключевые слова
$keywords = isset($_GET["s"]) ? $_GET["s"] : "";

// запрос товаров
$stmt = $product->search($keywords);
$num = $stmt->rowCount();

// проверяем, найдено ли больше 0 записей
if ($num > 0) {
    // массив товаров
    $products_arr = array();
    $products_arr["records"] = array();

    // получаем содержимое нашей таблицы
    // fetch() быстрее чем fetchAll()
    while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {

        // извлечём строку
        extract($row);
        $product_item = array(
            "id" => $id,
            "name" => $name,
            "description" => html_entity_decode($description),
            "price" => $price,
            "category_id" => $category_id,
            "category_name" => $category_name
        );
        array_push($products_arr["records"], $product_item);
    }
    // код ответа - 200 OK
    http_response_code(200);

    // покажем товары
    echo json_encode($products_arr);
} else {
    // код ответа - 404 Ничего не найдено
    http_response_code(404);

    // скажем пользователю, что товары не найдены
    echo json_encode(array("message" => "Товары не найдены."), JSON_UNESCAPED_UNICODE);
}

9.2 Создание метода search()

В папке objects откройте product.php и добавьте метод search().

// метод для поиска товаров
function search($keywords)
{
    // поиск записей (товаров) по "названию товара", "описанию товара", "названию категории"
    $query = "SELECT
            c.name as category_name, p.id, p.name, p.description, p.price, p.category_id, p.created
        FROM
            " . $this->table_name . " p
            LEFT JOIN
                categories c
                    ON p.category_id = c.id
        WHERE
            p.name LIKE ? OR p.description LIKE ? OR c.name LIKE ?
        ORDER BY
            p.created DESC";

    // подготовка запроса
    $stmt = $this->conn->prepare($query);

    // очистка
    $keywords = htmlspecialchars(strip_tags($keywords));
    $keywords = "%{$keywords}%";

    // привязка
    $stmt->bindParam(1, $keywords);
    $stmt->bindParam(2, $keywords);
    $stmt->bindParam(3, $keywords);

    // выполняем запрос
    $stmt->execute();

    return $stmt;
}

10. Пагинация товаров

10.1 Создание файла read_paging.php

В папке product создайте файл read_paging.php со следующим кодом.

<?php

// установим HTTP-заголовки
header("Access-Control-Allow-Origin: *");
header("Content-Type: application/json; charset=UTF-8");

// подключение файлов
include_once "../config/core.php";
include_once "../shared/utilities.php";
include_once "../config/database.php";
include_once "../objects/product.php";

// utilities
$utilities = new Utilities();

// создание подключения
$database = new Database();
$db = $database->getConnection();

// инициализация объекта
$product = new Product($db);

// запрос товаров
$stmt = $product->readPaging($from_record_num, $records_per_page);
$num = $stmt->rowCount();

// если больше 0 записей
if ($num > 0) {

    // массив товаров
    $products_arr = array();
    $products_arr["records"] = array();
    $products_arr["paging"] = array();

    // получаем содержимое нашей таблицы
    while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {

        // извлечение строки
        extract($row);
        $product_item = array(
            "id" => $id,
            "name" => $name,
            "description" => html_entity_decode($description),
            "price" => $price,
            "category_id" => $category_id,
            "category_name" => $category_name
        );
        array_push($products_arr["records"], $product_item);
    }

    // подключим пагинацию
    $total_rows = $product->count();
    $page_url = "{$home_url}product/read_paging.php?";
    $paging = $utilities->getPaging($page, $total_rows, $records_per_page, $page_url);
    $products_arr["paging"] = $paging;

    // установим код ответа - 200 OK
    http_response_code(200);

    // вывод в json-формате
    echo json_encode($products_arr);
} else {

    // код ответа - 404 Ничего не найдено
    http_response_code(404);

    // сообщим пользователю, что товаров не существует
    echo json_encode(array("message" => "Товары не найдены"), JSON_UNESCAPED_UNICODE);
}

10.2 Создание файла core.php

Этот файл содержит нашу базовую конфигурацию, такую как базовый URL и переменные пагинации.

Откройте папку config и создайте в ней файл core.php со следующим содержимым.

<?php

// показывать сообщения об ошибках
ini_set("display_errors", 1);
error_reporting(E_ALL);

// URL домашней страницы
$home_url = "http://localhost/api/";

// страница указана в параметре URL, страница по умолчанию одна
$page = isset($_GET["page"]) ? $_GET["page"] : 1;

// установка количества записей на странице
$records_per_page = 5;

// расчёт для запроса предела записей
$from_record_num = ($records_per_page * $page) - $records_per_page;

10.3 Создание метода readPaging()

В папке objects откройте файл product.php и добавьте метод readPaging(). Этот метод вернет список записей, ограниченный тем, что мы установили в $records_per_page файла core.php.

// получение товаров с пагинацией
public function readPaging($from_record_num, $records_per_page)
{
    // выборка
    $query = "SELECT
            c.name as category_name, p.id, p.name, p.description, p.price, p.category_id, p.created
        FROM
            " . $this->table_name . " p
            LEFT JOIN
                categories c
                    ON p.category_id = c.id
        ORDER BY p.created DESC
        LIMIT ?, ?";

    // подготовка запроса
    $stmt = $this->conn->prepare($query);

    // свяжем значения переменных
    $stmt->bindParam(1, $from_record_num, PDO::PARAM_INT);
    $stmt->bindParam(2, $records_per_page, PDO::PARAM_INT);

    // выполняем запрос
    $stmt->execute();

    // вернём значения из базы данных
    return $stmt;
}

10.4 Создание метода count()

Так же в классе Product (файл product.php) добавьте метод count() для подсчёта количества товаров.

// данный метод возвращает кол-во товаров
public function count()
{
    $query = "SELECT COUNT(*) as total_rows FROM " . $this->table_name . "";

    $stmt = $this->conn->prepare($query);
    $stmt->execute();

    $row = $stmt->fetch(PDO::FETCH_ASSOC);

    return $row["total_rows"];
}

10.5 Получение массива пагинации

В корне создайте папку shared, в ней файл utilities.php со следующим кодом.

<?php

class Utilities
{
    public function getPaging($page, $total_rows, $records_per_page, $page_url)
    {
        // массив пагинации
        $paging_arr = array();

        // кнопка для первой страницы
        $paging_arr["first"] = $page > 1 ? "{$page_url}page=1" : "";

        // подсчёт всех товаров в базе данных для подсчета общего количества страниц
        $total_pages = ceil($total_rows / $records_per_page);

        // диапазон ссылок для показа
        $range = 2;

        // отображать диапазон ссылок вокруг текущей страницы
        $initial_num = $page - $range;
        $condition_limit_num = ($page + $range) + 1;
        $paging_arr["pages"] = array();
        $page_count = 0;

        for ($x = $initial_num; $x < $condition_limit_num; $x++) {
            // убедимся, что $x > 0 И $x <= $total_pages
            if (($x > 0) && ($x <= $total_pages)) {
                $paging_arr["pages"][$page_count]["page"] = $x;
                $paging_arr["pages"][$page_count]["url"] = "{$page_url}page={$x}";
                $paging_arr["pages"][$page_count]["current_page"] = $x == $page ? "yes" : "no";
                $page_count++;
            }
        }

        // кнопка для последней страницы
        $paging_arr["last"] = $page < $total_pages ? "{$page_url}page={$total_pages}" : "";

        // формат json
        return json_encode($paging_arr);
    }
}

11. Получение категорий

11.1 Создание класса Category

Откройте папку objects и создайте новый файл category.php со следующим кодом.

<?php

class Category
{
    // соединение с БД и таблицей "categories"
    private $conn;
    private $table_name = "categories";

    // свойства объекта
    public $id;
    public $name;
    public $description;
    public $created;

    public function __construct($db)
    {
        $this->conn = $db;
    }

    // здесь будет метод для получение всех категорий товаров
}

11.2 Создание метода readAll()

С помощью данного метода мы получим все категории товаров из базы данных. замените комментарий // здесь будет метод для получение всех категорий товаров следующим кодом:

// метод для получения всех категорий товаров
public function readAll()
{
    $query = "SELECT
                id, name, description
            FROM
                " . $this->table_name . "
            ORDER BY
                name";

    $stmt = $this->conn->prepare($query);
    $stmt->execute();

    return $stmt;
}

11.3 Создание файла read.php

Создайте новую папку category в корне, и в ней файл read.php со следующим кодом.

<?php

// установим HTTP-заголовки
header("Access-Control-Allow-Origin: *");
header("Content-Type: application/json; charset=UTF-8");

// подключение файлов для соединения с БД и файл с объектом Category
include_once "../config/database.php";
include_once "../objects/category.php";

// создание подключения к базе данных
$database = new Database();
$db = $database->getConnection();

// инициализация объекта
$category = new Category($db);

// получаем категории
$stmt = $category->readAll();
$num = $stmt->rowCount();

// проверяем, найдено ли больше 0 записей
if ($num > 0) {

    // массив для записей
    $categories_arr = array();
    $categories_arr["records"] = array();

    // получим содержимое нашей таблицы
    while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {

        // извлекаем строку
        extract($row);
        $category_item = array(
            "id" => $id,
            "name" => $name,
            "description" => html_entity_decode($description)
        );
        array_push($categories_arr["records"], $category_item);
    }
    // код ответа - 200 OK
    http_response_code(200);

    // покажем данные категорий в формате json
    echo json_encode($categories_arr);
} else {

    // код ответа - 404 Ничего не найдено
    http_response_code(404);

    // сообщим пользователю, что категории не найдены
    echo json_encode(array("message" => "Категории не найдены"), JSON_UNESCAPED_UNICODE);
}

API готов к использованию!

Frontend часть данного приложения (продолжение) — jQuery + AJAX + JSON.

Если вам понравилась данная статья, рекомендую к прочтению создание регистрации и авторизации в php с использованием JWT.

Время на прочтение
10 мин

Количество просмотров 14K

Иногда бывает необходимо развернуть не большое рест апи для своего сайта, сделанного по технологии СПА (Vue, React или др.) без использования каких-либо фреймворков, CMS или чего-то подобного, и при этом хочется воспользоваться обычным php хостингом с минимальными усилиями на внедрение и разработку.  При этом там же желательно разместить и сам сайт СПА (в нашем случае на vue).

Использование php позволяет для построения ендпоинтов апи использовать даже статические php файлы, размещаемые просто в папках на хостинге, которые предоставляют результат при непосредственном обращении к ним. И хотя, видимо в своё время, такой подход послужил широкому распространению php мы рассмотрим далее более программистский подход к созданию апи, который очень похож на используемый в библиотеке Node.js Express и поэтому интуитивно понятен, и прост для освоения. Для это нам понадобиться библиотека «pecee/simple-router».

Далее мы предполагаем, что у вас уже есть среда для запуска кода локально (LAMP, XAMP, docker) или как-то иначе и у вас настроено перенаправление всех запросов на индексный файл (index.php). Кроме, того мы предполагаем, что вы можете устанавливать зависимости через composer.

Структура проекта

На Рис.1. представлена общая структура проекта. Точкой входа является файл

На Рис.1. представлена общая структура проекта. Точкой входа является файл

index.phpв папке web. Сама папка webявляется публично доступной папкой, и должна быть указана в настройках сервера как корневая. В папке configбудут находится настройки роутов наших ендпоинтов. В папке controllerбудут обработчики ендпоинтов маршрутов. В папке middlewaresмы разместим промежуточные обработчике роутов для выполнения авторизации перед началом основного кода ендпоинта. В папках exceptions, views и models будут соответственно исключения, html шаблон и объектные модели. Полный код проекта тут.

Инсталляция и запуск

Для работы необходимо инсталлировать следующее содержимое composer.json (composer install в корне проекта).

// composer.json
{
    "require": {
        "pecee/simple-router": "*",
        "lcobucci/jwt": "^3.4",
        "ext-json": "*"
    },
    "autoload": {
        "psr-4": {
            "app\": ""
        }
    }
}

Обратите внимание, что ‘app’ объявлено как префикс для namespace. Данный префикс будет использоваться при объявлении неймспейсов классов.

Запуск всего остального кода происходит вызовом статического метода Router::route() в файле index.php

<?php
//index.php

use PeceeSimpleRouterSimpleRouter as Router;

require_once __DIR__ . '/../vendor/autoload.php';
require_once (__DIR__ . '/../config/routes.php');
    
Router::start();

Так же тут подключаются роуты определённые в файле config/routes.php.

 Подключение SPA на Vue.js 2 к проекту на php

Если вы развёртываете сборку vue отдельно от апи, то этот раздел можно пропустить.

Рассмотрим теперь то, как подключить проект на vue в данной конфигурации с использованием соответствующих маршрутов. Для этого содержимое сборки необходимо поместить в папку web.  В файле маршрутов (‘/config/routes.php’) прописываем два правила:

<?php

use Pecee{
    SimpleRouterSimpleRouter as Router
};

Router::setDefaultNamespace('appcontrollers');
Router::get('/', 'VueController@run'); // правило 1
Router::get('/controller', 'VueController@run')
    ->setMatch('//([w]+)/'); // правило 2

Для пустого (корневого) маршрута ‘/’ вызывается метод run класса VueController. Второе правило указывает что для любого явно незаданного пути будет тоже вызываться VueController, чтобы обработка маршрута происходила на стороне vue. Это правило всегда должно быть последним, чтобы оно срабатывало только тогда, когда другие уже не сработали. Метод run представляет собой просто рендеринг файла представления с помощью метода renderTemplate(), определённого в родительском классе контроллера.   Здесь мы также устанавливаем префикс для классов методы которых используются в роутах с помощью setDefaultNamespace.

<?php

namespace appcontrollers;

class VueController extends AbstractController
{
    public function run()
    {
        return $this->renderTemplate('../views/vue/vue_page.php');
    }
}

В свою очередь представление vue_page.php тоже просто отрисовка индексного файла сборки vue.

<?php
// vue_page.php
include (__DIR__ . '/../../web/index.html');

Итого мы подключили проект на vue к проекту на php, который уже готов к развертыванию на хостинге. Данный подход можно использовать для любых проектов на php. Осталось только рассмотреть, что собой представляет родительский класс AbstractController.

<?php

namespace appcontrollers;

use PeceeHttpRequest;
use PeceeHttpResponse;
use PeceeSimpleRouterSimpleRouter as Router;

abstract class AbstractController
{
    /**
     * @var Response
     */
    protected $response;
    /**
     * @var Request
     */
    protected $request;

    public function __construct()
    {
        $this->request = Router::router()->getRequest();
        $this->response =  new Response($this->request);
    }

    public function renderTemplate($template) {
        ob_start();
        include $template;
        return ob_get_clean();
    }

    public function setCors()
    {
        $this->response->header('Access-Control-Allow-Origin: *');
        $this->response->header('Access-Control-Request-Method: OPTIONS');
        $this->response->header('Access-Control-Allow-Credentials: true');
        $this->response->header('Access-Control-Max-Age: 3600');
    }
}

В конструкторе класса AbstractController определяются поля $request и $response. В $request хранится распарсенный классом PeceeHttpRouter запрос. А $response будет использоваться для создания ответов на запросы к апи. Определённый здесь метод renderTemplate используется для рендеринга представлений (html страниц). Кроме того, здесь определён метод устанавливающий заголовки для работы с политикой CORS. Его следует использовать если запросы к апи происходят не с того же адреса, т.е. если сборка vue запускается на другом веб-сервере. Теперь перейдём непосредственно к созданию апи.

Создание REST API эндпоинтов

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

<?php

namespace appmiddlewares;

use PeceeHttpMiddlewareIMiddleware;
use PeceeHttpRequest;

class ProccessRawBody implements IMiddleware
{

    /**
     * @inheritDoc
     */
    public function handle(Request $request): void
    {
        $rawBody = file_get_contents('php://input');

        if ($rawBody) {
            try {
             $body = json_decode($rawBody, true);
             foreach ($body as $key => $value) {
                 $request->$key = $value;
             }
            } catch (Throwable $e) {

            }
        }
    }
}

Здесь мы считываем из входного потока и помещаем полученное в объект $request для дальнейшего доступа из кода в контроллерах. ProccessRawBody реализует интерфейс IMIddleware обязательный для всех middleware.

Теперь создадим группу роутов для работы с апи использующее данный промежуточный слой.

<?php
// routes.php
Router::group([
    'prefix' => 'api/v1',
    'middleware' => [
        ProccessRawBody::class
    ]
], function () {
    Router::post('/auth/sign-in', 'AuthController@signin');
    Router::get('/project', 'ProjectController@index');
});

У этой группы определён префикс «api/v1» (т.е. полный путь запроса должен быть например ‘/api/v1/auth/sign-in’), и ранее определённое нами middleware ProccessRawBody::class, так что в контроллерах наследованных от AbstractController доступны входные переменные через $request. AuthController рассмотрим чуть позже сейчас же мы уже можем воспользоваться методами не требующими авторизации, как например ProjectController::index.

<?php

namespace appcontrollers;

class ProjectController extends AbstractController
{
    public function index():string
    {
	// Какая-то логика для получения данных тут

        return $this->response->json([
            [
                'name' => 'project 1'
            ],
            [
                'name' => 'project 2'
            ]
        ]);
    }
}

Как видим, на входящий запрос, в ответе возвращаются данные о проектах.

Остальные роуты создаются аналогичным образом.

Авторизация по JWT токену

Теперь перейдём к роутам требующим авторизации. Но перед этим реализуем вход и получение jwt-токена. Для создания токена и его валидации мы будем использовать библиотеку “ lcobucci/jwt” Всё это будет у нас выполнятся по роуту определённому ранее ‘/auth/sign-in’. Соответственно в AuthController::singin у нас прописана логика выдачи jwt-токена после авторизации пользователя.

<?php

namespace appcontrollers;

use appmodelsRequest;
use ArgumentCountError;
use DateTimeImmutable;
use LcobucciJWTConfiguration;
use LcobucciJWTSignerHmacSha256;
use LcobucciJWTSignerKeyInMemory;

class AuthController extends AbstractController
{
    public function signin()
    {
	      // Тут код авторизующий пользователя

        $config = Configuration::forSymmetricSigner(
            new Sha256(),
            InMemory::plainText('секретный_ключ')
        );
        $now   = new DateTimeImmutable();
        $token = $config->builder()
            // Configures the issuer (iss claim)
            ->issuedBy('http://example.com')
            // Configures the audience (aud claim)
            ->permittedFor('http://example.org')
            // Configures the id (jti claim)
            ->identifiedBy('4f1g23a12aa')
            // Configures the time that the token was issue (iat claim)
            ->issuedAt($now)
            // Configures the expiration time of the token (exp claim)
            ->expiresAt($now->modify('+2 minutes'))
            // Configures a new claim, called "uid"
            ->withClaim('uid', $user->id)
            // Configures a new header, called "foo"
            ->withHeader('foo', 'bar')
            // Builds a new token
            ->getToken($config->signer(), $config->signingKey());
        
        return $this->response->json([
            'accessToken' => $token->toString()
        ]);
    }
}

Здесь используется симметричная подпись для jwt с использованием секретного ключа ‘секретный_ключ’. По нему будет проверятся валидность токена при запросах к апи. Ещё можно использовать асимметричную подпись с использованием пары ключей.

Можно также отметить, что можно создавать сколько угодно клаймов ->withClaim(‘uid’, $user->id) и сохранять там данные которые можно будет потом извлекать из ключа. Например, id пользователя для дальнейшей идентификации запросов от этого пользователя. Токен выдан на 2 минуты (->expiresAt($now->modify(‘+2 minutes’))) после чего он становится не валидным. ->issuedBy и ->permittedFor используются для oath2.

Теперь создадим группу роутов защищённую авторизацией. Для этого определим для группы роутов промежуточный слой Authenticate::class.

<?php
//routes.php
Router::group([
    'prefix' => 'api/v1',
    'middleware' => [
        ProccessRawBody::class
    ]
], function () {

    Router::post('/auth/sign-in', 'AuthController@signin');
    Router::get('/project', 'ProjectController@index');

    Router::group([
        'middleware' => [
            Authenticate::class
        ]
    ], function () {
        // authenticated routes
        Router::post('/project/create', 'ProjectController@create');
        Router::post('/project/update/{id}', 'ProjectController@update')
            ->where(['id' => '[d]+']);
    });
});

Как видите, группа с авторизацией объявлена внутри группы с префиксом “api/v1 ”. Рассмотрим роут ‘/project/update/{id}’. Здесь объявлен параметр id который определён как число. В метод update, контроллера Projectcontroller будет передана переменная $id содержащая значение этого параметра. Ниже приведён пример запроса и ответ.

<?php

namespace appcontrollers;

class ProjectController extends AbstractController
{
    /**
     * post /api/v1/project/update/3
     * body:
        {
            "project": {
                "prop": "value"
            }
        }
     */
    public function update(int $id): string
    {
				// код обновляющий проект
        return $this->response->json([
            [
                'response' => 'OK',
                'request' => $this->request->project,
                'id' => $id
            ]
        ]);
    }
}

Вернёмся теперь к промежуточному слою Authenticate::class с помощью которого происходит авторизация запросов к апи.

<?php

namespace appmiddlewares;

use appexceptionsNotAuthorizedHttpException;
use DateTimeImmutable;
use LcobucciClockFrozenClock;
use LcobucciJWTConfiguration;
use LcobucciJWTSignerHmacSha256;
use LcobucciJWTSignerKeyInMemory;
use LcobucciJWTValidationConstraintSignedWith;
use LcobucciJWTValidationConstraintValidAt;
use PeceeHttpMiddlewareIMiddleware;
use PeceeHttpRequest;

class Authenticate implements IMiddleware
{
    public function handle(Request $request): void
    {
        $headers = getallheaders();
        $tokenString = substr($headers['Authorization'] ?? '', 7);

        $config = Configuration::forSymmetricSigner(
            new Sha256(),
            InMemory::plainText('секретный_ключ')
        );

        $token = $config->parser()->parse($tokenString);

        if (
            !$config->validator()->validate(
                $token,
                new SignedWith(
                    new Sha256(),
                    InMemory::plainText('секретный_ключ')
                ),
                new ValidAt(new FrozenClock(new DateTimeImmutable()))
            )
        ) {
            throw new NotAuthorizedHttpException('Токен доступа не валиден или просрочен');
        }
        $userId = $token->claims()->get('uid');
        $request['uid'] = $userId;
    }
}

Здесь, считывается заголовок ‘Authorization: Bearer [token]’ (так называемая bearer авторизация) и извлекается оттуда токен, которые клиенты получают после логина и должны посылать со всеми запросами, требующими авторизацию. Далее с помощью парсера jwt-токен-строчка парсится. И дальше с помощью валидатора распарсенный токен валидируется. Метод validate() возвращает true or false. В случае не валидного токена выбрасывается исключение NotAuthorizedException. Если токен валидный, то мы извлекаем из него id пользователя $token->claims()->get(‘uid’) и сохраняем в переменную запроса $request, чтобы его можно было использовать дальше в контроллере. NotAuthorizedException определяется следующим образом:

<?php

namespace appexceptions;

class NotAuthorizedHttpException extends Exception
{

}

В завершении рассмотрим ещё обработку ошибок. В файле routes.php запишем следующие строчки:

<?php
//routes.php
Router::error(function(Request $request, Exception $exception) {
    $response = Router::response();
    switch (get_class($exception)) {
        case NotAuthorizedHttpException::class: {
            $response->httpCode(401);
            break;
        }
        case Exception::class: {
            $response->httpCode(500);
            break;
        }
    }
    if (PROD) {
        return $response->json([]);
    } else {
        return $response->json([
            'status' => 'error',
            'message' => $exception->getMessage()
        ]);
    }
});

В итоге файл routes.php будет выглядеть следующим образом:

Рис. 2. Итоговая структура проекта

Рис. 2. Итоговая структура проекта
<?php
//routes.php
use appexceptions{
    NotAuthorizedHttpException
};
use appmiddlewares{
    Authenticate,
    ProccessRawBody
};
use Pecee{
    HttpRequest,
    SimpleRouterSimpleRouter as Router
};

const PROD = false;

Router::setDefaultNamespace('appcontrollers');

Router::get('/', 'VueController@run');

Router::group([
    'prefix' => 'api/v1',
    'middleware' => [
        ProccessRawBody::class
    ]
], function () {
    Router::post('/auth/sign-in', 'AuthController@signin');
    Router::get('/project', 'ProjectController@index');
    Router::group([
        'middleware' => [
            Authenticate::class
        ]
    ], function () {
        // authenticated routes
        Router::post('/project/create', 'ProjectController@create');
        Router::post('/project/update/{id}', 'ProjectController@update')
            ->where(['id' => '[d]+']);
    });
});

Router::get('/controller', 'VueController@run')
    ->setMatch('//([w]+)/');

Router::error(function(Request $request, Exception $exception) {
    $response = Router::response();
    switch (get_class($exception)) {
        case NotAuthorizedHttpException::class: {
            $response->httpCode(401);
            break;
        }
        case Exception::class: {
            $response->httpCode(500);
            break;
        }
    }
    if (PROD) {
        return $response->json([]);
    } else {
        return $response->json([
            'status' => 'error',
            'message' => $exception->getMessage()
        ]);
    }
});

Заключение

В итоге у нас получилось небольшое, простое REST api для небольших проектов которое можно использовать на обычном php хостинге с минимальными трудозатратами на его (хостинга) настройку. Полный код проекта тут.

Больше настроек роутов можно найти здесь. Вместо рассмотренной библиотеки «pecee/simple-router» можно использовать любую другую аналогичную библиотеку или даже микрофреймворк Slim.

Пс. Если вы используете публичный репозиторий или придерживаетесь бестпрактис, то не следует хранит секретный ключ в коде. Для этого можно использовать переменные среды или локальные файлы, которые не добавляются в репозиторий. Код работы с jwt токенами можно выделить в отдельный класс в папке services.


февраль
5
, 2017

Я не знаю ни одного php-фреймворка. Это печально и стыдно, но законом пока не запрещено. А при этом поиграться с REST API хочется. Проблема в том, что php по умолчанию поддерживает только $_GET и $_POST. А для RESTful-сервиса надобно уметь работать еще и с PUT, DELETE и PATCH. И не очень очевидно, как культурно обработать множество запросов вида GET http://site.ru/users, DELETE http://site.ru/goods/5 и прочего непотребства. Как завернуть все подобные запросы в единую точку, универсально разобрать их на части и запустить нужный код для обработки данных?

Почти любой php-фреймворк умеет делать это из коробки. Например, Laravel, где роутинг реализован понятно и просто. Но что если нам не нужно прямо сейчас заниматься изучением новой большой темы, а хочется просто быстро завести проект с поддержкой REST API? Об этом и пойдет речь в статье.

Что должен уметь наш RESTful-сервис?

1. Поддерживать все 5 основных типов запросов: GET, POST, PUT, PATCH, DELETE.

2. Разруливать разнообразные маршруты вида

POST /goods

PUT /goods/{goodId}

GET /users/{userId}/info

и прочие сколь угодно длинные цепочки.

Внимание: это статья не про основы REST API

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

Какой функционал мы будем поддерживать?

Рассмотрим 2 сущности — товары и пользователи.

Для товаров возможности следующие:

  • 1. GET /goods/{goodId} — Получение информации о товаре
  • 2. POST /goods — Добавление нового товара
  • 3. PUT /goods/{goodId} — Редактирование товара
  • 4. PATCH /goods/{goodId} — Редактирование некоторых параметров товара
  • 5. DELETE /goods/{goodId} — Удаление товара

По пользователям для разнообразия рассмотрим несколько вариантов с GET

  • 1. GET /users/{userId} — Полная информация о пользователе
  • 2. GET /users/{userId}/info — Только общая информация о пользователе
  • 3. GET /users/{userId}/orders — Список заказов пользователя

Как это заработает на нативном PHP?

Первое, что мы сделаем — это настроим .htaccess так, чтобы все запросы перенаправлялись на файл index.php.
Именно он и будет заниматься извлечением данных.

Второе — определимся, какие данные нам нужны и напишем код для их получения — в index.php.

Нас интересуют 3 типа данных:

  • 1. Метод запроса (GET, POST, PUT, PATCH или DELETE)
  • 2. Данные из URL-a, например, users/{userId}/info — нужны все 3 параметра
  • 3. Данные из тела запроса

И третье, напишем код, который запускает нужные функции.
Функции разбиты по файлам, все по феншую, добавить новые пути и методы для RESTful-сервиса будет очень просто.

.htaccess

Создадим в корне проекта файл .htaccess

    RewriteEngine On
    RewriteCond %{REQUEST_FILENAME} !-f
    RewriteRule ^(.+)$ index.php?q=$1 [L,QSA]

Этими загадочными строками мы повелеваем делать так:

1 — направить все запросы любого вида на царь-файл index.php

2 — сделать строку в URL-е доступной в index.php в get-параметре q.
То есть данные из URL-а вида /users/{userId}/info мы достанем из $_GET[‘q’].

index.php

Рассмотрим index.php строка за строкой.
Для начала получим метод запроса.

    // Определяем метод запроса
    $method = $_SERVER['REQUEST_METHOD'];

Затем данные из тела запроса

    // Получаем данные из тела запроса
    $formData = getFormData($method);

Для GET и POST легко вытащить данные из соответствующих массивов $_GET и $_POST.
А вот для остальных методов нужно чуть извратиться.
Код для них вытаскивается из потока php://input, код легко гуглится, я всего лишь написал общую обертку — функцию getFormData($method)

    // Получение данных из тела запроса
    function getFormData($method) {
    
        // GET или POST: данные возвращаем как есть
        if ($method === 'GET') return $_GET;
        if ($method === 'POST') return $_POST;
    
        // PUT, PATCH или DELETE
        $data = array();
        $exploded = explode('&', file_get_contents('php://input'));
    
        foreach($exploded as $pair) {
            $item = explode('=', $pair);
            if (count($item) == 2) {
                $data[urldecode($item[0])] = urldecode($item[1]);
            }
        }
    
        return $data;
    }

То есть мы получили нужные данные, скрыв все детали в getFormData — ну и отлично.
Переходим к самому интересному — роутингу.

    // Разбираем url
    $url = (isset($_GET['q'])) ? $_GET['q'] : '';
    $url = rtrim($url, '/');
    $urls = explode('/', $url);

Выше мы узнали, что .htaccess подложит нам параметры из URL-a в q-параметр массива $_GET.
То есть в $_GET[‘q’] попадет примерно такая строка: users/10. Независимо от того, каким методом мы запрос дергаем.

А explode(‘/’, $url) преобразует нам эту строку в массив, с которым уже можно работать.
Таким образом, составляйте сколько угодно длинные цепочки запросов, например,

GET /goods/page/2/limit/10/sort/price_asc

И будьте уверены, получите массив

    $urls = array('goods', 'page', '2', 'limit', '10', 'sort', 'price_asc');

Теперь у нас есть все данные, нужно сделать с ними что-нибудь полезное.
А сделают это всего лишь 4 строки кода

    // Определяем роутер и url data
    $router = $urls[0];
    $urlData = array_slice($urls, 1);
    
    // Подключаем файл-роутер и запускаем главную функцию
    include_once 'routers/' . $router . '.php';
    route($method, $urlData, $formData);

Улавливаете? Мы заводим папку routers, в которую складываем файлы, манипулирующие одной сущностью: товарами или пользователями.
При этом договариваемся, что название файлов совпадают с первым параметром в urlData — он и будет роутером, $router.
А из urlData этот роутер нужно убрать, он нам больше не нужен и используется только для подключения нужного файла.
array_slice($urls, 1) и вытащит нам все элементы массива, кроме первого.

Теперь осталось подключить нужный файл-роутер и запустить функцию route с тремя параметрами.
Что же это за function route? Условимся, что в каждом файле-роутере будет определена такая функция,
которая по входным параметрам определит, какое действие инициировал пользователь, и выполнит нужный код.
Сейчас это станет понятнее. Рассмотрим первый запрос — получение данных о товаре.

GET /goods/{goodId}

Файл routers/goods.php

    // Роутер
    function route($method, $urlData, $formData) {
        
        // Получение информации о товаре
        // GET /goods/{goodId}
        if ($method === 'GET' && count($urlData) === 1) {
            // Получаем id товара
            $goodId = $urlData[0];
    
            // Вытаскиваем товар из базы...
    
            // Выводим ответ клиенту
            echo json_encode(array(
                'method' => 'GET',
                'id' => $goodId,
                'good' => 'phone',
                'price' => 10000
            ));
    
            return;
        }
    
        // Возвращаем ошибку
        header('HTTP/1.0 400 Bad Request');
        echo json_encode(array(
            'error' => 'Bad Request'
        ));
    
    }

Содержимое файла — это одна большая функция route,
которая в зависимости от переданных параметров выполняет нужные действия.
Если метод GET и в urlData передан 1 параметр (goodId), то это запрос о получении данных о товаре.

Внимание: пример очень упрощенный

В реале, конечно же, нужно дополнительно проверять входные параметры, например, что goodId — это число.
Вместо того, чтобы писать код здесь, Вы, вероятно, подключите нужный класс.
И для получения товара создадите объект оного класса и вызовете у него какой-то метод.

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

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

Давайте попробуем на примере: откройте консоль браузера и выполните код

    $.ajax({url: '/examples/rest/goods/10', method: 'GET', dataType: 'json', success: function(response){console.log('response:', response)}})

Код отправит запрос на сервер, где я развернул подобное приложение и выведет ответ.
Убедитесь, что интересующий наш маршрут /goods/10 действительно отработал.
На вкладке Network Вы заметите такой же запрос.

И да, /examples/rest — это корневой путь нашего тестового приложения на webdevkin.ru

Если Вам привычнее пользоваться curl-ом в консоли, то запустите в терминале это — ответ будет тот же самый, да еще и с заголовками от сервера.

    curl -X GET https://webdevkin.ru/examples/rest/goods/10 -i

В конце функции мы написали такой код.

    // Возвращаем ошибку
    header('HTTP/1.0 400 Bad Request');
    echo json_encode(array(
        'error' => 'Bad Request'
    ));

Он значит, что если мы ошиблись с параметрами или запрашиваемый маршрут не определен, то вернем клиенту 400-ю ошибку Bad Request.
Добавьте, например, к URL-у что-то вроде goods/10/another_param и увидите ошибку в консоли и ответ 400 — кривой запрос не прошел.

По http-кодам ответов сервера

Мы не будем заморачиваться с выводом разных кодов, хотя по REST-у это и стоит делать.
Клиентских ошибок много. Даже в нашем простом случае уместна 405 в случае неправильно переданного метода.
Намеренно не хочу усложнять.

В случае успеха сервер у нас всегда вернет 200 ОК.
По хорошему, при создании ресурса стоит отдавать 201 Created.
Но опять-таки в плане упрощения эти тонкости мы отбросим, а в реальном проекте Вы их легко реализуете сами.

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

POST /goods

Добавление нового товара

    // Добавление нового товара
    // POST /goods
    if ($method === 'POST' && empty($urlData)) {
        // Добавляем товар в базу...

        // Выводим ответ клиенту
        echo json_encode(array(
            'method' => 'POST',
            'id' => rand(1, 100),
            'formData' => $formData
        ));
        
        return;
    }

urlData сейчас пустой, но зато используется formData — мы ее просто выведем клиенту.

Как сделать «правильно»?

Согласно канонам REST в post-запросе следует отдавать обратно только id созданной сущности или url, по которому эту сущность можно получить.
То есть в ответе будет или просто число — {goodId}, или /goods/{goodId}.

Почему я написал «правильно» в кавычках? Да потому, что REST — это набор не жестких правил, а рекомендаций.
И как будете реализовывать именно Вы, зависит от Ваших предпочтений или уже принятых соглашений на конкретном проекте.

Просто имейте в виду, что другой программист, читающий код и осведомленный о REST-подходе,
будет ожидать в ответе на post-запрос id созданного объекта или url, по которому можно get-запросом вытащить данные об этом объекте.

Тестим из консоли

    $.ajax({url: '/examples/rest/goods/', method: 'POST', data: {good: 'notebook', price: 20000}, dataType: 'json', success: function(response){console.log('response:', response)}})

curl

    curl -X POST https://webdevkin.ru/examples/rest/goods/ --data "good=notebook&price=20000" -i

PUT /goods/{goodId}

Редактирование товара

    // Обновление всех данных товара
    // PUT /goods/{goodId}
    if ($method === 'PUT' && count($urlData) === 1) {
        // Получаем id товара
        $goodId = $urlData[0];

        // Обновляем все поля товара в базе...

        // Выводим ответ клиенту
        echo json_encode(array(
            'method' => 'PUT',
            'id' => $goodId,
            'formData' => $formData
        ));

        return;
    }

Здесь уже все данные используются по-полной.
Из urlData вытаскивается id товара, а из formData — свойства.

Тестим из консоли

    $.ajax({url: '/examples/rest/goods/15', method: 'PUT', data: {good: 'notebook', price: 20000}, dataType: 'json', success: function(response){console.log('response:', response)}})

curl

    curl -X PUT https://webdevkin.ru/examples/rest/goods/15 --data "good=notebook&price=20000" -i

PATCH /goods/{goodId}

Частичное обновление товара

    // Частичное обновление данных товара
    // PATCH /goods/{goodId}
    if ($method === 'PATCH' && count($urlData) === 1) {
        // Получаем id товара
        $goodId = $urlData[0];

        // Обновляем только указанные поля товара в базе...

        // Выводим ответ клиенту
        echo json_encode(array(
            'method' => 'PATCH',
            'id' => $goodId,
            'formData' => $formData
        ));

        return;
    }

Тестим из консоли

    $.ajax({url: '/examples/rest/goods/15', method: 'PATCH', data: {price: 25000}, dataType: 'json', success: function(response){console.log('response:', response)}})

curl

    curl -X PATCH https://webdevkin.ru/examples/rest/goods/15 --data "price=25000" -i

К чему эти понты с PUT и PATCH?

Разве одного PUT не достаточно? Разве не выполняют они одно и то же действие — обновляют данные объекта?

Именно так — внешне действие одно. Разница в передаваемых данных.

PUT предполагает, что на сервер передаются все поля объекта, а PATCH — только измененные.
Те, которые переданы в теле запроса.
Обратите внимание, что в предыдущем PUT мы передали и название товара, и цену.
А в PATCH — только цену. То есть мы отправили на сервер только измененные данные.

Нужен ли Вам PATCH — решайте сами. Но помните о том читающем код программисте, о котором я упоминал выше.

DELETE /goods/{goodId}

Удаление товара

    // Удаление товара
    // DELETE /goods/{goodId}
    if ($method === 'DELETE' && count($urlData) === 1) {
        // Получаем id товара
        $goodId = $urlData[0];

        // Удаляем товар из базы...

        // Выводим ответ клиенту
        echo json_encode(array(
            'method' => 'DELETE',
            'id' => $goodId
        ));
        
        return;
    }

Тестим из консоли

    $.ajax({url: '/examples/rest/goods/20', method: 'DELETE', dataType: 'json', success: function(response){console.log('response:', response)}})

curl

    curl -X DELETE https://webdevkin.ru/examples/rest/goods/20 -i

С DELETE-запросом все понятно. Теперь давайте рассмотрим работу с пользователями — роутер users и соответственно, файл users.php

GET /users/{userId}

Получение всех данных о пользователе. Если GET-запрос вида /users/{userId}, то мы вернем всю информацию о пользователе,
если дополнительно указывается /info или /orders, то соответственно, только общую информацию или список заказов.

    // Роутер
    function route($method, $urlData, $formData) {
        
        // Получение всей информации о пользователе
        // GET /users/{userId}
        if ($method === 'GET' && count($urlData) === 1) {
            // Получаем id товара
            $userId = $urlData[0];
    
            // Вытаскиваем все данные о пользователе из базы...
    
            // Выводим ответ клиенту
            echo json_encode(array(
                'method' => 'GET',
                'id' => $userId,
                'info' => array(
                    'email' => 'webdevkin@gmail.com',
                    'name' => 'Webdevkin'
                ),
                'orders' => array(
                    array(
                        'orderId' => 5,
                        'summa' => 2000,
                        'orderDate' => '12.01.2017'
                    ),
                    array(
                        'orderId' => 8,
                        'summa' => 5000,
                        'orderDate' => '03.02.2017'
                    )
                )
            ));
    
            return;
        }
    
    
        // Возвращаем ошибку
        header('HTTP/1.0 400 Bad Request');
        echo json_encode(array(
            'error' => 'Bad Request'
        ));
    
    }

Тестим из консоли

    $.ajax({url: '/examples/rest/users/5', method: 'GET', dataType: 'json', success: function(response){console.log('response:', response)}})

curl

    curl -X GET https://webdevkin.ru/examples/rest/users/5 -i

GET /users/{userId}/info

Общая информация о пользователе

    // Получение общей информации о пользователе
    // GET /users/{userId}/info
    if ($method === 'GET' && count($urlData) === 2 && $urlData[1] === 'info')  {
        // Получаем id товара
        $userId = $urlData[0];

        // Вытаскиваем общие данные о пользователе из базы...

        // Выводим ответ клиенту
        echo json_encode(array(
            'method' => 'GET',
            'id' => $userId,
            'info' => array(
                'email' => 'webdevkin@gmail.com',
                'name' => 'Webdevkin'
            )
        ));

        return;
    }

Тестим из консоли

    $.ajax({url: '/examples/rest/users/5/info', method: 'GET', dataType: 'json', success: function(response){console.log('response:', response)}})

curl

    curl -X GET https://webdevkin.ru/examples/rest/users/5/info -i

GET /users/{userId}/orders

Получение списка заказов пользователя

    // Получение заказов пользователя
    // GET /users/{userId}/orders
    if ($method === 'GET' && count($urlData) === 2 && $urlData[1] === 'orders')  {
        // Получаем id товара
        $userId = $urlData[0];

        // Вытаскиваем данные о заказах пользователя из базы...

        // Выводим ответ клиенту
        echo json_encode(array(
            'method' => 'GET',
            'id' => $userId,
            'orders' => array(
                array(
                    'orderId' => 5,
                    'summa' => 2000,
                    'orderDate' => '12.01.2017'
                ),
                array(
                    'orderId' => 8,
                    'summa' => 5000,
                    'orderDate' => '03.02.2017'
                )
            )
        ));

        return;
    }

Тестим из консоли

    $.ajax({url: '/examples/rest/users/5/orders', method: 'GET', dataType: 'json', success: function(response){console.log('response:', response)}})

curl

    curl -X GET https://webdevkin.ru/examples/rest/users/5/orders -i

Итоги и исходники

Исходники из примеров статьи — здесь

Как видим, организовать поддержку REST API на нативном php оказалось не так уж и сложно и вполне законными способами.
Главное — это поддержка маршрутов и нестандартных для php методов PUT, PATCH и DELETE.

Основной код, реализовывающий эту поддержку, уместился в 3 десятка строк index.php.
Остальное — это уже обвязка, которую можно реализовать как угодно.
Я предложил это сделать в виде подключаемых файлов-роутеров, имена которых совпадают с сущностями Вашего проекта.
Но можно подключить фантазию и найти более интересное решение.

Что еще почитать по теме

Анонсы статей, обсуждения интернет-магазинов, vue, фронтенда, php, гита.

Истории из жизни айти и обсуждение кода.

Антон Шевчук // Web-разработчик

При разработке фреймворка Bluz передо мной встала задача реализовать полноценный RESTful сервис. Задача с первого взгляда простая, но камнем преткновения стало отсутствие полноценного RFC для реализации оного, так что пришлось покопать и поразмыслить, с результатами моих «раскопок» я и спешу с вами поделиться.

Если хотите познакомиться с первоисточниками, которые я нашёл, то вот они:

  • Книга Web API Design
  • Книга RESTful API design
  • Презентация ReSTful web services via RFC-2616
  • Сайт с REST & WOA Wiki
  • Сайт REST CookBook
  • Сайт REST API tutorial
  • RFC 2616 – Hypertext Transfer Protocol — HTTP/1.1
  • RFC 4918 – HTTP Extensions for Web Distributed Authoring and Versioning (WebDAV)

RESTful нынче стал трендом при создании API для сервисов, т.е. если перед вами стоит задача реализовать API для вашего проекта, то вам скажут большое спасибо, если вы будете следовать REST’у.

«Стал трендом» — это я конечно же утрирую, данный подход был описан ещё в 2000-м году, и с тех пор по чуть-чуть отвоёвывает позиции у RPC

Я бы с радостью вам начал рассказывать основы основ, но только боюсь сделать медвежью услугу, поэтому посчитаю читателя уже подготовленным и начну с небольшой таблицы, которая наглядно свяжет REST c CRUD (табличка с wikipedia):

Create Read Update Delete
POST GET PUT DELETE
/books Создание записи Список книг Обновить данные книг Удалить все
/books/42 Ошибка Данные книги Обновить данные Удалить книгу

В данном примере наглядно показана структура URL которую следует наследовать:

/:collection/
/:collection/:uid

В первом случае мы будем манипулировать набором элементов, во втором — какой-то конкретной записью под неким UID.

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

Чтение данных

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

id firstName lastName
1 Ivan Ivanov
2 Petr Petrov
3 Sidr Sidorov
.. .. ..

Ну, а теперь можно начать работать с ней, первым делом получим список записей с сервера:

>> GET /users/

<< HTTP/1.1 200 OK
[
  {id:1, firstName:"Ivan", lastName:"Ivanov"},
  {id:2, firstName:"Petr", lastName:"Petrov"},
  {id:3, firstName:"Sidr", lastName:"Sidorov"}
]

Если нам сразу все записи не нужны, то можно получить данные какой-либо конкретной записи:

>> GET /users/2

<< HTTP/1.1 200 OK
{id:2, firstName:"Petr", lastName:"Petrov"}

Если запрашиваемой записи нет, то и ответ будет соответствующим:

>> GET /users/42

<< HTTP/1.1 404 Not Found

Количество записей в базе данных может быть неподъёмным для одного запроса, так что лучше их бить на страницы, но для реализации постраничной навигации никакой RFC не принято, и тут можно пойти несколькими путями:
— используя заголовки предназначенные для работы с Partial Content (так работает Dojo Toolkit REST Store):

>> GET /users/
>> Range: items=0-2

<< HTTP/1.1 206 Partial Content
<< Content-Range: items 0-2/3
[
  {id:1, firstName:"Ivan", lastName:"Ivanov"},
  {id:2, firstName:"Petr", lastName:"Petrov"}
]

— используя GET параметры для запроса, а информации о количестве записей передавать непосредственно в теле ответа:

>> GET /users/?offset=0&limit=2

<< HTTP/1.1 200 OK
{
  rows:[
    {id:1, firstName:"Ivan", lastName:"Ivanov"},
    {id:2, firstName:"Petr", lastName:"Petrov"}
  ],
  meta:{
    total:3,
    offset:0,
    limit:2
  }
}

Оба способа имеют ряд преимуществ и недостатков, в первом случае это работа с заголовками, хоть это и true-way, но не очень прозрачно и проверять работу не так уж и просто становится (к примеру в IDE PHPStorm есть тулза, которая позволит проверить работу REST сервиса ;). Второй вариант хорош всем, кроме изменения структуры данных в ответе, к примеру для backbone вам потребуется написать обработчик, который позволит работать с такой коллекцией.

В действительности, я бы рекомендовал не надеяться на разработчиков, и в любом случае использовать некий limit по умолчанию, будет это 10, 100 или 1 000 — зависит от решаемой задачи

Что ещё может нам понадобиться? Фильтрация и сортировка:

>> GET /users/?filters=created(2012-01-01+)
>> GET /users/?sort=lastname-,firstname+
>> GET /users/?sort=lastname&order=desc

Тут уже как хотите так и городите, лишь соблюдайте одно условие — строка GET запроса должна легко читаться незатейливыми «пользователями» вашего сервиса.

Так же, создатели правильных API, рекомендуют уменьшить количество трафика, указав явно поля, которые нам необходимы:

>> GET /users/?fields=id,lastname

Создание записи

Для создания записи используется наш старый знакомый — метод POST:

>> POST /users/
firstName=Petrik&lastName=Petrenko

<< HTTP/1.1 201 Created
<< Location: /users/4

Тут стоит обратить внимание на ответ сервера, можно заметить код 201, и заголовок Location, который указывает нам, где нынче расположен новый пользователь.

Заголовок Location в данном примере не заставит браузер перейти на данный URL, он лишь информационный, а что с ним делать в дальнейшем – это уже решать клиентскому приложению

Ещё один момент, если вы создаёте сервис для приложения на backbone, то данная библиотека отправляет данные в JSON формате, следовательно запрос на сервер будет выглядеть несколько иначе:

>> POST /users/
>> Content-Type:application/json
{"firstName":"Petrik","lastName":"Petrenko"}

На PHP подобный запрос можно обработать следующим образом:

$request = file_get_contents('php://input');
$data = (array) json_decode($request);

О чём ещё стоит упомянуть — это проверка данных на сервере, если говорить привычным языком, то речь пойдёт о «валидации», и как вы уже понимаете, RFC нам тут не помощник, и надо что-то сочинить, вот моё сочинение:

>> POST /users/
>> Content-Type:application/json
{"firstName":"#"}

<< HTTP/1.1 400 Bad Request
<< Content-Type:application/json
{"error":"Invalid format of 'firstName'","errorCode":12345,"errorInfo":"http://developers.api.example.com/?error=12345"}
// or
{"errors":{"firstName":"Invalid format"},"errorCode":12345,"errorInfo":"http://developers.api.example.com/?error=12345"}

Обращу ваше внимание на следующие нюансы — это текст ошибки для пользователя (а не только разработчика), код ошибки для разработчика приложения и да, самая замечательная часть это ссылка на документацию к API, где разработчик сможет найти информацию о том, как устранить ошибку в своём коде (или добавить обработчик). Этот последний пункт такой «няшный», всем рекомендую.

И не забывайте, если вы попробуете передать POST’ом данные для обновление записи, то сервер вернёт ошибку:

>> POST /users/3
firstName=Petrik&lastName=Petrenko

<< HTTP/1.1 400 Bad Request

Возможно ответ 501 Not Implemented тоже будет уместен, но мне кажется 400-ая ошибка тут больше подходит, т.к. это не проблема сервера, это у клиента ошибка в коде, и лезет его код не тем методом, да и не потому адресу

Изменение данных

Для изменения данных RESTful предполагает использования двух методов PUT и PATCH, различия в них лишь в том, что PUT предполагает замену записи полностью, а PATCH должен обновлять лишь данные, которые пришли в запросе.

Начнём с метода PUT, присылаем значение всех полей:

>> PUT /users/2
firstName=Petr&lastName=Petrenko

<< HTTP/1.1 200 OK

Можно данные передавать и как JSON:

>> PUT /users/2
>> Content-Type:application/json
{"firstName":"Petr","lastName":"Petrenko"}

<< HTTP/1.1 200 OK

Если же запись не найдена:

>> PUT /users/42
firstName=Petr&lastName=Petrenko

<< HTTP/1.1 404 Not Found

Если ничего не изменилось (у меня есть сомнения на этот счёт):

>> PUT /users/2
firstName=Petr&lastName=Petrov

<< HTTP/1.1 304 Not Modified

Если же рассматривать метод PATCH, то на сервер отправляем лишь различия:

>> PATCH /users/2
lastName=Petrov

<< HTTP/1.1 200 OK

В API можно так же реализовать изменение нескольких записей за раз (это верно и для PUT и для PATCH методов):

>> PATCH /users
>> Content-Type:application/json
[{"id":1,firstName":"Petrik"}, {"id":2,"firstName":"Metrik"}]

<< HTTP/1.1 200 OK

Как быть в случае, если получилось внести изменения лишь в часть данных, возможно это 207 Multi-Status, но в спецификацию не вчитывался

Если уж я начал разговор о PHP, то в нём с PUT и PATCH методами не всё так гладко, и для получения данных потребуется чутка изловчиться:

$request = file_get_contents('php://input');

if ($_SERVER['CONTENT_TYPE'] == 'application/x-www-form-urlencoded') {
    // plain
    parse_str($request, $data);
} elseif ($_SERVER['CONTENT_TYPE'] == 'application/json') {
    // or JSON
    $data = (array) json_decode($request);
}

// result
print_r($data); // ['firstName'=>'Petr', 'lastName'=>'Petrenko']

Многие сервисы используют метод PUT как псевдоним к методу PATCH, да и наш «любимый» браузер не так давно научился работать с методом PATCH, так что можете пойти проверенным путём, никто на вас обиды держать не будет

Удаление данных

О, ну тут всё просто, нам потребуется метод DELETE:

>> DELETE /users/3

<< HTTP/1.1 204 No Content

Самое интересное, но вариант удаления всех записей за раз тоже возможен:

>> DELETE /users/

<< HTTP/1.1 204 No Content

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

Связанные данные

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

>> POST /users/1/pets
>> Content-Type:application/json
{petName:"Barsik", petFamily:"Cat"}

>> POST /users/1/pets
>> Content-Type:application/json
{id:1, petName:"Tuzik", petFamily:"Dog"}

Получаем список всех животных:

>> GET /users/1/pets

<< HTTP/1.1 200 OK
[
  {id:1, petName:"Barsik", petFamily:"Cat"},
  {id:1, petName:"Tuzik", petFamily:"Dog"}
]

Если нас заинтересовал лишь кот:

>> GET /users/1/pets/1

<< HTTP/1.1 200 OK
{id:1, petName:"Barsik", petFamily:"Cat"}

Получается следующая структура URL:

/:collection/:uid/:relation
/:collection/:uid/:relation/:rid

Обратите внимание, у нас тут имено связанные сущности, питомцы в нашей системе не могут существовать без хозяина, поэтому нет endpoint’а /pets/ и методов которые к нему обращаются. Но возможна ситуация, когда у вас в проекте связанные сущности могут спокойно существовать самостоятельно, возможно это сайт для питомника или ветеринарной клиники.

Формат данных

Ещё один важный момент — это формат возвращаемых данных, по умолчанию рекомендую использовать JSON формат как наиболее простой и достаточно популярный, с его обработкой не должно возникнуть проблем ни у одного разработчика. Но если таки возникла необходимость поддерживать несколько форматов, то надо каким-то образом сказать серверу какой формат нам нужен. Тут тоже несколько путей:
— передавать ещё один параметр в запросе

>> GET /users/?format=xml

— указывать формат как часть пути (именно этот путь рекомендуют в книге Web API Design)

>> GET /users.xml

— передавать заголовок Accept (мне этот вариант наиболее симпатичен)

>> GET /users
>> Accept: application/json

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

>> GET /users
>> Accept: application/xhtml+xml

<< HTTP/1.1 406 Not Acceptable
<< Content-Type:application/json
{"accept": ["application/json", "application/javascript"]}

Даже если ваш выбор пал на второй или третий вариант, вам возможно придется пойти на компромиссы, вот есть формат JSONP, и специально для него вам потребуется добавлять параметр в адресную строку, который и будет содержать тот самый callback, так что не всё так однозначно и легко

Локализация

Рецепт реализации мультиязычности для сервера схож с поддержкой форматов:

>> GET /users
>> Accept-Language: da, en-gb;q=0.8, en;q=0.7

<< HTTP/1.1 406 Not Acceptable
<< Content-Type:application/json
{"accept-language": ["ru", "ua"]}

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

Существует ещё один способ указания доступного типа данных и языков — это использование заголовка Alternates, но как это будет стыковаться с 406-м заголовком мне не совсем понятно (вроде как Alternates предполагает 200 код ответа), но всё же приведу пример:

>> GET /users
>> Accept-Language: da, en-gb;q=0.8, en;q=0.7

<< HTTP/1.1 406 Not Acceptable
<< Alternates: {"/users" 1.0 {language ru}}
<< Content-Type:application/json
{"accept-language": ["ru", "ua"]}

Версионность API

Если ваш сервис просуществует довольно продолжительное время, то скорей всего его постигнет участь любого другого долгожителя, а именно — расширение и изменение функционала, и как следствие у вас появится несколько версий API, и тут я полностью согласен с авторами книги «Web API Design» — лучше всего указывать версию API как часть пути:

>> GET /1.0/users

>> GET /v1.0/users

Данный подход удобен, практичен и нагляден, ему следуют и Twitter (1й вариант) и Facebook (второй пример).

Вместо вывода

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

P.S.

А ещё хочу порекомендовать замечательную тулзу для документирования и тестирования RESTful API — Swagger.

P.P.S.

А есть ещё тулза для тестирования RESTful API — Postman, и её можно прикрутить к вашей CI.

P.P.P.S.

Недавное читал в ХНУРЭ лекцию про RESTful API, теперь по этой теме у меня появились слайды:

In this tutorial, I’ll teach you how to build a simple REST API with PHP and MySQL.

REST has become the de facto standard when it comes to exposing data via APIs and building web services. In fact, most web applications these days access and expose data via REST APIs. With the popularity of front-end frameworks that can consume REST APIs effortlessly, it’s always going to be a plus for you if your web application exposes REST APIs.

In this article, we’ll build a simple demo application, which allows you to fetch a list of users from the MySQL database via a REST endpoint.

Setting Up the Skeleton

In this section, we’ll briefly go through the project structure.

Let’s have a look at the following structure.

1
├── Controller
2
│   └── Api
3
│       ├── BaseController.php
4
│       └── UserController.php
5
├── inc
6
│   ├── bootstrap.php
7
│   └── config.php
8
├── index.php
9
└── Model
10
    ├── Database.php
11
    └── UserModel.php

Let’s try to understand the project structure.

  • index.php: the entry-point of our application. It will act as a front-controller of our application.
  • inc/config.php: holds the configuration information of our application. Mainly, it will hold the database credentials.
  • inc/bootstrap.php: used to bootstrap our application by including the necessary files.
  • Model/Database.php: the database access layer which will be used to interact with the underlying MySQL database.
  • Model/UserModel.php: the User model file which implements the necessary methods to interact with the users table in the MySQL database.
  • Controller/Api/BaseController.php: a base controller file which holds common utility methods.
  • Controller/Api/UserController.php: the User controller file which holds the necessary application code to entertain REST API calls.

So that’s the basic setup that we are going to implement in the rest of the post.

Create a Database and Model Classes

In this section, we’ll create a database and the users table. We’ll also create the necessary model classes that will be used to fetch users from a database.

Create a Database and the Users Table

Create the rest_api_demo database by executing the following command in your MySQL terminal. (Access this with the command mysql from the command line.)

1
$CREATE DATABASE rest_api_demo;

You could also use a tool like phpMyAdmin if you prefer working with your databases that way.

Once the rest_api_demo database is created, go ahead and create the users table by running the following statements.

1
$use rest_api_demo;
2
$CREATE TABLE `users` (
3
  `user_id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
4
  `username` varchar(60) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '',
5
  `user_email` varchar(100) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '',
6
  `user_status` int(11) NOT NULL DEFAULT '0',
7
  PRIMARY KEY (`user_id`)
8
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

That should create the users table in the rest_api_demo database. You will also want to populate this table with a few dummy records for testing purposes. Insert a few records, and you’re good to go!

Create Model Classes

In this section, we’ll create the necessary model classes.

Create the Model/Database.php file with the following contents.

1
<?php
2
class Database
3
{
4
    protected $connection = null;
5

6
    public function __construct()
7
    {
8
        try {
9
            $this->connection = new mysqli(DB_HOST, DB_USERNAME, DB_PASSWORD, DB_DATABASE_NAME);
10
    	
11
            if ( mysqli_connect_errno()) {
12
                throw new Exception("Could not connect to database.");   
13
            }
14
        } catch (Exception $e) {
15
            throw new Exception($e->getMessage());   
16
        }			
17
    }
18

19
    public function select($query = "" , $params = [])
20
    {
21
        try {
22
            $stmt = $this->executeStatement( $query , $params );
23
            $result = $stmt->get_result()->fetch_all(MYSQLI_ASSOC);				
24
            $stmt->close();
25

26
            return $result;
27
        } catch(Exception $e) {
28
            throw New Exception( $e->getMessage() );
29
        }
30
        return false;
31
    }
32

33
    private function executeStatement($query = "" , $params = [])
34
    {
35
        try {
36
            $stmt = $this->connection->prepare( $query );
37

38
            if($stmt === false) {
39
                throw New Exception("Unable to do prepared statement: " . $query);
40
            }
41

42
            if( $params ) {
43
                $stmt->bind_param($params[0], $params[1]);
44
            }
45

46
            $stmt->execute();
47

48
            return $stmt;
49
        } catch(Exception $e) {
50
            throw New Exception( $e->getMessage() );
51
        }	
52
    }
53
}

This is a database access layer class, which allows us to set up a connection to the MySQL database. Apart from the connection setup, it contains generic methods like select and executeStatement that allow us to select records from a database. We won’t use the Database class directly; instead, we will create corresponding model classes that extend the Database class in order to access the underlying MySQL database.

Next, let’s create the Model/UserModel.php class with the following contents.

1
<?php
2
require_once PROJECT_ROOT_PATH . "/Model/Database.php";
3

4
class UserModel extends Database
5
{
6
    public function getUsers($limit)
7
    {
8
        return $this->select("SELECT * FROM users ORDER BY user_id ASC LIMIT ?", ["i", $limit]);
9
    }
10
}

It’s important to note that the UserModel class extends the Database class.

Apart from that, it contains the getUsers method, which allows us to select users from the MySQL database. It’s mandatory to pass the $limit parameter, which makes sure that it won’t select all records at once.

Of course, you could define more methods in the UserModel class as per your requirements. We’ll keep things simple in the context of this tutorial.

So now we have our database and model classes set up. In the next section, we’ll see how to create controllers and the remaining files in our demo application.

Create Application Layer Components

In this section, we’ll create the remaining files that are required for our demo application to work.

The inc Directory

For starters, we’ll create the necessary configuration files.

Create the inc/config.php file with the following contents.

1
<?php
2
define("DB_HOST", "localhost");
3
define("DB_USERNAME", "demo");
4
define("DB_PASSWORD", "demo");
5
define("DB_DATABASE_NAME", "rest_api_demo");

Make sure to update all the values with the actual ones that you’re using in your installation.

Next, go ahead and create the inc/bootstrap.php file with the following contents.

1
<?php
2
define("PROJECT_ROOT_PATH", __DIR__ . "/../");
3

4
// include main configuration file

5
require_once PROJECT_ROOT_PATH . "/inc/config.php";
6

7
// include the base controller file

8
require_once PROJECT_ROOT_PATH . "/Controller/Api/BaseController.php";
9

10
// include the use model file

11
require_once PROJECT_ROOT_PATH . "/Model/UserModel.php";
12
?>

Firstly, we’ve initialized the PROJECT_ROOT_PATH constant with the directory root of our application. In this way, we could use the PROJECT_ROOT_PATH constant to prepare absolute paths in our application. Next, we’ve included the config.php file, which holds the database connection information. Finally, we’ve included controller and model files.

So that’s it for setting up the common files in our application.

The Controller Directory

In this section, we’ll implement controllers that hold the majority of our application logic.

The BaseController.php File

Create the Controller/Api/BaseController.php file with the following contents. The BaseController class contains the utility methods that are used by other controllers.

1
<?php
2
class BaseController
3
{
4
    /**

5
     * __call magic method.

6
     */
7
    public function __call($name, $arguments)
8
    {
9
        $this->sendOutput('', array('HTTP/1.1 404 Not Found'));
10
    }
11

12
    /**

13
     * Get URI elements.

14
     * 

15
     * @return array

16
     */
17
    protected function getUriSegments()
18
    {
19
        $uri = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
20
        $uri = explode( '/', $uri );
21

22
        return $uri;
23
    }
24

25
    /**

26
     * Get querystring params.

27
     * 

28
     * @return array

29
     */
30
    protected function getQueryStringParams()
31
    {
32
        return parse_str($_SERVER['QUERY_STRING'], $query);
33
    }
34

35
    /**

36
     * Send API output.

37
     *

38
     * @param mixed  $data

39
     * @param string $httpHeader

40
     */
41
    protected function sendOutput($data, $httpHeaders=array())
42
    {
43
        header_remove('Set-Cookie');
44

45
        if (is_array($httpHeaders) && count($httpHeaders)) {
46
            foreach ($httpHeaders as $httpHeader) {
47
                header($httpHeader);
48
            }
49
        }
50

51
        echo $data;
52
        exit;
53
    }
54
}

Let’s go through all the BaseController class methods quickly.

The __call method is a magic method, and it’s called when you try to call a method that doesn’t exist. We’re using this opportunity to throw the HTTP/1.1 404 Not Found error when someone tries to call a method which we haven’t implemented. If this sounds confusing to you, don’t worry—it will make more sense when we test our application in the next section.

Next, there’s the getUriSegments method, which returns an array of URI segments. It’s useful when we try to validate the REST endpoint called by the user. Following that, there’s the getQueryStringParams method, which returns an array of query string variables that are passed along with the incoming request.

Finally, there’s the sendOutput method, which is used to send the API response. We’ll call this method when we want to send the API response to the user.

The UserController.php File

Next, create the Controller/Api/UserController.php file with the following contents.

1
<?php
2
class UserController extends BaseController
3
{
4
    /**

5
     * "/user/list" Endpoint - Get list of users

6
     */
7
    public function listAction()
8
    {
9
        $strErrorDesc = '';
10
        $requestMethod = $_SERVER["REQUEST_METHOD"];
11
        $arrQueryStringParams = $this->getQueryStringParams();
12

13
        if (strtoupper($requestMethod) == 'GET') {
14
            try {
15
                $userModel = new UserModel();
16

17
                $intLimit = 10;
18
                if (isset($arrQueryStringParams['limit']) && $arrQueryStringParams['limit']) {
19
                    $intLimit = $arrQueryStringParams['limit'];
20
                }
21

22
                $arrUsers = $userModel->getUsers($intLimit);
23
                $responseData = json_encode($arrUsers);
24
            } catch (Error $e) {
25
                $strErrorDesc = $e->getMessage().'Something went wrong! Please contact support.';
26
                $strErrorHeader = 'HTTP/1.1 500 Internal Server Error';
27
            }
28
        } else {
29
            $strErrorDesc = 'Method not supported';
30
            $strErrorHeader = 'HTTP/1.1 422 Unprocessable Entity';
31
        }
32

33
        // send output

34
        if (!$strErrorDesc) {
35
            $this->sendOutput(
36
                $responseData,
37
                array('Content-Type: application/json', 'HTTP/1.1 200 OK')
38
            );
39
        } else {
40
            $this->sendOutput(json_encode(array('error' => $strErrorDesc)), 
41
                array('Content-Type: application/json', $strErrorHeader)
42
            );
43
        }
44
    }
45
}

It’s important to note that the UserController class extends the BaseController class. Ideally, this class would contain the action methods that are associated with the REST endpoints that are defined for the user entity. In our case, for example, the /user/list REST endpoint corresponds to the listAction method. In this way, you can also define other methods for other REST endpoints.

The listAction method is used to get a list of users from the MySQL database. It contains the whole logic of the /user/list REST endpoint.

In the listAction method, we’ve initialized a couple of variables like $requestMethod and $arrQueryStringParams in the first place. Next, we check if the user has called the user/list endpoint with the GET method; otherwise, we won’t process further. Finally, we create the UserModel object and call the getUsers method to fetch a list of users from a database. We’ve also used the json_encode function to convert an array into a JSON object before it’s sent to the user.

Lastly, we’ve used the sendOutput method to send the JSON response to the user. It’s important to note that the response content-type header value is set to application/json since we’re sending the JSON response.

Similarly, you could define other methods for other endpoints as well.

The index.php File

The index.php file is the entry-point of our application. Let’s see how it looks.

1
<?php
2
require __DIR__ . "/inc/bootstrap.php";
3

4
$uri = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
5
$uri = explode( '/', $uri );
6

7
if ((isset($uri[2]) && $uri[2] != 'user') || !isset($uri[3])) {
8
    header("HTTP/1.1 404 Not Found");
9
    exit();
10
}
11

12
require PROJECT_ROOT_PATH . "/Controller/Api/UserController.php";
13

14
$objFeedController = new UserController();
15
$strMethodName = $uri[3] . 'Action';
16
$objFeedController->{$strMethodName}();
17
?>

Firstly, we’ve used parse_url and explode functions to initialize URI segments into the $uri array variable. Next, we’re validating the URI segments. Finally, we’ve initialized the UserController controller and called the corresponding action method.

With that, we’ve created all the necessary files in our demo REST application. In the next section, we’ll see how to call it from the end-user perspective.

How to Call Our REST API

In this section, we’ll see how to call our demo application. In our application, we’ve built a REST endpoint to get a list of users.

Let’s see how the URL of our endpoint looks:

1
// https://localhost/index.php/{MODULE_NAME}/{METHOD_NAME}?limit={LIMIT_VALUE}

2
http://localhost/index.php/user/list?limit=20

If you recall the index.php file, we checked if the $uri[2] variable is set to user. Also, the $uri[3] variable value would act as a method name. In the above case, the $uri[3] variable is set to list. Thus, it would end up calling the listAction method of the UserController class.

The output should look like this:

1
[
2
   {
3
      "user_id":1,
4
      "username":"Bob",
5
      "user_email":"bob@gmail.com",
6
      "user_status":0
7
   },
8
   {
9
      "user_id":2,
10
      "username":"John",
11
      "user_email":"john@gmail.com",
12
      "user_status":1
13
   },
14
   {
15
      "user_id":3,
16
      "username":"Mark",
17
      "user_email":"mark@gmail.com",
18
      "user_status":1
19
   },
20
   {
21
      "user_id":4,
22
      "username":"Ville",
23
      "user_email":"ville@gmail.com",
24
      "user_status":0
25
   }
26
]

As you can see, it returns a list of users as a JSON object. Apart from that, if there’s any application error, it would be returned as a JSON object as well for debugging purposes.

Conclusion

Today, we discussed how you can build a REST application with PHP and MySQL. For demonstration purposes, we created a demo application which allows you to fetch a list of users from a MySQL database via a REST API.

Did you find this post useful?

Sajal Soni

Software Engineer, FSPL, India

I’m a software engineer by profession, and I’ve done my engineering in computer science. It’s been around 14 years I’ve been working in the field of website development and open-source technologies.

Primarily, I work on PHP and MySQL-based projects and frameworks. Among them, I’ve worked on web frameworks like CodeIgnitor, Symfony, and Laravel. Apart from that, I’ve also had the chance to work on different CMS systems like Joomla, Drupal, and WordPress, and e-commerce systems like Magento, OpenCart, WooCommerce, and Drupal Commerce.

I also like to attend community tech conferences, and as a part of that, I attended the 2016 Joomla World Conference held in Bangalore (India) and 2018 DrupalCon which was held in Mumbai (India). Apart from this, I like to travel, explore new places, and listen to music!

В современной веб-разработке принято разделять backend-разработку (то, что выполняется на сервере – например, приложение на PHP) от frontend-разработки (то, что выполняется в браузере пользователя – JavaScript). Frontend выполняет запросы на backend и отрисовывает данные, которые backend ему возвращает. Но каким образом происходит этот обмен? Чем они обмениваются? Как выглядят данные, которые передаются между бэкендом и фронтендом? Об этом и пойдёт речь в данном уроке.

JSON

В уроке про composer мы с вами уже сталкивались с форматом JSON. И я вам в том уроке советовал погуглить об этом формате. Еще не сделали этого? Тогда сейчас – самое время.

Вжух!

Итак, вы уже знаете о формате JSON. Так вот, этот формат – это номер 1 среди форматов для обмена между современными приложениями. При этом бэкенд, который обменивается с клиентом в формате JSON, называется API (англ. application programming interface — программный интерфейс приложения). API принимает в качестве запроса JSON и отвечает тоже JSON-ом. Ну, точнее, не всегда именно JSON-ом, он может работать и в другом формате – XML, например. Но вся суть API в том, что он работает не с HTML, который красиво рендерится в браузере и приятен для восприятия человеком. API работает в формате, с которым удобно работать другим программам. Одна программа передаёт JSON в API, и получает от него ответ в формате JSON.

Пишем API

В этом уроке мы с вами напишем простейшее API для работы со статьями.

Первое, что нам следует сделать – это создать новый фронт-контроллер, который будет предназначен специально для работы в формате JSON.

Создаём в папке www папку api. А внутри нее – файл .htaccess:

www/api/.htaccess

RewriteEngine On

RewriteCond %{SCRIPT_FILENAME} !-d
RewriteCond %{SCRIPT_FILENAME} !-f

RewriteRule ^(.*)$ ./index.php?route=$1 [QSA,L]

И рядом с ним файл index.php

www/api/index.php

<?php
echo 123;

Проверяем, что всё работает, перейдя по адресу: http://myproject.loc/api/

Ответ API

Теперь попробуем вывести что-нибудь в формате json.

В PHP есть встроенные функции для работы с json. Нас будут интересовать прежде всего две: json_encode() и json_decode(). Первая позволяет представить какую-то сущность в json-формате.

www/api/index.php

<?php

$entity = [
    'kek' => 'cheburek',
    'lol' => [
        'foo' => 'bar'
    ]
];

echo json_encode($entity);

Обновим страничку и увидим следующее:

Ответ API JSON

Кроме того, когда сервер отвечает в фомате JSON, стоит отправлять соответствующий заголовок клиенту:

www/api/index.php

<?php

require __DIR__ . '/../../vendor/autoload.php';

$entity = [
    'kek' => 'cheburek',
    'lol' => [
        'foo' => 'bar'
    ]
];

header('Content-type: application/json; charset=utf-8');
echo json_encode($entity);

Теперь поставьте в свой браузер расширение JSON formatter.

И снова обновите страничку. Вы увидите, что ответ сервера стало гораздо проще читать – это расширение добавляет форматирование, чтобы ответ было легче воспринимать человеку.

Форматированный JSON

Теперь давайте сделаем наш API в ООП-стиле. Мы будем использовать ту же архитектуру MVC, в которой компонент View вместо рендеринга HTML-шаблонов будет выводить JSON. Давайте сделаем у View метод для вывода JSON-а.

src/MyProject/View/View.php

public function displayJson($data, int $code = 200)
{
    header('Content-type: application/json; charset=utf-8');
    http_response_code($code);
    echo json_encode($data);
}

Теперь создадим контроллер, который позволит работать со статьями через API. Создаём сначала папку Api внутри Controllers, а затем добавляем наш новый контроллер:

src/MyProject/Controllers/Api/ArticlesApiController.php

<?php

namespace MyProjectControllersApi;

use MyProjectControllersAbstractController;
use MyProjectExceptionsNotFoundException;
use MyProjectModelsArticlesArticle;

class ArticlesApiController extends AbstractController
{
    public function view(int $articleId)
    {
        $article = Article::getById($articleId);

        if ($article === null) {
            throw new NotFoundException();
        }

        $this->view->displayJson([
            'articles' => [$article]
        ]);
    }
}

Теперь создаём отдельный роутинг для API:

src/routes_api.php

<?php

return [
    '~^articles/(d+)$~' => [MyProjectControllersApiArticlesApiController::class, 'view'],
];

И, наконец, пишем фронт-контроллер для API.

www/api/index.php

<?php

require __DIR__ . '/../../vendor/autoload.php';

try {
    $route = $_GET['route'] ?? '';
    $routes = require __DIR__ . '/../../src/routes_api.php';

    $isRouteFound = false;
    foreach ($routes as $pattern => $controllerAndAction) {
        preg_match($pattern, $route, $matches);
        if (!empty($matches)) {
            $isRouteFound = true;
            break;
        }
    }

    if (!$isRouteFound) {
        throw new MyProjectExceptionsNotFoundException('Route not found');
    }

    unset($matches[0]);

    $controllerName = $controllerAndAction[0];
    $actionName = $controllerAndAction[1];

    $controller = new $controllerName();
    $controller->$actionName(...$matches);
} catch (MyProjectExceptionsDbException $e) {
    $view = new MyProjectViewView(__DIR__ . '/../templates/errors');
    $view->displayJson(['error' => $e->getMessage()], 500);
} catch (MyProjectExceptionsNotFoundException $e) {
    $view = new MyProjectViewView(__DIR__ . '/../templates/errors');
    $view->displayJson(['error' => $e->getMessage()], 404);
} catch (MyProjectExceptionsUnauthorizedException $e) {
    $view = new MyProjectViewView(__DIR__ . '/../templates/errors');
    $view->displayJson(['error' => $e->getMessage()], 401);
}

Всё, теперь можно зайти на наш API и проверить как выводится статья: http://myproject.loc/api/articles/1

Статья но не вся

Но вот незадача – вместо полей статьи мы видим только две фигурные скобки — {}. А всё потому, что функция json_encode не умеет преобразовывать в JSON объекты. Однако, можно её «научить». Для этого нужно чтобы класс реализовывал специальный интерфейс – JsonSerializable и содержал метод jsonSerialize(). Этот метод должен возвращать представление объекта в виде массива. Я предлагаю сделать такой метод на уровне ActiveRecordEntity, чтобы все его наследники автоматически могли преобразовываться в JSON.

Добавляем реализацию интерфейса:

src/MyProject/Models/ActiveRecordEntity.php

abstract class ActiveRecordEntity implements JsonSerializable

и добавляем метод, который представит объект в виде массива:

public function jsonSerialize()
{
    return $this->mapPropertiesToDbFormat();
}

Обновляем страничку http://myproject.loc/api/articles/1 и вуаля — статья в JSON-формате!

Статья в формате JSON

Postman

Но что, если мы захотим изменить нашу статью с помощью API? Для этого нам нужно отправить в API запрос в формате JSON. В реальном приложении для этого используется фронтенд на JS. А в целях разработки – специальные инструменты, позволяющие отпралять такие запросы. Одним из таких инструментов является приложение Postman. Скачайте, установите и запустите.

В контроллере добавим еще один метод:

src/MyProject/Controllers/Api/ArticlesApiController.php

public function add()
{
    $input = json_decode(
        file_get_contents('php://input'),
        true
    );
    var_dump($input);
}

Здесь php://input – это входной поток данных. Именно из него мы и будем получать JSON из запроса. file_get_contents – читает данные из указанного места, в нашем случае из входного потока. А json_decode декодирует json в структуру массива. После чего мы просто выводим массив с помощью var_dump().

Добавляем для него роут:

src/routes_api.php

<?php

return [
    '~^articles/(d+)$~' => [MyProjectControllersApiArticlesApiController::class, 'view'],
    '~^articles/add$~' => [MyProjectControllersApiArticlesApiController::class, 'add'],
];

И заполняем Postman данными, как на скриншоте:
Postman

После этого жмём кнопку Send. Прокручиваем ниже до ответа и выбираем вкладку Preview.
Ответ API

Тут мы видим вывод var_dump той структуры, которую мы отправили в POST-запросе в формате JSON.
Давайте вынесем функционал чтения входных данных в абстрактный контроллер:

src/MyProject/Controllers/AbstractController.php

protected function getInputData()
{
    return json_decode(
        file_get_contents('php://input'),
        true
    );
}

И теперь во всех контроллерах мы сможем получать входные данные вот так:

src/MyProject/Controllers/Api/ArticlesApiController.php

public function add()
{
    $input = $this->getInputData();
    var_dump($input);
}

Давайте теперь сделаем функционал, который позволит сохрянять в базу данных статью, пришедшую в формате JSON.

src/MyProject/Controllers/Api/ArticlesApiController.php

public function add()
{
    $input = $this->getInputData();
    $articleFromRequest = $input['articles'][0];

    $authorId = $articleFromRequest['author_id'];
    $author = User::getById($authorId);

    $article = Article::createFromArray($articleFromRequest, $author);
    $article->save();

    header('Location: /api/articles/' . $article->getId(), true, 302);
}

Разумеется, здесь также стоит добавить авторизацию и проверять, является ли авторизованный пользователь тем, кто указан в авторе статьи. Но это учебный и упрощенный пример, который показывает сам принцип работы с JSON-API.

Снова возвращаемся в Postman и повторно жмем Send.

Прокручиваем вниз до ответа, но на этот раз переходим во вкладку Pretty.
Ответ API в формате JSON

Как видим, статья успешно добавилась и выводится в формате JSON по адресу http://myproject.loc/api/articles/id_статьи.

REST API

То что мы сейчас с вами написали – это простейший учебный пример API. Есть более сложные системы для реализации API. Они позволяют привязывать роутинг к конкретному типу запроса. Например, POST-запрос по адресу http://myproject.loc/api/articles/1 вызовет в контроллере экшн update, который будет обновлять статью с id=1. А GET-запрос по тому же адресу будет вызывать экшн view, который будет просто возвращать статью.

То есть для одного и того же адреса мы отправляем разные типы запросов – POST, GET, PUT, DELETE. И в зависимости от типа запроса будут вызваны разные экшены. В рамках текущего курса мы этого делать не будем – ограничимся простым примером, чтобы вы просто понимали концепцию.

При этом структура запроса и ответа как правило одинаковые – мы можем посмотреть статью в формате JSON. Чтобы обновить её – мы тоже отправляем статью в формате JSON, с теми же полями.

Вот этот стиль взаимодействия с API в формате JSON, когда мы используем одну и ту же структуру данных для запроса и ответа, и используем разные типы запросов для разных действий – называется REST API. Запомните это, об этом могут спросить на собеседовании: «Что такое REST API». И вы скажете, что это когда:

  1. Запрос и ответ имеют одинаковую структуру
  2. Используются разные типы запросов (GET, POST, PUT, DELETE и другие).
  3. Используется формат, с которым удобно работать другим программам (чаще всего JSON, но могут быть и другие – например, XML).

Заключение

Стоит отметить, что API используется не только для взаимодействия между фронтендом и бэкендом, но еще и для взаимодействия между разными сервисами на бэкенде. В одном проекте может быть несколько приложений на бэкенде, которые общаются между собой по API. Один сервис отправляет в другой сервис сообщение в JSON-формате. Тот его принимает и преобразует JSON в данные для работы.

Конечно, тут все зависит от компании – где-то вообще не используют API и рендерят HTML-шаблоны, а где-то наоборот – на бэкенде ни одного HTML-тега. В любом случае, основы HTML вы уже знаете, а большего вам, как бэкендеру, о фронтенде и знать ничего не нужно. Многие когда начинают проходить мои курсы спрашивают — а будет ли курс по CSS. И я отвечаю — нет. Большую часть работы вы будете писать код на PHP, скорее всего разрабатывая API и вообще не касаясь фронтенда.

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

loader

API. PHP-сервер. GET-запрос.

API (программный интерфейс приложения, интерфейс прикладного программирования) (англ. application programming interface, API) — описание способов (набор классов, процедур, функций, структур или констант), которыми одна компьютерная программа может взаимодействовать с другой программой.

WEB-сервер — сервер, принимающий HTTP-запросы от клиентов, обычно веб-браузеров, и выдающий им HTTP-ответы, как правило, вместе с HTML-страницей, изображением, файлом, медиа-потоком или другими данными.

HTTP (англ. HyperText Transfer Protocol — «протокол передачи гипертекста») — протокол прикладного уровня передачи данных, изначально — в виде гипертекстовых документов в формате HTML, в настоящее время используется для передачи произвольных данных.

Основой HTTP является технология «клиент-сервер», то есть предполагается существование:

  • Потребителей (клиентов), которые инициируют соединение и посылают запрос;
  • Поставщиков (серверов), которые ожидают соединения для получения запроса, производят необходимые действия и возвращают обратно сообщение с результатом.

Структура HTTP-сообщения

Каждое HTTP-сообщение состоит из трёх частей, которые передаются в указанном порядке:

  • Стартовая строка (англ. Starting line) — определяет тип сообщения;
  • Заголовки (англ. Headers) — характеризуют тело сообщения, параметры передачи и прочие сведения;
  • Тело сообщения (англ. Message Body) — непосредственно данные сообщения. Обязательно должно отделяться от заголовков пустой строкой.

Стартовая строка

Стартовые строки различаются для запроса и ответа. Строка запроса выглядит так:

Метод URI HTTP/Версия

Здесь:

  • Метод (англ. Method) — тип запроса, одно слово заглавными буквами. Cписок методов для версии 1.1 представлен ниже.
  • URI определяет путь к запрашиваемому документу.
  • Версия (англ. Version) — пара разделённых точкой цифр. Например: 1.0.

Чтобы запросить страницу данной статьи, клиент должен передать строку (задан всего один заголовок):

GET /wiki/HTTP HTTP/1.0
Host: ru.wikipedia.org

Стартовая строка ответа сервера имеет следующий формат: HTTP/Версия КодСостояния Пояснение, где:

  • Версия — пара разделённых точкой цифр, как в запросе;
  • Код состояния (англ. Status Code) — три цифры. По коду состояния определяется дальнейшее содержимое сообщения и поведение клиента;
  • Пояснение (англ. Reason Phrase) — текстовое короткое пояснение к коду ответа для пользователя. Никак не влияет на сообщение и является необязательным.

Например, стартовая строка ответа сервера на предыдущий запрос может выглядеть так:

Методы

Метод HTTP (англ. HTTP Method) — последовательность из любых символов, кроме управляющих и разделителей, указывающая на основную операцию над ресурсом. Обычно метод представляет собой короткое английское слово, записанное заглавными буквами. Обратите внимание, что название метода чувствительно к регистру.

Сервер может использовать любые методы, не существует обязательных методов для сервера или клиента. Если сервер не распознал указанный клиентом метод, то он должен вернуть статус 501 (Not Implemented). Если серверу метод известен, но он неприменим к конкретному ресурсу, то возвращается сообщение с кодом 405 (Method Not Allowed). В обоих случаях серверу следует включить в сообщение ответа заголовок Allow со списком поддерживаемых методов.

GET

Используется для запроса содержимого указанного ресурса.

Клиент может передавать параметры выполнения запроса в URI целевого ресурса после символа «?»:

GET /path/resource?param1=value1&param2=value2 HTTP/1.1

POST

Применяется для передачи пользовательских данных заданному ресурсу.

Коды состояния

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

201 Webpage Created
403 Access allowed only for registered users
507 Insufficient Storage

Заголовки

Заголовки HTTP (англ. HTTP Headers) — это строки в HTTP-сообщении, содержащие разделённую двоеточием пару параметр-значение. Заголовки должны отделяться от тела сообщения хотя бы одной пустой строкой.

Примеры заголовков:

Server: Apache/2.2.11 (Win32) PHP/5.3.0
Last-Modified: Sat, 16 Jan 2010 21:16:42 GMT
Content-Type: text/plain; charset=windows-1251
Content-Language: ru

Тело сообщения

Тело HTTP-сообщения (message-body), если оно присутствует, используется для передачи тела объекта, связанного с запросом или ответом.

REST API

Это способ взаимодействия сайтов и веб-приложений с сервером. Его также называют RESTful.

Термин состоит из двух аббревиатур, которые расшифровываются следующим образом. API (Application Programming Interface) — это код, который позволяет двум приложениям обмениваться данными с сервера. На русском языке его принято называть программным интерфейсом приложения. REST (Representational State Transfer) — это способ создания API с помощью протокола HTTP. На русском его называют «передачей состояния представления».

Технологию REST API применяют везде, где пользователю сайта или веб-приложения нужно предоставить данные с сервера. Например, при нажатии иконки с видео на видеохостинге REST API проводит операции и запускает ролик с сервера в браузере. В настоящее время это самый распространенный способ организации API. Он вытеснил ранее популярные способы SOAP и WSDL.

У RESTful нет единого стандарта работы: его называют «архитектурным стилем» для операций по работе с серверов.

Принципы REST API

У RESTful есть 7 принципов написания кода интерфейсов.

Отделения клиента от сервера (Client-Server). Клиент — это пользовательский интерфейс сайта или приложения, например, поисковая строка видеохостинга. В REST API код запросов остается на стороне клиента, а код для доступа к данным — на стороне сервера. Это упрощает организацию API, позволяет легко переносить пользовательский интерфейс на другую платформу и дает возможность лучше масштабировать серверное хранение данных.

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

Кэшируемость (Casheable). В данных запроса должно быть указано, нужно ли кэшировать данные (сохранять в специальном буфере для частых запросов). Если такое указание есть, клиент получит право обращаться к этому буферу при необходимости.

Единство интерфейса (Uniform Interface). Все данные должны запрашиваться через один URL-адрес стандартными протоколами, например, HTTP. Это упрощает архитектуру сайта или приложения и делает взаимодействие с сервером понятнее.

Многоуровневость системы (Layered System). В RESTful сервера могут располагаться на разных уровнях, при этом каждый сервер взаимодействует только с ближайшими уровнями и не связан запросами с другими.

Предоставление кода по запросу (Code on Demand). Серверы могут отправлять клиенту код (например, скрипт для запуска видео). Так общий код приложения или сайта становится сложнее только при необходимости.

Начало от нуля (Starting with the Null Style). Клиент знает только одну точку входа на сервер. Дальнейшие возможности по взаимодействию обеспечиваются сервером.

Стандарты

Сам по себе RESTful не является стандартом или протоколом. Разработчики руководствуются принципами REST API для создания эффективной работы с сервером для своих сайтов и приложений. Принципы позволяют выстраивать серверную архитектуру с помощью других протоколов: HTTP, URL, JSON и XML.

Это отличает REST API от метода простого протокола доступа к объектам SOAP (Simple Object Access Protocol), созданного Microsoft в 1998 году. В SOAP взаимодействие по каждому протоколу нужно прописывать отдельно только в формате XML. Также в SOAP нет кэшируемости запросов, более объемная документация и реализация словаря, отдельного от HTTP. Это делает стиль REST API более легким в реализации, чем стандарт SOAP.

Несмотря на отсутствие стандартов, при создании REST API есть общепринятые лучшие практики, например:

  • использование защищенного протокола HTTPS
  • использование инструментов для разработки API Blueprint и Swagger
  • применение приложения для тестирования Get Postman
  • применение как можно большего количества HTTP-кодов (список)
  • архивирование больших блоков данных

Архитектура

REST API основывается на протоколе передачи гипертекста HTTP (Hypertext Transfer Protocol). Это стандартный протокол в интернете, созданный для передачи гипертекста. Сейчас с помощью HTTP отправляют любые другие типы данных.

Каждый объект на сервере в HTTP имеет свой уникальный URL-адрес в строгом последовательном формате. Например, второй модуль обучающего видео про Python будет храниться на сервере по адресу http://school.ru/python/2.

В REST API есть 4 метода HTTP, которые используют для действий с объектами на серверах:

  • GET (получение информации о данных или списка объектов)
  • DELETE (удаление данных)
  • POST (добавление или замена данных)
  • PUT (регулярное обновление данных)

Такие запросы еще называют идентификаторами CRUD: create (создать), read (прочесть), update (обновить) delete (удалить). Это стандартный набор действий для работы с данными. Например, чтобы обновить видео про Python по адресу http://school.ru/python/2 REST API будет использовать метод PUT, а для его удаления — DELETE.

В каждом HTTP-запросе есть заголовок, за которым следует описание объекта на сервере — это и есть его состояние.

Языки программирования для разработки WEB-серверов

В принципе WEB-сервер можно написать на любом языке. Но удобнее разрабатывать сервер на тех языках, где есть нативная (на уровне языка) или внешняя (с помощью подключаемых модулей) поддержка HTTP-протокола.

К таким относятся (список не полный, тут только то с чем я сам работал или «на слуху»):

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

  • Java (Spring) — строго типизированный объектно-ориентированный язык программирования общего назначения.

  • Node.js (Express) — программная платформа, основанная на движке V8 (транслирующем JavaScript в машинный код), превращающая JavaScript из узкоспециализированного языка в язык общего назначения. Node.js добавляет возможность JavaScript взаимодействовать с устройствами ввода-вывода через свой API, написанный на C++, подключать другие внешние библиотеки, написанные на разных языках, обеспечивая вызовы к ним из JavaScript-кода. Node.js применяется преимущественно на сервере, выполняя роль веб-сервера. Сервер для проекта cinema.kolei.ru написан на этом языке и код, если интересно, можно посмотреть в этом репозитории в каталоге cinema.

  • Python Django — (в русском языке встречаются названия пито́н или па́йтон) — высокоуровневый язык программирования общего назначения с динамической строгой типизацией.

  • C# (Asp.NET Core) — представляет технологию для создания веб-приложений на платформе .NET, развиваемую компанией Microsoft. В качестве языков программирования для разработки приложений на ASP.NET Core используются C# и F#.

Синтаксис PHP

Пробежимся по верхушкам:

Переменные — язык динамически типизируемый, поэтому типы при объявлении переменных можно не использовать. Переменной одного типа в любой момент может быть присвоено значение другого типа. Ключевых слов для объявления переменных тоже нет — переменная создается в момент присваивания ей значения (но если попытаться считать переменную до её объявления, то получим исключение). Первым символом в названии переменной должен быть знак $ (доллар)

$myVariable = 0;
$myVariable = "а может не 0";

Массивы — пустой массив можно создать либо функцией array, либо просто присвоив пустой массив

$myArray = array();
$myArray = [];

Массивы бывают обычные и ассоциативные (пара ключ — значение)

$simpleArray = [1, 2, 3];
$associativeArray = [
    'one' => 'value',
    'two' => 'value'
];

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

$string = 'это строка';
$anotherString = "это тоже строка, но она поддерживает переносn и может включать переменные $string";

Функции — функции объявляются ключевым словом function, тело функции заключается в фигурные скобки — обычный Си-подобный синтаксис

function someFunction($firstParam, $secondParam)
{
    return $firstParam.$secondParam;
}

$concat = someFunction('раз', 'два');

Обратите внимание, для склеивания строк используется символ . (точка), знак + (плюс) используется только с числовыми переменными. Это проистекает из динамической типизации, PHP не знал бы что делать в следующем случае:

$a = 1;
$b = 2;
$c = $a + $b; // 3 или 12 ?

Конкатенация (операция .) интерпретирует данные как строки

Классы

Объявление класса начинается с ключевого слова class, если класс является наследником какого-то класса, то родительский класс указывается после ключевого слова extends

Конструктором класса является функция с именем __construct

class ApiServer extends ParentClass
{
    // свойство класса
    private $var;

    // конструктор класса
    public function __construct(){
        // ЛОКАЛЬНАЯ переменная
        $var = 0;

        // свойство класса
        $this->var = 1;

        // метод класса
        $this->someName();
    }

    private function someName($someVar = null) {
        ...
    }
}

Обратите внимание, обращение к свойствам и методам класса производится через ключевое слово $this

Разработка API-сервера на PHP

API будем писать похожее на то, что использовалось для проекта «база». Отличия обусловлены тем, что сервер на PHP является stateless (не хранящим состояние). Поэтому без использования дополнительных механизмов (база данных, Redis, Mongo) нам негде хранить токен и будем использовать «базовую» авторизацию.

Таким образом, запросы login и logout нам не понадобятся, сразу реализуем методы получения данных (примеры запросов в формате плагина REST Client редактора VSCode).

### Запрос списка продукции
GET {{url}}/Product
Authorization: Basic ZXNtaXJub3Y6MTExMTAz

Обратите внимание, вместо токена используется заголовок Authorization. В этом заголовке первое слово обозначает алгоритм авторизации (они бывают разные), а второе это закодированная base64 строка логин:пароль (позже, когда мы вернёмся к C#, я покажу как сформировать эту строку программно, а пока можете её получить используя онлайн кодировщики base64).

WEB-сервер

Точкой входа сервера по-умолчанию являются файлы index.html или index.php.

Создайте файл index.php:

<?php

    // тут можно писать код

?>

Описываем класс сервера и создаём его (при этом вызовется конструктор)

<?php

// декларация класса
class ApiServer
{
    public function __construct(){
        print_r($_SERVER);
    }
}

// создание экземпляра
new ApiServer();

?>

Функция print_r выводит в stdout (стандартный поток вывода) содержимое переменной

Переменная $_SERVER внутренняя глобальная переменная языка PHP, она содержит параметры запроса и возвращает примерно такое:

Array
(
    [DOCUMENT_ROOT] => /home/kei/[ЙОТК]/API_PHP
    [REMOTE_ADDR] => 127.0.0.1
    [REMOTE_PORT] => 39956
    [SERVER_SOFTWARE] => PHP 7.4.3 Development Server
    [SERVER_PROTOCOL] => HTTP/1.1
    [SERVER_NAME] => localhost
    [SERVER_PORT] => 8000
    [REQUEST_URI] => /Product
    [REQUEST_METHOD] => GET
    [SCRIPT_NAME] => /index.php
    [SCRIPT_FILENAME] => /home/kei/[ЙОТК]/API_PHP/index.php
    [PATH_INFO] => /Product
    [PHP_SELF] => /index.php/Product
    [HTTP_USER_AGENT] => vscode-restclient
    [HTTP_AUTHORIZATION] => Basic ZXNtaXJub3Y6MTExMTAz
    [HTTP_ACCEPT_ENCODING] => gzip, deflate
    [HTTP_HOST] => localhost:8000
    [HTTP_CONNECTION] => close
    [PHP_AUTH_USER] => esmirnov
    [PHP_AUTH_PW] => 111103
    [REQUEST_TIME_FLOAT] => 1638434071.8106
    [REQUEST_TIME] => 1638434071
)

Нам, для начала, интересны параметры:

  • REQUEST_METHOD — метод запроса (GET, POST и т.д.)
  • PATH_INFO — путь запроса (что именно мы хотим получить, в нашем случае /Product). Есть ещё параметр REQUEST_URI, но в нём хранится путь вместе с параметрами запроса (например, /Product?id=1)
  • PHP_AUTH_USER — логин пользователя (если использовалась базовая авторизация)
  • PHP_AUTH_PW — пароль пользователя (если использовалась базовая авторизация)

Разберём на примере простой скрипт:

class ApiServer
{
    private $db = null;

    public function __construct(){
        // результат в формате JSON
        header('Content-Type: application/json; utf-8');

        try {
            
            switch($_SERVER['REQUEST_METHOD'])
            {
                case 'GET': 
                    $this->processGet($_SERVER['PATH_INFO']);
                    break;
                // case 'POST':
                //     $this->processPost($_SERVER['PATH_INFO']);
                //     break;
            }
        } catch (Throwable $th) {
            header("HTTP/1.1 500 Server error");
            $response['error'] = $th->getMessage();
            // выводим в stdout JSON-строку
            echo json_encode($response, JSON_UNESCAPED_UNICODE);
        }
    }

    private function processGet($path)
    {
        switch($path)
        {
            case '/Product':
                $this->connect();
                $this->auth();
                
                // получаем данные
                $response = $this->db
                    ->query("SELECT * FROM Product")
                    ->fetchAll(PDO::FETCH_ASSOC);
                echo json_encode($response, JSON_UNESCAPED_UNICODE);
                break;
            default:
                header("HTTP/1.1 404 Not Found");
        }
    }

    private function auth()
    {
        if(!isset($_SERVER['PHP_AUTH_USER']) || !isset($_SERVER['PHP_AUTH_PW']))
            throw new Exception('Не задан логин/пароль');

        // подгатавливаем шаблон запроса (вместо реальных данных - алиасы)
        $query = $this->db
            ->prepare("SELECT * FROM User WHERE login=:login")

        // меняем алиасы на реальные данные
        $query->bindValue(':login', $_SERVER['PHP_AUTH_USER']);

        // отправляем запрос на сервер
        $query->execute();

        // читаем полученный результат
        $user = $query->fetch(PDO::FETCH_ASSOC);
        
        if ($user == null)
            throw new Exception('Пользователь не найден');

        if ($user->password != $_SERVER['PHP_AUTH_PW'])
            throw new Exception('Не верный пароль');

        return $user;
    }

    private function connect()
    {
        // пытаемся подключиться к MySQL серверу
        $this->db = new PDO(
            "mysql:host=kolei.ru;
                port=3306;
                dbname=ТУТ-НАЗВАНИЕ-БАЗЫ;
                charset=UTF8", 
            "ТУТ-ЛОГИН-MYSQL", 
            "ТУТ-ПАРОЛЬ-MYSQL"
        );
    }
}
  • private $db = null — ссылка на базу данных, получается после успешной авторизации

  • header — встроенный метод PHP, добавляет строку в заголовок ответа

  • echo — встроенная команда PHP, выводит данные в stdout (всё, что попадёт в выходной поток, станет телом ответа)

  • json_encode($response, JSON_UNESCAPED_UNICODE) — встроенный метод PHP, преобразует данные в JSON-строку

  • isset — встроенный метод PHP, проверяет существует ли указанная переменная

  • запрос данных из бд (список всех строк)

    $response = $this->db
        ->query("SELECT * FROM Product")
        ->fetchAll(PDO::FETCH_ASSOC);
  • Запрос с параметрами

    подгатавливаем шаблон запроса (вместо реальных данных — алиасы)

    $query = $this->db
        ->prepare("SELECT * FROM User WHERE login=:login")

    меняем алиасы на реальные данные. Это необходимо для защиты от SQL-иньекций (злоумышленник может в качестве логина послать строку 'ха-ха-ха'; drop table User;)

    $query->bindValue(':login', $_SERVER['PHP_AUTH_USER']);

    выполняем запрос на сервер

    читаем полученный результат (метод fetch возвращает одну запись (строку))

    $user = $query->fetch(PDO::FETCH_ASSOC);
  • подключение к БД mysql

    $this->db = new PDO(
        "mysql:host=kolei.ru;port=3306;dbname={$_SERVER['PHP_AUTH_USER']};charset=UTF8", 
        $_SERVER['PHP_AUTH_USER'], 
        $_SERVER['PHP_AUTH_PW']);

Запустить локальный сервер для отладки можно из командной строки в каталоге проекта

Либо, если нам нужен доступ к этому АПИ с другого устройства (а эмулятор андроида это другое устройство)

Но в этом случае нужно и обращаться к апи не по localhost, а по IP-адресу (можно узнать командой ipconfig)

Параметры GET-запроса

Параметры GET-запроса передаются прямо в URL. Отделяются от пути знаком вопроса. Между собой разделяются знаком &. Представляют собой пары ключ=значение. Например, так может выглядеть запрос материала по нужному продукту:

GET {{url}}/Material?product_id=1

PHP автоматически разбирает URL и параметры GET-запроса нам доступны через глобальную переменную $_GET:

$productId = $_GET['product_id'];

POST-запросы

POST-запросы отличаются тем, что содержат данные в «теле» запроса

Формат данных определяется заголовком Content-Type

  • application/x-www-form-urlencoded — формат по-умолчанию, представляет собой те же пары ключ=значение, что и в GET-запросе (только без знака вопроса). Автоматически распознается PHP и заносится в глобальный массив переменных $_POST

  • application/json — данные передаются в виде JSON-строки (сериализованы). Автоматически не распознаются, приходится программно читать из потока данных:

    $rawData = file_get_contents('php://input');
    $json = json_decode($rawData);

    В переменной $json будет JSON-объект. Данные из него извлекаются как из класса -> стрелочным синтаксисом.

    Если кому-то удобнее работать с ассоциативными массивами, то можно в функции json_decode добавить второй параметр true

    $json = json_decode($rawData, true);

Понравилась статья? Поделить с друзьями:
  • Как написать report на английском образец
  • Как написать reference letter
  • Как написать readme github
  • Как написать psytrance
  • Как написать proposal на английском пример