CodeIgniter 4. Работа с базой данных
31-08-2019Время чтения ~ 7 мин.CodeIgniter 14213
Работа с базой данных всегда была сильной стороной 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>
<p>Unable to find any news for you.</p>
<?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';
Почему разработчики фреймворка сделали нестандартное решение, остаётся загадкой.
Вопрос не по теме статьи, но по теме Codeigniter 4.
Напишите статью по работе с IncomingRequest Class.
Не совсем понятно в документации.
Подумаю. Но вообще это по сути оболочка для функции parse_url() + пара «плюшек». :-)
Кстати, любые вопросы можно задавать на форуме самим разработчикам Codeigniter4.
https://forum.codeigniter.com/forum-28.html
Их немного и, как правило, они оперативно отвечают.
А когда будет продолжение ? Я статью прочитал сразу как Вы её опубликовали. И вот уже больше года жду продолжения. Думаю и н6е я один. Расчитываете ли вообще продолжать ? Хорошего материала по CodeIgniter и так не найти...
Спасибо за ответ. Очень понятный и всеобъемлящий.