CodeIgniter 4. Работа с базой данных

Работа с базой данных всегда была сильной стороной CodeIgniter. В 4-й версии сохранился практически тот же самый подход, что и в прошлых версиях, хотя есть отличия. В первую очередь стоит отметить, что SQL Query Builder (Active Records в первой и второй версиях) теперь вынесен отдельным классом. Если раньше SQL-запрос строился прямо в объекте базы ($db), то сейчас это нужно делать отдельно.

Основной объект базы теперь содержит не так много методов, наиболее используемый из которых query().

Но самое главное это то, что CodeIgniter 4 поддерживает Named Bindings, что очень сильно его приближает к стандартной работе с PDO.

Для начала работы с базой следует создать базу любым способом, например с помощью phpMyAdmin. После этого нужно указать параметры доступа в конфигурации: файл app/Config/Database.php. Обратите внимание, что доступен параметр «DSN», что указывает на ещё один шаг к полноценному PDO.

Для тестирования мы пока просто создадим таблицу news и наполним её тестовыми данными в ручном режиме. Выполните SQL-запросы:

CREATE TABLE news (
        id int(11) NOT NULL AUTO_INCREMENT,
        title varchar(128) NOT NULL,
        slug varchar(128) NOT NULL,
        body text NOT NULL,
        PRIMARY KEY (id),
        KEY slug (slug)
);

И записи:

INSERT INTO news VALUES
(1,'Elvis sighted','elvis-sighted','Elvis was sighted at the Podunk internet cafe. It looked like he was writing a CodeIgniter app.'),
(2,'Say it isn\'t so!','say-it-isnt-so','Scientists conclude that some programmers have a sense of humor.'),
(3,'Caffeination, Yes!','caffeination-yes','World\'s largest coffee shop open onsite nested coffee shop for staff only.');

Пропишем роутинг:

$routes->get('news', 'News::index');
$routes->get('news/(:segment)', 'News::page/$1');

То есть по адресу http://example.com/news будет выводится список всех записей. А по адресу http://example.com/news/SLUG — уже конкретная запись.

Начнём с модели.

В CodeIgniter для моделей предназначен каталог app/Models. Разместим в нём файл NewsModel.php:

namespace App\Models;
 
use CodeIgniter\Model;
 
class NewsModel extends Model
{
    protected $table = 'news';
    
    public function getNews($slug = false)
    {
        if ($slug === false)
            return $this->findAll();
        else
            return $this->asArray()->where(['slug' => $slug])->first();
    }
}

Основной метод getNews(), который возвращает набор записей: если $slug не указан, то возвращаются все записи, иначе только одна.

Поскольку мы наследуемся от системного класса CodeIgniter\Model, то используем его методы. Если сравнивать с предыдущими версиями фреймворка, системная модель сильно «разжирела»: в ней появилось много методов, полей, которые по задумке разработчиков должны упростить работу с базой.

Теперь делаем контролёр. Файл app/Controllers/News.php:

namespace App\Controllers;
 
use App\Models\NewsModel;
use CodeIgniter\Controller;
 
class News extends Controller
{
    public function index()
    {
        $model = new NewsModel();
        
        $data = [
            'news'  => $model->getNews(),
            'title' => 'All pages',
        ];
        
        echo view('news/header', $data);
        echo view('news/overview', $data);
        echo view('news/footer');
    }
    
    public function page($slug = NULL)
    {
        $model = new NewsModel();
        
        $page = $model->getNews($slug);
        
        if ($page)
        {
            $data['title'] = $page['title'];
            $data['page'] = $page;
        }
        else
        {
            $data['title'] = 'Page not found';
            $data['page']['title'] = 'Page not found';
            $data['page']['body'] = '404...';
        }
        
        echo view('news/header', $data);
        echo view('news/page', $data);
        echo view('news/footer');
    }
}

Методы index() и page() указаны в роутинге. Мы подключаем нашу NewsModel, получаем от неё данные и дальше формируем вывод.

Я специально оставил этот код приближенный к туториалу, поскольку такой подход наиболее часто и встречается при разработке, что создаёт ряд проблем.

Теперь сделаем «вьюшки». Все они в каталоге Views/news:

Файл header.php:

<!doctype html><html>
<head>
    <title><?= esc($title) ?></title>
</head>
<body>

Файл footer.php:

</body></html>

Файл page.php:

<h1><?= $page['title'] ?></h1>
<div><?= $page['body'] ?></div>

Файл overview.php:

<h1><?= $title ?></h1>
<?php if (!empty($news) and is_array($news)) : ?>
    <ul>
    <?php foreach ($news as $news_item): ?>
         <li><a href="<?= 'news/' . $news_item['slug'] ?>"><?= $news_item['title'] ?></a></li>
    <?php endforeach; ?>
    </ul>
<?php else : ?>
    <h3>No News</h3>
    Unable to find any news for you.
<?php endif ?>

Вот, собственно и всё: набираем в браузере http://example.com/news и видим список ссылок записей.

Проблемы данного подхода

Первая проблема здесь в том, что системный класс Модели жестко завязан на базу данных. Это сильно противоречит концепции MVC, где модель должна инкапсулировать всю бизнес-логику, а не только работу с БД. Расширяя такую модель, мы тянем за собой очень много лишнего кода: посмотрите файл system/Model.php.

В методе нашего класса getNews() нет четкого понимания того, как он работает. Алгоритм функции должен быть ясным. Здесь же, вместо прямой работы с базой, используется прослойка через дополнительные методы. Возможно это и уменьшило размер кода, но не внесло в него ясности. Когда алгоритм выборки будет сложней, всё равно придётся использовать класс базы и эта прослойка окажется не нужной.

Что касается контролёра, то в нём модель используется только для получения выборки из базы. Дальше в контролёре выполняется ряд условий, поскольку выборка может оказаться пустой. Данные передаются в шаблонизатор, поэтому следует учесть этот момент. То есть контролёр взял на себя роль настоящей «модели» и от этого «распух». Когда логика простая и мало кода, это не является проблемой, но, как только код усложнится, то это приводит к ТТУК («Толстые, тупые, уродливые контроллеры»). Это фундаментальная ошибка, которая описыват работу с базой в туториале CodeIgniter с очень давних времён, и которая тиражируется большинством разработчиков.

Строго говоря, эта проблема не только в CodeIgniter, но в других php-фреймворках.

Ну и последняя проблема: как я указывал ранее — файлы модуля разбросаны по разным каталогам.

Нормальный вариант

К счастью CodeIgniter теперь позволяет следовать более-менее правильному подходу. Сделаем похожий пример на той же таблице.

Пропишем роутинг:

$routes->get('page', 'App\Modules\Page\Page::index');
$routes->get('page/(:segment)', 'App\Modules\Page\Page::showPage/$1');

Из него понятно, что все файлы модуля будут в одном каталоге app/Modules/Page. А адрес будет http://example.com/page

Начнем с контролёра. Файл app/Modules/Page/Page.php:

namespace App\Modules\Page;
 
class Page extends \CodeIgniter\Controller
{
    protected $view;
    protected $model;
    
    public function __construct()
    {
        $this->view = new View\View();
        $this->model = new Model\Model();
    }
    
    public function index()
    {
        $data = $this->model->getAllPages();
        $this->view->output('show-all', $data);
    }
    
    public function showPage($slug = '')
    {
        $data = $this->model->getPage($slug);
        $this->view->output('show-page', $data);
    }
}

Здесь мы используем свои модель и представление. Полученные данные от модели передаются в представление.

Файл представления app/Modules/Page/View/View.php:

namespace App\Modules\Page\View;
 
class View
{
    private $tmpl;
    
    public function __construct()
    {
        $this->tmpl = new \CodeIgniter\View\View('', APPPATH . 'Modules/Page/Layout/');
    }
    
    public function output($template, $data)
    {
        $this->tmpl->resetData();
        $this->tmpl->setData($data);
        
        echo $this->tmpl->render($template);
    }
}

Здесь мы подключаем системный класс шаблонизатора, где указываем, что файлы для парсинга будут находится в каталоге Layout.

Файл для вывода всех записей app/Modules/Page/Layout/show-all.php:

<!doctype html>
<html><head>
    <title><?= esc($title) ?></title>
</head>
<body>
    <h1><?= $header ?></h1>
    <ul>
    <?php
        foreach($pages as $page) {
            echo '<li><a href="page/' . $page['slug'] . '">' . $page['title'] . '</a></li>';
        }
    ?>
    </ul>
</body>
</html>

И файл для вывода одиночной записи app/Modules/Page/Layout/show-page.php:

<!doctype html>
<html><head>
   <title><?= esc($page['title']) ?></title>
</head>
<body>
   <h1><?= $page['title'] ?></h1>
   <div><?= $page['body'] ?></div>
</body>
</html>

Теперь рассмотрим модель app/Modules/Page/Model/Model.php

namespace App\Modules\Page\Model;
 
class Model
{
    private $db;
    
    public function __construct()
    {
        $this->db = \Config\Database::connect();
    }
    
    public function getAllPages()
    {
        $query = $this->db->query('SELECT * FROM news');
        $pages = $query->getResultArray();
 
        $data['title'] = 'All Pages';
        $data['header'] = 'All Pages';
        $data['pages'] = $pages;
        
        return $data;
    }
    
    public function getPage($slug)
    {
        $query = $this->db->query('SELECT * FROM news WHERE slug = :slug:', ['slug' => $slug]);
        
        /* # либо вариант с SQL-Builder
        $builder = $this->db->table('news');
        $builder->select('*');
        $builder->where('slug', $slug);
        $query = $builder->get();
        */
        
        $page = $query->getRowArray();
        if (empty($page)) {
            $page['title'] = 'Page not found';
            $page['body'] = '404...';
        }
        
        $data['page'] = $page;
        
        return $data;
    }
}

Важное отличие от предыдущего варианта в том, что здесь мы напрямую работаем с базой данных. На выходе уже готовый массив данных, который полностью годен для вывода через шаблонизатор. Логика, которая в предудущем примере была в контролёре, теперь, как и положено расположена в модели.

Запросы к базе выполняются с помощью метода query(). Для примера я разместил код для работы через SQL Builder.

Обратите внимание на то, как CodeIgniter использует именованные параметры в запросе: с обрамлением символом :с двух сторон. Это очень похоже на стандарт PDO, но не является им, поскольку в PDO следует использовать только один символ :с начала.

$sql = 'SELECT * FROM news WHERE slug = :slug';

Почему разработчики фреймворка сделали нестандартное решение, остаётся загадкой.

Комментариев: 3 RSS

1Александр Соловей17-09-2019 10:19

Вопрос не по теме статьи, но по теме Codeigniter 4.

Напишите статью по работе с IncomingRequest Class.

Не совсем понятно в документации.

2MAX17-09-2019 10:36

Подумаю. Но вообще это по сути оболочка для функции parse_url() + пара «плюшек». :-)

3Георгий18-10-2019 10:53

Кстати, любые вопросы можно задавать на форуме самим разработчикам Codeigniter4.

https://forum.codeigniter.com/forum-28.html

Их немного и, как правило, они оперативно отвечают.

Оставьте комментарий!

Комментарий будет опубликован после проверки. Вы соглашаетесь с правилами сайта.

(обязательно)