27 февраля 2010, 05:15 (5397 дней назад, №8784)Об универсальной CMS "Engine"
Примерно десять лет назад мной была начата разработка веб-движка, как сейчас бы сказали - CMS. Изначально, он предназначался всего лишь для автоматической вёрстки HTML (картинки + текст) в несколько "газетных" полос, одинаковой высоты. Поскольку я тогда время от времени делал на заказ различные сайты, постепенно этот проект стал развиваться. Задачи возникали весьма разнообразные, поэтому архитектура движка (под рабочим названием "Engine") была задумана универсальной, позволяющей:
1) реализовать любой сайт, не переписывая движок 2) добавлять на ходу новые возможности, которые изначально не предполагались. Ценой, на которую я осмысленно пошёл, стало увеличение нагрузки на сервер, что на практике ни разу не стало существенной проблемой.
Движок реализован на PHP/MySQL.. Главным понятием в системе являются т.н. "объекты". Это название не связано с ООП (до определенного момента весь код вообще был чисто процедурным). Объектами является всё, что может хранится в базе и меняться. Это, к примеру, страницы сайта, разделы/темы/сообщения форума, личные сообщения, файлы, категории, опросы, задачи, пользователи, ..., и (особо подчеркну): связи между объектами, также являющиеся объектами.
С точки зрения структуры базы, центральной является таблица objects. В ней регистрируются все объекты (функцией RegisterObject), каждому присваивается уникальный uid. При регистрации, и в ходе работы, каждый объект имеет ряд характеристик: тип, класс, даты создания/изменения/чтения, уровни доступа для различных операций, имя шаблона для вывода, пользователь, который создал и последним изменял (что позволяет в любой момент узнать, что и как происходило в системе, пусть даже год назад) и т.д.
Тип объекта определяет, в какой таблице хранится его содержательная часть - данные.
Возьмём конкретный пример. Наиболее распространённым типом объекта является "text". У этого типа существует ряд "подтипов", называемых "класс". Это, к примеру, news (новость в ленте новостей), doc (страница) , forum_message (сообщение в форум), forum_topic (тема форума), forum_section (раздел форума), privmsg (личное сообщение). Логика здесь простая - все перечисленные сущности очень близки по смыслу, а следовательно методы работы с ними - похожи.
Допустим, мы хотим представить тему форума и в ней пару сообщений.
В таблице objects для этого у нас будет как минимум три объекта - один text:forum_topic и два text:forum_message
Кроме того, в таблице texts будет тоже три объекта, с теми же uid. Там будут храниться заголовки и текст.
Соответственно, SELECT * FROM objects, texts WHERE objects.uid = texts.uid AND objects.uid = $obj_uid даст нам полную информацию об объекте.
СВЯЗИ
Но, нам нужно не просто хранить эти три объекта. Два из них (сообщения) должны быть связаны с третьим (темой). Для этого существуют связи (устанавливаются функцией LinkObjects). Любые связи хранятся в таблице links и являются такими же объектами. Помимо собственного uid, связь содержит информацию о том, какие объекты она связывает (uid_1, uid_2) и её тип. Так, forum_message связаны с forum_topic связью типа LINK_MESSAGE.
Отметки о прочитанных темах и сообщениях фиксируются путём установления связей LINK_USER_READ между пользователем и темой/сообщением.
Понятно, что поскольку связи хранятся отдельно, это часто приводит к лишнему SELECT'у из базы. Впрочем, в жизни посещаемый сайт с миллионами записей в базе (десятками тысяч сообщений в форуме) работал вполне прилично. Кроме того, такой подход (помимо универсальности и постоянства структуры базы) даёт ряд возможностей:
1. Контекст.
Мы можем отображать или редактировать объект не сам по себе, а в контексте другого объекта. Для этого у связи есть поля n, comment, tpl и некоторые другие. К примеру, возьмём распространённую ситуацию - галерею с несколькими разделами. В ней есть фотографии. Причем, некоторые встречаются одновременно в двух разделах.
Необходимо было:
а) Расставлять их в нужном для каждого раздела порядке
б) Показывать различные подписи к одному и тому же снимку, опубликованному в разных разделах.
Это как раз те случаи, когда информация хранится в связи (links.n, links.comment соответственно).
Еще одна ситуация - допустим, на сайт загружена картинка, которая опубликована и в ленте новостей и в галерее. Понятно, что её оформление (рамка, фон, отступы и прочее) в этих двух ситуациях должны быть разными. Здесь и используется links.tpl - в нём хранится имя темплейта для вывода объекта взависимости от того, к чему он относится. Если в links.tpl ничего не указано, используется темплейт из objects.tpl. Если и там ничего нет, используется дефолтный (для данного типа/класса объекта).
Вообще говоря, хранение данных внутри связи - не вполне красивое решение и продиктовано отчасти эффективностью, отчасти простотой. Следующим логическим шагом были..
2. Связи между связями и объектами.
Пример - реализация опросов/голосований. Схема такая:
Имеется корневой объект poll (это конкретный опрос, содержащий сам вопрос) и ряд объектов dec (это возможные варианты ответов). Объекты poll и dec связаны связями типа LINK_DEC.
Когда пользователь выбирает один из вариантов ответа, это фиксируется не счётчиками, а установлением связи (типа LINK_LINK) между _связью_ poll--LINK_DEC--dec и пользователем user.
Это даёт а) возможность отзыва голоса/переголосования б) возможность посмотреть, кто как голосовал в) возможность для пользователя прокомментировать свой выбор (комментарий будет помещен в эту самую связь типа LINK_LINK).
Стоит упомянуть, что для опроса poll вариантами ответов могут быть необязательно dec, но и любые объёкты (к примеру, страничка сайта, картинка, новость и т.п.). Таким образом была реализована возможность оценки - т.е. выставления пользователем баллов объекту и даже комментирование его (число баллов записывается в links.power, возможный комментарий в links.comment).
Эта схема позволяет реализовать самые сложные варианты голосований, оценок и вообще фиксации мнения пользователя о чём-либо.
И ЕЩЕ О ГИБКОСТИ СИСТЕМЫ
Когда понадобилось не просто публиковать ленту новостей, а еще и комментировать новости, это было сделано безболезненно - введением связи между новостью (news) и темой форума (forum_topic). Тема создавалась для каждой новости и заодно привязывалась к отдельному разделу форума (forum_section).
Таким образом, при желании можно было посмотреть все комментарии к новостям и работать с ними как с любыми другими темами и сообщениями форума (подписаться на новые комментарии, к примеру).
Аналогичная ситуация была, когда темы форумы привязывались к задачам (tasks) и статьям бюджета (things) на сайте Chaos Constructions.
Иными словами, над любым объектом можно потенциально производить любую работу. Оценивать, комментировать, помечать прочитанным, прицеплять к нему другие объекты - нет никаких технических ограничений - любая такая доработка сводится главным образом к тому, как это должно быть отображено.
Можно создавать сложные многоуровневые каталоги при помощи объектов cat (категории) и связей LINK_CAT к категоризуемым (причем, любого типа) объёктам.
Структуры и взаимосвязи между объектами могут быть любыми.
Так, если для файла изображения недостаточно иконки одного размера, можно сделать 10 размеров, последовательно соединяя между собой объекты file связями LINK_FILE.
А связь типа LINK_REDIR позволяет при клике на объект показывать другой объект (использовалась для перехода по URL при клике на картинку).
Замечу, что поскольку всё от форума до страниц и новостей хранится и регистрируется по одним принципам, поиск по сайту позволяет искать сразу везде, при необходимости с ограничением по типу объектов, периоду времени и пользователям их создавшим или редактировавшим.
О ДРУГИХ ХАРАКТЕРИСТИКАХ ОБЪЕКТОВ
Поскольку добавлять для каждого объекта новые поля из-за каждой новой характеристики не хотелось (тем более, выборка по ним как правило была не нужна), появилось поле options (TINY TEXT), в котором записывались, в текстовом формате, различные опции. К примеру:
sort: dt_crt
sortdir: desc
для объекта cat (категория) означали, что все объекты с ней связанные, при показе надо отсортировать по дате их создания, в сторону убывания.
Объекты text могли быть "статическими" и "динамическими". В первом случае их содержимое (текст странички, к примеру) хранились в базе данных, в таблице texts. Во втором, в таблице texts указывался путь или URL по которому нужно было взять эти данные в момент показа.
Любые объекты помимо их uid могли иметь также alias'ы. Т.е. во всех функциях можно было указывать не только uid объектов, но и их произвольное имя в виде текстовой строки (к примеру, ...&uid=mainpage... вместо ...&uid=37826... в query string).
Что касается прав доступа, то они реализованы довольно примитивно. Для каждого объекта в таблице objects были поля, где хранился минимально необходимый уровень доступа на разные операции (any access, create, read, write, delete). Почти с самого начала (судя по черновикам - с 2001 года) я планировал реализовать так называемую capability based схему - с выдачей пользователю ключей на определенные операции и их проверкой, но серьезной практической необходимости так и не возникло.
Одним из усовершенствований стала многоязычность. Каждый объект, для которого существует перевод, имеет копии на всех необходимых языках. Один из объектов является основным. К нему происходит обращение в любом случае. В options у него прописаны uid/alias всех переведенных объёктов:
alterlang:deu=cat_wallpapers_deu
alterlang:fra=cat_wallpapers_fra
При наличии хотя бы одной записи alterlang, вместо основного объекта показывается соответствующий текущему языку. Соответственно, объекты у которых alterlang отсутствует, показываются на языке по-умолчанию.
КАКИЕ ЕЩЕ БЫЛИ ОБЪЕКТЫ
seqs (последовательности) - позволяют задать закономерность для последовательного показа нескольких объектов (например, баннеров или объявлений). Т.е. при показе объекта seq в этом месте последовательно демонстрировались объекты, по закону, указанному в этом seq (случайному, по-порядку, в указанном порядке).
notices (уведомления) - любого пользователя можно подписать на уведомление об изменении состояния объекта (создан новый объект интересующего типа, изменён или удалён определенный объект и т.п.)
В одном из проектов, для сайта поддержки игры, потребовалось реализовать каталог юнитов (танки, самолёты, орудия и т.п.) Причем, у каждого юнита могло быть установлено вооружение, в свою очередь снаряженное боеприпасами. Для этого были добавлены типы объектов units, weapons, shells. Взаимосвязи, как обычно, устанавливались через links.
Данные каталога использовались одновременно для просмотра посетителями сайта и для экспорта в саму игру. Так как для русскоязычной и иностранной аудитории были сделаны два разных (по содержимому и структуре) сайта, а каталог нужен и там и там, данные именно по units/weapons/shells периодически синхронизировались, что оказалось непростой задачей.
Для сайта Chaos Constructions, а также для багтрэкера было добавлено еще несколько типов объектов:
tasks (задачи), things (статьи бюджета), items (конкурсные работы), contacts (контакты).
С ТОЧКИ ЗРЕНИЯ РЕДАКТОРА САЙТА
Поскольку система никогда не была "коробочной", её установка и обслуживание требовало квалификации и знания внутренней, весьма нетривиальной структуры. Тем не менее, некоторые идеи и по сей день кажутся мне правильными.
Так например, во многих CMS редактирование содержимого сайта осуществляется в отдельном разделе, а результат смотрится уже на самом сайте. В нашем случае это было реализовано иначе. После логина привилегированного пользователя, на страницах сайта появлялись кнопки типа "редактировать", "удалить" и т.п., так что можно было просто редактировать конкретную часть той страницы, на которой находимся, даже не зная структуры сайта.
Интерфейс редактора так и не был доведён до user-friendly состояния - сначала были более важные задачи, а затем всё это стало неактуальным.
ЭПИЛОГ
Для меня эта система - один из лучших примеров работы "двойного назначения", когда один и тот же проект, с одной стороны, успешно использовался для ряда коммерческих сайтов (часть из которых эксплуатируются до сих пор) и в качестве bugtracker'a при их разработке. С другой - применялся для некоммерческих проектов - таких, как сайты Chaos Constructions 2004-2008 (кроме 2009), включая систему регистрации / подготовки показа конкурсных работ и project management'a.
В 2007 году Стас (фамилию и ник он попросил не упоминать) перевёл значительную часть системы из процедурной в ОО, а также придумал и реализовал ряд упомянутых выше возможностей (в ходе жарких споров со мной :).
Я сам не слишком люблю ООП, но когда система становится сложной - это неизбежно. Тот период был кульминацией развития движка - мы оба и сейчас считаем, что с точки зрения архитектуры и потенциальных возможностей разработка уникальна (по сравнению с известными существующими).
В завершение, предлагаю заинтересовавшимся посмотреть видео (90 минут), где Стас рассказывает как раз об ОО части и архитектуре одной из последних версий системы. Рассказ был записан мной и предназначался для нового сотрудника (Дима, за кадром) и меня (я в тот период уже выполнял функции PM'a, за кадром задаю разные глупые и умные вопросы).