Админка магазина на 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. Карточка товара
Истории из жизни айти и обсуждение кода.