Чат PHP-разработчиков
Обновление в БД через Active Record

Обновление с помощью Active Record

В этом уроке мы научимся обновлять данные в базе данных в стиле объектно-ориентированного подхода. Для этого, как вы уже наверное догадались, мы будем использовать всё те же сущности ActiveRecord.

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

Давайте для изменения статей сделаем отдельный роут (^articles/(\d+)/edit$):

src/routes.php

Код доступен только после покупки курса ООП в PHP.

Теперь добавим в контроллере новый экшн edit, в котором мы пока просто будем получать статью и выводить её с помощью var_dump();

src/MyProject/Controllers/ArticlesController.php

Код доступен только после покупки курса ООП в PHP.

Перейдём по новому URL и убедимся, что все работает - http://myproject.loc/articles/1/edit

Вывод статей для редактирования

Теперь, предположим, что мы решили изменить этот объект. Давайте изменим у этого объекта свойства name и text (не забудьте добавить сеттеры для этих полей в классе Article):

src/MyProject/Controllers/ArticlesController.php

Код доступен только после покупки курса ООП в PHP.

Посмотрим на результат.
Новые свойства объекта

Как видим, свойства у объекта успешно были изменены. Но это никак не повлияло на его состояние в базе данных.

Данные в БД не изменились

И это логично, ведь мы после этого не выполнили запросов на обновление записи в базе. Давайте реализуем этот механизм.

Давайте создадим метод save() в классе ActiveRecordEntity, который будет сохранять текущее состояние обоъекта в базе.

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

Код доступен только после покупки курса ООП в PHP.

где во все поля подставить текущие значения у объекта.

Но ведь у разных наследников класса ActiveRecordEntity разные свойства. Вопрос – как их получить, не привязываясь к конкретному классу. Да с помощью рефлексии, которую мы изучили на прошлом занятии!

Алгоритм у нас будет такой:

  1. Получаем имена свойств объекта с помощью рефлексии, например, authorId
  2. Преобразовываем это значение из camelCase в строку_с_подчеркушками, например, author_id – именно так называется поле в базе данных
  3. Составляем результирующий запрос на обновление записи в базе данных.

Итак, давайте теперь сделаем это!
Для начала давайте напишем метод, который будет преобразовывать строки типа authorId в author_id.
Это можно сделать с помощью регулярного выражения: перед каждой заглавной буквой мы добавляем символ подчеркушки «_», а затем приводим все буквы к нижнему регистру:

src/MyProject/Models/ActiveRecordEntity.php

Код доступен только после покупки курса ООП в PHP.

Отлично, теперь давайте напишем метод, который прочитает все свойства объекта и создаст массив вида:

Код доступен только после покупки курса ООП в PHP.

src/MyProject/Models/ActiveRecordEntity.php

Код доступен только после покупки курса ООП в PHP.

Здесь мы получили все свойства, и затем каждое имяСвойства привели к имя_свойства. После чего в массив $mappedProperties мы стали добавлять элементы с ключами «имя_свойства» и со значениями этих свойств.

Давайте посмотрим, что у нас получилось. Выведем массив, полученный с помощью этого метода в методе save().

Код доступен только после покупки курса ООП в PHP.

Теперь вызовем этот метод сущности в контроллере:

src/MyProject/Controllers/ArticlesController.php

Код доступен только после покупки курса ООП в PHP.

Посмотрим на то, что этот код выдаёт.
Массив с именами колонок в БД

Отлично! Мы имеем структуру, которая соответствует структуре в базе данных. Теперь на её основе можно построить запрос!

Но перед этим нам стоит обратить внимание, что метод save() может быть вызван как у объекта, который уже есть в базе данных, так и у нового (если мы создали его с помощью new Article и заполнили ему свойства). Для первого нам нужно будет выполнить UPDATE-запрос, а для второго - INSERT-запрос. Как понять, с каким типом объекта мы работаем? Да всё проще простого – у объекта, которому соответствует запись в базе, свойство id не равно null, а если мы только создали объект, у него свойства id ещё нет.

Поэтому нам нужно разделить логику метода save() для этих двух случаев. Вот что у нас должно получиться:

src/MyProject/Models/ActiveRecordEntity.php

Код доступен только после покупки курса ООП в PHP.

В этом уроке мы напишем реализацию только одного метода – update(). Синтаксис запроса выглядит следующим образом:

Код доступен только после покупки курса ООП в PHP.

После этого нам нужно подставить в запрос параметры:

Код доступен только после покупки курса ООП в PHP.

У нас есть массив column1 = value1, column2 = value2. Всё что нам нужно – разделить его на 2 массива:

  1. будет содержать строки: column1 = :param1
  2. будет содержать ключ => значение вида: [:param1 => value1]
    и собрать из этих частей готовый запрос!

Чтобы было проще понимать, что происходит, будем делать это поэтапно. Для начала напишем код, который будет создавать два этих массива:

Код доступен только после покупки курса ООП в PHP.

После этого мы получим следующий результат:
Почти готовый запрос на UPDATE

Теперь дело за малым – сформировать запрос:

Код доступен только после покупки курса ООП в PHP.

Результат:
Сформированный запрос

Остаётся только выполнить этот запрос и передать нужные параметры!

Код доступен только после покупки курса ООП в PHP.

Сейчас этот скрипт ничего не выведет, но если мы зайдём в базу данных, мы обнаружим, что запись, соответствующая нашей статье – изменилась!
Обновленные данные в БД

Давайте теперь попробуем посмотреть нашу обновленную статью, перейдя по адресу http://myproject.loc/articles/1

Обновленная статья

Итак, в этом уроке мы создали универсальный метод, который позволит обновлять записи в бд для любых объектов, являющимися наследниками класса ActiveRecordEntity.

Домашнее задание

Напишите самостоятельно метод insert(), который будет добавлять в базу новую запись. Не торопитесь, разбейте задачу на несколько компонентов и решите, какую последовательность действий нужно сделать.

Онлайн обучение PHP
Путь с полного нуля до джуниора!
Начать бесплатно
Читайте также
Курс программирования на PHP
Подготовка до уровня устройства на работу!
Начать бесплатно
Комментарии (24)


Sparky

ArticleController:

public function edit(int $articleId): void
    {
        $article = Article::getById($articleId);

        if ($article === null)
        {
            $this->view->renderHtml('errors/404.php', [], 404);
            return;
        }

        $article1 = new Article();
        $article1->setName('Новая статья1 name');
        $article1->setText('Новая статья1 text');
        $article1->setAuthorId('1');
        $article1->save();
    }

ActiveRecordEntity

private function insert(array $mappedProperties): void
    {
        $params2values = [];
        $index = 1;

        foreach ($mappedProperties as $column => $value)
        {
            if (in_array(null, $mappedProperties))
            {
                unset($mappedProperties[array_search(null, $mappedProperties)]);
            }
        }
        foreach ($mappedProperties as $column => $value)
        {
            $params[] = ':param' . $index; // :param1
            $columns[] = $column;
            $params2values[':param' . $index] = $value;
            $index++;
        }

        $sql = 'INSERT INTO `' . static::getTableName() . '` (' . implode(', ', $columns) .
            ') VALUES (' . implode(', ', $params) . ');';
        $db = Db::getInstance();
        $db->query($sql, $params2values, static::class);
    }

Скажите, я правильно понял, если мы хотим изменить существующую позицию в БД, мы делаем $article = Article::getById($articleId) в контроллере и работаем с этим объектом, а если мы создаем новую сущность, то $article1 = new Article() ?
И как избежать использование foreach два раза, криво вроде как-то выглядит? :)

ivashkevich

Привет, сначала ответ на твой первый вопрос - да, всё правильно.

По поводу кода и второго вопроса:

  • в контроллере должен быть отдельный экшн для создания новой сущности (например - create), edit - только для уже существующих записей.
  • первый foreach можно грохнуть вместе со всем его содержимым - не повлияет ни на что. Также советую изучить функцию array_filter.
  • вместо :param{$index} можно использовать :{$column} - будет понятнее.

В целом - всё хорошо, есть небольшая путаница в контроллере, но это ничего страшного, со временем всё будет ;)

Kirill.K

ArticlesController:

public function create() {
        $article = new Article;
        $article->setName('Название новой статьи');
        $article->setText('Текст новой статьи');
        $article->setAuthor(1);
        $article->save();
    }

ActiveRecordEntity:

//задаём калбэк функцию
    private function crossOutNull($con) {
        return($con != null);
    }

    private function insert(array $mappedProperties): void {

        //оставляем только заданные параметры
        $mappedPropertiesDeclaredColumns = array_filter($mappedProperties, 'self::crossOutNull');

        //создаём два массива для формирования SQL запроса
        $articlesColumn = [];
        $articlesValues = [];
        foreach ($mappedPropertiesDeclaredColumns as $column => $value) {
            $articlesColumn[] = $column;
            $articlesValues[] = (is_string($value) ? "'" . $value . "'" : $value);
        }

        $sql = 'INSERT INTO ' . static::getTableName() . '(' . implode(', ', $articlesColumn) . ') VALUES (' . implode(', ', $articlesValues) . ')';

        $db = Db::getInstance();
        $db->query($sql, [], static::class);
    }
ivashkevich

Привет, нужно исправить:

  1. нельзя значение, пришедшее извне помещать в запрос без какой-либо фильтрации - всегда используйте параметризованные значения. В самом запросе только подстановки вида :param1, :param2. Сами значения - в параметрах, с которыми нужно выполнить запрос;
  2. исправление предыдущего пункта приведет к отсутствию необходимости в приведении значений к строкам/не строкам
  3. фильтрация null-значений - это лишнее
Kirill.K

Спасибо, всё принял к сведению, ну и подробное описание обнаружил в последующем уроке)

ArtemijeKA

"Преобразовываем это значение из camelCase в строку_с_подчеркушками, например, author_id – именно так называется поле в базе данных"

Можно ли в БД имена столбцов писать сразу в camelCase или так не принято?

ivashkevich

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

ArtemijeKA

Где можно почитать про
'/(?<!^)[A-Z]/', '_$0'
В частности, что такое < ! и _$0

ivashkevich
[A-Z] - берём большие буквы

(?<!^) - а это означает, что при этом самую первую букву в начале строки мы не берем, даже если она большая

_$0 - это знак подчеркивания, за которым следует нулевое совпадение в регулярке (нулевое - это вся строка, попавшая под регулярку. В нашем случае - это одна большая буква). Таким образом, с помощью preg_replace, мы заменяем все большие буквы A - Z на _A - _Z. А затем с помощью strtolower приводим всю строку к нижнему регистру.
demyanovpaul@yandex.ru

Напишите самостоятельно метод insert(), который будет добавлять в базу новую запись. Не торопитесь, разбейте задачу на несколько компонентов и решите, какую последовательность действий нужно сделать.


private function insert(array $mappedProperties): void
{
        $mappedPropertiesNotNull = array_filter($mappedProperties);

        $columns = [];
        $params = [];
        $params2values = [];
        $index = 1;
        foreach ($mappedPropertiesNotNull as $column => $value) {
            $params[] = ':param' . $index; // :params
            $columns[] = $column; // columns
            $params2values[':param' . $index] = $value; // [:param => value]
            $index++;
        }

        $sql = 'INSERT INTO ' . static::getTableName() . '(' . implode(', ', $columns) . ') VALUES (' . implode(', ', $params) . ')';

        $db = Db::getInstance();
        $db->query($sql, $params2values, static::class);
}
ivashkevich

Отлично! Очень рад твоей скорости)

tomsonst

ArticlesController

public function create():void
    {
        $article = new Article();
        $article->setName('Новый статья');
        $article->setText('Новый текста');
        $article->authorId = '1';
        $article->createdAt = date(c);
        $article->save();
    }

ActiveRecordEntity

private function insert(array $mappedProperties): void
    {
        $columns2params = [];
        $params2values = [];
        $index = 1;
        foreach ($mappedProperties as $column => $value) {
            $param = ':param' . $index; // :param1
            $columns2params[] = $column . ' = ' . $param; // column1 = :param1
            $params2values[':param' . $index] = $value;
            $index++;
        }

        $sql = 'INSERT INTO ' . static::getTableName() . ' SET ' . implode (', ', $columns2params);
        $db = Db::getInstance();
        $db->query($sql, $params2values, static::class);
    }

Чувствую что-то тут не так)

ivashkevich

В каком именно месте ты это чувствуешь?)

tomsonst

Стоит ли cteatedAt так задавать? Или есть возможность это автоматизировать?

ivashkevich

Можно на уровне объектов оперировать с DateTime. А при сохранении и чтении из БД уже преобразовывать к строковому представлению. Как определять, что это поле с датой? Можно сделать специальные аннотации PhpDoc-ом к этому свойству в объекте, и определять с помощью рефлексии.

g--nokoder

Где-то подсмотрел, но думаю, что понимание темы - это успех)

Article Controller

public function create(): void
    {
        $article2 = new Article();
        $article2->setName('Новая статья 2');
        $article2->setText('Новый текст 2');
        $article2->setAuthorId(1);
        $article2->save();
    }

ActiveRecord

private function insert(array $mappedProperties): void
    {
        $mappedPropertiesNotNull = array_filter($mappedProperties);

        $columns = [];
        $params = [];
        $params2values = [];
        $index = 1;
        foreach ($mappedPropertiesNotNull as $column => $value) {
            $params[] = ':param' . $index;
            $columns[] = $column;
            $params2values[':param' . $index] = $value;
            $index++;
        }

        $sql = 'INSERT INTO ' . static::getTableName() . '(' . implode(', ', $columns) . ')' . ' VALUES (' . implode(', ', $params) . ' )';
        $db = Db::getInstance();
        $db->query($sql, $params2values, static::class);

    }
ilyaOrlov

Где-то подсматривал, потому что пока не всё так легко, как хотелось бы)

ArticleController.php

public function create()
    {
        $article = new Article;

        $article -> setName('Название новой статьи');
        $article -> setText('Текст новой статьи');
        $article -> setAuthorId(1);

        $article -> save();
    }

ActiveRecordEntity.php

private function insert(array $mappedProperties): void
    {
        var_dump($mappedProperties);
        $mappedPropertiesNotNull = array_filter($mappedProperties);

        $columns = [];
        $params = [];
        $params2values = [];
        $index = 1;

        foreach ($mappedPropertiesNotNull as $column => $value) {
            $params[] = ':param' . $index;
            $columns[] = $column; 
            $params2values[':param' . $index] = $value; 
            $index++;
        }

        $sql = 'INSERT INTO ' . static::getTableName() . '(' . implode(', ', $columns) . ') VALUES (' . implode(', ', $params) . ')';

        $db = Db::getInstance();
        $db->query($sql, $params2values, static::class);
    }

Правильно ли я использую роут?
Routes.php

'~^articles/create$~' => [\MyProject\Controllers\ArticlesController::class, 'create']
ivashkevich

Всё хорошо, при обращении к свойствам и методам объектов не нужно ставить пробелы:

$article->save();
ilyaOrlov

Всё понял) Спасибо

alepawka
 private function insert(array $mappedProperties) : void
        {
            $param2values = [];
            $index = 1;
            foreach ($mappedProperties as $column =>$value) {
                $param = ':param' . $index;
                $columns[] = $column;
                $colums2params[] = $param;
                $param2values[':param' . $index] = $value;
                $index++;
            }
            var_dump($columns);
            $sql = 'INSERT INTO `' . static::getTableName() .'` (' . implode(',', $columns) . ') VALUES (' .  implode(',', $colums2params) . ');';
            var_dump($sql);
            $db = Db::getInstance();
            $db -> query($sql, $param2values, static::class);

        }

//добавил в route.php

'~^articles/(\d+)/create$~' => [\MyProject\Controllers\ArticleController::class, 'create'],
ivashkevich

var_dump в рабочем коде быть не должно - удаляйте перед отправкой на проверку. При обращении к свойствам и методам объектов с помощью стрелочки, её не нужно окружать пробелами.

Самый понятный курс PHP
Онлайн-уроки в удобное время!
Начать бесплатно
Популярное за сутки
Онлайн-курсы PHP и MySQL
Обучение с полного нуля до уровня джуниора!
Начать бесплатно
Сейчас читают
Онлайн-курсы PHP и MySQL
Обучение с полного нуля до уровня джуниора!
Начать бесплатно
Новые статьи
Логические задачи с собеседований