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);
    }

проверяем:

5
  1. Добавить в БД таблицу, в которой будут хранится возможные типы объектов. В таблице должно быть, как минимум три поля id, название и изображение
  2. Добавить страницу с которой можно будет добавлять новые типы объектов
  3. В навигации, а также при добавлении новых объектов в списке выводить значения из этой таблицы