- Добавить в БД таблицу, в которой будут хранится возможные типы объектов. В таблице должно быть, как минимум три поля id, название и изображение
- Добавить страницу с которой можно будет добавлять новые типы объектов
- В навигации, а также при добавлении новых объектов в списке выводить значения из этой таблицы
CRUD / Создание объектов. POST-запросы
Ну вот наконец-то мы и добрались до POST запросов. В отличие от GET запросов, информация о которых (ключи-значения) видна в адресной строке, данные у POST-запроса зашифрованы в тело запроса.
Также данные POST запросов не светятся в логах nginx сервера. Что уменьшает вероятность потенциального злоумышленника получить к ним данные. А вот GET запросы видны в логах в чистом виде.
Можно, например, глянуть лог nginx в laragon вот так:
и увидеть там прямо наши запросы которые мы делали, когда использовали поиск:
Также на длину адресной строки есть ограничения. Поэтому гонять длинные данные через GET запросы не выйдет.
Собственно, поэтому для передачи больших объемов данных придумали POST запросы. Которые позволяют в зашифрованном виде передавать практически ничем неограниченные объемы данных.
И именно для создания новых сущностей в БД мы будем их использовать.
Создаем контроллер
И так создаем новый контроллер и назовем его SpaceObjectCreateController
<?php
require_once "BaseSpaceTwigController.php";
class SpaceObjectCreateController extends BaseSpaceTwigController {
public $template = "space_object_create.twig";
}
создадим ему форму в шаблоне, где можно прописать все поля (пока без картинки)
{% extends "__layout.twig" %}
{% block content %}
<h1>Добавление объекта глубокого космоса</h1>
<hr>
<form class="row g-3">
<div class="col-4">
<label class="form-label">Название</label>
<input type="text" class="form-control" name="title">
</div>
<div class="col-4">
<label class="form-label">Краткое описание</label>
<input type="text" class="form-control" name="description">
</div>
<div class="col-4">
<label class="form-label">Тип</label>
<select class="form-control" name="type">
<option value="галактика">Галактика</option>
<option value="туманность">Туманность</option>
</select>
</div>
<div class="col-12">
<textarea name="info" placeholder="Полное описание..." class="form-control" rows="5"></textarea>
</div>
<div class="col-12 text-end">
<button type="submit" class="btn btn-primary">Добавить</button>
</div>
</form>
{% endblock %}
получится так,
можете попробовать как-нибудь по-другому скомпоновать. Добавить цвета. В общем, чтобы было веселее.
Добавим в роутер
$router->add("/space-object/create", SpaceObjectCreateController::class);
ну и ссылку в навигацию
Обрабатываем пост запрос
Давайте попробуем заполнить поля и попробовать нажать на кнопку добавить:
как мы уже видели в прошлом задании, при клике на кнопку происходит перегрузка страницы, а значения с формы попадают в параметры.
Это стандартное поведение формы. На самом деле мы можем указать переход на другую страницу. Для этого мы должны поменять атрибут action у тега формы. Например, после создания объекта часто логично оказаться на странице с полным список объектов. Мы можем сделать это так:
давайте повторим попытку:
кстати у нас на главной странице добавлена обработка параметра type поэтому попытка создания пока срабатывает у нас как фильтр.
В общем это не совсем то что мы хотим. Так что давайте оставим атрибут action пустым:
пустой атрибут равносилен отсутствию.
И так, я хочу реализовать такое поведение, чтобы при нажатии на кнопку добавить я оказывался на той же странице, но чтобы мне там выводилось какое-нибудь сообщение о том что объект сохранен.
Плюс я не хочу, чтобы в адресной строке были видны какие-то данные.
Почему так важно, чтобы случайные данные не попали в адресную строку?
Дело в том, что в реальном приложении там могут оказываться не только такие безобидные данные как название или описание, там вполне могут оказаться пароли в открытом виде, номер телефонов, всякие секретные токены и т.п. И все эти данные потом оказываются в истории браузера, где их можно при желании посмотреть.
Как раз для таких целей и будем использовать POST запрос.
Чтобы форма при нажатии на кнопку отправляла POST запрос, надо тегу form добавить атрибут method
давайте попробуем теперь отправить форму:
выглядит как будто ничего не произошло. Но на самом деле был вызов контроллера SpaceObjectCreateController только метод был не GET а POST.
Это можно посмотреть в консольке (F12 или Ctrl+Shift+I)
то есть тут видно и метод запроса, и его параметры.
Надо вообще понимать, что всегда, когда вы в браузере просто руками набираете в адресной строке какой-нибудь url, то отправляется GET запрос. И в ответ приходит содержимое страницы как результат этого запроса.
Когда вы у формы у которой в method указано “POST” нажимаете кнопку с type=submit, то отправляется POST запрос. И в ответ приходит содержимое страницы как результат этого запроса.
Так как у нас сейчас контроллеры не умеют отличать POST запрос от GET запроса, то результат отправки формы полностью совпадают с реакцией на открытие страницы для добавления объекта.
Давайте научим наш контроллер различать виды запросов.
Сначала нам надо понять, как определить тип запроса. Для этого в глобальном объекте $_SERVER
есть ключ REQUEST_METHOD
, попробуем его вывести:
class SpaceObjectCreateController extends BaseSpaceTwigController {
public $template = "space_object_create.twig";
public function get()
{
echo $_SERVER['REQUEST_METHOD'];
parent::get();
}
}
тестим:
В принципе неплохо. Только получается что в независимости от типа запроса мы попадаем в функцию get, которая выводит тип запроса.
Наверное, было бы неплохо, чтобы у нас был бы отдельно метод get и отдельно метод post под разные типы запросов.
Идем в BaseController и добавляем там метод process_response
abstract class BaseController {
// ...
// новая функция
public function process_response() {
$method = $_SERVER['REQUEST_METHOD']; // вытаскиваем метод
if ($method == 'GET') { // если GET запрос то вызываем get
$this->get();
} else if ($method == 'POST') { // если POST запрос то вызываем get
$this->post();
}
}
// уберем тут abstract, и просто сделаем два пустых метода под get и post запросы
public function get() {}
public function post() {}
}
теперь пойдем в Router.php и поменяем там вызов метода get контроллера на вызов process_ response
class Router {
// ...
public function get_or_default($default_controller) {
// ...
return $controllerInstance->process_response(); // теперь тут process_response вместо get
}
}
попробуем теперь вызвать нашу форму
так как у нас нет обработчика у метода post он показывает пустую страницу.
Я могу, например, сделать чтобы метод post просто вызывал метод get, вот так:
class SpaceObjectCreateController extends BaseSpaceTwigController {
public $template = "space_object_create.twig";
public function get()
{
echo $_SERVER['REQUEST_METHOD'];
parent::get();
}
// добавил
public function post() {
$this->get(); // вызываю get
}
}
смотрим:
Как видим просто открывается та же страница.
Но допустим я хочу, чтобы выводилось какое-нибудь дополнительно сообщение, типа объект создан.
Тогда мне надо чтобы из метода post я могу подправить context контроллера и передать его в get.
Для этого давайте немного подкрутим BaseController
:
abstract class BaseController {
// ...
public function process_response() {
$method = $_SERVER['REQUEST_METHOD'];
$context = $this->getContext(); // вызываю context тут
if ($method == 'GET') {
$this->get($context); // а тут просто его пробрасываю внутрь
} else if ($method == 'POST') {
$this->post($context); // и здесь
}
}
public function get(array $context) {} // ну и сюда добавил в качестве параметра
public function post(array $context) {} // и сюда
}
подкрутим TwigBaseController.php
:
class TwigBaseController extends BaseController {
// ...
public function get(array $context) { // добавил аргумент в get
echo $this->twig->render($this->template, $context); // а тут поменяем getContext на просто $context
}
}
И обновим Controller404.php
class Controller404 extends BaseSpaceTwigController {
public $template = "404.twig";
public $title = "Страница не найдена";
public function get(array $context)
{
http_response_code(404);
parent::get($context);
}
}
теперь возвращаемся в SpaceObjectCreateController
и правим там:
class SpaceObjectCreateController extends BaseSpaceTwigController {
public $template = "space_object_create.twig";
public function get(array $context) // добавили параметр
{
echo $_SERVER['REQUEST_METHOD'];
parent::get($context); // пробросили параметр
}
public function post(array $context) { // добавили параметр
$context['message'] = 'Вы успешно создали объект'; // добавили сообщение
$this->get($context); // пробросили параметр
}
}
и добавим вывод сообщения в шаблоне
проверяем:
мы конечно пока ничего не добавили, но иллюзию создали
Добавляем наконец-то объект
Чтобы реально добавить объект нам надо считать параметры которые были на форме. По аналогии с GET запросом мы можем получить доступ к ним через $_POST
для этого пишем:
public function post(array $context) {
// получаем значения полей с формы
$title = $_POST['title'];
$description = $_POST['description'];
$type = $_POST['type'];
$info = $_POST['info'];
// создаем текст запрос
$sql = <<<EOL
INSERT INTO space_objects(title, description, type, info, image)
VALUES(:title, :description, :type, :info, '')
EOL;
// подготавливаем запрос к БД
$query = $this->pdo->prepare($sql);
// привязываем параметры
$query->bindValue("title", $title);
$query->bindValue("description", $description);
$query->bindValue("type", $type);
$query->bindValue("info", $info);
// выполняем запрос
$query->execute();
$context['message'] = 'Вы успешно создали объект';
$context['id'] = $this->pdo->lastInsertId(); // получаем id нового добавленного объекта
$this->get($context);
}
ну и для полного счастья давайте ссылку на этот объект добавим в сообщение
тестируем:
ляпота! =)
Добавление картинки
Теперь давайте реализуем привязку изображения к объект. Если у нас изображение это просто ссылка на картинку в интернете, то тут все просто, бери текстовое поле и пихай туда текст
Но если мы хотим добавить возможность выбрать картинку с компьютера пользователя. Тут возникают сложности, так как теперь мы должны сами загрузить картинку на свой сервер (в нашем случае в папку public) и сформировать ссылку на это изображение.
Попробуем это сделать.
Сначала нам понадобится добавить специальный инпут для выбора файла https://getbootstrap.com/docs/5.0/forms/form-control/#file-input
запихать его куда-нибудь сюда
ну и назвать его image
теперь глянем что происходит, когда мы делаем пост запрос с картинкой.
Кстати по понятным причинам нельзя загрузить картинку через GET запрос, ведь изображение может вполне весить несколько мегабайт, а если речь идет о файлах то и несколько гигабайт, ни одна адресная строка не выдержит таких данных. Но это так просто к справке…
И так, идем в наш SpaceObjectCreateController
, отключим там добавление записей в БД чтобы не наплодить лишних
и добавим вывод данных из $_POST
class SpaceObjectCreateController extends BaseSpaceTwigController {
public $template = "space_object_create.twig";
public function post(array $context) {
$title = $_POST['title'];
$description = $_POST['description'];
$type = $_POST['type'];
$info = $_POST['info'];
// добавил
echo "<pre>";
print_r($_POST);
echo "</pre>";
// ...
тестим
как видно, информации об изображении нет в пост запросе. Где ж его взять?
Дело в том, что информация о файлах хранится в еще одном специальном объект который называет $_FILES
, попробуем его вывести
public function post(array $context) {
$title = $_POST['title'];
$description = $_POST['description'];
$type = $_POST['type'];
$info = $_POST['info'];
echo "<pre>";
// print_r($_POST);
print_r($_FILES); // добавил
echo "</pre>";
пробуем еще раз:
хм, теперь выводит пустой массив…
В общем тут есть еще один тонкий момент, чтобы форма начала отправлять данные файла на сервер надо добавить ей еще один атрибут, вот так:
снова запускаем
о, пошли данные =)
Схема данных простая: есть ключ который советует значению, указанному в name
на форме, и информация о файле, включая, название, тип, размер и путь куда PHP сохранил данные на время выполнения запроса.
Таким образом, чтобы файлик стал достоянием нашего сайта, надо его скопировать в папку public. Для этого рекомендуется создать подпапку. Обычно ее называют media
, и если вы используете систему контроля версий, то эту папку надо добавить в gitignore, так как на любом более-менее активном сайта она через пару недель начинает весить гигабайты данных.
Добавляем
и теперь правим контроллер чтобы он копировал туда файлик:
class SpaceObjectCreateController extends BaseSpaceTwigController {
public $template = "space_object_create.twig";
public function post(array $context) {
$title = $_POST['title'];
$description = $_POST['description'];
$type = $_POST['type'];
$info = $_POST['info'];
// вытащил значения из $_FILES
$tmp_name = $_FILES['image']['tmp_name'];
$name = $_FILES['image']['name'];
// используем функцию которая проверяет
// что файл действительно был загружен через POST запрос
// и если это так, то переносит его в указанное во втором аргументе место
move_uploaded_file($tmp_name, "../public/media/$name");
проверим что файл действительно загрузился:
можно даже открыть его напрямую по ссылке, у меня файл называется 1615886740141917748.jpg
поэтому ссылка будет http://localhost:9007/media/1615886740141917748.jpg, вот:
и осталось теперь эту ссылку, без адреса сервера http://localhost:9007 передать в базу на этот файл
public function post(array $context) {
// ...
$tmp_name = $_FILES['image']['tmp_name'];
$name = $_FILES['image']['name'];
move_uploaded_file($tmp_name, "../public/media/$name");
$image_url = "/media/$name"; // формируем ссылку без адреса сервера
$sql = <<<EOL
INSERT INTO space_objects(title, description, type, info, image)
VALUES(:title, :description, :type, :info, :image_url) -- передаем переменную в запрос
EOL;
$query = $this->pdo->prepare($sql);
$query->bindValue("title", $title);
$query->bindValue("description", $description);
$query->bindValue("type", $type);
$query->bindValue("info", $info);
$query->bindValue("image_url", $image_url); // подвязываем значение ссылки к переменной image_url
$query->execute();
// а дальше как обычно
$context['message'] = 'Вы успешно создали объект';
$context['id'] = $this->pdo->lastInsertId();
$this->get($context);
}
проверяем: