ORM - Object Relational Mapping

01.07.2018 в 14:25
28028
+1018

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 из БД

Как видим, в результате мы получили массив объектов класса 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

И о чудо! Теперь у нас массив объектов класса 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>';
    }
}

Посмотрим на результат:
Результат магического метода __set()

Видим, что этот метод был вызван по два раза для свойств 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() – именно он и занимается преобразованием. Вот что происходит внутри этого метода:

  1. Функция ucwords() делает первые буквы в словах большими, первым аргументом она принимает строку со словами, вторым аргументом – символ-разделитель (то, что стоит между словами). После этого строка string_with_smth преобразуется к виду String_With_Smth
  2. Функция str_replace() заменяет в получившейся строке все символы ‘_’ на пустую строку (то есть она просто убирает их из строки). После этого мы получаем строку StringWithSmth
  3. Функция lcfirst() просто делает первую букву в строке маленькой. В результате получается строка stringWithSmth. И это значение возвращается этим методом.

Таким образом, если мы передадим в этот метод строку «created_at», он вернёт нам строку «createdAt», если передадим «author_id», то он вернёт «authorId». Именно то, что нам нужно!

Так вот в методе __set() я получаю нужное мне имя для свойства объекта из имени, переданного в аргументе $name, а затем задаю в свойство с получившимся именем переданное значение.

Посмотрим теперь на результат:

Преобразование подчеркушек в camelCase

Как видим, теперь наши свойства 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.

loader
01.07.2018 в 14:25
28028
+1018
Комментарии
Этот урок набрал набрал достаточно большое количество комментариев и дальнейшее его комментирование отключено. Если вы хотели убедиться в правильности выполнения ДЗ или у вас возник вопрос по уроку, посмотрите ранее добавленные комментарии, кликнув по кнопке ниже. Скорее всего вы найдете там то, что искали. Если это не помогло - задайте вопрос в чате в телеграме - https://t.me/php_zone
Логические задачи с собеседований