Обновление с помощью Active Record
В этом уроке мы научимся обновлять данные в базе данных в стиле объектно-ориентированного подхода. Для этого, как вы уже наверное догадались, мы будем использовать всё те же сущности ActiveRecord.
Давайте представим, что у нас есть объект класса Article, который был прочитан из базы данных, и который мы хотим изменить.
Давайте для изменения статей сделаем отдельный роут (^articles/(\d+)/edit$):
src/routes.php
<?php
return [
'~^articles/(\d+)$~' => [\MyProject\Controllers\ArticlesController::class, 'view'],
'~^articles/(\d+)/edit$~' => [\MyProject\Controllers\ArticlesController::class, 'edit'],
'~^$~' => [\MyProject\Controllers\MainController::class, 'main'],
];
Теперь добавим в контроллере новый экшн edit, в котором мы пока просто будем получать статью и выводить её с помощью var_dump();
src/MyProject/Controllers/ArticlesController.php
<?php
namespace MyProject\Controllers;
use MyProject\Models\Articles\Article;
use MyProject\View\View;
class ArticlesController
{
/** @var View */
private $view;
public function __construct()
{
$this->view = new View(__DIR__ . '/../../../templates');
}
public function view(int $articleId): void
{
$article = Article::getById($articleId);
if ($article === null) {
$this->view->renderHtml('errors/404.php', [], 404);
return;
}
$this->view->renderHtml('articles/view.php', [
'article' => $article
]);
}
public function edit(int $articleId): void
{
$article = Article::getById($articleId);
if ($article === null) {
$this->view->renderHtml('errors/404.php', [], 404);
return;
}
var_dump($article);
}
}
Перейдём по новому URL и убедимся, что все работает - http://myproject.loc/articles/1/edit
Теперь, предположим, что мы решили изменить этот объект. Давайте изменим у этого объекта свойства name и text (не забудьте добавить сеттеры для этих полей в классе Article):
src/MyProject/Controllers/ArticlesController.php
public function edit(int $articleId): void
{
/** @var Article $article */
$article = Article::getById($articleId);
if ($article === null) {
$this->view->renderHtml('errors/404.php', [], 404);
return;
}
$article->setName('Новое название статьи');
$article->setText('Новый текст статьи');
var_dump($article);
}
Как видим, свойства у объекта успешно были изменены. Но это никак не повлияло на его состояние в базе данных.
И это логично, ведь мы после этого не выполнили запросов на обновление записи в базе. Давайте реализуем этот механизм.
Давайте создадим метод save() в классе ActiveRecordEntity, который будет сохранять текущее состояние объекта в базе.
Для того, чтобы обновить запись в базе данных, нам нужно выполнить запрос в MySQL:
UPDATE `articles` SET `id`=1,`author_id`=1,`name`='Новое название статьи',`text`='Новый текст статьи',`created_at`='2018-06-26 22:06:21' WHERE id = 1
где во все поля подставить текущие значения у объекта.
Но ведь у разных наследников класса ActiveRecordEntity разные свойства. Вопрос – как их получить, не привязываясь к конкретному классу. Да с помощью рефлексии, которую мы изучили на прошлом занятии!
Алгоритм у нас будет такой:
- Получаем имена свойств объекта с помощью рефлексии, например, authorId
- Преобразовываем это значение из camelCase в строку_с_подчеркушками, например, author_id – именно так называется поле в базе данных
- Составляем результирующий запрос на обновление записи в базе данных.
Итак, давайте теперь сделаем это!
Для начала давайте напишем метод, который будет преобразовывать строки типа authorId в author_id.
Это можно сделать с помощью регулярного выражения: перед каждой заглавной буквой мы добавляем символ подчеркушки «_», а затем приводим все буквы к нижнему регистру:
src/MyProject/Models/ActiveRecordEntity.php
...
private function camelCaseToUnderscore(string $source): string
{
return strtolower(preg_replace('/(?<!^)[A-Z]/', '_$0', $source));
}
Отлично, теперь давайте напишем метод, который прочитает все свойства объекта и создаст массив вида:
[
'название_свойства1' => значение свойства1,
'название_свойства2' => значение свойства2
]
src/MyProject/Models/ActiveRecordEntity.php
...
private function mapPropertiesToDbFormat(): array
{
$reflector = new \ReflectionObject($this);
$properties = $reflector->getProperties();
$mappedProperties = [];
foreach ($properties as $property) {
$propertyName = $property->getName();
$propertyNameAsUnderscore = $this->camelCaseToUnderscore($propertyName);
$mappedProperties[$propertyNameAsUnderscore] = $this->$propertyName;
}
return $mappedProperties;
}
private function camelCaseToUnderscore(string $source): string
{
return strtolower(preg_replace('/(?<!^)[A-Z]/', '_$0', $source));
}
Здесь мы получили все свойства, и затем каждое имяСвойства привели к имя_свойства. После чего в массив $mappedProperties мы стали добавлять элементы с ключами «имя_свойства» и со значениями этих свойств.
Давайте посмотрим, что у нас получилось. Выведем массив, полученный с помощью этого метода в методе save().
public function save(): void
{
$mappedProperties = $this->mapPropertiesToDbFormat();
var_dump($mappedProperties);
}
private function mapPropertiesToDbFormat(): array
{
$reflector = new \ReflectionObject($this);
$properties = $reflector->getProperties();
$mappedProperties = [];
foreach ($properties as $property) {
$propertyName = $property->getName();
$propertyNameAsUnderscore = $this->camelCaseToUnderscore($propertyName);
$mappedProperties[$propertyNameAsUnderscore] = $this->$propertyName;
}
return $mappedProperties;
}
private function camelCaseToUnderscore(string $source): string
{
return strtolower(preg_replace('/(?<!^)[A-Z]/', '_$0', $source));
}
Теперь вызовем этот метод сущности в контроллере:
src/MyProject/Controllers/ArticlesController.php
public function edit(int $articleId): void
{
/** @var Article $article */
$article = Article::getById($articleId);
if ($article === null) {
$this->view->renderHtml('errors/404.php', [], 404);
return;
}
$article->setName('Новое название статьи');
$article->setText('Новый текст статьи');
$article->save();
}
Посмотрим на то, что этот код выдаёт.
Отлично! Мы имеем структуру, которая соответствует структуре в базе данных. Теперь на её основе можно построить запрос!
Но перед этим нам стоит обратить внимание, что метод save() может быть вызван как у объекта, который уже есть в базе данных, так и у нового (если мы создали его с помощью new Article и заполнили ему свойства). Для первого нам нужно будет выполнить UPDATE-запрос, а для второго - INSERT-запрос. Как понять, с каким типом объекта мы работаем? Да всё проще простого – у объекта, которому соответствует запись в базе, свойство id не равно null, а если мы только создали объект, у него свойства id ещё нет.
Поэтому нам нужно разделить логику метода save() для этих двух случаев. Вот что у нас должно получиться:
src/MyProject/Models/ActiveRecordEntity.php
...
public function save(): void
{
$mappedProperties = $this->mapPropertiesToDbFormat();
if ($this->id !== null) {
$this->update($mappedProperties);
} else {
$this->insert($mappedProperties);
}
}
private function update(array $mappedProperties): void
{
//здесь мы обновляем существующую запись в базе
}
private function insert(array $mappedProperties): void
{
//здесь мы создаём новую запись в базе
}
...
В этом уроке мы напишем реализацию только одного метода – update(). Синтаксис запроса выглядит следующим образом:
UPDATE table_name
SET column1 = :param1, column2 = :param2, ...
WHERE condition;
После этого нам нужно подставить в запрос параметры:
:param1 = value1
:param2 = value2
У нас есть массив column1 = value1, column2 = value2. Всё что нам нужно – разделить его на 2 массива:
- будет содержать строки: column1 = :param1
- будет содержать ключ => значение вида: [:param1 => value1]
и собрать из этих частей готовый запрос!
Чтобы было проще понимать, что происходит, будем делать это поэтапно. Для начала напишем код, который будет создавать два этих массива:
private function update(array $mappedProperties): void
{
$columns2params = [];
$params2values = [];
$index = 1;
foreach ($mappedProperties as $column => $value) {
$param = ':param' . $index; // :param1
$columns2params[] = $column . ' = ' . $param; // column1 = :param1
$params2values[$param] = $value; // [:param1 => value1]
$index++;
}
var_dump($columns2params);
var_dump($params2values);
}
После этого мы получим следующий результат:
Теперь дело за малым – сформировать запрос:
private function update(array $mappedProperties): void
{
$columns2params = [];
$params2values = [];
$index = 1;
foreach ($mappedProperties as $column => $value) {
$param = ':param' . $index; // :param1
$columns2params[] = $column . ' = ' . $param; // column1 = :param1
$params2values[$param] = $value; // [:param1 => value1]
$index++;
}
$sql = 'UPDATE ' . static::getTableName() . ' SET ' . implode(', ', $columns2params) . ' WHERE id = ' . $this->id;
var_dump($sql);
var_dump($params2values);
}
Остаётся только выполнить этот запрос и передать нужные параметры!
private function update(array $mappedProperties): void
{
$columns2params = [];
$params2values = [];
$index = 1;
foreach ($mappedProperties as $column => $value) {
$param = ':param' . $index; // :param1
$columns2params[] = $column . ' = ' . $param; // column1 = :param1
$params2values[$param] = $value; // [:param1 => value1]
$index++;
}
$sql = 'UPDATE ' . static::getTableName() . ' SET ' . implode(', ', $columns2params) . ' WHERE id = ' . $this->id;
$db = Db::getInstance();
$db->query($sql, $params2values, static::class);
}
Сейчас этот скрипт ничего не выведет, но если мы зайдём в базу данных, мы обнаружим, что запись, соответствующая нашей статье – изменилась!
Давайте теперь попробуем посмотреть нашу обновленную статью, перейдя по адресу http://myproject.loc/articles/1
Итак, в этом уроке мы создали универсальный метод, который позволит обновлять записи в бд для любых объектов, являющимися наследниками класса ActiveRecordEntity.
Актуальная версия проекта на гитхабе.
Комментарии