Данный материал продолжает цикл Разрабатываем сайт. Он полностью опирается на предыдущие наработки, поэтому нужно обязательно прочитать вышеуказанный цикл, если вы этого не делали.
Код для этого выпуска лежит на github в ветке relation.
Итак, после завершения первичной разработки сайта у нас есть база данных MySQL и сайт на Apache + PHP, с помощью которого мы можем наполнять базу и читать из неё данные.
Хотя мы успешно пользуемся базой данных MySQL для хранения заметок, на самом деле мы используем её не по назначению, так как и MySQL, и другие базы на основе SQL – реляционные. Что это значит, мы сейчас рассмотрим.
Апгрейд заметок
Напомню, что наши данные – это заметки, которые имеют несколько полей: дату создания, заголовок и тело.
Предположим, мы решили в структуру заметки добавить ещё одно поле – категорию. Теперь каждая заметка может иметь свою категорию (автомобили, компьютеры, кино и т.п.)
Я в двух словах опишу, какие изменения надо сделать, но делать их по-настоящему мы не будем, так как это неправильное решение.
Очевидно, что для хранения категории нужно добавить в таблицу note ещё одно поле, назовём его category. Оно будет типа "строка".
Далее, мы меняем HTML-форму редактирования заметки, добавляя туда элемент <input type="text" name="category">. Теперь мы можем ввести категорию при редактировании и сохранить её.
И хотя эта схема вполне работоспособна, мы сразу сталкиваемся с некоторыми неприятностями.
1. Ошибки в названиях
Допустим, мы добавили заметку и указали для неё категорию "ретро-игры". Через некоторое время мы добавили ещё одну заметку и указали категорию "ретро игры".
Что произошло? Мы хотели в общем-то назначить одну и ту же категорию двум заметкам, но из-за невнимательности или забывчивости ввели две разные категории. Эти строки отличаются всего лишь одним знаком, но для компьютера это, конечно, совершенно разные значения.
Можно делать и другие ошибки: например, в одном месте пишем "программирование с нуля", а в другом "программирование для начинающих". Понятно, что мы имели в виду одно и то же, просто не придерживались системы в названиях.
2. Проблема переименования
Предположим, мы добавили 100 заметок с категорией "собаки", а потом поняли, что категория должна называться "собаководство". Значит, нам придётся открыть каждую из 100 заметок и исправить в ней категорию. Есть способ полегче: написать запрос к базе данных, который изменит категории всех записей, где указана категория "собаки", на "собаководство". Но мы же не для того строили сайт и делали пользовательские интерфейсы, чтобы руками писать запросы к базе?
3. Проблема хранения
Так как мы добавили текстовое поле в таблицу note, очевидно, что размер записи стал больше. В нашем случае это не существенно, но в целом надо представлять себе, что будет при масштабировании: несколько лишних байт умножить на миллион записей это уже несколько лишних мегабайт. Да, всё ещё несущественно, но важен принцип, плюс существуют такие проблемы оптимизации, которые мы ещё не проходили.
Реляция – это отношение
Для решения вышеописанных проблем мы поступаем так:
Создаём отдельную таблицу category. В ней будут следующие поля:
- id – уникальный идентификатор категории, целое число с автоинкрементом.
- title – наименование категории, строка
Далее пишем CRUD-контроллер, создаём представления и модель для категорий, то есть банально копируем всё то, что было сделано для заметок, поменяв названия файлов и полей в нескольких местах.
Отлично, теперь мы можем редактировать заметки, и также можем редактировать категории. Но как назначать категории заметкам?
Модифицируем таблицу note. Добавим в неё поле category_id, это целое число, которое соответствует id категории из таблицы category.
Чтобы назначить категорию заметке, мы записываем в category_id идентификатор категории.
Что это нам даёт:
- Мы не можем ошибиться в наименовании категории
- Нам не нужно каждый раз заново набирать наименование категории
- Мы можем переименовать категорию, и при этом не нужно редактировать заметки
- Мы сэкономили место в базе – вместо строки храним целое число
Мы создали реляцию, или отношение. Категории хранятся отдельно от заметок, но между категориями и заметками существует отношение, заданное с помощью category_id.
С точки зрения категории это отношение называется "один ко многим", то есть одна категория может относиться ко многим заметкам. А с точки зрения заметки это отношение "один к одному", так как одна заметка может иметь только одну категорию. Почему бы и не много? Обсудим потом.
Отношения есть везде
Реляционность используется очень широко. В программировании, например, когда вы свойству одного объекта присваиваете другой объект:
$a = new A();
$b = new B();
$b->child = $a;
Каждый объект имеет уникальный идентификатор: это его адрес в памяти. Вот этот адрес, или ссылку на объект, вы и присваиваете свойству child объекта $b. Тем самым устанавливая отношение между объектами $a и $b. Вы можете поменять какие-то данные в объекте $a, и чтобы их актуализировать в $b->child, там не нужно ничего менять – это просто ссылка, и она всегда показывает на актуальный объект $a.
Когда вы читаете текст закона и в нём встречаете ссылку на другой закон – это тоже отношение, где уникальным id является номер закона и статьи. В общем, вокруг нас существует большой и удивительный мир отношений.
Реализуем форму с выбором категории
Достаточно лирики. У нас осталась одна нерешённая проблема. Мы наладили отношения между таблицами, но как конкретно мы будем присваивать заметке category_id? Вот, допустим, форма редактирования заметки:
У неё есть поле "Категория", которое можно заполнить. Но заполнять его надо идентификатором, то есть числом, так как это id категории.
Что, естественно, чудовищно неудобно. Мы должны знать, у какой категории какой id. Допустим, мы хотим присвоить заметке категорию "автомобили". Мы идём в базу, смотрим, какой id у категории "автомобили" (например, 15), и в поле категории вводим число 15. Ну так, конечно, нельзя поступать с людьми.
Решение в том, чтобы дать пользователю готовый список категорий. Тогда ему не надо ничего вводить руками, а только выбрать категорию из списка.
Делаем выпадающий список
Для того, чтобы в HTML отобразить выпадающий список, мы используем элемент <select>:
<select name="category_id">...</select>
Каждая позиция в списке задаётся элементом <option>:
Выглядеть это будет так:
Ну собственно вы знаете, как выглядит выпадающий список. Работает же он так: всякий раз в выпадающем списке выбран один из элементов <option>. У каждого <option> есть атрибут value. В него мы помещаем id категории. То есть внешне на странице мы видим названия категорий, а по факту выбираем не название, а id. И именно выбранный id будет отправлен на сервер, когда мы нажмём кнопку "Сохранить".
Формирование списка
Сейчас список категорий задан вручную. Но нам нужно брать категории из базы и формировать список динамически.
Для этого, когда мы передаём модель заметки в представление формы, нужно дополнительно получить из базы список категорий и передать его тоже. А уже в представлении просто перебрать его циклом и сгенерировать элементы <option>.
Поэтому в контроллере заметок (controllers/note.php) дописываем функцию для получения списка категорий:
И далее, в функции update(), где мы вызываем render(), нужно передать в render() список категорий, чтобы представление могло им воспользоваться... Но не тут-то было. Для передачи в render() доступен только один параметр ($data), и он уже занят – это модель заметки:
function render($container, $view, $data)
Что делать?
Мы можем просто расширить сигнатуру функции render(), чтобы передавать туда ещё один параметр:
function render($container, $view, $data, $data2)
Но мы уже должны видеть будущие проблемы. А что, если потом параметров потребуется не два, а больше? А что, если количество параметров в разных вызовах будет вообще разное?
Мы можем решить этот вопрос, немного изменив логику представления. Получив параметр $data, оно будет считать его не единичной моделью, а массивом, в котором хранятся все необходимые данные. Очевидно, что в массив мы можем запихать сколько угодно параметров, например так:
['model' => $model, 'categories' => $categories]
Представление будет обращаться не просто к $data, а к $data['model'], если ему нужна модель, или к $data['categories'], если нужны категории.
И это замечательно, только такое обращение чисто стилистически громоздко.
Экстракция данных
В PHP есть интересная функция extract(). Если в неё передать массив из пар "ключ – значение", то она создаст переменные с именами как у ключей, и со значениями как у... да, значений.
Например, вот такой массив:
$data = ['a' => 1, 'b' => 2];
Если передать его в extract():
extract($data);
То в программе возникнут переменные $a и $b, которые равны 1 и 2. После чего мы можем ими пользоваться:
$c = $a + $b;
Это то, что нам надо, чтобы представление обращалось не к громоздкому массиву, а к простым именам переменных.
Мы передадим в render() в качестве переменной $data такой массив:
['model' => html_convert($model), 'categories' => $categories]
A render() сразу же сделает extract($data), и переменные $model и $categories станут доступны представлению:
Меняем код представления (views/note/form.php): вместо переменной $data теперь используем переменную $model. Ну а добравшись до генерации списка категорий, используем переменную $categories:
Всё, теперь мы можем видеть список категорий, когда редактируем заметку. Конечно, чтобы его увидеть, нужно сначала в базу добавить несколько категорий.
Остались ещё две проблемы: повторное редактирование и просмотр. А также: что будет, если удалить категорию? Их мы будем решать в следущем выпуске.
Я привел здесь фрагменты кода контроллера и представлений. Полный код вы найдёте на гитхабе. Я также добавил в проект файл cat.sql, в котором содержатся актуальные команды для создания таблиц.
Читайте дальше: