CodeIgniter 4. Работа с базой данных
31-08-2019Reading time ~ 7 min.CodeIgniter 13323
Работа с базой данных всегда была сильной стороной 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 и так не найти...
Спасибо за ответ. Очень понятный и всеобъемлящий.