ORM - Object Relational Mapping
ORM или Object-Relational Mapping (объектно-реляционное отображение) — технология программирования, которая связывает базы данных с концепциями объектно-ориентированных языков программирования.
Иными словами, это такой принцип, согласно которому мы работаем с базой данных в коде на уровне объектов. То есть структура и данные в базе данных будут иметь отражение в наших объектах в коде. И при работе с этими нашими объектами, мы будем изменять нашу базу данных, и наоборот. Вот как мы это будем делать:
- таблицам будут соответствовать отдельные классы. Например, таблице articles будет соответствовать класс Article.
- в классах будут описаны свойства объектов. Каждое свойство будет соответствовать полю в таблице. Например, будет свойство ->authorId, оно будет соответствовать столбцу author_id в таблице articles.
- мы будем работать с объектами таких классов. Каждый такой объект соответствует одной записи в базе данных. То есть объект класса Article будет соответствовать одной строке в таблице articles.
Как видите, суть ORM крайне проста - объекты имеют своё отражение в базе данных. При этом в коде происходит работа на уровне объектов – вот так правильно делать это при объектно-ориентированном подходе.
Реализуем свою ORM
В течение этого курса мы с вами разработаем свою собственную ORM-систему – она будет позволять получать записи из базы данных в виде объектов, а также сохранять «объекты» в базу данных. В этом уроке мы сделаем наиболее простую часть этого функционала – научимся «читать объекты» из базы данных.
Первым делом давайте отредактируем наш класс Article, представляющий собой статью. В базе данных у нас есть следующие поля: id, name, text, author_id, created_at. Давайте сделаем в нашем классе свойства объектов, которые будут соответствовать этим полям.
src/MyProject/Models/Articles/Article.php
<?php
namespace MyProject\Models\Articles;
use MyProject\Models\Users\User;
class Article
{
/** @var int */
private $id;
/** @var string */
private $name;
/** @var string */
private $text;
/** @var int */
private $authorId;
/** @var string */
private $createdAt;
}
Теперь нужно каким-то образом при получении статей из базы данных создать объекты этого класса и заполнить их свойства значениями из базы данных. Для этого в PDO есть специальный режим. Всё что нужно сделать – это указать класс, объекты которого нужно создать. Давайте откроем класс Db и изменим метод query() следующим образом:
src/MyProject/Services/Db.php
public function query(string $sql, array $params = [], string $className = 'stdClass'): ?array
{
$sth = $this->pdo->prepare($sql);
$result = $sth->execute($params);
if (false === $result) {
return null;
}
return $sth->fetchAll(\PDO::FETCH_CLASS, $className);
}
Третьим аргументом в этот метод будет передаваться имя класса, объекты которого нужно создавать. По умолчанию это будут объекты класса stdClass – это такой встроенный класс в PHP, у которого нет никаких свойств и методов.
В метод fetchAll() мы передали специальную константу - \PDO::FETCH_CLASS, она говорит о том, что нужно вернуть результат в виде объектов какого-то класса. Второй аргумент – это имя класса, которое мы можем передать в метод query().
Теперь зайдём в наш контроллер MainController и сделаем вывод результата запроса с помощью var_dump().
src/MyProject/Controllers/MainController.php
<?php
namespace MyProject\Controllers;
use MyProject\Services\Db;
use MyProject\View\View;
class MainController
{
/** @var View */
private $view;
/** @var Db */
private $db;
public function __construct()
{
$this->view = new View(__DIR__ . '/../../../templates');
$this->db = new Db();
}
public function main()
{
$articles = $this->db->query('SELECT * FROM `articles`;');
var_dump($articles);
return;
$this->view->renderHtml('main/main.php', ['articles' => $articles]);
}
}
Как видим, в результате мы получили массив объектов класса stdClass, у которых есть public-свойства, соответствующие именам столбцов в базе данных. В PHP мы можем задавать свойства объектов на лету, даже если они не были определены в классе. Это называется динамическим объявлением свойств. Если свойства у объекта нет, но мы попытаемся его задать – будет создано новое публичное свойство.
Давайте теперь попробуем в качестве класса передать имя класса Article:
src/MyProject/Controllers/MainController.php
<?php
namespace MyProject\Controllers;
use MyProject\Models\Articles\Article;
use MyProject\Services\Db;
use MyProject\View\View;
class MainController
{
/** @var View */
private $view;
/** @var Db */
private $db;
public function __construct()
{
$this->view = new View(__DIR__ . '/../../../templates');
$this->db = new Db();
}
public function main()
{
$articles = $this->db->query('SELECT * FROM `articles`;', [], Article::class);
var_dump($articles);
return;
$this->view->renderHtml('main/main.php', ['articles' => $articles]);
}
}
И о чудо! Теперь у нас массив объектов класса Article! Однако, есть проблема. Свойства объектов ->authorId и ->createdAt остались со значениями null, но при этом у нас динамически добавилось два публичный свойства ->author_id и ->created_at. Так произошло из-за несоответствия имён столбцов в базе данных и свойств объектов класса Article.
Магический метод __set()
Эту проблему с несоответствием имён легко решить с помощью магического метода __set($name, $value) – если этот метод добавить в класс и попытаться задать ему несуществующее свойство, то вместо динамического добавления такого свойства, будет вызван этот метод. При этом в первый аргумент $name, попадёт имя свойства, а во второй аргумент $value – его значение. А внутри этого метода мы уже сможем решить, что с этими данными делать.
В качестве примера давайте добавим в класс Article этот метод. Всё, что он будет делать – это говорить о том, что он был вызван и какие аргументы были в него переданы.
src/MyProject/Models/Articles/Article.php
<?php
namespace MyProject\Models\Articles;
use MyProject\Models\Users\User;
class Article
{
/** @var int */
private $id;
/** @var string */
private $name;
/** @var string */
private $text;
/** @var int */
private $authorId;
/** @var string */
private $createdAt;
public function __set($name, $value)
{
echo 'Пытаюсь задать для свойства ' . $name . ' значение ' . $value . '<br>';
}
}
Видим, что этот метод был вызван по два раза для свойств author_id и created_at. Обратите внимание – мы только выводили сообщения на экран, больше мы ничего с этими данными не сделали. Поэтому теперь в самих объектах класса Article этих свойств нет.
Ещё раз о том, что же произошло. В тот момент, когда наш код с помощью PDO пытался сделать $this->created_at = что-то, вызывался метод __set() и просто выводил сообщение на экран. Давайте теперь в этом методе сделаем так, чтобы свойства снова устанавливались. Сделать это проще простого:
public function __set($name, $value)
{
echo 'Пытаюсь задать для свойства ' . $name . ' значение ' . $value . '<br>';
$this->$name = $value;
}
$name – имя свойства, $value – его значение. Ничего сложного. Давайте снова запустим код:
Видим, что эти свойства снова появились.
А теперь мы можем внутри этого метода сделать так, чтобы задавалось свойство с именем не $name, а какое-нибудь другое. Скажем, если туда передаётся $name равное ‘author_id’, то чтобы оно преобразовывалось в ‘authorId’ и мы задавали уже нужное свойство класса. Итак, задача – преобразовать строки вида string_with_smth в stringWithSmth.
Я сделал это вот так:
<?php
namespace MyProject\Models\Articles;
use MyProject\Models\Users\User;
class Article
{
/** @var int */
private $id;
/** @var string */
private $name;
/** @var string */
private $text;
/** @var int */
private $authorId;
/** @var string */
private $createdAt;
public function __set($name, $value)
{
$camelCaseName = $this->underscoreToCamelCase($name);
$this->$camelCaseName = $value;
}
private function underscoreToCamelCase(string $source): string
{
return lcfirst(str_replace('_', '', ucwords($source, '_')));
}
}
Я добавил специальный метод underscoreToCamelCase() – именно он и занимается преобразованием. Вот что происходит внутри этого метода:
- Функция ucwords() делает первые буквы в словах большими, первым аргументом она принимает строку со словами, вторым аргументом – символ-разделитель (то, что стоит между словами). После этого строка string_with_smth преобразуется к виду String_With_Smth
- Функция str_replace() заменяет в получившейся строке все символы ‘_’ на пустую строку (то есть она просто убирает их из строки). После этого мы получаем строку StringWithSmth
- Функция lcfirst() просто делает первую букву в строке маленькой. В результате получается строка stringWithSmth. И это значение возвращается этим методом.
Таким образом, если мы передадим в этот метод строку «created_at», он вернёт нам строку «createdAt», если передадим «author_id», то он вернёт «authorId». Именно то, что нам нужно!
Так вот в методе __set() я получаю нужное мне имя для свойства объекта из имени, переданного в аргументе $name, а затем задаю в свойство с получившимся именем переданное значение.
Посмотрим теперь на результат:
Как видим, теперь наши свойства authorId и createdAt у объектов имеют нужные значения.
Давайте сделаем геттеры для свойств id, name и text:
src/MyProject/Models/Articles/Article.php
<?php
namespace MyProject\Models\Articles;
use MyProject\Models\Users\User;
class Article
{
/** @var int */
private $id;
/** @var string */
private $name;
/** @var string */
private $text;
/** @var int */
private $authorId;
/** @var string */
private $createdAt;
public function __set($name, $value)
{
$camelCaseName = $this->underscoreToCamelCase($name);
$this->$camelCaseName = $value;
}
/**
* @return int
*/
public function getId(): int
{
return $this->id;
}
/**
* @return string
*/
public function getName(): string
{
return $this->name;
}
/**
* @return string
*/
public function getText(): string
{
return $this->text;
}
private function underscoreToCamelCase(string $source): string
{
return lcfirst(str_replace('_', '', ucwords($source, '_')));
}
}
Теперь мы можем работать с этими объектами в коде. Например – обращаться к геттерам в шаблонах.
templates/main/main.php
<?php include __DIR__ . '/../header.php'; ?>
<?php foreach ($articles as $article): ?>
<h2><a href="/articles/<?= $article->getId() ?>"><?= $article->getName() ?></a></h2>
<p><?= $article->getText() ?></p>
<hr>
<?php endforeach; ?>
<?php include __DIR__ . '/../footer.php'; ?>
И теперь снова начнём передавать наши статьи во View внутри контроллера:
src/MyProject/Controllers/MainController.php
<?php
namespace MyProject\Controllers;
use MyProject\Models\Articles\Article;
use MyProject\Services\Db;
use MyProject\View\View;
class MainController
{
/** @var View */
private $view;
/** @var Db */
private $db;
public function __construct()
{
$this->view = new View(__DIR__ . '/../../../templates');
$this->db = new Db();
}
public function main()
{
$articles = $this->db->query('SELECT * FROM `articles`;', [], Article::class);
$this->view->renderHtml('main/main.php', ['articles' => $articles]);
}
}
Всё прекрасно работает. А в следующих уроках мы с вами научимся добавлять объекты в базу данных.
Текущая версия проекта на github.
Комментарии