Админка магазина на vue.js. Урок 3. REST API на чистом PHP
Чтобы развивать админку и дальше, нам нужно уметь работать с категориями товаров, брендами и товарами. То есть получать, добавлять, изменять и удалять их на сервере. В общем, серверное API. В нашем случае отлично подойдет REST API со стандартным набором операций CRUD: create, read, update, delete. Я уже писал статью Простой RESTful сервис на нативном php, на ее основе мы и напишем API для админки интернет-магазина.
Подробно расписывать именно принципы REST API на чистом php я не буду, потому что получится повторение указанной статьи. Пробегусь только по основным моментам. Но сначала кратко расскажу, о чем пойдет речь в статье.
Первое. Основной код написан на чистом php, никаких фреймворков.
Второе. Поддерживаются такие запросы:
GET /categories
POST /categories
PUT /categories/{id}
DELETE /categories/{id}
GET /brands
POST /brands
PUT /brands/{id}
DELETE /brands/{id}
GET /products
GET /products/?categoryId={categoryId}&offset={offset}&limit={limit}
GET /products/{id}
POST /products
PUT /products/{id}
DELETE /products/{id}
Как видим, категории и бренды мы умеем получать из базы только сразу все, а товары дополнительно фильтруем по id категории плюс прикручиваем пагинацию через offset и limit. Дополнительно можем получить данные по одному товару. Добавление, изменение и удаление у всех стандартное. Разница только в передаваемых в теле запроса параметрах, это увидим потом.
Третье. На всякий случай мы предполагаем, что это не последняя версия админки, поэтому поместим реализацию в папку /api/v1/. То есть запросы будут обращаться по адресу /admin/api/v1/categories. В - версионирование.
Четвертое. Ходить в базу MySql мы будем не через mysqli, как раньше, а через pdo и подготовленные запросы, тем самым уменьшая риски sql-инъекций.
Пятое. По сравнению с упомянутой статьей двухгодичной давности Простой RESTful сервис на нативном php, сейчас код будет немного доработан. Идея останется та же самая, но код будет более структурированным и появится вменяемая обработка ошибок.
Шестое. Я не хочу менять структуру базы для магазина, которую мы создали давным-давно, когда еще делали фильтры. Но для нового API админки мы будем использовать другие названия полей. Скептики скажут, что это моя блажь и исправление старых косяков с наименованиями. Я же оптимист и отвечу, что это мы с вами применяем паттерн Адаптер. Во как! До этого у меня в блоге таких слов не было, но уж очень хочется сойти за умного, не удержался.
И седьмое. В проекте будет подключена и использована библиотека underscore.php. Если Вы занимаете фронтендом, то возможно, уже поняли, что эта библиотека делает. Если еще не знакомы с ней, то потратьте 5 минут, бегло пробегитесь по набору функций. Будет полезно, рекомендую.
Это вкратце, дальше будут подробности. Если Вас не интересует серверная часть, можете просто скачать исходники и не забивать голову. В следующих уроках мы будем просто использовать готовое API.
Если же Вам интересно, как сделать REST API на нативном php или еще что-то из семи пунктов выше, то читайте дальше.
Подготовка, index.php, хелперы и обработка ошибок
Начинаем. В папке админ создадим папку api, а в ней папку v1. А уже в ней index.php с таким содержимым
// Подключаем библиотеки и хелперы
include_once 'lib/underscore.php';
include_once 'common/helpers.php';
// Получаем данные из запроса
$data = \Helpers\getRequestData();
$router = $data['router'];
// Проверяем роутер на валидность
if (\Helpers\isValidRouter($router)) {
// Подключаем файл-роутер
include_once "routers/$router.php";
// Запускаем главную функцию
route($data);
} else {
// Выбрасываем ошибку
\Helpers\throwHttpError('invalid_router', 'router not found');
}
Разбираем. Сначала подключаем lib/underscore.php (найдете в исходниках) и common/helpers.php (напишем руками чуть позже). Дальше в массив $data получаем все данные запроса:
1. method - http-метод (GET, POST, PUT или DELETE)
2. formData - данные из тела запроса, например, при добавлении товара будет array('title' => 'iPhone SE', 'price' => 20000, 'rating' => 5)
3. urlData - данные из урла, например, в запросе PUT /categories/5 urlData будет array('categories', '5')
4. router - какой роутер нужно запускать или с какой сущностью мы имеем дело, например, categories, brands или products.
Функция isValidRouter проверяет список доступных роутеров. Для чего это нужно? Дальше у нас подключается файл роутера с тем же названием и запускается функция route, которая все и разруливает. Чтобы не проверять файлы на существование, лучше проверим валидность роутера. И в последней строке, если роутер неверный, то выдадим ошибку с кодом и текстом ошибки.
Да, чуть не забыл. Чтобы все запросы вида admin/api/v1/... перенаправлялись на index.php, нужно еще немного. Если у Вас apache, то закиньте рядом с index.php файлик .htaccess с содержимым
RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^(.+)$ index.php?q=$1 [L,QSA]
Если nginx, то в конфигах сайта добавьте такой location
location /admin/api/v1/ {
try_files $uri $uri/ /admin/api/v1/index.php?q=$uri&$args;
}
Теперь идем писать код функций-хелперов из index.php: getRequestData, isValidRouter и throwHttpError
Вот что нужно для первой функции. Файл common/helpers.php
namespace Helpers;
use PDO;
// Получение данных из тела запроса
function getFormData($method) {
// GET или POST: данные возвращаем как есть
if ($method === 'GET') {
$data = $_GET;
} else if ($method === 'POST') {
$data = $_POST;
} else {
// 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]);
}
}
}
// Удаляем параметр q
unset($data['q']);
return $data;
}
// Получаем все данные о запросе
function getRequestData() {
// Определяем метод запроса
$method = $_SERVER['REQUEST_METHOD'];
// Разбираем url
$url = (isset($_GET['q'])) ? $_GET['q'] : '';
$url = trim($url, '/');
$urls = explode('/', $url);
// Убираем из api-запросов префикс admin/api/v1
$urlData = array_slice($urls, 3);
return array(
'method' => $method,
'formData' => getFormData($method),
'urlData' => $urlData,
'router' => $urlData[0]
);
}
Подробности в той же статье Простой RESTful сервис на нативном php, не хочется повторять то же самое. Лучше посмотрим на следующие 2 функции: isValidRouter и throwHttpError.
// Проверка роутера на валидность
function isValidRouter($router) {
return in_array($router, array(
'categories',
'brands',
'products'
));
}
Здесь тупо составляем "белый список" роутеров и проверяем на вхождение в массив. И вывод ошибки запроса
// Выводим 400 ошибку http-запроса
function throwHttpError($code, $message) {
header('HTTP/1.0 400 Bad Request');
echo json_encode(array(
'code' => $code,
'message' => $message
));
}
В throwHttpError передаем 2 параметра. Зачем два? Первый code пригодится на клиенте, чтобы понять, что пошло не так. Забегая вперед, примеры кодов ошибок: invalid_parameters, category_exists, brand_not_exists и уже знакомый invalid_router. То есть, мы их можем использовать, чтобы показать пользователю, что он сделал не так. Использовать это или нет, дело Ваше. Я, например, в уроках вряд ли буду засорять код сообщениями на каждый отдельный код ошибки, но знайте, что такая возможность есть. Второй параметр message содержит более менее читаемый текст о причине ошибки, который уже можно вывести пользователю. Именно message мы и будем использовать дальше в уроках.
Можно было сделать метод более общим и передавать в него еще и http-код ответа, а не зашивать 400. Но мне кажется, что для админки магазина, которой пользуется ограниченный круг людей, это уже совсем лишнее, никому эти разные коды не нужны.
PDO. Подключаемся к MySql и пишем вспомогательные функции
Может, обратили внимание на строчку use PDO; в самом начале helpers.php? Она разрешит нам использовать в пространстве имен Helpers возможности pdo. Первой функцией будет подключение к базе.
// Подключение к БД
function connectDB() {
$host = 'localhost';
$user = 'root';
$password = 'root';
$db = 'webdevkin';
$dsn = "mysql:host=$host;dbname=$db;charset=utf8";
$options = array(
PDO::MYSQL_ATTR_INIT_COMMAND => "SET NAMES utf8",
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC
);
return new PDO($dsn, $user, $password, $options);
}
Тут ничего хитрого, только не забудьте проставить свои доступы вместо моих секурных root/root. Дальше напишем несколько вспомогательных функций, которые пригодятся нам при работе дальше. А заодно посмотрим, как работать с этим pdo.
Сначала сделаем проверку, существует ли категория с указанным id
// Проверка, существует ли категория с таким id
function isExistsCategoryById($pdo, $id) {
$query = 'select id from categories where id=:id';
$data = $pdo->prepare($query);
$data->bindParam(':id', $id, PDO::PARAM_INT);
$data->execute();
return count($data->fetchAll()) === 1;
}
Это самый стандартный пример для работы с базой через pdo. Сначала мы пишем запрос, где вместо данных ставятся плейсхолдеры типа :id. Потом подставляем вместо плейсхолдеров данные с определенным типом (в нашем случае integer) и выполняем запрос.
Ниже еще 5 функций, работают они аналогично.
// Проверка, существует ли категория с таким названием
function isExistsCategoryByTitle($pdo, $title) {
$query = 'select id from categories where category=:title';
$data = $pdo->prepare($query);
$data->bindParam(':title', $title);
$data->execute();
return count($data->fetchAll()) === 1;
}
// Проверка, существует ли бренд с таким id
function isExistsBrandById($pdo, $id) {
$query = 'select id from brands where id=:id';
$data = $pdo->prepare($query);
$data->bindParam(':id', $id, PDO::PARAM_INT);
$data->execute();
return count($data->fetchAll()) === 1;
}
// Проверка, существует ли бренд с таким названием
function isExistsBrandByTitle($pdo, $title) {
$query = 'select id from brands where brand=:title';
$data = $pdo->prepare($query);
$data->bindParam(':title', $title);
$data->execute();
return count($data->fetchAll()) === 1;
}
// Проверка, существует ли товар с таким id
function isExistsProductById($pdo, $id) {
$query = 'select id from goods where id=:id';
$data = $pdo->prepare($query);
$data->bindParam(':id', $id, PDO::PARAM_INT);
$data->execute();
return count($data->fetchAll()) === 1;
}
// Проверка, существует ли товар с таким названием
function isExistsProductByTitle($pdo, $title) {
$query = 'select id from goods where good=:title';
$data = $pdo->prepare($query);
$data->bindParam(':title', $title);
$data->execute();
return count($data->fetchAll()) === 1;
}
Все, в helpers.php сегодня ничего добавлять не будем, пора реализовывать самое интересное - роутеры.
Работа с категориями. Роутер categories
Сначала создадим папку routers, а в ней файл categories.php. Помните, в index.php мы запускали функцию route? Еще упоминали, что она все разрулит. Давайте посмотрим, каким образом
// Роутинг, основная функция
function route($data) {
// GET /categories
if ($data['method'] === 'GET' && count($data['urlData']) === 1) {
echo json_encode(getCategories());
exit;
}
// POST /categories
if ($data['method'] === 'POST' && count($data['urlData']) === 1 && isset($data['formData']['title'])) {
$title = $data['formData']['title'];
echo json_encode(addCategory($title));
exit;
}
// PUT /categories/5
if ($data['method'] === 'PUT' && count($data['urlData']) === 2 && isset($data['formData']['title'])) {
$id = (int)$data['urlData'][1];
$title = $data['formData']['title'];
echo json_encode(updateCategory($id, $title));
exit;
}
// DELETE /categories/5
if ($data['method'] === 'DELETE' && count($data['urlData']) === 2) {
$id = (int)$data['urlData'][1];
echo json_encode(deleteCategory($id));
exit;
}
// Если ни один роутер не отработал
\Helpers\throwHttpError('invalid_parameters', 'invalid parameters');
}
Здесь понаписана последовательная обработка четырех запросов:
GET /categories
POST /categories
PUT /categories/{id}
DELETE /categories/{id}
Обратите внимание на условия. Обязательно проверяется http-метод, количество параметров в самом урле, а еще для добавления и изменения категории параметр title из тела запроса. При выполнении условий мы просто запускаем соотвествующий метод обработки с нужными данными, а результат возвращаем через json_encode. В конце, если ни один роутер не отработал, выбрасываем исключение throwHttpError с кодом invalid_parameters.
Остается написать код для получения, добавления, редактирования и удаления категорий. Он скучный и однотипный, поэтому без разбора просто портянка кода чуть ниже. Из примечательного там только то, что названия передаваемых по API параметров и названия колонок в mysql-таблицах не совпадают. Например, в таблице название категории это category, а в API мы используем title. Это и есть тот самый страшный паттерн АДАПТЕР. Типа переходника между нужным нам интерфейсом и тем, что имеется. Боюсь, что разочаровал Вас таким описанием умного паттерна, но мне больше нравится такое определение, чем то, что в википедии. Вы только почитайте. Адаптер (англ. Adapter) — структурный шаблон проектирования, предназначенный для организации использования функций объекта, недоступного для модификации, через специально созданный интерфейс. Господи, это же жесть какая-то, как вы в этих паттернах вообще разбираетесь?
Ладно, философию в сторону, ниже код. Если будут вопросы, задавайте в комментариях.
// Возвращаем все категории
function getCategories() {
$pdo = \Helpers\connectDB();
$query = 'select id, category from categories';
$data = $pdo->prepare($query);
$data->execute();
return array(
'meta' => array(),
'records' => __::map($data->fetchAll(), function($item) {
return array(
'id' => (int)$item['id'],
'title' => $item['category']
);
})
);
}
// Добавление категории
function addCategory($title) {
$pdo = \Helpers\connectDB();
// Если категория существует, то выбрасываем ошибку
if (\Helpers\isExistsCategoryByTitle($pdo, $title)) {
\Helpers\throwHttpError('category_exists', 'category already exists');
exit;
}
// Добавляем категорию в базу
$query = 'insert into categories (category) values (:title)';
$data = $pdo->prepare($query);
$data->bindParam(':title', $title);
$data->execute();
// Новый айдишник для добавленной категории
$newId = (int)$pdo->lastInsertId();
return array(
'id' => $newId,
'title' => $title
);
}
// Обновление категории
function updateCategory($id, $title) {
$pdo = \Helpers\connectDB();
// Если категория не существует, то выбрасываем ошибку
if (!\Helpers\isExistsCategoryById($pdo, $id)) {
\Helpers\throwHttpError('category_not_exists', 'category not exists');
exit;
}
// Обновляем категорию в базе
$query = 'update categories set category=:title where id=:id';
$data = $pdo->prepare($query);
$data->bindParam(':id', $id, PDO::PARAM_INT);
$data->bindParam(':title', $title);
$data->execute();
return array(
'id' => $id,
'title' => $title
);
}
// Удаление категории
function deleteCategory($id) {
$pdo = \Helpers\connectDB();
// Если категория не существует, то выбрасываем ошибку
if (!\Helpers\isExistsCategoryById($pdo, $id)) {
\Helpers\throwHttpError('category_not_exists', 'category not exists');
exit;
}
// Удаляем категорию из базы
$query = 'delete from categories where id=:id';
$data = $pdo->prepare($query);
$data->bindParam(':id', $id, PDO::PARAM_INT);
$data->execute();
return array(
'id' => $id
);
}
Работа с брендами. Роутер brands
С брендами мы работаем абсолютно так же, как и с категориями. Те же операции, те же параметры, та же структура таблицы. Только название таблицы другое (brands) и поле названия бренда (brand). Я даже код копипастить не буду, возьмите в исходниках.
Работа с товарами. Роутер products
Код в products.php очень похож на предыдущие, но с тремя отличиями:
1. Добавляется запрос GET /products/{id} - вытаскиваем данные по одному товару
2. Запрос GET /products/ поддерживает пагинацию и фильтрацию по id категории. Это достигается передачей необязательных get-параметров categoryId, offset и limit
3. При добавлении и редактировании товаров в теле запроса передается больше параметров. Не один title, а такой набор: title, categoryId, brandId, price и rating. Никакой разницы нет, просто приходится делать больше проверок и sql-запросы будут длиннее.
Давайте я выложу всю портянку, а потом рассмотрим эти 3 пункта подробнее.
// Роутинг, основная функция
function route($data) {
// GET /products
if ($data['method'] === 'GET' && count($data['urlData']) === 1) {
$options = $data['formData'];
echo json_encode(getProducts($options));
exit;
}
// GET /products/5
if ($data['method'] === 'GET' && count($data['urlData']) === 2) {
$id = (int)$data['urlData'][1];
echo json_encode(getProduct($id));
exit;
}
// POST /products
if (
$data['method'] === 'POST' &&
count($data['urlData']) === 1 &&
isset($data['formData']['title']) &&
isset($data['formData']['categoryId']) &&
isset($data['formData']['brandId']) &&
isset($data['formData']['price']) &&
isset($data['formData']['rating'])
) {
$title = $data['formData']['title'];
$categoryId = (int)$data['formData']['categoryId'];
$brandId = (int)$data['formData']['brandId'];
$price = (int)$data['formData']['price'];
$rating = (int)$data['formData']['rating'];
echo json_encode(addProduct($title, $categoryId, $brandId, $price, $rating));
exit;
}
// PUT /products/5
if (
$data['method'] === 'PUT' &&
count($data['urlData']) === 2 &&
isset($data['formData']['title']) &&
isset($data['formData']['categoryId']) &&
isset($data['formData']['brandId']) &&
isset($data['formData']['price']) &&
isset($data['formData']['rating'])
) {
$id = (int)$data['urlData'][1];
$title = $data['formData']['title'];
$categoryId = (int)$data['formData']['categoryId'];
$brandId = (int)$data['formData']['brandId'];
$price = (int)$data['formData']['price'];
$rating = (int)$data['formData']['rating'];
echo json_encode(updateProduct($id, $title, $categoryId, $brandId, $price, $rating));
exit;
}
// DELETE /products/5
if ($data['method'] === 'DELETE' && count($data['urlData']) === 2) {
$id = (int)$data['urlData'][1];
echo json_encode(deleteProduct($id));
exit;
}
// Если ни один роутер не отработал
\Helpers\throwHttpError('invalid_parameters', 'invalid parameters');
}
// Возвращаем все товары
function getProducts($options) {
$pdo = \Helpers\connectDB();
$meta = array();
$query = 'select g.id, g.good, g.category_id, g.brand_id, b.brand, g.price, g.rating from goods g, brands b where g.brand_id = b.id';
// Фильтруем по категории
if (
isset($options['categoryId']) &&
is_numeric($options['categoryId'])
) {
$query .= ' and g.category_id = :categoryId';
$meta['categoryId'] = (int)$options['categoryId'];
}
// Пагинация
if (
isset($options['offset']) &&
is_numeric($options['offset']) &&
isset($options['limit']) &&
is_numeric($options['limit'])
) {
$query .= ' limit :offset, :limit';
$meta['offset'] = (int)$options['offset'];
$meta['limit'] = (int)$options['limit'];
}
$data = $pdo->prepare($query);
foreach ($meta as $key => $value) {
$data->bindValue(':' . $key, $value, PDO::PARAM_INT);
}
$data->execute();
return array(
'meta' => $meta,
'records' => __::map($data->fetchAll(), function($item) {
return array(
'id' => (int)$item['id'],
'title' => $item['good'],
'categoryId' => (int)$item['category_id'],
'brandId' => (int)$item['brand_id'],
'brand' => $item['brand'],
'price' => (int)$item['price'],
'rating' => (int)$item['rating']
);
})
);
}
// Возвращаем информацию по одному товару
function getProduct($id) {
$pdo = \Helpers\connectDB();
// Если товар не существует, то выбрасываем ошибку
if (!\Helpers\isExistsProductById($pdo, $id)) {
\Helpers\throwHttpError('product_not_exists', 'product not exists');
exit;
}
$query = 'select g.id, g.good, g.category_id, g.brand_id, b.brand, g.price, g.rating from goods g, brands b where g.id=:id and g.brand_id = b.id';
$data = $pdo->prepare($query);
$data->bindParam(':id', $id, PDO::PARAM_INT);
$data->execute();
$item = $data->fetch();
return array(
'id' => $id,
'title' => $item['good'],
'categoryId' => (int)$item['category_id'],
'brandId' => (int)$item['brand_id'],
'brand' => $item['brand'],
'price' => (int)$item['price'],
'rating' => (int)$item['rating']
);
}
// Добавление товара
function addProduct($title, $categoryId, $brandId, $price, $rating) {
$pdo = \Helpers\connectDB();
// Если товар существует, то выбрасываем ошибку
if (\Helpers\isExistsProductByTitle($pdo, $title)) {
\Helpers\throwHttpError('product_exists', 'product already exists');
exit;
}
// Если категория не существует, то выбрасываем ошибку
if (!\Helpers\isExistsCategoryById($pdo, $categoryId)) {
\Helpers\throwHttpError('category_not_exists', 'category not exists');
exit;
}
// Если бренда не существует, то выбрасываем ошибку
if (!\Helpers\isExistsBrandById($pdo, $brandId)) {
\Helpers\throwHttpError('brand_not_exists', 'brand not exists');
exit;
}
// Добавляем товар в базу
$query = 'insert into goods (good, category_id, brand_id, price, rating, photo) values (:title, :categoryId, :brandId, :price, :rating, :photo)';
$data = $pdo->prepare($query);
$data->bindParam(':title', $title);
$data->bindParam(':categoryId', $categoryId, PDO::PARAM_INT);
$data->bindParam(':brandId', $brandId, PDO::PARAM_INT);
$data->bindParam(':price', $price, PDO::PARAM_INT);
$data->bindParam(':rating', $rating, PDO::PARAM_INT);
$data->bindValue(':photo', ''); // bindParam работает только с переменными
$data->execute();
// Новый айдишник для добавленного товара
$newId = (int)$pdo->lastInsertId();
return getProduct($newId);
}
// Обновление товара
function updateProduct($id, $title, $categoryId, $brandId, $price, $rating) {
$pdo = \Helpers\connectDB();
// Если товар не существует, то выбрасываем ошибку
if (!\Helpers\isExistsProductById($pdo, $id)) {
\Helpers\throwHttpError('product_not_exists', 'product not exists');
exit;
}
// Если категория не существует, то выбрасываем ошибку
if (!\Helpers\isExistsCategoryById($pdo, $categoryId)) {
\Helpers\throwHttpError('category_not_exists', 'category not exists');
exit;
}
// Если бренда не существует, то выбрасываем ошибку
if (!\Helpers\isExistsBrandById($pdo, $brandId)) {
\Helpers\throwHttpError('brand_not_exists', 'brand not exists');
exit;
}
// Обновляем товар в базе
$query = 'update goods set good=:title, category_id=:categoryId, brand_id=:brandId, price=:price, rating=:rating where id=:id';
$data = $pdo->prepare($query);
$data->bindParam(':id', $id, PDO::PARAM_INT);
$data->bindParam(':title', $title);
$data->bindParam(':categoryId', $categoryId, PDO::PARAM_INT);
$data->bindParam(':brandId', $brandId, PDO::PARAM_INT);
$data->bindParam(':price', $price, PDO::PARAM_INT);
$data->bindParam(':rating', $rating, PDO::PARAM_INT);
$data->execute();
return getProduct($id);
}
// Удаление товара
function deleteProduct($id) {
$pdo = \Helpers\connectDB();
// Если товар не существует, то выбрасываем ошибку
if (!\Helpers\isExistsProductById($pdo, $id)) {
\Helpers\throwHttpError('product_not_exists', 'product not exists');
exit;
}
// Удаляем товар из базы
$query = 'delete from goods where id=:id';
$data = $pdo->prepare($query);
$data->bindParam(':id', $id, PDO::PARAM_INT);
$data->execute();
return array(
'id' => $id
);
}
Кода вроде много, но он действительно мало чем отличается от работы с категориями. Давайте разберем отличия. Посмотрим на код получения одного товара.
// Возвращаем информацию по одному товару
function getProduct($id) {
$pdo = \Helpers\connectDB();
// Если товар не существует, то выбрасываем ошибку
if (!\Helpers\isExistsProductById($pdo, $id)) {
\Helpers\throwHttpError('product_not_exists', 'product not exists');
exit;
}
$query = 'select g.id, g.good, g.category_id, g.brand_id, b.brand, g.price, g.rating from goods g, brands b where g.id=:id and g.brand_id = b.id';
$data = $pdo->prepare($query);
$data->bindParam(':id', $id, PDO::PARAM_INT);
$data->execute();
$item = $data->fetch();
return array(
'id' => $id,
'title' => $item['good'],
'categoryId' => (int)$item['category_id'],
'brandId' => (int)$item['brand_id'],
'brand' => $item['brand'],
'price' => (int)$item['price'],
'rating' => (int)$item['rating']
);
}
Небольшая хитрость только в том, что нам понадобится сходить в две таблицы: товаров и брендов. Возвращаем и id бренда, и его название.
Дальше посмотрим, как мы работаем с кучей параметров на примере добавления товара. В route пишем
// POST /products
if (
$data['method'] === 'POST' &&
count($data['urlData']) === 1 &&
isset($data['formData']['title']) &&
isset($data['formData']['categoryId']) &&
isset($data['formData']['brandId']) &&
isset($data['formData']['price']) &&
isset($data['formData']['rating'])
) {
$title = $data['formData']['title'];
$categoryId = (int)$data['formData']['categoryId'];
$brandId = (int)$data['formData']['brandId'];
$price = (int)$data['formData']['price'];
$rating = (int)$data['formData']['rating'];
echo json_encode(addProduct($title, $categoryId, $brandId, $price, $rating));
exit;
}
Ничем не отличается от добавления категории или бренда, только больше проверок isset и больше параметров передается в функцию добавления addProduct.
А вот и код самой addProduct
// Добавление товара
function addProduct($title, $categoryId, $brandId, $price, $rating) {
$pdo = \Helpers\connectDB();
// Если товар существует, то выбрасываем ошибку
if (\Helpers\isExistsProductByTitle($pdo, $title)) {
\Helpers\throwHttpError('product_exists', 'product already exists');
exit;
}
// Если категория не существует, то выбрасываем ошибку
if (!\Helpers\isExistsCategoryById($pdo, $categoryId)) {
\Helpers\throwHttpError('category_not_exists', 'category not exists');
exit;
}
// Если бренда не существует, то выбрасываем ошибку
if (!\Helpers\isExistsBrandById($pdo, $brandId)) {
\Helpers\throwHttpError('brand_not_exists', 'brand not exists');
exit;
}
// Добавляем товар в базу
$query = 'insert into goods (good, category_id, brand_id, price, rating, photo) values (:title, :categoryId, :brandId, :price, :rating, :photo)';
$data = $pdo->prepare($query);
$data->bindParam(':title', $title);
$data->bindParam(':categoryId', $categoryId, PDO::PARAM_INT);
$data->bindParam(':brandId', $brandId, PDO::PARAM_INT);
$data->bindParam(':price', $price, PDO::PARAM_INT);
$data->bindParam(':rating', $rating, PDO::PARAM_INT);
$data->bindValue(':photo', ''); // bindParam работает только с переменными
$data->execute();
// Новый айдишник для добавленного товара
$newId = (int)$pdo->lastInsertId();
return getProduct($newId);
}
Думаю, тоже не удивил. Вместо одной проверки isExists целых три: на существование товара с таким названием (мы предполагаем, что названия всех товаров уникальны) и проверка существования категории и бренда. И чуть длиннее sql-запрос с привязкой параметров. Из интересного разве что привязка photo. Дело в том, что мы пока не будем грузить фото и сохранять его в базу, но база требует явно указать photo. Ну так спроектировали, извините. Вы в своей базе сделаете по-человечески, зададите дефолтное значение прямо в таблице, а нам сейчас приходится проставлять photo руками. Но так как bindParam привязывает только переменные. то пустую строку мы привяжем через bindValue
$data->bindValue(':photo', '');
И третий пункт, фильтрация по категории и пагинация в получении товаров. Вот так это выглядит
// Возвращаем все товары
function getProducts($options) {
$pdo = \Helpers\connectDB();
$meta = array();
$query = 'select g.id, g.good, g.category_id, g.brand_id, b.brand, g.price, g.rating from goods g, brands b where g.brand_id = b.id';
// Фильтруем по категории
if (
isset($options['categoryId']) &&
is_numeric($options['categoryId'])
) {
$query .= ' and g.category_id = :categoryId';
$meta['categoryId'] = (int)$options['categoryId'];
}
// Пагинация
if (
isset($options['offset']) &&
is_numeric($options['offset']) &&
isset($options['limit']) &&
is_numeric($options['limit'])
) {
$query .= ' limit :offset, :limit';
$meta['offset'] = (int)$options['offset'];
$meta['limit'] = (int)$options['limit'];
}
$data = $pdo->prepare($query);
foreach ($meta as $key => $value) {
$data->bindValue(':' . $key, $value, PDO::PARAM_INT);
}
$data->execute();
return array(
'meta' => $meta,
'records' => __::map($data->fetchAll(), function($item) {
return array(
'id' => (int)$item['id'],
'title' => $item['good'],
'categoryId' => (int)$item['category_id'],
'brandId' => (int)$item['brand_id'],
'brand' => $item['brand'],
'price' => (int)$item['price'],
'rating' => (int)$item['rating']
);
})
);
}
Никаких секретов, просто при наличии соответствующих параметров добавляем в sql-запрос условие "where g.category_id = :categoryId" или "limit :offset, :limit"
Я немного побаловался с привязкой параметров через массив $meta (хз, почему почему так назвал - meta) В общем, дело в том, что параметры привязываются после подготовки запроса $pdo->prepare($query). Но привязывать все возможные параметры нельзя, pdo будет ругаться. Поэтому нужно или проверять isset-ы по 2 раза, первый при формировании sql-запроса, второй - при привязке. Или вот так, короче и универсальнее. Например, если мы захотим добавить сортировку, то нужно будет дописать только
// Сортировка
if (
isset($options['sortBy']) &&
isset($options['sortDir'])
) {
$query .= ' sort by :sortBy :sortDir';
$meta['sortBy'] = $options['sortBy'];
$meta['sortDir'] = $options['sortDir'];
}
И все, будет чуть меньше копипасты.
Впрочем, это уже на будущее, а на этом серверный урок закончен.
Еще раз напомню, исходники скачивать здесь. Если хотите подробнее разобраться с реализацией REST API на чистом php, то статья здесь
До встречи на четвертом уроке. Там уже будет только клиентская часть и только vue.js
Все уроки админки на vue.js
- Урок 1. Список товаров
- Урок 2. Фильтры и сортировки
- Урок 3. Новое REST API на чистом PHP
- Урок 4. Правим клиентский код под новое REST API и находим багу
- Урок 5. Разбиваем приложение на компоненты
- Урок 6. Инструмент vue-cli и vue-компоненты
- Урок 7. Flux и Vuex - общие вопросы
- Урок 8. Vuex на практике
- Урок 9. Перерабатываем фильтры
- Урок 10. Добавляем и удаляем бренды
- Урок 11. Обрабатываем ошибки на клиенте и сервере
- Урок 12. Редактируем бренды
- Урок 13. Роутинг
- Урок 14. Карточка товара
Истории из жизни айти и обсуждение кода.