Использование ORM для работы с БД

Перейдём к рассмотрению вопроса работы с базой данных. В настоящее время нет необходимости писать низкоуровневые SQL-CRUD-запросы для работы с сущностями базы данных, для этих целей созданы ORM (Object-Relational Mapping) обёртки, которые позволяют делать это легко и удобно в стиле ООП. Хотя все рассматриваемые нами фреймворки (а именно: Yii, Symfony и Laravel) не ограничивают нас в средствах работы с базой данных (то есть, если у нас сложная структура БД или нам нравится на каждое действие писать чистый SQL, то мы можем это делать беспрепятственно), однако имеют в своих арсеналах конкретные реализации ORM, которые заметно облегчают взаимодействие с БД.

ActiveRecord в Yii

Чтобы получить данные из таблицы company, нам требуется лишь создать файл common/models/Company.php и поместить в него следующий код:

<?php

namespace common\models;

use yii\db\ActiveRecord;

class Company extends ActiveRecord
{
}

Это достаточно, чтобы получить доступ к данным и уже начать их использовать, но так как мы создали таблицу во множественном числе: companies, а фреймворк будет искать по умолчанию таблицу company, то нам нужно переопределить метод tableName():

<?php
...
class Company extends ActiveRecord
{
    public static function tableName(): string
    {
        return '{{%companies}}';
    }
}

Если обернуть название таблицы в двойные фигурные скобки, то это поможет предотвратить возможные коллизии с именем таблицы при формировании SQL запросов. Если перед названием таблицы поставить знак процента %, то вместо него в запросе автоматически будет подставлен префикс для таблиц (если он указан в настройках приложения).

Класс модели, как можно было заметить, мы поместили не в папку frontend, а в папку common, так как данный класс является «общим», то есть может быть использован не только для пользовательской части нашего приложения, но в будущем и для админ-части.

Чтобы класс был идентифицирован фреймворком, как класс-модель, нужно чтобы он наследовался от yii\db\ActiveRecord.

Теперь можно использовать данный класс для получения данных в контроллере:

<?php
...
use common\models\Company;

class CompanyController extends Controller
{
    /**
     * Displays list of companies.
     *
     * @return string
     */
    public function actionIndex(): string
    {
        $companies = Company::find()->orderBy('name')->all();
	$result = [];
		
	foreach ($companies as $company) {
		$result[] = 'ID: ' . $company->id . '. Company name: ' . $company->name;
	}
		
	return implode("<br>", $result);
    }
}

Хорошим правилом при создании классов-моделей является указание списка полей, имеющихся в таблице, в виде phpDoc-комментария для класса.

<?php
...
/**
 * Company model
 *
 * @property integer $id
 * @property string $name
 * @property string $description
 * @property timestamp $created_at
 * @property timestamp $updated_at
 */
class Company extends ActiveRecord
{
...
}

Осталось создать аналогичные классы для остальных сущностей. Перейдём к рассмотрению ORM, которая идёт по-умолчанию с Symfony.

Doctrine  в Symfony

В качестве ORM фреймворк Symfony по-умолчанию предлагает использовать Doctrine. Однако не навязывает её использование, можно вовсе обойтись без неё, а можно использовать какую-либо иную ORM, например, Propel. Но в рамках приложения FunnyApp мы будем использовать именно Doctrine.

В Doctrine есть такой инструмент, как reverse engineering, который позволяет нам сгенерировать классы-сущности по готовой структуре базы данных.

Для этого у нас уже должна быть создана структура базы данных, должны быть заданы все необходимые связи между таблицами, а также, конечно же, должно быть настроено соединение с нашей БД. После этого в папке проекта нужно выполнить 3 консольные команды:

php bin/console doctrine:mapping:import --force AppBundle yml
php bin/console doctrine:mapping:convert annotation ./src
php bin/console doctrine:generate:entities AppBundle

Первая команда сгенерирует нам yml файлы, которые будут описывать структуру каждой таблицы и её связи. Вторая команда должна сгенерировать классы-сущности… Однако в рамках той структуры приложения, которая у нас сейчас есть, мы получаем: No Metadata Classes to process. В чём проблема? Может надо не ./src, а полный путь указать до сгенерированных файлов? Нет, не помогает… Можно погуглить, поискать на Stack Overflow, но мне ничего так и не помогло… Пока не заметил я одну деталь: по-умолчанию Symfony сгенерировала нам AppBundle, который положила сразу в ./src, однако, если посмотреть повнимательнее на то, что написано в документации по reverse engineering, то можно заметить, что в консольных командах используется «полное» название бандла: AcmeBlogBundle, а у нас «обрезанный» бандл без префикса (в данном случае Acme), который, как правило, указывает на вендора. Как оказалось, именно из-за этого вторая команда и не выполняется корректно, то есть скрипт не находит файлы, из которых надо делать сущности.

Как сгенерировать и зарегистрировать в системе новый бандл можно прочитать в документации:
http://symfony.com/doc/current/bundles.html

А мы подкорректируем наш «обрезанный» бандл и поместим его в папку Funny. Таким образом наш бандл будет лежать в папке src/Funny/AppBundle и будет иметь пространство имён:

namespace Funny\AppBundle;

Помимо того, что нам нужно переименовать корневой файл бандла src/Funny/AppBundle/AppBundle.php в FunnyAppBundle.php, а также переименовать внутри и название самого класса и поправить неймспейс, не забудем внести поправки в контроллерах бандла, в конфигурации роутов (теперь для идентификации экшена нужно использовать «полное» название бандла FunnyAppBundle) а также поправить код регистрации бандла в app/AppKernel.php.

Итак, удаляем папку src/Funny/AppBundle/Resources, если мы её ещё не удалили и запускаем команды:

php bin/console doctrine:mapping:import --force FunnyAppBundle yml
php bin/console doctrine:mapping:convert annotation ./src
php bin/console doctrine:generate:entities FunnyAppBundle

Итак, первая команда создаёт yml файлы, описывающие структуру таблиц и их связи в базе данных, вторая команда создаёт классы-сущности, согласно данным из yml файлов, а третья команда создаёт getter’ы и setter’ы для полей наших сущностей.

Теперь можно в контроллере получить данные из базы и отобразить их пользователю. В качестве примера выведем список компаний в экшене FunnyAppBundle:Company:list. Для этого нам потребуется файл src/Funny/AppBundle/Controller/CompanyController.php.

<?php
...
class CompanyController extends Controller
{
    /**
     * Displays list of companies.
     *
     * @return Response
     */
     public function listAction(): Response
     {
        $repository = $this->getDoctrine()->getRepository('FunnyAppBundle:Companies');
	$companies = $repository->findAll();
		
	foreach ($companies as $company) {
		$result[] = 'ID: ' . $company->getId() . '. Company name: ' . $company->getName();
	}
		
	return new Response(implode("<br>", $result));
     }
...
}

Получить все записи из таблицы companies достаточно просто. Сначала получаем объект Doctrine, на котором вызываем метод получения репозитория, который отвечает за таблицу companies, а затем просто вызываем findAll(). Вот и всё, можно пройтись циклом по всем записям и сформировать нужную строку для вывода пользователю. Отметим один момент: в Yii мы обращаемся непосредственно к полям объекта, а тут мы получаем данные через геттеры.

Когда я открыл результат в браузере, оказалось, что вместо русских символов в названиях компаний отображаются «ромбики», что говорит нам о проблемах с кодировкой. Проблема решилась добавлением строк:

options:
    1002: "SET NAMES 'UTF8' COLLATE 'utf8_unicode_ci'"

в настройки подключения к базе в файл конфигурации app/config/config.yml.

doctrine:
    dbal:
        driver:   pdo_mysql
        host:     "%database_host%"
        port:     "%database_port%"
        dbname:   "%database_name%"
        user:     "%database_user%"
        password: "%database_password%"
        charset: UTF8
        options:
            1002: "SET NAMES 'UTF8' COLLATE 'utf8_general_ci'" 

Eloquent в Laravel

Данный фреймворк предлагает «из коробки» использовать ORM Eloquent, которая представляет собой реализацию паттерна ActiveRecord, то есть нам нужно лишь создать класс-модель, который будет наследоваться от Illuminate\Database\Eloquent\Model и можно уже использовать его для получения данных из базы.

Для облегчения создания модели можно воспользоваться консольной командой:

php artisan make:model Models/Company

где Models — это папка, в которой будут храниться наши модели, а Company — название самой модели, которая будет связана с таблицей companies. В итоге получим файл app/Models/Company.php с содержимым:

<?php
namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Company extends Model
{
    //
}

По умолчанию действует соглашение, что названию модели в единственном числе Company, соответствует таблица в базе во множественном числе companies. В случае, если название модели состоит из нескольких слов, например, PortfolioItem, то данной модели будет соответствовать таблица в базе, записанная в стиле «snake case» во множественном числе, то есть portfolio_items.

Теперь можно использовать данную модель для получения данных в контроллере app/Http/Controllers/CompanyController.php:

<?php

namespace App\Http\Controllers;

use Illuminate\Routing\Controller;
use App\Models\Company;

class CompanyController extends Controller
{
    /**
     * Displays list of companies.
     *
     * @return string
     */
     public function index(): string
     {
        $companies = Company::all();
	$result = [];
		
	foreach ($companies as $company) {
	    $result[] = 'ID: ' . $company->id . '. Company name: ' . $company->name;
	}
		
	return implode("<br>", $result);
     }
...
}