Система управления контентом CAIRO

Руководство разработчика

Виктор Грищенко


Содержание

1. Модель данных Cairo
1.1. Сущности как модель данных
1.2. Поля сущности
1.2.1. Типы полей
1.2.2. Важность полей
1.2.3. Ограничения на значения полей
1.2.4. Значения полей по умолчанию
1.2.5. Служебная информация о сущности
1.3. Связи между сущностями
1.3.1. Точки вложения
1.3.2. Жесткие и символические ссылки
1.4. Пользователи и группы
1.4.1. Пользователи
1.4.2. Группы
1.5. Права доступа
2. Разработка сайта на базе платформы Cairo
2.1. Ресурс данных
2.1.1. Настройки доступа к БД и описание каталогов ресурса
2.1.2. Настройка вывода модулей
2.1.3. Настройка учетной записи пользователя
2.2. Пустой проект
2.2.1. Файлы настроек веб-ресурса
2.2.2. Встроенные утилиты
2.2.3. Скрипты и шаблоны
2.3. Архитектура веб-сайта
2.3.1. Структура веб-приложения
2.3.2. Работа с базой данных
2.3.3. Обработка ошибок
2.3.4. Интернационализация веб-интерфейса
2.3.5. Шаблоны страниц
2.4. Выборка и вывод данных
2.4.1. Базовые операции выборки данных
2.4.2. Вывод данных
2.4.3. Расширенные возможности выборки
2.5. Операции изменения данных
2.5.1. Создание новой сущности
2.5.2. Обновление сущности
2.6. Дополнительные возможности
2.6.1. Работа с формами
2.6.2. CAPTCHA
2.6.3. Работа с изображениями
2.6.4. Кэширование данных
2.6.5. Работа с пользователями
2.6.6. Работа со справочниками
3. Архитектура Cairo
3.1. Структура каталогов
3.1.1. Введение
3.1.2. Каталоги ядра системы
3.1.3. Каталоги интерфейса
3.1.4. Каталоги модулей
3.1.5. Каталог настроек
3.1.6. Каталоги данных пользователя
3.1.7. Каталог стилей оформления
3.1.8. Прочие каталоги
3.2. Хранение данных
3.2.1. Введение
3.2.2. Хранение сущностей
3.2.3. Хранение файлов и изображений
3.3. Модули
3.3.1. Введение
3.3.2. Архитектура модулей и способы их интеграции в систему
4. Расширение возможностей системы
4.1. Правила именования
4.1.1. Введение
4.1.2. Имена классов
4.1.3. Имена файлов
4.1.4. Таблицы сущностей
4.2. Создание нового модуля
4.2.1. Создание класса модуля
4.2.2. Создание обработчиков событий
4.2.3. Создание блоков интерфейса модулей
4.2.4. Регистрация модуля в системе
4.3. Создание нового типа поля
4.3.1. Создание класса типа поля
4.3.2. Создание шаблонов поля
4.3.3. Регистрация поля в системе
A. Файлы настроек
/etc/core.ini - Настройки ядра системы
/etc/fields.ini - Настройки типов полей.
/etc/login.ini - Настройки ресурсов
/etc/import.ini - Настройка импорта для каждого отдельного ресурса данных
/etc/mimes.ini - Настройки MIME-типов
/etc/interface/menu.ini - Перечень модулей, отображаемых в главном меню интерфейса администратора.
/etc/interface/tabs.ini - Настройки закладок модулей.

Список иллюстраций

1.1. Точки вложения сущности
1.2. Включение одной сущности в другую через несколько точек вложения
1.3. Ограничения на тип ссылок в точке вложения
1.4. Права на сущность
2.1. Архитектура веб-приложения
2.2. Простейшая структура данных
2.3. Методы создания эскизов изображения
2.4. Стандартный диалог HTTP-аутентификации
3.1. Структура каталогов системы

Список таблиц

2.1. Глобальные массивы, формируемые скриптом init.php
2.2. Методы, используемые для выборки результатов SQL-запросов
2.3. Характеристики классов создания CAPTCHA
2.4. Параметры учетной записи пользователя
A.1. Общие настройки
A.2. Настройки языка интерфейса
A.3. Параметры режима отладки
A.4. Настройки файлов
A.5. Настройки адресов электронной почты
A.6. Настройки интерфейса администратора
A.7. Параметры ресурса
A.8. Настройки закладок модулей

Список примеров

2.1. Пример обработки данных, полученных методом GET
2.2. Использование скалярных маркеров и констант
2.3. Использование маркер-массивов и маркеров-хешей
2.4. Использование именованных маркеров
2.5. Обработка ошибок внутри функций
2.6. Обработка ошибок с помощью конструкции while
2.7. Пример простого скрипта и шаблона
2.8. Примеры использования методов экранирования данных в шаблонах
2.9. Пример управления кэшем браузера
2.10. Пример работы с кэшем сервера
2.11. Получение параметров сущности
2.12. Получение списка сущностей без применения фильтров
2.13. Получение списка сущностей в виде перечня ассоциативных массивов.
2.14. Получение списка сущностей одного типа.
2.15. Получение списка сущностей разных типов.
2.16. Вывод списка сущностей с панелью постраничности
2.17. Вывод одной сущности
2.18. Получение списка сущностей, вложенных в определенную сущность
2.19. Получение списка сущностей, вложенных в определенную сущность
2.20. Получение списка новостей, созданных в 2006-м году
2.21. Получение списка новостей с фильтрацией по названию
2.22. Получение количества событий в ленте новостей
2.23. Сортировка лент новостей в алфавитном порядке
2.24. Сортировка лент новостей в порядке, обратном заданному в системе администрирования
2.25. Создание сущности программно
2.26. Создание сущности с помощью формы
2.27. Программное обновление параметров сущности
2.28. Обновление сущности с помощью формы
2.29. Добавление полей в форму
2.30. Добавление кнопок на форму
2.31. Обработка данных в форме
2.32. Определение собственного префикса полей
2.33. Включение изображения CAPTCHA в шаблон страницы
2.34. Обработка введенного пользователем значения CAPTCHA
2.35. Использование CAPTCHA вместе с cfForm
2.36. Пример работы с кэшем
2.37. Пример работы с кэшем
2.38. Аутентификация пользователя на сайте
2.39. Авторизация пользователя на странице
2.40. Создание пользователя
2.41. Форма регистрации пользователей
4.1. Оформление файла, содержащего класс

Глава 1. Модель данных Cairo

Условно, систему Cairo можно разделить на следующие части:

  • обобщенная структура данных;

  • классы для работы со структурой данных;

  • сервисные классы и классы элементов управления;

  • универсальный веб-интерфейс администратора;

Обобщенная структура данных Cairo предназначена для хранения и каталогизации структурированной информации. Использование такой модели данных позволяет в короткие сроки создавать сложные информационные ресурсы с разделением прав доступа и сложными взаимосвязями между объектами. Используя возможности системы контроля прав доступа, можно ограничить вывод определенных данных на страницах веб-ресурса и наделить пользователей различными правами на управление этими данными. Определение взаимосвязей позволяет системе контролировать информационное содержимое ресурса и не позволяет выходить за рамки, определенные разработчиком.

Этот раздел посвящен рассмотрению универсальной модели данных Cairo. Описание классов работы с данными и сервисных классов можно найти в документе "Система управления контентом CAIRO: Справочное руководство по API".

1.1. Сущности как модель данных

В настоящий момент используется множество способов хранения данных на сайтах: простые файлы, XML, реляционные Системы Управления Базами Данных (СУБД). Эти подходы носят универсальный характер, поэтому их использование требует существенных затрат времени и сил при разработке веб-ресурса.

Система Cairo базируется на реляционной модели данных, расширяя и адаптируя ее под нужды веб-разработчиков. Она добавляет собственные типы данных, дополнительные проверки ввода и контроль ссылочной целостности. Подавляющее большинство типов контента может быть описано в Cairo с использованием только веб-интерфейса.

Базовым понятием, относящимся к универсальной модели данных Cairo, является сущность. Сущность - минимальный элемент информационной структуры. Ее можно сравнить с объектом в объектно-ориентированном проектировании (ООП). В виде сущностей могут быть представлены новости, категории каталога, товары, отзывы, а также множество других типов контента, которые понадобятся при разработке сайта.

Необходимо различать такие понятия, как тип сущности и экземпляр сущности. Понятие "тип сущности" относится к набору однородных элементов, экземпляр сущности относится к конкретному элементу. Например, типом сущности может быть город, а экземплярами: Киев, Лондон, Москва. Продолжая проведение параллелей с ООП, можно сравнить тип сущности с классом, описывающим множество объектов, объединенных общими признаками. Разработчик самостоятельно решает, какие типы сущностей ему нужны в каждом конкретном случае и может с легкостью создавать новые и редактировать существующие типы.

Тип сущностей определяется набором полей и связей с другими типами. Поле - поименованная характеристика сущности. Поля используются для определения того, какая информация о сущности должна хранится. Примерами полей для сущности типа "Город" являются: "Название", "Страна", "Население" и т.п. Параметры полей используются для вывода информации на страницах веб-ресурса и верификации данных при создании и обновлении сущностей.

Между собой сущности связываются по принципу "родитель-ребенок". При чем у одного родителя может быть множество детей (вложений) и одна сущность может быть вложена в несколько родителей. Это позволяет создавать гибкие структуры данных и позволяет повторно использовать контент. Например, в каталог товаров могут вкладываться избранные товары и подкаталоги, которые, в свою очередь, содержат вложенные товары.

Наряду с понятием "Тип сущности", существует понятие "Подтип сущности". Например, для типа "Товар" можно определить подтипы "Телевизор", "Фотоаппарат". Подтипы расширяют набор полей типа и могут иметь собственные, не зависящие от базового, наборы типов, доступных для связывания. Механизм подтипов чем-то напоминает механизм наследования в ООП. В этом случае тип можно рассматривать как базовый класс, а подтип, как порожденный. Если у типа есть подтипы, то он называется "главным" или "абстрактным". Существование сущностей абстрактного типа не допускается.

1.2. Поля сущности

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

1.2.1. Типы полей

Тип поля можно сравнить с типами данных в языках программирования либо с типами полей в базах данных. Последнее более уместно, поскольку, фактически, модель данных Cairo является расширением реляционной модели данных.

В системе предусмотрено более 20 встроенных типов полей: целое число; вещественное; строка; дата/время; файл; изображение и т.д. Разработчик может добавлять собственные типы полей. Подробнее этот вопрос рассматривается в разделе "Создание нового типа поля".

Большинство типов полей являются простыми: они хранятся в одном поле БД и не используют внешние источники данных. К таким типам относятся "строка", "дата", "время", "e-mail". Типы полей "изображение", "файл", "денежный" являются сложными. Для хранения значений полей таких типов выделяется несколько полей БД, кроме того, часть данных может храниться вне БД, например, в файловой системе. Так реализовано хранение изображений: в БД хранится только описание, а само изображение хранится в файловой системе. Данный подход позволяет эффективнее использовать ресурсы сервера.

Кроме полей данных, использующих ресурсы файловой системы, существуют типы полей, данные которых хранятся в нескольких таблицах. К таким полям относятся "Справочник", "Пользователь", "Группа". При работе через интерфейсы доступа к данным Cairo связывание таблиц происходит автоматически.

1.2.2. Важность полей

Существует три степени важности поля: поле может быть обязательными, важными и необязательными. В случае, если обязательное или важное поле остается незаполненным, при работе через веб-интерфейс администратора, система выдает предупреждение. Текст предупреждения может быть отредактирован. В отличие от обязательных полей, важные поля могут иметь пустые значения.

1.2.3. Ограничения на значения полей

Ограничения на значения полей сущности в первую очередь зависят от типа поля. Так, значение поля типа «строка» не может быть больше 255 символов, а тип поля «изображение» поддерживает только определенные графические форматы.

Кроме того, можно устанавливать дополнительные ограничения на значения полей. Например, длина заголовка новости может быть ограничена 160 символами, а главное изображение новости не должно превышать 600 пикселей в ширину.

Если введенное значение не соответствует установленным ограничениям, то система не позволит сохранить информацию. В таком случае, при работе через веб-интерфейс администратора, выдается предупреждение. Текст предупреждения может быть отредактирован.

Подсказка

Проверка данных осуществляется и при работе с системой через классы Cairo. Но о выводе сообщений пользователю должен позаботиться разработчик.

1.2.4. Значения полей по умолчанию

Значения полей, используемые по умолчанию, применяются при внесении данных через веб-интерфейс администратора Cairo. Например, при создании сущности типа "Новость" в поле "Дата" может автоматически подставляться текущая дата, а для поля "Наличие на складе", сущности товар, значением по умолчанию является "Присутствует на складе".

1.2.5. Служебная информация о сущности

Помимо произвольного набора полей, каждая сущность независимо от типа имеет стандартные поля, предназначенные для хранения служебной информации. К таким полям относятся:

  • Идентификатор сущности

  • Заголовок сущности

  • Дата создания сущности

  • Имя пользователя, добавившего сущность

  • Дата изменения сущности

  • Имя автора последних изменений

  • Владелец сущности

  • Группа сущности

  • Права на сущность

Идентификатор сущности представляет собой уникальный номер сущности в системе.

Заголовок сущности используется для ее идентификации в универсальном интерфейса администратора.

Дата создания и последнего изменения сущности, идентификатор пользователя, добавившего сущность, а также идентификатор пользователя, изменившего сущность, фиксируются автоматически и не могут быть отредактированы.

Назначение полей "Владелец", "Группа" и "Права" будет рассмотрено в разделе "Система контроля прав доступа".

1.3. Связи между сущностями

В системе Cairo используются связи вида "Родитель-ребенок". Дочернюю сущность называют вложением родительской сущности. Также говорят, что родительская сущность содержит в себе дочернюю сущность. Например, сущность типа "Тема форума" содержит в себе сущности типа "Сообщение", или, другими словами, сущности типа "Сообщение" являются вложениями сущностей типа "Тема форума".

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

Новые сущности создаются как вложения уже существующих сущностей. При этом, существующие сущности также могут быть связаны друг с другом. Например, одна новость может присутствовать в нескольких лентах новостей, один товар – добавлен в несколько категорий каталога.

В системе существует понятие "Корневая сущность". Корневой является сущность, которая не имеет родительских сущностей. Корневые сущности добавляются в процессе создания структуры данных.

Число связей между сущностями не ограничено, таким образом, сущность может иметь любое количество как дочерних, так и родительских сущностей.

1.3.1. Точки вложения

Сущности связываются друг с другом строго в соответствии с правилами, которые определяются для типов сущностей.

Так, например, в типе сущности "Категория каталога" задается возможность вкладывать товары в категории каталога; в типе сущности "Товар" определяется то, что товар может содержать отзывы; отзывы, в свою очередь, не могут содержать вложений, что также задано в типе сущности "Отзыв".

Связи сущностей определяются с помощью точек вложения. Точка вложения может содержать сущности только одного типа. При этом, для типа сущности может быть определено несколько точек вложения. Например, сущность типа "Отель" может содержать и сущности типа "Номер отеля", и "Фото отеля". Для этого в типе сущности "Отель" создаются две точки вложения: "Перечень номеров отеля" и "Галерея изображения отеля" (рис. 1.1). Точка вложения может содержать сущности любого из подтипов, относящихся к заданному для нее типу.

Рисунок 1.1. Точки вложения сущности

Точки вложения сущности

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

Рисунок 1.2. Включение одной сущности в другую через несколько точек вложения

Включение одной сущности в другую через несколько точек вложения

Одна сущность может быть прикреплена к другой в нескольких точках вложения. Так, некоторая модель мобильного телефона может относиться и к моделям бизнес-класса, и к моделям среднего класса одновременно (см. рис. 1.2).

1.3.2. Жесткие и символические ссылки

Экземпляры сущностей связаны друг с другом при помощи ссылок. В системе поддерживается ссылочная целостность данных: при удалении родительского объекта, все ссылки, которыми он связан с дочерними объектами, удаляются автоматически.

Следует различать удаление ссылки на сущность и удаление сущности. Так как количество ссылок на сущность не ограничено, удаление ссылки на сущность не всегда влечет за собой удаление самой сущности.

Автоматическое удаление дочерней сущности при удалении одной из родительских сущностей зависит от типов ссылок, которыми дочерняя сущность привязана к родительским, а также количества этих ссылок. Ссылки бывают жесткими и символическими.

При создании сущности, между ней и родительской сущностью возникает жесткая ссылка. Символическая ссылка отличается от жесткой тем, что при удалении символической ссылки, все остальные ссылки на данную сущность: как жесткие, так и символические, остаются нетронутыми. Удаление сущности происходит при удалении последней жесткой ссылки на нее. При удалении сущности все символические ссылки на нее удаляются автоматически.

Таким образом, если необходимо, чтобы связь между сущностями ни при каких обстоятельствах не была разорвана автоматически – достаточно связать эти сущности жесткой ссылкой. Если же связь между сущностями зависит от других ссылок на дочернюю сущность – следует использовать символическую ссылку.

Рисунок 1.3. Ограничения на тип ссылок в точке вложения

Ограничения на тип ссылок в точке вложения

Точка вложения сущности может содержать и жесткие, и символические ссылки. Однако, существует возможность ограничить точку вложении таким образом, что в нее можно будет добавлять только символические ссылки. В таком случае, вложениями могут выступать только существующие сущности. Эта возможность может быть использована, например, при организации связей между сопутствующими товарами в каталоге (рис. 1.3) . Если ссылки на сопутствующие товары всегда будут символическими, то в качестве сопутствующих будут использоваться только те товары, которые уже добавлены в каталог (на рисунке символические ссылки обозначены пунктиром). В результате сопутствующий товар всегда будет доступен из каталога. А главное, при удалении товара из каталога, он автоматически будет удален из числа сопутствующих, но не наоборот.

1.4. Пользователи и группы

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

1.4.1. Пользователи

Каждый зарегистрированный пользователь системы имеет личную учетную запись. К учетной записи относятся логин и пароль пользователя, а также дополнительная информация, такая как e-mail, телефон и т.п. Базовый набор полей может быть расширен индивидуальными полями для каждого отдельного ресурса.

В системе выделены две специальных учетных записи:

  • root - администратор системы;

  • anonymous - неавторизированный пользователь.

Администратор системы имеет абсолютные права на сущности, а также доступ к модулям системной настройки (таким как "Управление структурой данных", "Управление пользователями" и др.). Учетную запись администратора невозможно удалить - она должна всегда присутствовать в системе.

Учетная запись анонимного пользователя используется в случае, когда пользователь не прошел авторизацию. Анонимные пользователи не имеют доступа к интерфейсу администратора, однако, могут работать с сущностями через веб-интерфейсы, созданные на базе Cairo. Например, в форуме может понадобиться возможность создания сообщений неавторизированными пользователями; это относится и к отзывам, которые анонимные пользователи могут добавлять к товарам в каталоге.

1.4.2. Группы

Пользователи могут объединяться в группы по некоторому признаку. Группа создается для наделения входящих в нее пользователей общими привилегиями. Один пользователь может входить в несколько групп.

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

1.5. Права доступа

В определенных ситуациях требуется разграничить доступ пользователей к той или иной информации. Например, запретить группе пользователей просматривать или редактировать отдельные сущности. Для этих целей в Cairo используется система контроля прав доступа.

В качестве прототипа системы прав был выбран подход, использующийся в операционных системах семейства UNIX. Каждая сущность относится к одному пользователю, который называется "владельцем", и одной группе. Кроме того для сущности определены три триплета флажков, ограничивающих привилегии на эту сущность для владельца, группы и остальных пользователей (см. рис. 1.4).

Важно

Права на сущность, ее группу и владельца может изменять только ее текущий владелец и пользователь root.

Рисунок 1.4. Права на сущность

Права на сущность

Для каждой сущности устанавливается три набора прав:

  • права для владельца сущности;

  • права для пользователей, принадлежащих к группе сущности;

  • права для остальных пользователей.

В каждом наборе прав определены права для трех типов операций над сущностью:

  • чтение (r);

  • запись (w);

  • изменение списка вложений (x).

Таким образом, права доступа к отдельной сущности определяются тремя триплетами вида "[rwx rwx rwx]", где первый триплет устанавливает права владельца сущности, второй - права для пользователей, принадлежащих группе этой сущности и третий - права всех остальных пользователей.

Следует отметить, что права «х» (на управление вложениями сущности) достаточно только для добавления вложения. Чтобы удалить вложение необходимо иметь как право на управление вложениями для родительской сущности, так и право на редактирование удаляемого вложения.

Вновь созданные сущности принадлежат той же группе, что и родитель этой сущности. Это позволяет организовать самоподдерживающуюся информационную систему, которая автоматически ограничивает развивающуюся подветвь данных той группой, к которой принадлежит родитель этой ветви. Права для вновь создаваемых сущностей задаются на уровне типа сущности. По умолчанию, при создании типа, система предлагает значение [rwx r-- r--]. Такой набор прав означает, что владелец имеет права на все операции над сущностью, а пользователи, относящиеся к группе сущности, как и все остальные пользователи, имеют лишь право на чтение сущности. Но эта настройка может быть изменена как при создании типа сущностей, так и для уже существующего типа.

Глава 2. Разработка сайта на базе платформы Cairo

Платформа Cairo предоставляет удобные визуальный и программный интерфейсы для работы со структурированными данными. Все данные объединяются в древовидную иерархическую структуру, которая позволяет организовывать большие массивы информации и эффективно ими управлять.

Большая часть рутинных операций: проверка введенных данных, генерация JavaScript-проверок, поддержка ссылочной целостности, создание эскизов изображений и проч. реализованы с помощью утилит и классов, предоставляемых Cairo. Для использования этих функций разработчику достаточно подключить необходимую библиотеку и написать несколько строк кода.

Использование предоставляемых классов не является обязательным. Допускается использовать сторонние библиотеки, но библиотеки Cairo могут значительно облегчить работу программиста и сократить время разработки.

В данном разделе будет рассмотрено, как следует использовать возможности, предоставляемые системой, для ускорения и облегчения процесса разработки веб-ресурсов.

2.1. Ресурс данных

Разработка любого сайта с использованием Cairo начинается с создания и настройки ресурса данных. Ресурс данных представляет собой совокупность информации, необходимой для сайта. Сюда входят данные, хранящиеся в БД, информация об их структуре, списки пользователей и групп, а также файлы и изображения, хранимые в файловой системе сервера.

В системе администрирования каждый ресурс может иметь собственные настройки. Все настройки описываются в каталоге /etc. К изменяемым настройкам относятся:

  • настройки доступа к базе данных

  • местоположение файлов ресурса

  • доступ пользователей к интерфейсу администрирования

  • дополнительные поля учетной записи пользователя

  • настройки модулей

Обязательными для указания являются только первые три пункта, настройки учетной записи и модулей, по умолчанию, берутся общие. Рассмотрим настройки ресурса данных на примере создания нового ресурса.

2.1.1. Настройки доступа к БД и описание каталогов ресурса

Разработка нового ресурса начинается с создания новой базы данных и импорта в нее файла-шаблона структуры Cairo. Файл шаблона находится в каталоге установки: <CairoDir>/dump/cairo-dummy.sql. Установив этот дамп, вы получите заготовку для структуры данных нового ресурса. Теперь необходимо описать этот ресурс в системе для того, чтобы получить к нему доступ.

Первый файл (а часто и единственный), в котором следует описать новый ресурс - <CairoDir>/etc/login.ini. Описание ресурса, в общем виде, выглядит следующим образом:

[<Имя ресурса>]
dbhost        = <Хост БД>
dbuser        = <Имя пользователя БД>
dbpasswd      = <Пароль к БД>
dbname        = <Имя БД>
dbcharset     = <Кодировка>
dbversion     = <Версия MySQL>
authhost      = <Доменное имя, по которому доступен интерфейс администратора>
httphost      = <Доменное имя, по которому доступен ресурс>
basedir       = <Каталог установки ресурса>
uploaddir     = <Каталог для загруженных файлов>
cachedir      = <Каталог кэша>
frmtext_files = <Каталог файлов для полей типа "Форматированный текст">
auth_log      = <Включение/отключение журналирования авторизации>
auth_user     = <Разрешенные пользователи>
auth_group    = <Разрешенные группы>

<Настройки доступа к модулям>
        

Имя ресурса будет использоваться для идентификации ресурса в системе, в частности, оно будет доступно в перечне ресурсов при авторизации. Параметры dbhost, dbuser, dbpasswd, dbname означают соответственно имя хоста БД (или IP-адрес), логин и пароль к базе данный и ее имя. Эти параметры обычны для любых приложений, работающих с СУБД, и подробно рассматриваться не будут. Параметр dbcharset содержит название кодировки, которая будет установлена в качестве рабочей при открытии соединения с БД. Версия базы данных (dbversion) используется для определения команды установки рабочей кодировки. Для версий младше 4.0 используется команда

SET CHARACTER SET ...

для более поздних версий используется команда

SET NAMES ...

Версия БД должна записываться в файл в том виде, в котором ее возвращает команда:

SHOW VARIABLES LIKE "version";

Следующая группа параметров описывает имя хоста интерфейса администратора ресурса (authhost) и основной хост ресурса (httphost). Имя хоста интерфейса администратора используется для тонкой настройки поля "Ресурс" в форме авторизации. Представим себе следующую ситуацию: на сервере установлен один экземпляр системы Cairo, под управлением которой работает множество ресурсов данных. На странице авторизации интерфейса администратора Cairo в выпадающем списке "Ресурс" выводится перечень всех доступных ресурсов данных. Нам необходимо организовать работу двух клиентов: Алисы и Боба. При этом, система должна быть настроена так, чтобы в выпадающем списке ресурсов Алиса видела только свои ресурсы, а Боб - только свои. Чтобы решить поставленную задачу, следует создать виртуальный хост для интерфейса администратора, например, auth.cairo, и добавить к нему два псевдонима (alice.auth.cairo и bob.auth.cairo). После этого в настройках каждого ресурса данных Алисы в authhost записывается значение alice.auth.cairo, а для ресурса Боба соответственно bob.auth.cairo. В результате, на странице авторизации, доступной по адресу http://alice.auth.cairo/auth/ в списке ресурсов будут доступны только ресурсы Алисы, а на странице http://bob.auth.cairo/auth/ - только ресурсы Боба.

Основное имя хоста сайта (httphost) необходимо для внутренних нужд системы администрирования, например, при формировании URL картинок в форматированном тексте. Если сайт доступен по нескольким адресам (псевдонимам), то в значении параметра необходимо указать тот, который считается основным.

Ресурс в своей работе использует три каталога: basedir - каталог установки сайта (здесь необходимо указать значение, описанное в параметре DocumentRoot веб-сервера), uploaddir - каталог, в который будут загружаться изображения и файлы, cachedir - каталог кэша ресурса. Таким образом, перед созданием ресурса и его описанием в системе, необходимо создать виртуальный хост для него и создать каталоги для хранения изображений и кэша. При использовании WYSIWYG-редакторов может понадобится указать каталог, в котором будут храниться его загруженные изображения.

Важно

Система Cairo автоматически не интегрируется с WYSIWYG-редакторами, поэтому обеспечить передачу параметра frmtext_files в код редактора разработчик должен самостоятельно. На официальном сайте Cairo приведен пример интеграции системы с редактором FCKeditor.

По умолчанию, доступ к системе администрирования имеет только пользователь root. Для того, чтобы разрешить другим пользователям и группам работать с системой администрирования, необходимо указать в параметрах auth_user и auth_group перечень идентификаторов пользователей и групп соответственно. Например, если необходимо дать доступ к интерфейсу администратора группам "Менеджеры" (gid=4) и "Редакторы" (gid=5), то в файле login.ini будет следующая запись:

auth_user =
auth_group = 4, 5

Если же, кроме этого, необходимо дать доступ Бобу (uid=63), который не принадлежит ни группе "Редакторы", ни группе "Менеджеры", то необходимо будет ввести следующие изменения:

auth_user = 63
auth_group = 4, 5

Настройки доступа к модулям аналогичны настройкам доступа ко всему интерфейсу и имеют следующий формат:

<имя модуля>.accept_users = <UID 1>[, <UID 2>[, ...]]
<имя модуля>.accept_groups = <GID 1>[, <GID 2>[, ...]]

Система позволяет журналировать все попытки авторизации, являются ли они успешными или нет. Для этого достаточно установить значение "on" в параметре auth_log. После этого каждый раз, когда кто-нибудь отправит данные в форме авторизации, будет добавляться запись в файл <CairoDir>/var/log/<Имя БД ресурса>.auth.log.

После установки Cairo администратор имеет доступ к следующим модулям:

  • modtypes - Управление структурой данных

  • modusers - Управление пользователями

  • modimport - Импорт

  • modsearch - Поиск (не выводится в системе администрирования)

  • modlog - Журналирование (не выводится в системе администрирования)

Таким образом, если необходимо Бобу разрешить управлять структурой данных, то в файл настроек должны быть внесены следующие строки:

modtypes.accept_users = 63
modtypes.accept_groups =

Если мы хотим указать группы, имеющие доступ к модулю, то их идентификаторы перечисляются в параметре modtypes.accept_groups.

2.1.2. Настройка вывода модулей

Функциональность системы Cairo может быть расширена с помощью модулей. После установки системы она уже содержит стандартные модули, отвечающие за работу со структурой данных, справочниками, импортом и т.д. Настройки этих модулей универсальны и, в большинстве случаев, их изменение не требуется. Настройка правил отображения потребуется при создании дополнительных модулей. Поэтому вы можете пропустить этот раздел до тех пор, пока не появится необходимость создавать собственный модуль.

Модули выполняют две функции: расширение возможностей интерфейса администратора и обработка системных событий. В интерфейсе администратора модули могут появляться в двух видах: пункт в главном меню и в виде закладок на странице сущности. Процесс создания нового модуля подробно описан в разделе Создание нового модуля.

Настройка вывода модуля осуществляется в файлах <CairoDir>/etc/resources/<Имя БД ресурса>/interface/menu.ini и <CairoDir>/etc/resources/<Имя БД ресурса>/interface/tabs.ini для меню и вкладок соответственно. Обратите внимание, что используется именно имя базы данных, а не имя ресурса. Это связанно с тем, что символы, допустимые в имени ресурса могут не допускаться в файловой системе сервера. Файл <CairoDir>/etc/resources/<Имя БД ресурса>/interface/menu.ini представляет собой перечисление имен классов модулей в том порядке, в котором они должны выводиться в меню. Имена модулей должны заключаться в квадратный скобки и начинаться с новой строки. Например:

[modDict]

[modTypes]

[modImport]

[modUsers]

Файл <CairoDir>/etc/resources/<Имя БД ресурса>/interface/tabs.ini имеет структуру, аналогичную файлу меню, но он позволяет определить типы сущностей, на страницах которых будет появляться вкладка:

[modCustomImport]
accept_etypeid = 7, 8, 26

В параметре accept_etypeid через запятую перечисляются идентификаторы типов (etypeid), на которых будет появляться вкладка модуля. Работа страницы модуля реализуется внутри его класса.

2.1.3. Настройка учетной записи пользователя

Учетная запись пользователя имеет набор обязательных параметров: идентификатор (uid), ник (логин, nick), пароль (passw), E-mail (email), фамилия (lastname), имя (firstname), отчество (surname), пол (sex), дата рождения (birthday) и "о себе" (about). Однако, существует возможность расширить этот набор полей для каждого отдельного ресурса.

Настройки дополнительных полей производятся в файле <CairoDir>/etc/resources/<Имя БД ресурса>/locale/<Локаль>/user_addon.flds.ini. Под локалью понимается набор настроек, отличающихся для каждого отдельного языка. Локали устанавливаются на сервере администратором и содержат правила сравнения строк, форматы вывода дат, чисел и т.п. Настройки дополнительных полей пользователей могут отличаться для разных языков (как минимум будут отличаться названия полей, выводимые пользователю), поэтому они должны быть определены для всех языков, в которых работает интерфейс администратора. В большинстве случаев будет достаточно определить настройки только для локали ru_RU.

Формат файла user_addon.flds.ini аналогичен формату /etc/fields.ini. Он представляет собой обычный INI-файл, разделенный на секции. Каждая секция описывает одно поле и имеет следующий формат:

[<Имя поля>]
alias       = "<Псевдоним поля>"
type        = "<Тип поля>"
format      = "<Формат поля>"
template    = "<Префикс шаблона>"
size        = "<Размер поля>"
format_preg = "<Регулярное выражение, проверяющее корректность ввода>"
default_val = "<Значение по умолчанию>"
condition   = "<Дополнительное условие истинности>"
er_msg      = "<Сообщение об ошибке ввода>"
ntf_msg     = "<Сообщение об обязятельности поля>"
required    = "<Обязательность поля>"
comment     = "<Комментарий>"
visible     = "<Видимость поля>"

Параметр Имя поля должен содержать то имя, которое это поле имеет в БД. Подробное описание остальных полей можно найти в приложении Системные файлы настроек: /etc/fields.ini.

Внимание

Дополнительные поля учетной записи пользователя разработчик должен самостоятельно создать в БД, в противном случае система выдаст ошибку при работе с пользователями.

2.2. Пустой проект

Использование Cairo при разработке веб-ресурса практически не накладывает ограничений на структуру скриптов приложения. Тем не менее, мы рекомендуем использовать шаблон веб-ресурса, который поставляется вместе с дистрибутивом. Пустой проект представляет собой ZIP-архив, содержащий файлы настроек, шаблоны скриптов и некоторые вспомогательные библиотеки.

Для того, чтобы начать работу над новым веб-ресурсом необходимо создать виртуальный узел (virtual host) и распаковать архив пустого ресурса в корень созданного узла. Далее необходимо описать настройки веб-сайта и ресурса данных в файле <DocumentRoot>/etc/core.ini и после этого можно приступать к разработке.

Ниже будет подробно описан шаблонный ресурс, указаны его настройки, библиотеки и файлы, облегчающие разработку.

2.2.1. Файлы настроек веб-ресурса

Все настройки пустого проекта содержатся в каталоге <DocumentRoot>/etc. Этот каталог содержит:

  • core.ini - основные настройки проекта

  • errors.ini - сообщения об ошибках сервера

  • init.php - файл инициализации системы

  • metas.ini - содержимое META-тегов, которые являются общими для всех страниц сайта

  • mimes.ini - расширения файлов и соответствующие им MIME-типы

  • resource-ids.ini - идентификаторы, используемые при разработке программной части ресурса

Все файлы, за исключением init.php имеют INI-формат. Скрипт init.php инициализирует систему и устанавливает первоначальные настройки. В любом скрипте, в котором будут использоваться возможности Cairo, в самом начале должен подключатся этот файл с помощью строки:

  1. // Инициализируем систему
  2. require_once($_SERVER["DOCUMENT_ROOT"]."/etc/init.php");

Таблица 2.1. Глобальные массивы, формируемые скриптом init.php

Имя массиваОписание
$_CONFIGМассив основных настроек. В него помещаются все настройки, хранимые в файле core.ini.
$TYPESИдентификаторы типов сущностей. Берутся из секции [types] файла resource-ids.ini.
$ENTITIESИдентификаторы корневых сущностей. Берутся из секции [entities] файла resource-ids.ini.
$DICTIONARIESИдентификаторы справочников. Берутся из секции [dictionaries] файла resource-ids.ini.
$METASЗначения общих META-тегов. Берутся из файла metas.ini.

В таблице 2.1 перечислены глобальные массивы, определяемые в скрипте init.php. $_CONFIG представляет собой ассоциативный массив, в котором ключами являются имена параметров, а элементами - их значение. Массив заполняется на основе файла core.ini. Эти параметры используются как в скриптах ресурса, так и в классах ядра системы.

Массивы $TYPES, $ENTITIES и $DICTIONARIES формируются на основе файла resource-ids.ini. Разработчик вносит в этот файл информацию об идентификаторах типов, корневых сущностей и справочников и в последствии использует соответствующие массивы в качестве мнемонической замены числовым идентификаторам. Использование этого механизма не обязательно, тем не менее, подобный подход позволит значительно упростить разработку и поддержку сайта.

Массив $METAS содержит данные из файла metas.ini и автоматически добавляется к META-тегам в классе cfPage.

2.2.1.1. Основные настройки (core.ini)

Файл core.ini содержит настройки системы Cairo и индивидуальные настройки сайта. При необходимости описания новых настроек, их следует добавлять в этот файл.

Большая часть настроек аналогична системным и описана в приложении Файлы настроек: /etc/core.ini. Здесь мы рассмотрим только те настройки, которые специфичны для сайта и не используются системой.

2.2.1.1.1. Настройки отладки

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

Часть этих настроек (debug.show_php_errors, debug.show_stat, debug.sql_stat, debug.show_runtime_errors) является стандартной и описана в приложении Файлы настроек: /etc/core.ini. Оставшиеся настройки (debug.slow_pages.log, debug.slow_pages.time) отвечают за управление журналированием "медленных" страниц. Под медленной страницей понимается страница, которая обрабатывается больше определенного времени. При включенном debug.slow_pages.log информация о любом скрипте, обработка которого заняла более debug.slow_pages.time секунд будет занесена в файл <DocumentRoot>/var/log/slowscripts.log. Каждая запись включает в себя дату/время, когда скрипт закончил работу, время отработки скрипта и полный путь, который запрашивал пользователь (REQUEST_URI).

Внимание

Сохраняются записи только о скриптах, использующих для визуализации класс cfPage.

2.2.1.1.2. Настройки базы данных

По умолчанию сайт работает с одной базой данных. Ее настройки указываются в секции [database] файла core.ini. Если необходимо организовать работу ресурса с несколькими БД, то ее настройки могут описываться здесь же, но подключение и поддержку конкурентной работы двух соединений разработчик должен обеспечить самостоятельно.

2.2.1.1.3. Настройки интерфейса

Настройки интерфейса объединяют в себе те параметры, которые связаны непосредственно с веб-интерфейсом.

Общий заголовок сайта (параметр webface.title) содержит в себе ту часть содержимого тега <TITLE> ресурса, которая будет присутствовать на каждой странице в конце заголовка. Обработка настройки осуществляется в файле <DocumentRoot>/usr/skins/default/bin/page_header.tpl.php. Этот файл содержит общую верхнюю часть страницы.

Параметр webface.currency содержит идентификатор валюты, которая используется в качестве основной на сайте. Система Cairo производит автоматический пересчет данных денежного типа из валюты в которой они хранятся в основную валюту ресурса.

Включение опции сжатия контента (webface.gzip) позволяет значительно сократить объем информации, загружаемой пользователей. Опция имеет силу только при использовании класса cfPage.

Опция webface.cache.client позволяет управлять отдачей заголовков управления кэшем. Если она включена и разработчик принудительно не отключил кэширование на стороне клиента с помощью метода cfPage::clientCache(), то при генерации страницы класс cfPage отправит пользователю заголовки, которые позволят браузеру использовать свой кэш и не делать лишних обращений на сервер.

2.2.1.1.4. Настройки кэша сервера

Кроме управления кэшированием браузера, класс cfPage позволяет организовать кэширование сгенерированных страниц на стороне сервера с последующим использованием сгенерированной копии, что позволить сократить нагрузку на сервер.

Для того, чтобы включить кэширование на стороне сервера, необходимо указать значение on в параметре cache.mode. Время актуальности сохраненной копии страницы определяется в параметре cache.lifetime. Значение этого параметра задается в секундах и позволяет определить время, пока копия страницы считается актуальной.

; Время жизни кэша
cache.lifetime = 600

; Включено ли кэширование
cache.mode = on

2.2.1.2. Сообщения об ошибках сервера (errors.ini)

При разработке веб-ресурса часто приходится создавать свои страницы стандартных ошибок веб-сервера, оформленные в общем стиле сайта. В подавляющем большинстве случаев страницы ошибок имеют один вид и отличаются только заголовком и поясняющим текстом. В шаблонном проекте уже реализован стандартный механизм вывода сообщения об ошибке. Для каждого отдельного проекта разработчику достаточно просто изменить шаблон страницы исключительной ситуации.

Описания всех обрабатываемых ошибок хранятся в файле errors.ini. При желании программист может добавить собственные описания, например, добавив ошибку 401 - ошибка авторизации, или любую другую. Описания хранятся в INI-формате и каждая секция соответствует одной ошибке. В общем виде секция имеет следующий формат:

[<Код ошибки>]
header  = "<HTTP-заголовок ошибки>"
title   = "<Заголовок ошибки>"
message = "<Подробное описание ошибки>"

Код ошибки представляет собой целое число и соответствует кодам ошибок, описанным в стандарте HTTP 1.1. В поле header указывается HTTP-заголовок, который должен быть отправлен пользователю. Заголовок ошибки (title) - это название ошибки в том виде, в котором его увидит пользователь. Параметр message содержит подробное описание ошибки и возможные варианты ее исправления.

2.2.1.3. Содержимое META-тегов (metas.ini)

Этот файл позволяет определить значение мета-тегов по умолчанию. Если разработчик явно не переопределит значение тега, то будет выведено значение, указанное в файле metas.ini. Файл имеет INI-формат без секций. Каждое значение META-тега описывается парой "имя=значение", например, файл может иметь следующий вид:

keywords = "Example, Test, Probe"
description = "This is the common text for all pages on this site"
author = "John Smith <john@example.com>"

2.2.1.4. MIME-типы (mimes.ini)

Содержимое этого файла используется при загрузке посетителем файлов и изображений с использованием скрипта <DocumentRoot>/usr/utils/download.php, а также при генерации эскизов изображений скриптом <DocumentRoot>/usr/utils/thumbnails.php.

Файл состоит из двух секций: inline и attachment. Каждая секция содержит перечень расширений файлов и MIME-типы, им соответствующие. Секция inline содержит те типы файлов, которые должны открываться в окне браузера, а при загрузке файлов с расширениями, перечисленными в секции attachment, браузер пользователя будет открывать диалог сохранения. Типичный файл mimes.ini имеет следующий вид:

[inline]
jpg   = image/jpeg
jpe   = image/jpeg
jpeg  = image/jpeg
png   = image/png
gif   = image/gif
htm   = text/html
html  = text/html
swf   = application/x-shockwave-flash

js    = application/x-javascript

[attachment]
pdf   = application/pdf
xls   = application/vnd.ms-excel
doc   = text/vnd.ms-word
ps    = application/postscript
ppt   = application/vnd.ms-powerpoint
rtf   = application/x-rtf

При необходимости разработчик может добавлять собственные расширения файлов и соответствующие им MIME-типы. Новые настройки будут автоматически использоваться при загрузке файлов пользователем.

2.2.1.5. Идентификаторы структуры данных (resource-ids.ini)

При разработке веб-ресурса программисту понадобится использовать идентификаторы типов, связей, коневых сущностей и т.п. Использование числовых констант в этих целях может значительно усложнить поддержку проекта и переработку кода. Поэтому рекомендуется вместо числовых идентификаторов использовать их мнемонические заменители. Для этих целей обычно используют или константы, или массивы параметров. Рекомендуется все константы, связанные со структурой данных, описывать в файле resource-ids.ini. Это позволяет использовать единый подход к хранению всех настроек сайта.

Замечание

Все настройки в файл разработчик вносит самостоятельно.

Замечание

Подробнее механизм типов и подтипов будет рассмотрен ниже.

Файл resource-ids.ini разделен на три секции: types, entities, dicitonaries. В разделе types описываются идентификаторы типов, подтипов и точек связей между типами:

[types]
news_line.etypeid     = 2  // Идентификатор типа "Лента новостей"
news_line.etid        = 2  // Идентификатор подтипа "Лента новостей"
news_line.news.etacid = 1  // Идентификатор вложения "Новости" в "Ленту новостей"

news.etypeid     = 3  // Идентификатор типа "Новость"
news.etid        = 3  // Идентификатор подтипа "Новость"

Как видно из примера, для типа мы определяем идентификатор типа, подтипа и все допустимые точки вложений. Такие описания повторяются для всех типов, присутствующих в структуре данных. В коде программы доступ к идентификаторам типов осуществляется через глобальный массив $TYPES, например:

  1. $etidNews = $TYPES["news.etid"]

Аналогично идентификаторам типов, при разработке используются идентификаторы корневых сущностей. На самом деле, в этом файле можно указывать идентификаторы любых сущностей, но это имеет смысл только если сама сущность часто используется в коде. Описание идентификатора сущности выглядит следующим образом:

[entities]
news.entid = 1

Последним разделом файла идентификаторов является раздел справочников. Формат описания справочника полностью совпадает с форматом раздела сущностей:

[dictionaries]
country.dicid = 1
cities.dicid = 2

Использование файла resource-ids.ini не обязательно и разработчик может использовать непосредственно числовые идентификаторы. Но использование мнемонических обозначений вместо цифр позволит значительно упростить разработку и поддержку кода.

2.2.2. Встроенные утилиты

Пустой проект содержит в себе три утилиты:

  • <DocumentRoot>/usr/utils/download.php - загрузка пользователем файлов

  • <DocumentRoot>/usr/utils/thumbnail.php - создание эскизов изображений

  • <DocumentRoot>/usr/utils/captcha.php - вывод изображения CAPTCHA

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

2.2.3. Скрипты и шаблоны

После установки пустого ресурса становятся доступными две страницы: главная (<DocumentRoot>/bin/home.php) и страница ошибки(<DocumentRoot>/bin/error.php). На главной странице выводится строка "Hello world". Страница ошибки появляется при обращении к несуществующему документу, попытке доступа к закрытым каталогам и т.п. Кроме того, страница ошибки будет обрабатывать все ошибки, возникающие при работе скриптов. Скрипт главной страницы является примером оформления кода сайта. Он демонстрирует типичную структуру скрипта, пример работы с кэшем сервера и вызов файла шаблона.

Для обеих страниц оформлены шаблоны. Они доступны в каталоге <DocumentRoot>/usr/skins/default/bin. Шаблон каждой страницы разделен на три части: верх (page_header.tpl.php), низ (page_footer.tpl.php) и информационная часть, уникальная для каждой страницы. В шапке страницы содержится та часть HTML-документа, которая не меняется для всего сайта. Как минимум, в ней должны размещаться все теги, отвечающие за заголовок страницы, META-данные, тег определения кодировки и т.п. Аналогично шаблон низа страницы, например, коды счетчиков, авторские права и другую информацию, которая будет появляться в низу каждой страницы.

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

2.3. Архитектура веб-сайта

В настоящий момент существует множество подходов к построению структуры веб-приложения. Все они имеют свои плюсы и минусы. Использование Cairo не накладывает никаких ограничений на разработчика в выборе архитектуры веб-приложения. В данном разделе описывается одна из рекомендуемых к применению методик.

2.3.1. Структура веб-приложения

Для любого приложения подразумевается его дальнейшее развитие. Оно будет обрастать новой функциональностью, в нем будут исправляться ошибки, оптимизироваться запросы и т.п. И, если не придерживаться правил написания кода, то, вернувшись в будущем к старому проекту, может потребоваться много времени для того, чтобы вспомнить его внутреннее устройство.

В силу этого многие программисты вырабатывают для себя некий стандарт написания приложения и придерживаются его в своей работе. В этом разделе приводится пример организации структуры веб-приложения и способы его взаимодействия с внешним миром. Многое из изложенного многократно упоминалось в открытых источниках и для кого-то будет лишь перечислением прописных истин. Тем не менее, для части аудитории прочтение этого раздела может оказаться полезным.

2.3.1.1. Файлы и каталоги

Файлы любого веб-приложения можно разделить на несколько групп: скрипты, реализующие бизнес-логику, шаблоны страниц, настройки, служебные файлы. На самом деле таких групп гораздо больше, к ним можно отнести таблицы стилей, изображения, документы, которые пользователь может загрузить с сайта и многое другое. При создании структуры каталогов проекта не следует смешивать файлы разных групп в одном каталоге. Более того, иерархия каталогов должна быть интуитивно понятна.

Мы предлагаем использовать следующую структуру каталогов:

  • /bin - каталог скриптов, реализующих бизнес-логику

  • /etc - каталог настроек

  • /tmp - каталог временных файлов

  • /usr - каталог вспомогательных программ и библиотек

    • /skins - каталог тем оформления

    • /utils - каталог утилит

  • /var - каталог служебных файлов

В каталоге /bin располагаются скрипты, обрабатывающие запросы к страницам ресурса. Чаще всего один скрипт занимается обработкой класса страницю Например, скрипт article.php может выводить любую статью по переданному в параметре идентификатору, а home.php выводит только главную страницу сайта.

Содержимое каталога настроек подробно рассмотрено в разделе Файлы настроек веб-ресурса. Если при разработке проекта необходимо использовать дополнительные настройки, то они должны размещаться в каталоге /etc.

Каталог /var содержит файлы и директории, к которым скрипты имеют доступ на запись. К таким файлам относятся: файлы кэша, изображения и документы, загруженные администратором, журналы и т.п. При установке пустого проекта здесь создается четыре каталога: /var/cached, /var/frmtext-files, /var/log, /var/uploaded. Первый - каталог кэша, он используется для хранения эскизов изображений, кэша страниц и т.п. Каталог frmtext-files используется WYSIWYG-редактором для хранения собственных изображений. Директория /var/log используется для хранения файлов системных сообщений и уведомлений о других событиях, здесь же могут храниться пользовательские журналы приложения. В каталог /var/uploaded помещаются все файлы, загруженные с помощью интерфейса администратора, такие как изображения товаров, фото-галереи и т.д.

Если разработчик использует в проекте вспомогательные библиотеки, утилиты или другой код, то он должен располагаться внутри каталога /usr. Вместе с шаблонным проектом поставляются три утилиты: загрузка файлов, создание эскизов изображений и генерация изображения CAPTCHA. Они расположены в каталоге /usr/utils. Все дополнительные утилиты должны помещаться сюда же.

Часто возникает необходимость использовать нескольких тем оформления на сайте. Под темой оформления подразумевается набор HTML-шаблонов, изображений и таблиц стилей, используемых в дизайне сайта. Каждая тема оформления представляет собой каталог, расположенный по адресу /usr/skins. По умолчанию определена одна тема - default. При создании новых тем следует использовать ту же структуру каталогов, которая применена в теме по умолчанию.

Изначально тема оформления содержит два каталога: /bin и /media. Каталог /bin содержит шаблоны скриптов обработки страниц, описанных выше. В каталоге /media располагаются файлы таблиц стилей, изображения и файлы Javascript, используемые на сайте. Каждый тип файлов располагается в соответствующем подкаталоге. При этом в коде HTML доступен короткий путь, по которому можно получить доступ к содержимому директории. Например, ссылка на изображение оформляется следующим образом:

<img src="/media/images/logo.jpg" alt="Logo">

а подключение таблицы стилей выглядит так:

<link rel="stylesheet" type="text/css" href="/media/css/common.css">

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

2.3.1.2. Работа с данными

Одной из главных причин ошибок безопасности при разработке сайтов является некорректная работа с потоками данных. На рабочем сайте существует множество потоков данных: пользователи оставляют записи в гостевой книге, данные из БД выводятся на страницах, электронные сообщения отправляются через форму обратной связи и многое другое.

В каждом из потоков данные переходят через множество сред, например, при выводе сообщения в гостевой книге, информация, хранимая в БД, извлекается приложением, обрабатывается и выводится в HTML-документ. Т.е. проходит через три среды: база данных, приложение, HTML-документ. Внутри отдельной среды данные не представляют никакой угрозы, нарушение безопасности происходит на переходах между средами. Например, текст "<script language="javascript">...</script>" не несет никакой угрозы для БД и может бесконечно в ней хранится, но стоит лишь "забыть" экранировать эту строку при выводе на HTML-странице и в веб-приложении появляется XSS-уязвимость.

Таким образом, становится очевидным, что необходим промежуточный слой (драйвер) между всеми средами, с которыми работает приложение (см. рис. 2.1).

Рисунок 2.1. Архитектура веб-приложения

Архитектура веб-приложения

Платформа Cairo предоставляет библиотеки для организации взаимодействия приложения с базой данных, HTML-документом и заголовками протокола HTTP, так как это наиболее частоупотребимые среды работы веб-приложения. Эти драйверы подробно рассмотрены в разделах Работа с базой данных и Шаблоны страниц.

Еще одним важным потоком данных являются данные, передаваемые пользователем. Это могут быть как данные, переданные с использованием формы, так и параметры запроса, переданные методом GET или содержимое COOKIES. Для того, чтобы обезопасить приложение от возможных атак необходимо придерживаться одного простого правила: "нельзя доверять никаким данным, пришедшим со стороны пользователя". Это означает, во-первых, явное объявление всех переменных, используемых в скрипте, во-вторых - проверка ожидаемых типов параметров переданных пользователем. Выполнение этих двух правил значительно повысит безопасность веб-приложения и снизит вероятность успешной атаки на него.

Пример 2.1. Пример обработки данных, полученных методом GET

  1. <?php
  2. ...
  3. $id    = empty($_GET["id"])    ? 0  : (int)$_GET["id"];
  4. $categ = empty($_GET["categ"]) ? "" : (string)$_GET["categ"];
  5. ...
  6. ?>

В вышеприведенном примере видно как следует обрабатывать данные, пришедшие от пользователя методом GET. Аналогичным образом должны обрабатываться и данные POST-запросов, и содержимое COOKIE. Этот код должен выполнятся до первого обращения к переменным и нигде в скрипте прямое обращение к массивам $_GET, $_POST или $_COOKIE фигурировать не должно.

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

PHP предоставляет стандартные функции для работы с MySQL, однако, их неосторожное использование может повлечь за собой множество скрытых ошибок. Например, недостаточная проверка данных, введенных пользователем, может позволить злоумышленнику использовать SQL-инъекцию для получение неавторизированного доступа, порчи данных и многого другого. Поэтому платформа Cairo предлагает собственный интерфейс доступа к БД, который исключает возможность нарушения логики работы программы при использовании некорректных данных.

2.3.2.1. Передача параметров в запросы

В большинстве случаев разработчику не потребуется использовать прямой доступ к БД. Тем не менее, при работе с сущностями, также как и при прямом доступе к базе данных, используется один и тот же механизм передачи параметров в запросы. Поэтому ознакомление с этим разделом позволит писать более безопасный код, даже если вы не обращаетесь напрямую к БД.

Самым слабым местом безопасности при работе с БД является передача параметров в запрос. При использовании стандартного интерфейса к БД рекомендуется все передаваемые параметры экранировать с помощью функции mysql_real_escape_string():

  1. $res = mysql_query("SELECT * FROM `users` WHERE `login`='".mysql_real_escape_string($login)."'");

При этом код выглядит очень громоздко, особенно, если в запрос передается много параметров. Дмитрий Котеров для упрощения передачи параметров в запросы предложил использовать маркеры (placeholders). Идея подхода заключается в том, что запрос описывается полностью и в нем лишь обозначаются места, которые при выполнении будут заполнены значениями параметров. Например, вышеописанный запрос будет выглядеть следующим образом:

  1. $res = CSystem::sql_query("SELECT * FROM `users` WHERE `login`=?", $login);

Подсказка

Класс CSystem содержит в себе методы для работы с БД, а также некоторые вспомогательные функции. Он подключается автоматически при инициализации системы и сразу доступен для использования. Подробнее об этом классе можно узнать из документа "Система управления контентом CAIRO: Справочное руководство по API".

Как можно видеть, при использовании меркеров запись сократилась и код стал более читаемым. Система поддерживает большое число типов маркеров: простые, именованные, массивы, хэши и т.п. Все они позволяют упростить процесс программирования и сделать код более наглядным. Рассмотрим подробнее работу с маркерами.

В общем виде маркер имеет следующий синтаксис:

?[<символ типа маркера>][<имя маркера>]

Существует три основных типа маркеров: скалярный, массив и хэш. Каждому из этих типов соответствует собственный символ. Если символ типа не указан, то маркер рассматривается как скалярный. Скалярные маркеры используются для передачи в запрос простых типов данных: число, строковый, с плавающей точкой:

Пример 2.2. Использование скалярных маркеров и констант

  1. // Константа определяет имя таблицы
  2. $res  = CSystem::sql_query( "SELECT * FROM tbl WHERE a=? AND b=?", 10, "test");
  3.  
  4. // В результате запрос примет вид:
  5. // SELECT * FROM tbl WHERE a=10 AND b='test'

Следует обратить внимани на то, что кавычки добавились только в строковом параметре. В числовых параметрах кавычки не ставятся никогда. Это связано с тем, что при выборке одним из частоупотребимых параметров являются метки оператора LIMIT, а в них использование кавычек не допускается.

Маркеры-массивы используются когда необходимо в запросе использовать перечень значений, разделенных запятой. Хеши могут использоваться только в запросах UPDATE. Они выводят через запятую пары "ключ=значение".

Пример 2.3. Использование маркер-массивов и маркеров-хешей

  1. // Использование маркера-массива
  2. $res = CSystem::sql_query(
  3.     "SELECT * FROM tbl WHERE a IN (?@)",
  4.     array(10, 20, 30, 40, 50, 60, 70, 80, "one")
  5. );
  6.  
  7. // Откомпилированный запрос:
  8. // SELECT * FROM tbl WHERE a IN (10, 20, 30, 40, 50, 60, 70, 80, 'one')
  9.  
  10.  
  11. // Использование маркера-хэша
  12. $res = CSystem::sql_query(
  13.     "UPDATE tbl SET ?% WHERE a IN (?@)",
  14.     array(
  15.         "a" => "Alice",
  16.         "b" => "Bob",
  17.     ),
  18.     array(1,2,3,4)
  19. );
  20.  
  21. // Откомпилированный запрос:
  22. // UPDATE tbl SET a='Alice', b='Bob' WHERE a IN (1, 2, 3, 4)

Этот механизм также позволяет использовать именованные маркеры. В этом случае все параметры перечисляются в ассоциативном массиве, который передается методу во втором аргументе. Следует отметить, что в запросе могут быть либо только простые, либо только именованные маркеры - использовать их вперемешку нельзя.

Пример 2.4. Использование именованных маркеров

  1. $res = CSystem::sql_query(
  2.     "UPDATE tbl SET ?%fields WHERE a IN (?@ids)",
  3.     array(
  4.         "fields" => array(
  5.                         "a" => "Alice",
  6.                         "b" => "Bob",
  7.                     ),
  8.         "ids"    => array(1,2,3,4),
  9.     );
  10. );
  11.  
  12. // Откомпилированный запрос:
  13. // UPDATE tbl SET a='Alice', b='Bob' WHERE a IN (1, 2, 3, 4)

2.3.2.2. Выборка результатов запроса

Выборка результатов запросов - это та часть кода, которая практически без изменений используется в самых разных частях приложения. При использовании стандартных функций PHP запрос к БД и выборка его результатов выглядят примерно так:

  1. // Получаем список пользователей
  2. $res = mysql_query("SELECT * FROM users") or die("SQL query error");
  3.  
  4. $arUsers = array();
  5. while ( $cur = mysql_fetch_assoc($res) ) {
  6.     $arUsers[] = $cur;
  7. }

Этот код, с некоторыми вариациями, будет повторятся после каждого запроса к БД. При этом программа получается громоздкой и неаккуратной. Платформа Cairo содержит функции, упрощающие выборку данных из БД. Во все методы передается указатель на результат запроса и они возвращают данные в виде массивов (кроме метода CSystem::sql_result_element()).

Таблица 2.2. Методы, используемые для выборки результатов SQL-запросов

Имя методаОписание
CSystem::sql_result_array($result)Результат возвращается в виде двумерного массива, каждая строка которого представляет собой хэш "имя поля"=> "значение".
CSystem::sql_result_assoc($result, $key)Метод аналогичен предыдущему, но результат возвращает в виде хэша, значениями которого являются хэши "имя поля"=> "значение". Имя поля, которое будет использоваться в роли ключа передается во втором параметре.
CSystem::sql_result_element($result)Метод возвращает значение первого поля первого ряда.
CSystem::sql_result_ids($result)Метод возвращает массив, состоящий из значений первого столбца результата.
CSystem::sql_result_row($result)Метод возвращает только первый рядок из результата.

Рассмотрим пример, когда мы получаем список всех пользователей в системе. Пусть база данных имеет следующую структуру:

# Table "test_users" structure
CREATE TABLE `test_users` (
    `uid`  INT(11) NOT NULL PRIMARY KEY AUTO_INCREMENT,
    `name` VARCHAR(50)
);

# Table "test_users" data
INSERT INTO `test_users` VALUES (1, 'Alice');
INSERT INTO `test_users` VALUES (2, 'Bob');

Операция запроса будет всегда выглядеть следующим образом:

  1. $res = CSystem::sql_query("SELECT * FROM `test_users`");

После выполнения этой строки переменная $res будет содержать указатель на результат выполнения SQL-запроса. В дальнейшем этот результат может обрабатываться по-разному. В первую очередь представим все строки в виде перечня рядков:

  1. $users = CSystem::sql_result_array($res);
  2. var_dump($users);
  3.  
  4. // Результат:
  5. // array(2) {
  6. //   [0]=>
  7. //   array(2) {
  8. //     ["uid"]=>
  9. //     string(1) "1"
  10. //     ["name"]=>
  11. //     string(5) "Alice"
  12. //   }
  13. //   [1]=>
  14. //   array(2) {
  15. //     ["uid"]=>
  16. //     string(1) "2"
  17. //     ["name"]=>
  18. //     string(3) "Bob"
  19. //   }
  20. // }

Для того, чтобы получить ассоциативный массив пользователей с ключами по имени пользователя, то следует использовать метод CSystem::sql_result_assoc(). Естественно, надо быть уверенными в том, что в поле-ключе не будет повторяющихся значений. Если это все же произойдет, то в результирующем массиве будет присутствовать только одна запись с повторяющимся ключом.

  1. $users = CSystem::sql_result_assoc($res, "name");
  2. var_dump($users);
  3.  
  4. // Результат:
  5. // array(2) {
  6. //   ["Alice"]=>
  7. //   array(2) {
  8. //     ["uid"]=>
  9. //     string(1) "1"
  10. //     ["name"]=>
  11. //     string(5) "Alice"
  12. //   }
  13. //   ["Bob"]=>
  14. //   array(2) {
  15. //     ["uid"]=>
  16. //     string(1) "2"
  17. //     ["name"]=>
  18. //     string(3) "Bob"
  19. //   }
  20. // }

Можно выбрать только идентификаторы пользователей:

  1. $users = CSystem::sql_result_ids($res);
  2. var_dump($users);
  3.  
  4. // Результат:
  5. // array(2) {
  6. //   [0]=>
  7. //   string(1) "1"
  8. //   [1]=>
  9. //   string(1) "2"
  10. }

Также можно получить только первый ряд:

  1. $users = CSystem::sql_result_row($res);
  2. var_dump($users);
  3.  
  4. // Результат:
  5. // array(2) {
  6. //   ["uid"]=>
  7. //   string(1) "1"
  8. //   ["name"]=>
  9. //   string(5) "Alice"
  10. // }

Последним возможным вариантом выборки является выборка одного элемента:

  1. $users = CSystem::sql_result_element($res);
  2. var_dump($users);
  3.  
  4. // Результат:
  5. // string(1) "1"

Описанные методы предоставляют всю функциональность, которая может потребоваться при работе с БД. Они просты в использовании и обладают достаточной гибкостью.

2.3.3. Обработка ошибок

Удобный механизм обработки ошибок может значительно облегчить процесс разработки и отладки веб-приложения. Будучи хорошо спроектированным, такой механизм позволяет быстро локализовать место в коде, где произошла ошибка и получить дополнительные сведения, включая системные сообщения.

Используемый в Cairo механизм обработки ошибок позволяет не только определить строки, в которых произошла ошибка, но и проследить весь путь сообщения об ошибке.

В данном разделе детально рассматривается механизм обработки ошибок и способы его применения.

2.3.3.1. Системный стек ошибок

Ошибка представляет собой объект класса CError. Каждый раз, когда необходимо организовать возврат ошибки, создается новый объект CError и помещается в системный стек ошибок методом CCore::error_push().

Пример 2.5. Обработка ошибок внутри функций

  1. <?php
  2. function foo() {
  3.  
  4.     ...
  5.     do {
  6.         $sFname = "test.txt";
  7.         if (!$hdlFp = fopen($sFname, "r")) {
  8.             CCore::error_push(CError::create(
  9.                 "Can't open file",
  10.                 __FILE__,
  11.                 __LINE__,
  12.                 "",
  13.                 $sFname
  14.             ));
  15.             break;
  16.         }
  17.  
  18.         ...
  19.  
  20.         return true;
  21.     } while (false);
  22.  
  23.     // Обработка ошибок
  24.     ...
  25.     return false;
  26. }
  27. ?>

Все ошибки помещаются в системный стек и в последствии могут быть извлечены и записаны в файл журнала, а также выведены на экран или отправлены администратору по почте. Работа со стеком ошибок осуществляется через методы класса CCore.

2.3.3.2. Структура скрипта для качественной обработки ошибок

В предыдущем примере демонстрировалось, как можно организовать обработку ошибки внутри функции. Но, иногда, обработка ошибок необходима и вне тела функции. Стандартных средств обработки ошибок в PHP нет, но этот механизм может быть реализован при помощи цикла while.

Пример 2.6. Обработка ошибок с помощью конструкции while

  1. <?php
  2. ...
  3. do {
  4.  
  5.     $s_fname = "test.txt";
  6.     if (!$hdl_fp = fopen($s_fname, "r")) {
  7.         // Создаем фатальную ошибку
  8.         CCore::error_push(CError::create(
  9.             "Can't open file",
  10.             __FILE__,
  11.             __LINE__,
  12.             "",
  13.             $s_fname
  14.         ));
  15.         break;
  16.     }
  17.  
  18.     if (!fclose($hdl_fp)) {
  19.         // Создаем не фатальную ошибку
  20.         CCore::error_push(CError::create(
  21.             "Can't open file",
  22.             __FILE__,
  23.             __LINE__,
  24.             "",
  25.             $s_fname
  26.         ));
  27.     }
  28.     ...
  29.  
  30.     echo "OK";
  31.     exit;
  32.  
  33. } while (false);
  34. // Обработка фатальных ошибок
  35. ...
  36. ?>

Приведенная в примере конструкция позволяет организовать обработку как фатальных, так и не фатальных ошибок. То, каким образом должны обрабатываться ошибки, зависит от разработчика и требований приложения. Иногда достаточно просто их игнорировать, иногда необходимо выдавать пользователю сообщение об ошибке либо перенаправлять посетителя на закэшированный вариант страницы.

2.3.4. Интернационализация веб-интерфейса

Отдельный сайт может иметь несколько языковых версий. При этом должна быть обеспечена не только поддержка нескольких языков содержимого, но и интернационализация интерфейса: названий операций, сообщений, форматов дат, чисел и т.п.

Существует множество способов реализации многоязычности. Каждый из них имеет свои плюсы и минусы. В Cairo для интернационализации используется библиотека gettext, которая позволяет достаточно просто организовать вывод сообщений на разных языках, реализовать поддержку национальных форматов времени, дат, вещественных и целых чисел и многое другое. Использование библиотеки gettext обусловлено тем, что она, фактически, является стандартом де-факто для создания интернационализируемых приложений.

Все функции, необходимые для организации многоязычности, содержатся в библиотеке CCore.

2.3.4.1. Создание многоязычного интерфейса

Создание многоязычного интерфейса делится на две задачи:

  • оформление корректного вывода чисел, дат, денежных единиц и т.п.;

  • создание версий всех выводимых на сайте сообщений на нескольких языках.

Интернационализация основывается на понятии "локаль". Локаль - это набор правил, описывающих как необходимо сравнивать строки, в каком формате выводить дату, и т.п. Локали устанавливаются на сервер администратором. Скрипты пользователя могут работать только с теми локалями, которые имеются на сервере. В языке PHP для работы в условиях локали используются следующие функции:

  • setlocale() - Установка текущей локали;

  • localeconv() - Получение массива настроек текущей локали;

  • strcoll() - Сравнение строк;

  • strftime() - Формирование вывода даты/времени в соответствии с локалью.

Установка локали функцией setlocale() позволяет однозначно определить, в каком виде выдавать данные пользователю. Устанавливать локаль необходимо один раз в начале работы скрипта, иначе возможно перемешивание различных форматов вывода данных.

Второй стороной процесса интернационализации ресурса является предоставление пользователю всех сообщений системы на понятном ему языке. Для этого используются файлы переводов gettext. В них содержатся ключевые строки, обычно на английском языке, и переводы этих строк для одного языка.

Для того, чтобы определить, какой файл должен быть использован для поиска переводов строк, необходимо в начале скрипта вызвать метод CCore::bindtextdomain() и передать в качестве аргументов имя используемого файла (без расширения), а также путь доступа к данному файлу. Такой файл в терминах gettext называется доменом. Имя используемого домена устанавливается функцией CCore::textdomain().

Замечание

При создании сложных скриптов можно использовать несколько файлов с переводами. Для этого достаточно указать системе их местоположение при помощи функции CCore::bindtextdomain() и в нужные моменты переключать используемые файлы с помощью метода CCore::textdomain().

В скриптах все строки, которые должны быть переведены, оформляются в виде вызова функции CCore::lc():

  1. echo CCore::lc("Text to translate");

При выполнении скрипта gettext заменит все оформленные подобным образом строки их эквивалентом из файла переводов. Если такой эквивалент не будет найден, строка выведется в том виде, в котором она передавалась в функцию CCore::lc().

Функция CCore::lc() позволяет работать с несколькими файлами переводов без использования вызова CCore::textdomain(). Для этого во втором параметре следует указать имя файла переводов:

  1. echo CCore::lc("Text to translate", "messages");

Замечание

Все файлы переводов, используемые в CCore::lc() должны быть подключены с помощью функции CCore::bindtextdomain().

2.3.4.2. Создание файлов переводов

Файлы переводов gettext хранятся в отдельном каталоге. Они создаются с помощью специальных утилит, поставляемых вместе с библиотекой gettext. Например, для того, чтобы извлечь строки переводов из всех PHP-скриптов в каталоге, необходимо в консоли выполнить следующую операцию (только для UNIX-подобных операционных систем):

xgettext -c *.php -o translate.pot --keyword=lc

При этом в файле translate.pot будут собраны все строки из PHP-файлов, которые необходимо перевести. Файл будет содержать строки-оригиналы и пустые строки в местах, куда необходимо вписывать перевод.

Внимание

Файл должен заполняться в кодировке UTF-8.

После заполнения файла он должен быть переведен в бинарный формат. Для этого используется утилита msgfmt.

msgfmt -o translate.mo translate.pot

Файл, полученный после перекодировки, необходимо поместить в каталог локали. Для сайтов, построенных по шаблонному проекту, это путь имеет вид: <DocumentRoot>/etc/locale/<lang>/LC_MESSAGES.

После того как файл помещен в каталог локали, он может быть использован при написании скриптов.

2.3.5. Шаблоны страниц

Одним из главных показателей качественного веб-сайта является отделение кода от представления. Такой подход позволяет легко изменять дизайн проекта, не трогая его бизнес-логику. В идеальном случае дизайн может исправляться человеком, не имеющим высокой квалификации в программировании.

Одним из способов отделения кода от представления является использование шаблонизаторов, таких как Smarty или XTemplate. Основной идеей шаблонизаторов является полное исключение кода PHP из шаблона страницы. С другой стороны, язык PHP изначально разрабатывался как шаблонизатор и сам по себе великолепно справляется с этой задачей. Поэтому использование интерпретатора шаблонов является избыточной нагрузкой на сервер.

Однако, использование чистого PHP в качестве шаблонизатора лишает разработчика таких приятных дополнений, как система кэширования, генерация корректных заголовков Last-Modified, управление клиентским кэшированием и т.п. При использовании платформы Cairo эти и многие другие функции доступны в классе cfPage. Класс cfPage является оболочкой, которая предоставляет программный интерфейс для формирования шаблонов. Среди его функций следует отметить:

  • методы для экранирования данных при выводе

  • автоматическое определение значения заголовка Last-Modified на основе использованных сущностей

  • сжатие выходного потока (gzip)

  • подключение подшаблонов

  • управление кэшированием на стороне клиента

  • управление кэшированием на стороне сервера

Код шаблона выполняется в контексте класса, т.е. все методы доступны через указатель $this. При создании класса в конструктор следует передать полный путь к выводимому шаблону и явно указать все данные, которые будут из него доступны. Если шаблон не известен на этапе создания страницы, то его можно задать позднее с помощью метода cfPage::setTemplate(). В нижеследующем примере показано простейшее подключение шаблона и вывод данных внутри него.

Пример 2.7. Пример простого скрипта и шаблона

  1. <?php
  2. /**
  3. * Простой скрипт
  4. */
  5.  
  6. // Инициализируем систему
  7. require_once($_SERVER["DOCUMENT_ROOT"]."/etc/init.php");
  8.  
  9. // Подключаем класс cfPage
  10. require_once($GLOBALS["CFPARAMS"]["basedir"]."/interface/view/cfpage.class.php");
  11.  
  12. do {
  13.  
  14.     /*****************************
  15.      *
  16.      * Создаем страницу
  17.      *
  18.      *****************************/
  19.     if ( !$oPage = cfPage::create() ) {
  20.         CCore::error_push(CError::create(CCore::lc("Page creation error"), __FILE__, __LINE__));
  21.         break;
  22.     }
  23.  
  24.     // Сообщение, которое будем выводить в шаблоне
  25.     $message = "Hello world!";
  26.  
  27.     // Определяем используемый шаблон
  28.     if ( !$oPage->setTemplate($_CONFIG["general.tpldir"]."/bin/simple.tpl.php") ) {
  29.         CCore::error_push(CError::create(CCore::lc("Page creation error"), __FILE__, __LINE__));
  30.         break;
  31.     }
  32.  
  33.     // Регистрируем данные для использования в шаблоне
  34.     $oPage->addData("Message", $message);
  35.  
  36.     // Генерируем страницу
  37.     $oPage->build();
  38.  
  39. } while (false);
  40.  
  41. // Произошла ошибка
  42. // Подключаем стандартный обработчик
  43. require_once($_CONFIG["general.basedir"]."/bin/error.php");
  44. ?>
  1. <?php
  2. /**
  3. * Простой шаблон (simple.tpl.php)
  4. */
  5. ?>
  6. <html>
  7. <body>
  8.     <h1><?=$this->html($Message)?></h1>
  9. </body>
  10. </html>

Все данные, за редким исключением, не могут выводиться в шаблоне в том виде, в котором они существовали в приложении. Это обусловлено тем, что некоторые символы имеют особое значение и для каждого отдельного формата вывода спецсимволы должны быть обработаны по-своему. Например, спецсимволы HTML "&", "<", ">" в выводимых данных должны быть заменены на "&amp;", "&lt;", "&gt;" соответственно. Если же предполагается вывод в JavaScript, то все кавычки в строках должны экранироваться обратными слэшами. Класс cfPage предоставляет следующие методы для экранирования данных (на самом деле эти методы наследуются от базового класса cfView):

  • cfPage::html() - подготовка строки для вывода в HTML

  • cfPage::htmlAmp() - аналогично предыдущему, но символ "&" заменяться не будет

  • cfPage::js() - подготовка строки для вывода в JavaScript

  • cfPage::textToHtml() - преобразование многострокового текста в его аналог в HTML (переносы строк заменяются на "<br />")

Пример 2.8. Примеры использования методов экранирования данных в шаблонах

  1. <?php
  2.  
  3. // Простая строка
  4. $str  = "Hello world!";
  5.  
  6. // Многострочный текст
  7. $text = "Hello
  8. world!";
  9.  
  10. ?>
  11. <html>
  12. <body>
  13.  
  14.     <h3>Вывод простой строки в HTML</h3>
  15.     <p><?=$this->html($str)?></p>
  16.  
  17.     <h3>Вывод простой строки в JavaScript</h3>
  18.     <p><a href="javascript: alert('<?=$this->js($str)?>');">Click me</a></p>
  19.  
  20.     <h3>Вывод текста в HTML</h3>
  21.     <p><?=$this->textToHtml($text)?></p>
  22.  
  23.     <h3>Вывод текста в JavaScript</h3>
  24.     <p><a href="javascript: alert('<?=$this->js($text)?>');">Click me</a></p>
  25.  
  26. </body>
  27. </html>

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

При работе удобно выделять некоторые части шаблона в отдельные файлы (подшаблоны). Это позволяет избежать повторения одинакового кода и облегчает его поддержку и изменение. Например, страницу можно разбить на три части: шапка, основная (информативная) часть и низ. Причем шапка (заголовок, меню) и низ (счетчики, копирайты) будут одинаковыми для всех страниц сайта, а основная часть будет изменяться. Поэтому удобно вынести верх и низ в отдельные шаблоны и подключать их каждый раз при генерации страницы. Класс cfPage содержит метод cfPage::template(), который позволяет подключать подшаблоны. В метод передается имя подключаемого шаблона и класс его ищет в том же каталоге, где находится основной шаблон. Если имя метода указано с полным путем, то будет использован именно он.

Генерация большей части страниц связана с работой с сущностями. Класс cfPage автоматически определяет даты изменения сущностей, с которыми вы работали, выбирает из них последнюю и генерирует заголовок Last-Modified. Генерация заголовка происходит прозрачно для программиста и не требует никаких усилий с его стороны. Однако, следует учитывать что анализируются только сущности, и, если вы используете и другие источники данных, то придется позаботиться о корректной отдаче заголовков. Для этого достаточно воспользоваться методом CCore::update_last_modified() и передать ему дату последней модификации вашего источника данных в формате UNIX-timestamp. Если она окажется самой поздней, то будет использоваться при генерации заголовка Last-Modified.

Если установлена глобальная настройка $_CONFIG["webface.gzip"] и браузер посетителя поддерживает работу со сжатым контентом, то cfPage после завершения генерации сожмет полученный HTML с помощью алгоритма gzip и в таком виде отправит его пользователю. Использование сжатия контента позволяет значительно уменьшить размер страницы, тем самым уменьшив объем трафика, передаваемого клиенту.

Значительно уменьшить нагрузку на сервер позволяет использование кэширования. При разработке веб-приложения кэширование можно разделить на два типа: кэширование на стороне клиента и кэширование на стороне сервера. Кэширование на стороне клиента осуществляется средствами протокола HTTP. Подробное описание возможностей протокола можно найти в соответствующем стандарте (RFC 2068). Кэширование на стороне сервера заключается в сохранении в файловой системе для последующего использования результатов работы функций, программ и т.д. В контексте cfPage кэширование заключается в сохранении сгенерированной страницы и использовании сохраненной версии для последующей его отдачи по запросам посетителей.

Упрощенный вариант управления кэшем браузера заключается в использовании метода cfPage::clientCache(). Этот метод имеет один аргумент - флаг, который показывает включено ли кэширование на стороне пользователя или нет. По умолчанию кэширование включено. При включенном кэшировании браузера класс cfPage будет проверять изменилась ли страница с времени последнего просмотра и, если не изменилась, то пользователю будет отправлен соответствующий заголовок, и он увидит данные из своего локального кэша. При отключенном кэшировании таких проверок не происходит. Для более тонкой работы с кэшем браузера разработчику потребуется использовать заголовки протокола HTTP.

Пример 2.9. Пример управления кэшем браузера

  1.     if ( !$oPage = cfPage::create() ) {
  2.         CCore::error_push(CError::create(CCore::lc("Page creation error"), __FILE__, __LINE__));
  3.         break;
  4.     }
  5.  
  6.     if ( !$oPage->setTemplate($_CONFIG["general.tpldir"]."/bin/mytemplate.tpl.php") ) {
  7.         CCore::error_push(CError::create(CCore::lc("Page creation error"), __FILE__, __LINE__));
  8.         break;
  9.     }
  10.  
  11.     // Страница живет в кэше браузера 1 час
  12.     $oPage->addHeader("Expires", gmdate("D, d M Y H:i:s", time() + 3600)." GMT");
  13.     $oPage->addHeader("Cache-Control", "max-age=3600; public");
  14.  
  15.     $oPage->build();

Для работы с кэшем сервера достаточно после создания экземпляра класса cfPage вызвать метод cfPage::checkCache(). При этом будет проверено наличие файла в дисковом кэше и его актуальность. И, если он удовлетворяет условиям, то его содержимое будет отправлено пользователю. На этом скрипт завершит работу. Если же файла нет или он устарел, данные будут снова получены, а сформированный HTML-документ сохранится в кэш. Метод cfPage::checkCache().

Замечание

Класс cfPage позволяет кэшировать только сгенерированные страницы целиком. Другие варианты кэширования (кэширование результатов запросов, выполнения функций и т.п.) реализуются с помощью класса cfCache.

Пример 2.10. Пример работы с кэшем сервера

  1.     if ( !$oPage = cfPage::create() ) {
  2.         CCore::error_push(CError::create(CCore::lc("Page creation error"), __FILE__, __LINE__));
  3.         break;
  4.     }
  5.  
  6.     // Проверяем дисковый кэш
  7.     if ( !$oPage->checkCache() ) {
  8.         CCore::error_push(CError::create(CCore::lc("Page creation error"), __FILE__, __LINE__));
  9.         break;
  10.     }
  11.  
  12.     // Кэш не найден, генерируем страницу
  13.  
  14.     ....
  15.     // Получение данных для вывода на странице
  16.     ....
  17.  
  18.     if ( !$oPage->setTemplate($_CONFIG["general.tpldir"]."/bin/mytemplate.tpl.php") ) {
  19.         CCore::error_push(CError::create(CCore::lc("Page creation error"), __FILE__, __LINE__));
  20.         break;
  21.     }
  22.  
  23.     // Страница живет в кэше браузера 1 час
  24.     $oPage->addHeader("Expires", gmdate("D, d M Y H:i:s", time() + 3600)." GMT");
  25.     $oPage->addHeader("Cache-Control", "max-age=3600; public");
  26.  
  27.     $oPage->build();

2.4. Выборка и вывод данных

Упрощение работы со сложными структурами данных - главная цель создания Cairo. Структуры данных, используемые на большинстве сайтов, имеют схожие черты, их обобщение позволило создать мощный и гибкий инструмент для управления информацией.

Вся информационная модель платформы Cairo базируется на понятии "Сущность". Сущности являются узлами дерева информационной структуры и могут представлять самую разнородную информацию. Работа с данными может быть условно разделена на три типа операций:

  • чтение экземпляра сущности;

  • чтение списка сущностей;

  • добавление, обновление и удаление сущности.

Для работы с данными используются два класса: CEntity и CEntityList. Первый класс используется для работы с сущностью: создание, обновление, получение параметров по идентификатору и т.п. Второй - позволяет делать выборку сущностей с применением различных условий фильтрации, сортировок и многого другого. Далее будет показано как пользоваться этими классами, а отмечены некоторые тонкости работы со структурой данных системы Cairo.

2.4.1. Базовые операции выборки данных

В этом разделе будут рассмотрены базовые операции по выборке данных. Эта информация позволит понять основные принципы работы с данными в Cairo. В следующих разделах будут приведены более сложные методы выборки данных: группировка, сортировка, выборка с учетом древовидной структуры и т.п.

2.4.1.1. Получение информации о сущности

Для получения сущности любого типа используется класс CEntity. Единственный параметр, который необходимо передавать в конструктор - идентификатор сущности, параметры которой извлекаются из базы данных.

Пример 2.11. Получение параметров сущности

  1. <?php
  2. ...
  3. // Идентификатор сущности
  4. $ENTID = 54;
  5.  
  6. if (!$oEntity = CEntity::create($ENTID)) {
  7.     CCore::print_errors();
  8.     exit;
  9. }
  10. var_dump($oEntity->params);
  11. ...
  12. ?>

Приведенный пример получает из БД параметры сущности с идентификатором "54". Если такой сущности в базе нет, то будет выведено сообщение об ошибке. После создания экземпляра сущности все ее параметры будут храниться в ассоциативном массиве CEntity::params.

Кроме параметров сущности объект $oEntity содержит много вспомогательной информации: настройки типа сущности, полей и т.п. Подробнее о классе CEntity можно узнать в документе "Система управления контентом CAIRO: Справочное руководство по API".

2.4.1.2. Получение списка сущностей

Получение списка сущностей является ключевой операцией при организации информационных ресурсов. Она используется при выводе перечня, каталога, меню и во многих других задачах, часто встречающихся при разработке веб-ресурсов.

Для получения списка сущностей используется класс CEntityList. Класс позволяет получать данные в нескольких режимах:

  • получение сущностей одного подтипа;

  • получение сущностей одного типа;

  • получение сущностей разных типов.

Выбор режима влияет на набор полей, получаемых из БД. Если используется режим подтипа, то можно получать значения любых полей этого подтипа. В случае режима типа множество полей будет ограничено множеством полей типа. Если же необходимо получить список разнотипных сущностей, то будут доступны только поля, общие для всех сущностей.

Приведем пример получение списка сущностей с идентификатором подтипа "7" без применения дополнительных фильтраций.

Пример 2.12. Получение списка сущностей без применения фильтров

  1. <?php
  2. ...
  3. // Идентификатор подтипа
  4. $ETID = 7;
  5.  
  6. if (!$oList = CEntityList::create(0, $ETID)) {
  7.     CCore::print_errors();
  8.     exit;
  9. }
  10.  
  11. if (!$oList->get_list()) {
  12.     CCore::print_errors();
  13.     exit;
  14. }
  15. ...
  16. ?>

После выполнения приведенного кода, свойство $oList->items будет содержать массив экземпляров класса CEntity, причем, будут извлекаться все параметры для каждой сущности.

Следует отметить, что получение данных в виде массива объектов CEntity не обязательно и часто достаточно списка ассоциативных массивов. В следующем примере будет получен тот же список сущностей, но свойство $oList->items будет содержать уже список ассоциативных массивов.

Пример 2.13. Получение списка сущностей в виде перечня ассоциативных массивов.

  1. <?php
  2. ...
  3. // Идентификатор подтипа
  4. $ETID = 7;
  5.  
  6. if (!$oList = CEntityList::create(0, $ETID, CENTITYLIST_RT_ASSOC)) {
  7.     CCore::print_errors();
  8.     exit;
  9. }
  10.  
  11. if (!$oList->get_list()) {
  12.     CCore::print_errors();
  13.     exit;
  14. }
  15. ...
  16. ?>

В большинстве случаев, получение данных в виде ассоциативных массивов гораздо удобнее объектов. Во-первых, последующая работа с ассоциативным массивом проще, чем с перечнем объектов Во-вторых, объект может потребоваться только в том случае, если есть необходимость работать с каждой отдельной сущностью (обновление данных, работа со связями, др.). Если же такой работы не предвидится, то лучше использовать выборку в виде ассоциативного массива.

Для получения всех сущностей одного типа (даже если у них разные подтипы) следует использовать первый параметр конструктора CEntityList. В предыдущих примерах в первом параметре мы передавали ноль. Это означало, что мы используем выборку сущностей одного подтипа. Выборка же сущностей одного типа будет выглядеть следующим образом:

Пример 2.14. Получение списка сущностей одного типа.

  1. <?php
  2. ...
  3. // Идентификатор подтипа
  4. $ETYPEID = 4;
  5.  
  6. if (!$oList = CEntityList::create($ETYPEID, 0, CENTITYLIST_RT_ASSOC)) {
  7.     CCore::print_errors();
  8.     exit;
  9. }
  10.  
  11. if (!$oList->get_list()) {
  12.     CCore::print_errors();
  13.     exit;
  14. }
  15. ...
  16. ?>

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

Пример 2.15. Получение списка сущностей разных типов.

  1. <?php
  2. ...
  3.  
  4. if (!$oList = CEntityList::create(0, 0, CENTITYLIST_RT_ASSOC)) {
  5.     CCore::print_errors();
  6.     exit;
  7. }
  8.  
  9. if (!$oList->get_list()) {
  10.     CCore::print_errors();
  11.     exit;
  12. }
  13. ...
  14. ?>

2.4.2. Вывод данных

Предназначение любого веб-ресурса - предоставление удобного интерфейса для работы с данными. Чаще всего данные приходится просто выводить. В интернет сложились устоявшиеся стандарты на формы выводы данных, их всего две: вывод списка (возможно, разбитого на страницы), и вывод одной сущности (карточка товара, статья). Все остальные формы вывода можно рассматривать как варианты этих двух основных.

Во всех последующих примерах мы будем работать со структурой, представленной на рисунке:

Рисунок 2.2. Простейшая структура данных

Простейшая структура данных

Используемая структура данных представляет собой простейшую ленту новостей. Ее особенностью является то, что она может содержать новости двух типов: "Простые новости", состоящие из названия и текста и "Новости с фото", дополнительно содержащие изображение. Оба типа новостей представляют собой подтипы общего типа "Новость", что позволяет вкладывать их в единую точку вложения "Все новости". Точка вложения "Горячие новости" может содержать только уже существующие новости (символические ссылки) и позволяет отметить те новости, которые, например, надо показывать на главной странице сайта.

Получение параметров одной сущности (при известном идентификаторе) осуществляется с помощью класса CEntity, как это было описано выше. Параметры сущности доступны через свойство CEntity::$params. Работа с единичной сущностью предельно проста и рассматривать ее подробно не имеет смысла. Гораздо интереснее и разнообразнее является построение списков сущностей. И одной из задач, которые постоянно встречаются при выводе списков - организация постраничности.

За работу постраничности отвечает класс cfPaginator. Приведем пример скрипта, который использует разбиение списка на страницы.

Пример 2.16. Вывод списка сущностей с панелью постраничности

  1. <?php
  2. /**
  3. * Код ленты новостей
  4. *
  5. * Этот файл формирует массивы с новостями
  6. * и вызывает шаблон для их визуализации
  7. */
  8.  
  9. // Инициализируем систему
  10. require_once($_SERVER["DOCUMENT_ROOT"]."/etc/init.php");
  11.  
  12. // Подключаем класс cfPage
  13. require_once($GLOBALS["CFPARAMS"]["basedir"]."/interface/view/cfpage.class.php");
  14.  
  15. // Подключаем класс CEntityList
  16. require_once($_CONFIG["general.coredir"]."/centitylist.php");
  17.  
  18. // Подключаем класс cfPaginator
  19. require_once($GLOBALS["CFPARAMS"]["basedir"]."/interface/view/cfpaginator.class.php");
  20.  
  21.  
  22.  
  23. // Идентификатор ленты новостей
  24. define("NEWS_ENTID", 6);
  25.  
  26. // Идентификатор типа новостей (единый для простых и с фото)
  27. define("EVENT_ETYPEID", 4);
  28.  
  29.  
  30. // Читаем GET-параметры
  31. $page = empty($_GET["page"]) ? 0 : (int)$_GET["page"];
  32.  
  33. do {
  34.  
  35.     /*****************************
  36.      *
  37.      * Создаем страницу
  38.      *
  39.      *****************************/
  40.     if ( !$oPage = cfPage::create() ) {
  41.         CCore::error_push(CError::create(
  42.             CCore::lc("Page creation error"),
  43.             __FILE__,
  44.             __LINE__));
  45.         break;
  46.     }
  47.  
  48.  
  49.  
  50.     /****************************************
  51.      *
  52.      * Получение списка новостей
  53.      *
  54.      ****************************************/
  55.  
  56.     // Определяем номер страницы и ограничения LIMIT
  57.     $oPaginator = cfPaginator::create();
  58.  
  59.     // Определяем формат ссылки на панели постраничности
  60.     $oPaginator->setUrlFormat("?page=%p");
  61.  
  62.     // Определяем номер страницы
  63.     $oPaginator->setPage($page);
  64.  
  65.     // Формируем выражение LIMIT для запроса
  66.     $sqlLimit = $oPaginator->getFirstItem().", ".$oPaginator->getItemsPerPage();
  67.  
  68.  
  69.     if ( !$oList = CEntityList::create(EVENT_ETYPEID, 0, CENTITYLIST_RT_ASSOC) ) {
  70.         CCore::error_push(CError::create(
  71.             CCore::lc("Can't create entity list"),
  72.                 __FILE__,
  73.                 __LINE__,
  74.                 ""));
  75.         break;
  76.     }
  77.  
  78.     if ( !$oList->get_list( "",
  79.                             "",
  80.                             "",
  81.                             "",
  82.                             "",
  83.                             "",
  84.                             $sqlLimit,
  85.                             CENTITYLIST_COUNT_TOTAL,
  86.                             array()
  87.                           ) ) {
  88.         CCore::error_push(CError::create(
  89.             CCore::lc("Can't get entity list"),
  90.             __FILE__,
  91.             __LINE__));
  92.         break;
  93.     }
  94.  
  95.     // Задаем для постраничности общее количество новостей
  96.     $oPaginator->setItemsTotal($oList->items_total_num);
  97.  
  98.  
  99.  
  100.     /****************************************
  101.      *
  102.      * Подключаем шаблон
  103.      *
  104.      ****************************************/
  105.  
  106.     // Определяем используемый шаблон
  107.     if ( !$oPage->setTemplate($_CONFIG["general.tpldir"]."/bin/news.tpl.php") ) {
  108.         CCore::error_push(CError::create(
  109.             CCore::lc("Page creation error"),
  110.             __FILE__,
  111.             __LINE__));
  112.         break;
  113.     }
  114.  
  115.     // Регистрируем данные для использования в шаблоне
  116.     $oPage->addData("Paginator", $oPaginator);
  117.     $oPage->addData("News", $oList->items);
  118.  
  119.     // Генерируем страницу
  120.     $oPage->build();
  121.  
  122. } while (false);
  123.  
  124. // Произошла ошибка
  125. // Подключаем стандартный обработчик
  126. require_once($_CONFIG["general.basedir"]."/bin/error.php");
  127. ?>
  1. <?php
  2. /**
  3. * Шаблон ленты новостей (news.tpl.php)
  4. *
  5. * Этот файл выводит ленту новостей и
  6. * две панели постраничности.
  7. */
  8.  
  9. // Подключаем заголовок страницы
  10. $this->template("page_header.tpl.php");
  11.  
  12. $Paginator->build();
  13. foreach ( $News as $item ) {
  14. ?>
  15.     <p><a href="./news_event.php?id=<?=$this->html($item["entid"])?>"
  16.     ><?=$this->html($item["title"])?></a></p>
  17. <?php
  18. }
  19. $Paginator->build();
  20.  
  21. // Подключаем низ страницы
  22. $this->template("page_footer.tpl.php");
  23. ?>

Как вы видите, структура программы аналогична приведенной в примере 2.7. Главным отличием является то, что в центральной части скрипта (строки 50-96) мы выбираем из базы данных информацию о списке новостей. Выборка состоит из подготовки выражения LIMIT и получения данных из БД с помощью класса CEntitylist.

За разбиение списка сущностей на страницы отвечает класс cfPaginator (строки 56-66). В функции этого класса входит обработка номера страницы, определение по нему значения для выражения LIMIT и вывод панели навигации со ссылками на соседние страницы. По умолчанию класс cfPaginator разбивает список на страницы по 20 элементов в каждой, однако, разработчик с помощью метода cfPaginator::setItemsPerPage() может изменить это значение.

Выборка данных (строки 78-96) осуществляется с помощью метода CEntityList::get_list(). Метод может принимать 9 основных параметров: выбираемые поля, дополнительно подключаемые таблицы, условия отбора, условия фильтрации (дополнительные условия отбора), правила группировки, сортировки, ограничения на количество выбираемых записей, флаг подсчета общего количества сущностей (без учета ограничения на количество) и массив параметров запроса. В нашем примере мы указали только ограничения на количество записей (LIMIT). Использование остальных параметров описывается в нижеследующих разделах.

При работе со списками всегда можно использовать описанный шаблон. Если в скрипте извлекается несколько списков одного типа, то можно использовать один объект CEntityList, если же списки разных типов, то для каждого списка создается свой экземпляр класса.

Следует обратить внимание на параметры конструктора CEntityList. В данном случае мы использовали идентификатор типа (etypeid) это позволяет работать как с простыми новостями, так и с новостями с фото. Если требуется вывести только один из подтипов, то используется соответствующий идентификатор (etid), который передается во втором параметре конструктора.

На странице новости будет выводиться одна сущность. Имея идентификатор сущности, ее параметры легко получить с помощью класса CEntity. Для этого в метод CEntity::create() в первом параметре передается идентификатор сущности. Если при получении параметров сущности произошла ошибка, то метод вернет false и ошибка будет занесена в системный стек.

Пример 2.17. Вывод одной сущности

  1. <?php
  2. /**
  3. * Код страницы новости
  4. *
  5. * Этот файл формирует массив параметров
  6. * новости.
  7. */
  8.  
  9. // Инициализируем систему
  10. require_once($_SERVER["DOCUMENT_ROOT"]."/etc/init.php");
  11.  
  12. // Подключаем класс cfPage
  13. require_once($GLOBALS["CFPARAMS"]["basedir"]."/interface/view/cfpage.class.php");
  14.  
  15. // Подключаем класс CEntity
  16. require_once($_CONFIG["general.coredir"]."/centity.php");
  17.  
  18.  
  19.  
  20. // Идентификатор ленты новостей
  21. define("NEWS_ENTID", 6);
  22.  
  23. // Идентификатор типов новостей
  24. define("EVENT_SIMPLE_ETID", 6);
  25. define("EVENT_PHOTO_ETID"7);
  26.  
  27. // Читаем GET-параметры
  28. $entidEvent = empty($_GET["id"]) ? 0 : (int)$_GET["id"];
  29.  
  30. do {
  31.  
  32.     /*****************************
  33.      *
  34.      * Создаем страницу
  35.      *
  36.      *****************************/
  37.     if ( !$oPage = cfPage::create() ) {
  38.         CCore::error_push(CError::create(
  39.             CCore::lc("Page creation error"),
  40.             __FILE__,
  41.             __LINE__));
  42.         break;
  43.     }
  44.  
  45.  
  46.  
  47.     /****************************************
  48.      *
  49.      * Получение новости
  50.      *
  51.      ****************************************/
  52.  
  53.     // Получаем сущность
  54.     if ( !$oEvent = CEntity::create($entidEvent) ) {
  55.         CCore::error_push(CError::create(
  56.             CCore::lc("Can't create entity list"),
  57.                 __FILE__,
  58.                 __LINE__,
  59.                 ""));
  60.         break;
  61.     }
  62.  
  63.     // Поверяем, является ли полученная сущность новостью
  64.     if ( $oEvent->params["etid"] != EVENT_SIMPLE_ETID && $oEvent->params["etid"] != EVENT_PHOTO_ETID ) {
  65.         CCore::error_push(CError::create(
  66.             CCore::lc("Invalid entity type"),
  67.                 __FILE__,
  68.                 __LINE__,
  69.                 ""));
  70.         break;
  71.     }
  72.  
  73.  
  74.     // Переводим данные в массив
  75.     $arEvent = array(
  76.         "entid" => $oEvent->params["entid"],
  77.         "title" => $oEvent->params["title"],
  78.         "topic" => $oEvent->params["topic"],
  79.         "photo" => empty($oEvent->params["photo"]) ? array() : array(
  80.             "url"    => "/files/".$oEvent->params["entid"]."/photo/".$oEvent->params["photo_realname"],
  81.             "width"  => $oEvent->params["photo_width"],
  82.             "height" => $oEvent->params["photo_height"],
  83.         ),
  84.     );
  85.  
  86.  
  87.     /****************************************
  88.      *
  89.      * Подключаем шаблон
  90.      *
  91.      ****************************************/
  92.  
  93.     // Определяем используемый шаблон
  94.     if ( !$oPage->setTemplate($_CONFIG["general.tpldir"]."/bin/news_event.tpl.php") ) {
  95.         CCore::error_push(CError::create(
  96.             CCore::lc("Page creation error"),
  97.             __FILE__,
  98.             __LINE__));
  99.         break;
  100.     }
  101.  
  102.     // Регистрируем данные для использования в шаблоне
  103.     $oPage->addData("Event", $arEvent);
  104.  
  105.     // Генерируем страницу
  106.     $oPage->build();
  107.  
  108. } while (false);
  109.  
  110. // Произошла ошибка
  111. // Подключаем стандартный обработчик
  112. require_once($_CONFIG["general.basedir"]."/bin/error.php");
  113. ?>
  1. <?php
  2. /**
  3. * Страница новости
  4. *
  5. * Этот файл выводит страницу новости
  6. */
  7.  
  8. // Подключаем заголовок страницы
  9. $this->template("page_header.tpl.php");
  10. ?>
  11. <h1><?=$this->html($Event["title"])?></h1>
  12. <?
  13. if ( $Event["photo"] ) {
  14. ?>
  15. <p><img src="<?=$this->html($Event["photo"]["url"])?>"
  16.     width="<?=$this->html($Event["photo"]["width"])?>"
  17.     height="<?=$this->html($Event["photo"]["height"])?>"></p>
  18. <?
  19. }
  20. ?>
  21. <p><?=$this->textToHtml($Event["topic"])?></p>
  22. <?
  23. // Подключаем низ страницы
  24. $this->template("page_footer.tpl.php");
  25. ?>

Получение и обработка параметров сущности производится в строках 47-84. В первую очередь мы получаем параметры сущности по переданному идентификатору (строки 53-61). При этом мы можем получить сущность любого типа. Поэтому, сразу после получения сущности мы проверяем ее тип (строки 63-71). Если тип соответствует ожидаемому, то на основе полученной информации формируется массив $arEvent. Обратите внимание, как обрабатывается изображение. Изображение существует только у типа новости "Новость с фото", поэтому мы явно проверяем, установлен ли этот параметр. При получении изображения в массиве данных сущности регистрируются дополнительные элементы. В нашем случае это:

  • photo_realname - истинное имя файла изображения

  • photo_width - ширина изображения

  • photo_height - высота изображения

  • photo_filesize - размер файла изображения в байтах

  • photo_mime - MIME-тип изображения

Все эти параметры хранятся в БД вместе с именем файла, под которым изображение хранится на сервере. На основе этих параметров мы формируем URL, по которому доступно изображение, его высоту и ширину. URL изображения не существует реально на сервере. Он обрабатывается скриптами, поставляемыми с пустым проектом. Если вы не используете пустой проект, то вам придется самостоятельно заботится о загрузке файлов пользователем. Более подробно правила формирования URL'ов файлов и изображений рассматриваются в разделе Дополнительные возможности.

2.4.3. Расширенные возможности выборки

Рассмотренный пример выборки перечня сущностей представляют собой наиболее простой подход, Однако Cairo позволяет осуществлять выборки и с более сложными условиями. В этом разделе мы рассмотрим те способы выборки перечней сущностей, которые наиболее часть используются при создании сайтов.

2.4.3.1. Выборка сущностей с заданным родителем

В предыдущем разделе при получении списка сущностей мы не использовали никаких условий фильтрации, поэтому возвращаемый список содержал все новости, присутствующие в системе. И если у нас появится несколько лент новостей, то такой подход будет неуместным. Поэтому, в самом простом случае, необходимо явно указывать идентификатор сущности-родителя. Для этого строки 78-93 в примере 2.14 должны быть заменены на представленные ниже.

Пример 2.18. Получение списка сущностей, вложенных в определенную сущность

  1. ...
  2.     if ( !$oList->get_list( "",
  3.                             "",
  4.                             "ent_accept.entid=?entid",
  5.                             "",
  6.                             "",
  7.                             "",
  8.                             $sqlLimit,
  9.                             CENTITYLIST_COUNT_TOTAL,
  10.                             array(
  11.                                 "entid" => NEWS_ENTID,
  12.                             )
  13.                           ) ) {
  14.         CCore::error_push(CError::create(
  15.             CCore::lc("Can't get entity list"),
  16.             __FILE__,
  17.             __LINE__));
  18.         break;
  19.     }
  20. ...

В структуре данных Cairo таблица ent_accept отвечает за связи между сущностями. Она автоматически подключается и ее поля доступны для фильтрации и обработки. Наиболее часто при выборке будут использоваться два поля: ent_accept.entid (идентификатор родительской сущности) и ent_accept.etacid (идентификатор точки вложения). Идентификатор точки вложения имеет смысл использовать только, если сущность имеет две точки вложения одинакового типа, например, как в нашем случае с новостями. Лента новостей может включать в себя и "Все новости" и "Горячие новости" и обе эти точки вложения одного типа, поэтому, если мы хотим получить только горячие новости, то выборка списка должна выглядеть следующим образом:

Пример 2.19. Получение списка сущностей, вложенных в определенную сущность

  1. ...
  2.     // Идентификатор точки вложения "Горячие новости"
  3.     define("NEWS_HOT_ETACID", 4);
  4.  
  5.     if ( !$oList->get_list( "",
  6.                             "",
  7.                             "ent_accept.entid=?entid AND ent_accept.etacid=?etacid",
  8.                             "",
  9.                             "",
  10.                             "",
  11.                             $sqlLimit,
  12.                             CENTITYLIST_COUNT_TOTAL,
  13.                             array(
  14.                                 "entid"  => NEWS_ENTID,
  15.                                 "etacid" => NEWS_HOT_ETACID,
  16.                             )
  17.                           ) ) {
  18.         CCore::error_push(CError::create(
  19.             CCore::lc("Can't get entity list"),
  20.             __FILE__,
  21.             __LINE__));
  22.         break;
  23.     }
  24. ...

Замечание

Значения идентификаторов можно найти в системе администрирования в разделе "Управление структурой данных".

Внимание

Следует отметить, что если в выборке фильтровать только по полю ent_accept.etacid, то в результате будет возвращен список всех горячих новостей со всех лент. Если лента всего одна, то параметр ent_accept.entid можно не использовать.

При работе со ссылками на сущность следует иметь в виду, что если сущность представлена в нескольких точках вложения, то в результате выборки при недостаточной фильтрации она появится столько раз, сколько присутствует в системе. Например, в нашем случае, если после создания "Горячих" новостей сделать выборку без фильтрации по ent_accept.etacid, то все горячие новости в списке появятся два раза.

2.4.3.2. Выборка с дополнительными условиями

При создании ленты новостей часто используется элемент управления "Календарь", который позволяет посетителю увидеть новости за определенный год, месяц или день. Для этого при вызове метода CEntity::get_list() необходимо указать дополнительное значение параметра.

Пример 2.20. Получение списка новостей, созданных в 2006-м году

  1. ...
  2.     if ( !$oList->get_list( "",
  3.                             "",
  4.                             "YEAR(ent_add_time) = ?year",
  5.                             "",
  6.                             "",
  7.                             "",
  8.                             $sqlLimit,
  9.                             CENTITYLIST_COUNT_TOTAL,
  10.                             array(
  11.                                 "year" => "2006",
  12.                             )
  13.                           ) ) {
  14.         CCore::error_push(CError::create(
  15.             CCore::lc("Can't get entity list"),
  16.             __FILE__,
  17.             __LINE__));
  18.         break;
  19.     }
  20. ...

Для фильтрации сущностей использовался системный параметр ent_add_date, который содержит дату и время создания сущности.

Аналогичным образом можно работать не только с системными параметрами, но и с параметрами сущности. Например, чтобы вывести только те новости, в заголовок которых входит слово "горы".

Замечание

Здесь рассматривается только простейший вариант фильтрации. Организация полнотекстового поиска по сущностям будет рассмотрена ниже.

Пример 2.21. Получение списка новостей с фильтрацией по названию

  1. ...
  2.     if ( !$oList->get_list( "",
  3.                             "",
  4.                             "title LIKE ?filter",
  5.                             "",
  6.                             "",
  7.                             "",
  8.                             $sqlLimit,
  9.                             CENTITYLIST_COUNT_TOTAL,
  10.                             array(
  11.                                 "filter" => "%горы%",
  12.                             )
  13.                           ) ) {
  14.         CCore::error_push(CError::create(
  15.             CCore::lc("Can't get entity list"),
  16.             __FILE__,
  17.             __LINE__));
  18.         break;
  19.     }
  20. ...

Различные типы полей данных по-разному хранят информацию в БД (подробнее смотри "Система управления контентом CAIRO: Справочное руководство по API"). И при работе с ними через запросы следует учитывать эти тонкости.

2.4.3.3. Групповые операции и сортировка

Предположим, что мы добавили несколько лент новостей и у нас появилась страница выбора ленты новостей, на которой рядом с каждой лентой выводится количество новостей в ней. Код страницы лент новостей должен содержать три части: выборка всех лент, подсчет количества новостей в каждой из них и, возможно, удаление пустых лент. При этом будет использовано два вызова метода CCore::get_list().

Пример 2.22. Получение количества событий в ленте новостей

  1. ...
  2.     // Идентификатор ленты новостей
  3.     define("NEWS_ETID", 5);
  4.  
  5.     // Выбираем все ленты новостей в системе
  6.     if ( !$oList = CEntityList::create(NEWS_ETID, 0, CENTITYLIST_RT_ASSOC) ) {
  7.         CCore::error_push(CError::create(
  8.             CCore::lc("Can't create entity list"),
  9.                 __FILE__,
  10.                 __LINE__,
  11.                 ""));
  12.         break;
  13.     }
  14.  
  15.  
  16.     if ( !$oList->get_list( "",
  17.                             "",
  18.                             "",
  19.                             "",
  20.                             "",
  21.                             "",
  22.                             "",
  23.                             CENTITYLIST_COUNT_NONE,
  24.                             array()
  25.                           ) ) {
  26.         CCore::error_push(CError::create(
  27.             CCore::lc("Can't get entity list"),
  28.             __FILE__,
  29.             __LINE__));
  30.         break;
  31.     }
  32.  
  33.     // Собираем ленты новостей в массив
  34.     $news = array();
  35.     foreach ( $oList->items as $item ) {
  36.         $news[$item["entid"]] = array(
  37.             "entid" => $item["entid"],
  38.             "title" => $item["title"],
  39.             "count" => 0,
  40.         );
  41.     }
  42.  
  43.  
  44.  
  45.     // Если нашлись ленты новостей, подсчитываем количество событий в каждой
  46.     if ( $news ) {
  47.         if ( !$oList = CEntityList::create(EVENT_ETYPEID, 0, CENTITYLIST_RT_ASSOC) ) {
  48.             CCore::error_push(CError::create(
  49.                 CCore::lc("Can't create entity list"),
  50.                     __FILE__,
  51.                     __LINE__,
  52.                     ""));
  53.             break;
  54.         }
  55.  
  56.  
  57.         if ( !$oList->get_list( "ent_accept.entid AS parentid, COUNT(*) AS cnt_event",
  58.                                 "",
  59.                                 "ent_accept.entid IN (?@parentids)",
  60.                                 "",
  61.                                 "ent_accept.entid",
  62.                                 "",
  63.                                 "",
  64.                                 CENTITYLIST_COUNT_NONE,
  65.                                 array(
  66.                                     "parentids" => array_keys($news),
  67.                                 )
  68.                             ) ) {
  69.             CCore::error_push(CError::create(
  70.                 CCore::lc("Can't get entity list"),
  71.                 __FILE__,
  72.                 __LINE__));
  73.             break;
  74.         }
  75.  
  76.         foreach ( $oList->items as $item ) {
  77.             $news[$item["parentid"]]["count"] = $item["cnt_event"];
  78.         }
  79.     }
  80. ...

Обратите внимание, что при выборке не подсчитывается общее количество сущностей в списке (строки 23 и 64). Ленты новостей будут выводится без постраничности, и при подсчете количества новостей в каждой из них дополнительный запрос на подсчет общего числа новостей не нужен. Если же список лент сделать постраничным, то понадобится и подсчет их общего числа и использование класса cfPaginator.

После выполнения кода, приведенного в примере, массив $news будет содержать все ленты с числом новостей в каждой из них. Однако, перечень этот будет неупорядоченным. Для того, чтобы упорядочить список лент достаточно указать условия сортировки в шестом аргументе метода CEntityList::get_list().

Пример 2.23. Сортировка лент новостей в алфавитном порядке

  1. ...
  2.     // Выбираем все ленты новостей в системе
  3.     if ( !$oList = CEntityList::create(NEWS_ETID, 0, CENTITYLIST_RT_ASSOC) ) {
  4.         CCore::error_push(CError::create(
  5.             CCore::lc("Can't create entity list"),
  6.                 __FILE__,
  7.                 __LINE__,
  8.                 ""));
  9.         break;
  10.     }
  11.  
  12.  
  13.     if ( !$oList->get_list( "",
  14.                             "",
  15.                             "",
  16.                             "",
  17.                             "",
  18.                             "title ASC",
  19.                             "",
  20.                             CENTITYLIST_COUNT_NONE,
  21.                             array()
  22.                           ) ) {
  23.         CCore::error_push(CError::create(
  24.             CCore::lc("Can't get entity list"),
  25.             __FILE__,
  26.             __LINE__));
  27.         break;
  28.     }
  29. ...

В этом случае условие сортировки устанавливается явно. Если в точке вложения, из которой производится выборка, установлена возможность произвольного изменения порядка, то по умолчанию сущности будут отсортированы именно в этом порядке. Для того, чтобы отсортировать сущности в порядке, обратном заданному, методу CEntityList::get_list() передается аргумент "ent_content.pos DESC":

Пример 2.24. Сортировка лент новостей в порядке, обратном заданному в системе администрирования

  1. ...
  2.     // Выбираем все ленты новостей в системе
  3.     if ( !$oList = CEntityList::create(NEWS_ETID, 0, CENTITYLIST_RT_ASSOC) ) {
  4.         CCore::error_push(CError::create(
  5.             CCore::lc("Can't create entity list"),
  6.                 __FILE__,
  7.                 __LINE__,
  8.                 ""));
  9.         break;
  10.     }
  11.  
  12.  
  13.     if ( !$oList->get_list( "",
  14.                             "",
  15.                             "",
  16.                             "",
  17.                             "",
  18.                             "ent_content.pos DESC",
  19.                             "",
  20.                             CENTITYLIST_COUNT_NONE,
  21.                             array()
  22.                           ) ) {
  23.         CCore::error_push(CError::create(
  24.             CCore::lc("Can't get entity list"),
  25.             __FILE__,
  26.             __LINE__));
  27.         break;
  28.     }
  29. ...

2.5. Операции изменения данных

Изменение данных - важная функция любого веб-интерфейса начиная от простых гостевых до электронных магазинов и информационных порталов. По отношению к сущности могут осуществляться четыре вида операций: добавление, редактирование, создание ссылки и удаление ссылки. Удаление сущности происходит в тот момент, когда удаляется последняя ссылка на нее.

В этом разделе мы покажем, как программно реализовать изменение данных, а также приведем пример быстрого построения формы на сайте.

2.5.1. Создание новой сущности

Любые операции по изменению данных осуществляются с помощью класса CEntity. Класс содержит соответствующие методы и автоматически проверяет корректность введенной информации, а также следит за целостностностью структуры данных.

Для добавления новой сущности необходимо создать объект CEntity на основе массива параметров. Для этого подготовленный массив параметров сущности передается конструктору во втором аргументе. В нем обязательно должен быть определен элемент с ключом "etid", содержащий идентификатор типа создаваемой сущности. После этого вызывается метод CEntity::add(), которому передаются идентификаторы точки вложения и родителя. В случае, если при добавлении сущности произошла ошибка, то метод вернет false. Сообщения о ошибках будут помещены в свойство CEntity::etype::$check_errors. При успешном создании сущности ее идентификатор будет доступен через свойство CEntity::params["entid"]

Важно

Добавить сущность возможно только в том случае, если у текущего пользователя есть права на изменение списка вложений родительской сущности.

Пример 2.25. Создание сущности программно

  1. ...
  2.     // Идентификатор ленты новостей
  3.     define("NEWS_ENTID", 6);
  4.  
  5.     // Идентификатор подтипа простой новости
  6.     define("EVENT_SIMPLE_ETID", 6);
  7.  
  8.     // Идентификатор точки вложения "Горячие новости"
  9.     define("NEWS_EVENTS_ETACID", 3);
  10.  
  11.  
  12.     /****************************************
  13.      *
  14.      * Сохранение новости
  15.      *
  16.      ****************************************/
  17.  
  18.     $arParams = array(
  19.         "etid"  => EVENT_SIMPLE_ETID,
  20.         "title" => "Новость, добавленная программно",
  21.         "topic" => "Теперь мы умеем добавлять\nновости программно."
  22.     );
  23.  
  24.     if ( !$oEvent = CEntity::create(0, $arParams) ) {
  25.         CCore::error_push(CError::create(
  26.             CCore::lc("Entity creation error"),
  27.             __FILE__,
  28.             __LINE__));
  29.         break;
  30.     }
  31.  
  32.     if ( !$oEvent->add( NEWS_EVENTS_ETACID, NEWS_ENTID ) ) {
  33.         CCore::error_push(CError::create(
  34.             CCore::lc("Entity adding error"),
  35.             __FILE__,
  36.             __LINE__));
  37.         break;
  38.     }
  39. ...

Как вы видите, создание новой сущности не представляет собой ничего сложного. Гораздо более интересной задачей является вывод формы для создания сущности и ее обработка. В идеальном случае данные, введенные в форму, должны проверятся на стороне клиента средствами JavaScript и на стороне сервера. Платформа Cairo предоставляет удобный инструмент для создания и обработки форм - класс cfForm. С его помощью легко создавать формы не только для работы с сущностями, но и формы с произвольными наборами данных.

Пример 2.26. Создание сущности с помощью формы

  1. <?php
  2. /**
  3. * Код добавления новости
  4. *
  5. * Этот файл содержит форму добавления новости
  6. * и, если пользователь отправил форму,
  7. * добавляет их в систему.
  8. */
  9.  
  10. // Инициализируем систему
  11. require_once($_SERVER["DOCUMENT_ROOT"]."/etc/init.php");
  12.  
  13. // Подключаем класс cfPage
  14. require_once($GLOBALS["CFPARAMS"]["basedir"]."/interface/view/cfpage.class.php");
  15.  
  16. // Подключаем класс CEntity
  17. require_once($_CONFIG["general.coredir"]."/centity.php");
  18.  
  19. // Подключаем класс cfForm
  20. require_once($GLOBALS["CFPARAMS"]["basedir"]."/interface/view/cfform.class.php");
  21.  
  22.  
  23.  
  24. // Идентификатор ленты новостей
  25. define("NEWS_ENTID", 6);
  26.  
  27. // Идентификатор подтипа простой новости
  28. define("EVENT_SIMPLE_ETID", 6);
  29.  
  30. // Идентификатор точки вложения "Горячие новости"
  31. define("NEWS_EVENTS_ETACID", 3);
  32.  
  33.  
  34. do {
  35.  
  36.     /*****************************
  37.      *
  38.      * Создаем страницу
  39.      *
  40.      *****************************/
  41.     if ( !$oPage = cfPage::create() ) {
  42.         CCore::error_push(CError::create(
  43.             CCore::lc("Page creation error"),
  44.             __FILE__,
  45.             __LINE__));
  46.         break;
  47.     }
  48.  
  49.  
  50.  
  51.     /****************************************
  52.      *
  53.      * Работа с формой
  54.      *
  55.      ****************************************/
  56.     $oForm = cfForm::create();
  57.  
  58.     // Загрузка полей из типа
  59.     $oForm->loadFieldsFromEType(EVENT_SIMPLE_ETID);
  60.  
  61.     // Добавляем кнопку
  62.     $oForm->addButton("submit", "bt_submit", "Отправить");
  63.  
  64.     if ( $oForm->isSubmitted() ) {
  65.         // Обрабатываем данные, пришедшие от формы
  66.  
  67.         do {
  68.             // Проверяем данные
  69.             if ( !$oForm->checkData() ) {
  70.                 break;
  71.             }
  72.  
  73.             // Помещаем переданные данные в массив
  74.             $data = $oForm->getData();
  75.  
  76.             // Добавляем в массив данных идентификатор типа
  77.             $data["etid"] = EVENT_SIMPLE_ETID;
  78.  
  79.             // Сохраняем сущность
  80.             if ( !$oEvent = CEntity::create(0, $data) ) {
  81.                 CCore::error_push(CError::create(
  82.                     CCore::lc("Entity creation error"),
  83.                     __FILE__,
  84.                     __LINE__));
  85.                 break 2;
  86.             }
  87.  
  88.             if ( !$oEvent->add( NEWS_EVENTS_ETACID, NEWS_ENTID ) ) {
  89.                 CCore::error_push(CError::create(
  90.                     CCore::lc("Entity adding error"),
  91.                     __FILE__,
  92.                     __LINE__));
  93.                 break 2;
  94.             }
  95.  
  96.             // Сохранение прошло успешно
  97.             // Переадресовываем на страницу "Новость добавлена"
  98.             header("Location: event-added.php");
  99.             exit;
  100.  
  101.         } while (false);
  102.  
  103.     }
  104.  
  105.  
  106.  
  107.     /****************************************
  108.      *
  109.      * Подключаем шаблон
  110.      *
  111.      ****************************************/
  112.  
  113.     // Определяем используемый шаблон
  114.     if (!$oPage->setTemplate($_CONFIG["general.tpldir"]."/bin/add_news.tpl.php")) {
  115.         CCore::error_push(CError::create(
  116.             CCore::lc("Page creation error"),
  117.             __FILE__,
  118.             __LINE__));
  119.         break;
  120.     }
  121.  
  122.     // Регистрируем данные для использования в шаблоне
  123.     $oPage->addData("Form", $oForm);
  124.  
  125.     // Генерируем страницу
  126.     $oPage->build();
  127.  
  128. } while (false);
  129.  
  130. // Произошла ошибка
  131. // Подключаем стандартный обработчик
  132. require_once($_CONFIG["general.basedir"]."/bin/error.php");
  133. ?>
  1. <?php
  2. /**
  3. * Шаблон формы добавления новости
  4. *
  5. * Этот файл выводит форму добавления новости
  6. */
  7.  
  8. // Подключаем заголовок страницы
  9. $this->template("page_header.tpl.php");
  10.  
  11. // Рисуем форму
  12. $Form->build();
  13.  
  14. // Подключаем низ страницы
  15. $this->template("page_footer.tpl.php");
  16. ?>

Вся работа с формой осуществляется в стоках 51-103. После создания объекта cfForm мы загружаем в него поля для типа сущности "Простая новость". Этого достаточно для вывода формы. Сам вывод осуществляется в шаблоне вызовом метода cfForm::build().

Для проверки наличия отправленных через форму данных используется метод cfForm::isSubmitted(). Он возвращает true, если данные отправлены, и false, если нет.

Замечание

На самом деле метод cfForm::isSubmitted() выполняет еще одну функцию. Как только пользователь отправил данные методом POST cfForm::isSubmitted() регистрирует их в сессии и перегружает страницу. Таким образом исключается возможность повторной отправки данных через форму при обновлении страницы результата. Класс cfForm без вмешательства разработчика отслеживает наличие данных в сессии, проверяет отправлялись ли данные именно этой формой и очищает сессию.

В нашем примере вся обработка данных реализована в строках 64-103. Обработка данных начинается с проверки их корректности. Корректность данных формы проверяется с помощью метода cfForm::checkData(). В случае, если проверка закончилась неудачей, обработка данных прерывается и полученные ошибки выводятся средствами класса cfForm. В случае успешной проверки данные получаются в виде массива и сохраняются в сущность, как это было показано ранее.

После успешного сохранения пользователь перенаправляется на страницу, содержащую сообщение "Вы успешно добавили новость". Такое поведение не обязательно. Пользователя можно перенаправить на страницу всех новостей, или показать форму для добавления еще одной новости. Выбор решения зависит только от текущей необходимости и предпочтений разработчика.

2.5.2. Обновление сущности

Процесс обновления сущности аналогичен процессу ее создания. Для обновления данных используется метод CEntity::update(). При этом в массиве параметров сущности должны быть определены обновляемые поля и задан элемент с ключом entid, определяющий идентификатор сущности.

Пример 2.27. Программное обновление параметров сущности

  1. ...
  2.     // Идентификатор обновляемой сущности
  3.     define("EVENT_ENTID", 20);
  4.  
  5.     if ( !$oEvent = CEntity::create(EVENT_ENTID) ) {
  6.         CCore::error_push(CError::create(
  7.             CCore::lc("Entity creation error"),
  8.             __FILE__,
  9.             __LINE__));
  10.         break;
  11.     }
  12.  
  13.     // Обновляем заголовок новости
  14.     $oEvent->params["title"] = "Измененный заголовок новости";
  15.  
  16.     // Сохраняем изменения
  17.     if ( !$oEvent->update() ) {
  18.         CCore::error_push(CError::create(
  19.             CCore::lc("Entity update error"),
  20.             __FILE__,
  21.             __LINE__));
  22.         break;
  23.     }
  24. ...

Обратите внимание, что перед обновлением параметров сущности, мы получили их текущие значения. Далее мы покажем как организовать обновление параметров сущности через форму. Предположим, что идентификатор редактируемой сущности передается в GET-переменной id. Мы сознательно не работаем с авторизацией, поэтому права обновляемой сущности должны позволять ее редактировать анонимному пользователю.

Пример 2.28. Обновление сущности с помощью формы

  1. <?php
  2. /**
  3. * Код обновления новости
  4. *
  5. * Этот файл содержит форму обновления новости.
  6. */
  7.  
  8. // Инициализируем систему
  9. require_once($_SERVER["DOCUMENT_ROOT"]."/etc/init.php");
  10.  
  11. // Подключаем класс cfPage
  12. require_once($GLOBALS["CFPARAMS"]["basedir"]."/interface/view/cfpage.class.php");
  13.  
  14. // Подключаем класс CEntity
  15. require_once($_CONFIG["general.coredir"]."/centity.php");
  16.  
  17. // Подключаем класс cfForm
  18. require_once($GLOBALS["CFPARAMS"]["basedir"]."/interface/view/cfform.class.php");
  19.  
  20.  
  21.  
  22. // Обрабатываем переданный идентификатор
  23. $id = empty($_GET["id"]) ? 0 : (int)$_GET["id"];
  24.  
  25. do {
  26.  
  27.     /*****************************
  28.      *
  29.      * Создаем страницу
  30.      *
  31.      *****************************/
  32.     if ( !$oPage = cfPage::create() ) {
  33.         CCore::error_push(CError::create(
  34.             CCore::lc("Page creation error"),
  35.             __FILE__,
  36.             __LINE__));
  37.         break;
  38.     }
  39.  
  40.  
  41.  
  42.     /****************************************
  43.      *
  44.      * Получаем текущие параметры сущности
  45.      *
  46.      ****************************************/
  47.  
  48.     if ( !$oEvent = CEntity::create($id) ) {
  49.         CCore::error_push(CError::create(
  50.             CCore::lc("Entity creation error"),
  51.             __FILE__,
  52.             __LINE__));
  53.         break;
  54.     }
  55.  
  56.  
  57.  
  58.     /****************************************
  59.      *
  60.      * Работа с формой
  61.      *
  62.      ****************************************/
  63.     $oForm = cfForm::create();
  64.  
  65.     // Загрузка полей из типа
  66.     $oForm->loadFieldsFromEType($oEvent->params["etid"]);
  67.  
  68.     // Добавляем кнопку
  69.     $oForm->addButton("submit", "bt_submit", "Отправить");
  70.  
  71.     if ( $oForm->isSubmitted() ) {
  72.         // Обрабатываем данные, пришедшие от формы
  73.  
  74.         do {
  75.             // Проверяем данные
  76.             if ( !$oForm->checkData() ) {
  77.                 // Произошла ошибка проверки данных.
  78.                 // Прерываем обработку. Сообщения
  79.                 // об ошибках будут выведены пользователю
  80.                 // перед формой.
  81.                 break;
  82.             }
  83.  
  84.             // Объединяем массивы параметров
  85.             $oEvent->params = array_merge($oEvent->params, $oForm->getData());
  86.  
  87.             if ( !$oEvent->update( ) ) {
  88.                 CCore::error_push(CError::create(
  89.                     CCore::lc("Entity update error"),
  90.                     __FILE__,
  91.                     __LINE__));
  92.                 break 2;
  93.             }
  94.  
  95.             // Сохранение прошло успешно
  96.             // Переадресовываем на список новостей
  97.             header("Location: news.php");
  98.             exit;
  99.  
  100.         } while (false);
  101.  
  102.     }
  103.     else {
  104.         // Заносим в сущность текущие параметры сущности
  105.         $oForm->setData($oEvent->params);
  106.     }
  107.  
  108.  
  109.  
  110.     /****************************************
  111.      *
  112.      * Подключаем шаблон
  113.      *
  114.      ****************************************/
  115.  
  116.     // Определяем используемый шаблон
  117.     if (!$oPage->setTemplate($_CONFIG["general.tpldir"]."/bin/update_news.tpl.php")) {
  118.         CCore::error_push(CError::create(
  119.             CCore::lc("Page creation error"),
  120.             __FILE__,
  121.             __LINE__));
  122.         break;
  123.     }
  124.  
  125.     // Регистрируем данные для использования в шаблоне
  126.     $oPage->addData("Form", $oForm);
  127.  
  128.     // Генерируем страницу
  129.     $oPage->build();
  130.  
  131. } while (false);
  132.  
  133. // Произошла ошибка
  134. // Подключаем стандартный обработчик
  135. require_once($_CONFIG["general.basedir"]."/bin/error.php");
  136. ?>
  1. <?php
  2. /**
  3. * Шаблон формы обновления новости
  4. *
  5. * Этот файл выводит форму обновления новости
  6. */
  7.  
  8. // Подключаем заголовок страницы
  9. $this->template("page_header.tpl.php");
  10.  
  11. // Рисуем форму
  12. $Form->build();
  13.  
  14. // Подключаем низ страницы
  15. $this->template("page_footer.tpl.php");
  16. ?>

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

Единственной особенностью скрипта является использование функции array_merge() для обновления полей сущности (строка 81). Просто переписать новые данные нельзя, так как для обновления сущности используются служебные поля. Поэтому с помощью функции array_merge() мы обновляем только поля данных не затрагивая служебной информации.

2.6. Дополнительные возможности

Помимо работы с данными система Cairo предлагает удобные инструменты для разработки веб-приложений. Сюда входят: создание форм, работа с изображениями, кэширование данных и многое другое. В этом разделе мы покажем как использовать наиболее часто востребованные возможности, предоставляемые платформой.

2.6.1. Работа с формами

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

Поля в форму могут добавляться с помощью метода cfForm::add_field(). При создании поля как минимум должны быть определены его тип, системное имя и название, которое увидит пользователь.

Пример 2.29. Добавление полей в форму

  1. <?php
  2.     ...
  3.     // Создание формы
  4.     $oForm = cfForm::create();
  5.  
  6.     // Добавляем поле
  7.     $oForm->addField(fldAbstract::create(0, array(
  8.         "field_type"  => "string",            // Тип поля
  9.         "field_name"  => "nick",              // Имя поля
  10.         "field_alias" => "Имя пользователя"// Псевдоним поля
  11.     )));
  12.     ...
  13. ?>

Для работы с формой доступны все типы полей, зарегистрированные в системе Cairo. Их имена можно увидеть на форме добавления нового поля в типе сущности. Имя поля используется для программного доступа к его свойствам и содержимому. Данные, внесенные в форму пользователем также будут доступны из этой формы по имени поля. Псевдоним поля будет использоваться при генерации подписи к полю ввода, а также в сообщениях об ошибках.

Эти три параметра являются обязательными при создании экземпляра поля, однако, могут быть определены и другие. Полный перечень параметров можно найти в документе "Справочное руководство по API".

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

Пример 2.30. Добавление кнопок на форму

  1. <?php
  2.     ...
  3.     // Создание формы
  4.     $oForm = cfForm::create();
  5.  
  6.     // Добавляем кнопку "ОК"
  7.     $oForm->addButton(
  8.         "submit",    // Тип кнопки (допускается "submit", "button", "reset")
  9.         "btSubmit"// Системное имя кнопки
  10.         "OK"         // Заголовок кнопки
  11.     );
  12.  
  13.     // Добавляем кнопку очистки формы
  14.     $oForm->addButton(
  15.         "reset",     // Тип кнопки (допускается "submit", "button", "reset")
  16.         "btReset",   // Системное имя кнопки
  17.         "Очистить"   // Заголовок кнопки
  18.     );
  19.     ...
  20. ?>

В примере на форму добавляются две кнопки: кнопка отправки и кнопка очистки. Для очистки формы используется стандартный элемент управления Reset. При нажатии в информационные поля формы записываются значения по умолчанию. Допускается добавлять три вида кнопок: кнопка отправки формы ("submit"), кнопка очистки ("reset") и простая кнопка ("button"). Все эти типы соответствуют стандартным элементам управления HTML. Для работы с простыми кнопками следует использовать четвертый параметр метода cfForm::addButton(). В этом параметре передается строка JavaScript, которая будет выполнятся при нажатии на кнопку:

  1.     $oForm->addButton(
  2.         "button",
  3.         "btSimple",
  4.         "Просто кнопка",
  5.         "alert('Hello world')"
  6.     );

Если ни одна кнопка не добавлена, то форма будет выведена с единственной кнопкой "Отправить". В большинстве случаев определения списка полей и набора кнопок достаточно для определения вида формы. О более тонких методах настройки отображения формы можно узнать в документе "Система управления контентом Cairo: Справочное руководство по API". Если же возможностей стандартного класса cfForm недостаточно, то можно создать класс-наследник и описать в нем нужную функциональность.

Основной задачей при работе с формами является обработка данных. Класс cfForm предоставляет удобный интерфейс для верификации данных, вывода ошибок (если это необходимо), определении значений по умолчанию и многого другого. При использовании cfForm обработка данных в общей форме будет выглядеть следующим образом:

Пример 2.31. Обработка данных в форме

  1.     if ( $oForm->isSubmitted() ) {
  2.         // Форма была отправлена
  3.  
  4.         do {
  5.             // Проверяем данные
  6.             if ( !$oForm->checkData() ) {
  7.                 break;
  8.             }
  9.  
  10.             // Получаем переданные данные
  11.             $data = $oForm->getData();
  12.  
  13.             // Обработка данных, отправленных пользователем
  14.  
  15.         } while (false);
  16.  
  17.     }
  18.     else {
  19.         // Определяем содержимое формы по умолчанию
  20.         $oForm->setData(array());
  21.     }

Приведенный пример можно рассматривать как стандартный шаблон при работе с классом cfPage. Метод cfForm::isSubmitted() возвращает true, если пользователь отправил данные и false иначе. На самом деле метод cfForm::isSubmitted() совершает гораздо больше действий. Внутри метода проверяется отправлена ли форма, и, если отправлена, то все переданные данные регистрируются в сессии и страница перегружается (в первом аргументе метода можно указывать URL, на который будет перенаправляться пользователь). При повторном проходе данные из сессии регистрируются внутри класса и сессия очищается. После этого метод возвращает true. Двупроходная обработка формы позволяет избежать ситуации, когда посетитель после отправки формы нажимает кнопку "Назад" и видит предупреждение о повторной отправке данных в форме.

Внимание

Поскольку при обработке формы используется два прохода скрипта, то код обработки данных должен стоять как можно выше в скрипте страницы.

В строке 6 проверяются данные, введенные пользователем. Если данные не пройдут верификацию, то их обработка прерывается и пользователю будут выведены сообщения об ошибках. Далее вместо строки 13 следует вставить тот код, который будет обрабатывать данные. Это может быть сохранение сущности, как было показано выше, или, например, отправка электронного письма или любые другие действия.

При первом открытии формы может понадобится вывести значения по умолчанию. Для этого может использоваться метод cfForm::setData(). В него передается ассоциативный массив со значениями полей, которые используются в качестве значений по умолчанию. В нашем примере передается пустой массив (строка 22), но если значения должны быть определены, то вместо "array()" следует указать заполненный хеш.

При генерации HTML-кода формы к именам всех полей добавляется префикс. Префикс позволяет исключить проблемы с использованием в именах полей служебных слов JavaScript, конфликта идентификаторов на странице и т.д. По умолчанию используется префикс "fld_", но разработчик может определить собственный префикс. Для этого используется метод cfForm::setFieldPrefix():

Пример 2.32. Определение собственного префикса полей

  1. <?php
  2.     ...
  3.     // Создание формы
  4.     $oForm = cfForm::create();
  5.  
  6.     // Меняем префикс полей
  7.     $oForm->setFieldPrefix("myprefix_");
  8.  
  9.     ...
  10. ?>

Следует учесть, что не смотря на то, что в HTML используются имена полей с префиксами, на стороне сервера значения полей будут доступны по их оригинальным именам. Но, при работе с полями на стороне клиента (например, при использовании дополнительного JavaScript-кода) следует к их именам добавлять префикс.

2.6.2. CAPTCHA

В последнее время все чаще сайты атакуются роботами, рассылающими спам. Самым простым средством борьбы со спамом является использование CAPTCHA-изображений. CAPTCHA-изображение (Completely Automatic Public Turing Test to Tell Computers and Humans Apart - полностью автоматический тест Тьюринга для различения компьютеров и людей) представляет собой Картинку, содержимое которой может понять только человек. Подробнее об этом методе защиты можно прочитать на Wikipedia и на русском. Однако, следует иметь в виду, что использование CAPTCHA не защищает автоматически ваш сайт от нападений спаммеров, но может значительно усложнить их задачу.

Платформа Cairo предоставляет два класса для работы с CAPTCHA. Один из них использует только библиотеку GD, другой - внешние утилиты ImageMagic. Интерфейсы работы с классами абсолютно идентичны и отличаются только генерируемые изображения и их размер.

Таблица 2.3. Характеристики классов создания CAPTCHA

ПараметрcfSimpleCaptchacfPrettyCaptcha
Средства отрисовкиGDImageMagic
Размеры рисунка90x18125x50
Пример изображения

Работа с классами генерации CAPTCHA абсолютно идентична, поэтому далее мы будем рассматривать только класс cfPrettyCaptcha. При использовании cfSimpleCaptcha отличатся будут только строка подключения файла класса и вызов конструктора.

В шаблонном проекте Cairo работа с CAPTCHA организовывается предельно просто. Достаточно просто рядом с соответствующим полем ввода вывести изображение с URL "/captcha.png":

Пример 2.33. Включение изображения CAPTCHA в шаблон страницы

<html>
<head>
    <title>CAPTCHA form</title>
</head>
<body>
    <form action="" method="POST">
        <input type="text" name="captcha" value=""><br>
        <img src="/captcha.png">
        <input type="submit" value="Отправить">
    </form>
</body>
</html>

После того, как пользователь заполнил поле ввода и отправил информацию на сервер, необходимо проверить ее корректность. Корректность ввода проверяется с помощью метода cfCaptcha::checkInput(). В случае, если пользователь ввел корректные данные, то он возвращает true и false иначе.

Пример 2.34. Обработка введенного пользователем значения CAPTCHA

  1. <?php
  2. /**
  3. * Файл captcha.php
  4. */
  5. // Инициализируем систему
  6. require_once($_SERVER["DOCUMENT_ROOT"]."/etc/init.php");
  7.  
  8. // Подключаем класс cfPage
  9. require_once($GLOBALS["CFPARAMS"]["basedir"]."/interface/view/cfpage.class.php");
  10.  
  11. // Подключаем класс cfPrettyCaptcha
  12. require_once($GLOBALS["CFPARAMS"]["basedir"]."/interface/security/cfprettycaptcha.class.php");
  13.  
  14. do {
  15.  
  16.     /*****************************
  17.      *
  18.      * Создаем страницу
  19.      *
  20.      *****************************/
  21.     if ( !$oPage = cfPage::create() ) {
  22.         CCore::error_push(CError::create(CCore::lc("Page creation error"), __FILE__, __LINE__));
  23.         break;
  24.     }
  25.  
  26.     if ( $_SERVER["REQUEST_METHOD"] == "POST" ) {
  27.         // Были переданы данные из формы
  28.  
  29.         // Создаем объект CAPTCHA
  30.         $oCaptcha = cfPrettyCaptcha::create();
  31.  
  32.         // Обрабатываем входные данные
  33.         $captcha = empty($_POST["captcha"]) ? "" : trim($_POST["captcha"]);
  34.  
  35.         if ( $oCaptcha->checkInput($captcha) ) {
  36.             // Ввод совпал
  37.             ...
  38.         }
  39.         else {
  40.             // Ввод не совпал
  41.             ...
  42.         }
  43.     }
  44.  
  45.     // Определяем используемый шаблон
  46.     if ( !$oPage->setTemplate($_CONFIG["general.tpldir"]."/bin/captcha.tpl.php") ) {
  47.         CCore::error_push(CError::create(CCore::lc("Page creation error"), __FILE__, __LINE__));
  48.         break;
  49.     }
  50.  
  51.     // Регистрируем данные для использования в шаблоне
  52.     $oPage->addData("Message", $message);
  53.  
  54.     // Генерируем страницу
  55.     $oPage->build();
  56.  
  57. } while (false);
  58.  
  59. // Произошла ошибка
  60. // Подключаем стандартный обработчик
  61. require_once($_CONFIG["general.basedir"]."/bin/error.php");
  62. ?>

Если вы используете класс cfForm то подключить CAPTCHA будет еще проще. Платформа содержит специальный тип поля captcha, который позволяет включать изображения на форму так же, как и обычные поля. При этом явной проверки правильности ввода кода от разработчика не требуется. Эта проверка осуществляется при вызове метода cfForm::checkData().

Пример 2.35. Использование CAPTCHA вместе с cfForm

  1.     ...
  2.     $oForm = cfForm::create();
  3.  
  4.     // Загрузка полей из типа
  5.     $oForm->loadFieldsFromEType($oEvent->params["etid"]);
  6.  
  7.     // Добавляем поле Captcha
  8.     $oForm->addField(fldAbstract::create(0, array(
  9.         "field_type"  => "captcha",   // Тип поля
  10.         "field_name"  => "captcha",   // Имя поля
  11.         "field_alias" => "Код",       // Псевдоним поля
  12.     )));
  13.  
  14.     // Добавляем кнопку
  15.     $oForm->addButton("submit", "bt_submit", "Отправить");
  16.  
  17.     if ( $oForm->isSubmitted() ) {
  18.         // Обрабатываем данные, пришедшие от формы
  19.  
  20.         do {
  21.             // Проверяем данные
  22.             if ( !$oForm->checkData() ) {
  23.                 break;
  24.             }
  25.  
  26.             // Объединяем массивы параметров
  27.             $oEvent->params = array_merge($oEvent->params, $oForm->getData());
  28.  
  29.             if ( !$oEvent->update( ) ) {
  30.                 CCore::error_push(CError::create(
  31.                     CCore::lc("Entity update error"),
  32.                     __FILE__,
  33.                     __LINE__));
  34.                 break 2;
  35.             }
  36.  
  37.             // Сохранение прошло успешно
  38.             // Переадресовываем на список новостей
  39.             header("Location: news.php");
  40.             exit;
  41.  
  42.         } while (false);
  43.  
  44.     }
  45.     else {
  46.         // Заносим в сущность текущие параметры сущности
  47.         $oForm->setData($oEvent->params);
  48.     }
  49.     ...
  50.  

В приведенном примере поле Captcha автоматически будет добавлено в самом низу формы и при проверке данных правильность ввода кода пользователем будет контролироваться автоматически.

2.6.3. Работа с изображениями

Изображения являются неотъемлемой частью любого сайта. И основной операцией, производимой над изображениями на сайте, является генерация их уменьшенных вариантов (эскизов). При использовании шаблонного ресурса разработчику предоставляется удобный интерфейс для создания эскизов изображений.

Замечание

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

Работа с механизмом сжатия изображений осуществляется через URL. В общем виде URL сжатого изображения имеет следующий вид:

/thumbnails/<ENTID>/<Имя поля>/<Ширина>x<Высота>[x(cut|box)]/<Полное имя изображения>

Параметры ENTID и Имя поля призваны однозначно идентифицировать обрабатываемую картинку. Они представляют собой идентификатор сущности и имя поля из которого берутся данные об изображении. Например, допустим, что в системе определен тип данных "Новость", в котором поле photo содержит фотографию, прикрепленную к новости, и пусть новость будет иметь идентификатор 23. Тогда URL для создания эскиза 100x100 может иметь вид:

/thumbnails/23/photo/100x100/photo1.jpg

Где photo1.jpg - настоящее имя фотографии.

Важно

При создании эскиза будут проверены права доступа пользователя к изображению и только после этого он увидит эскиз. Все эскизы создаются только один раз. После этого на все запросы пользователей будет отдаваться эскиз из кэша. После обновления или удаления сущности все связанные с ней эскизы удаляются.

Следующая группа параметров определяет максимальные размеры эскиза и метод вписывания в них. Существует два метода вписывания: box и cut. Метод box полностью впишет исходное изображение в заданные размеры без обрезания краев. С другой стороны метод cut будет обрезать края изображения и подгонять его под заданные размеры.

Рисунок 2.3. Методы создания эскизов изображения

Методы создания эскизов изображения

На рисунке (a) показано исходное изображение. Рамкой обозначен запрашиваемый размер. На рисунке (b) представлен метод cut. Все, что не попало в рамку в результирующем эскизе присутствовать не будет. На рисунке (c) видно как работает метод box. Следует обратить внимание, что размер эскиза не будет 100x100, он будет ровно по границам изображения. По умолчанию используется метод box.

Для определения размеров эскиза при использовании метода box можно использовать метод CUtils::get_thumbnail_size(). В него передаются путь к файлу изображения на сервере и желаемые размеры эскиза. Метод возвращает ассоциативный массив с двумя элементами: width и height, которые содержат соответственно ширину и высоту эскиза.

Отдельно следует упомянуть о возможности создания эскизов с автоматическим приведением одного из размеров. Например, мы хотим, чтобы эскиз был ровно 100 пикселей шириной и не важно какой высоты. Для этого при формировании URL эскиза мы вместо высоты ставим 0:

/thumbnails/23/photo/100x0/photo1.jpg

При этом размеры эскиза также можно получить через CUtils::get_thumbnail_size(). Аналогично создаются эскизы изображений, ограниченные только по высоте, с той лишь разницей, что в ноль устанавливается запрашиваемая ширина.

2.6.4. Кэширование данных

Кэширование позволяет значительно снизить нагрузки на сервер. Мы уже встречались с кэшированием в Cairo, когда рассматривали класс cfPage. Однако, кроме кэширования страниц, платформа Cairo позволяет кэшировать произвольные данные.

В каждом проекте есть каталог для хранения кэша. По умолчанию это DocumentRoot/var/cache. Весь кэш делится на группы, каждой группе соответствует собственная поддиректория. Например, эскизы изображений хранятся в каталоге DocumentRoot/var/cache/thumbnails. Вся работа с кэшем осуществляется с помощью класса cfCache.

В большинстве случаев требуется только две операции при работе с кэшем: проверка наличия в кэше актуальных данных и сохранение сгенерированных данных в кэш. Для этого используются стандартные методы cfCache::get() и cfCache::save(). Приведем пример простой работы с кэшем.

Пример 2.36. Пример работы с кэшем

  1. <?php
  2. //Подключаем ядро
  3. require_once($_SERVER["DOCUMENT_ROOT"]."/etc/init.php");
  4.  
  5. // Подключаем класс работы с кэшем
  6. require_once($CFPARAMS["basedir"]."/system/cfcache.class.php");
  7.  
  8. // Создаем объект кэша
  9. $oCache = cfCache::create($_CONFIG["general.cachedir"]);
  10.  
  11. if ( $data = $oCache->get("my_data") ) {
  12.     // В кэше есть актуальные данные
  13.     $data = unserialize($data);
  14. }
  15. else {
  16.     // Актуальных данных в кэше нет
  17.  
  18.     ...
  19.     // Формируем массив $data
  20.     ...
  21.  
  22.     $oCache->save("my_data", serialize($data));
  23. }
  24. ...
  25. ?>

В примере для хранения массива $data используется ключ кэша "my_data". Каждому набору данных в кэше соответствует собственный уникальный ключ, определяемый разработчиком. И программист должен сам заботиться о его уникальности.

Для того, чтобы отделить различные данные в кэше используются группы. Все данные каждой группы хранятся в отдельном подкаталоге в директории кэширования. Если группа не указана, то данные будут хранится непосредственно в каталоге кэша. При этом имя группы должно содержать только латинские буквы, цифры, знак подчеркивания и минус, т.е. те символы, которые допускаются в именах файлов и каталогов.

Пример 2.37. Пример работы с кэшем

  1. <?php
  2. //Подключаем ядро
  3. require_once($_SERVER["DOCUMENT_ROOT"]."/etc/init.php");
  4.  
  5. // Подключаем класс работы с кэшем
  6. require_once($CFPARAMS["basedir"]."/system/cfcache.class.php");
  7.  
  8. // Создаем объект кэша
  9. $oCache = cfCache::create($_CONFIG["general.cachedir"]);
  10.  
  11. if ( $data = $oCache->get("my_data", "test_group") ) {
  12.     // В кэше есть актуальные данные
  13.     $data = unserialize($data);
  14. }
  15. else {
  16.     // Актуальных данных в кэше нет
  17.  
  18.     ...
  19.     // Формируем массив $data
  20.     ...
  21.  
  22.     $oCache->save("my_data", serialize($data), "test_group");
  23. }
  24. ...
  25. ?>

Класс cfCache предоставляет и другие функции по работе с кэшем: удаление записи из кэша, очистка группы или всего каталога и т.д. Подробнее о всех возможностях cfCache можно прочитать в документе "Система управления контентом CAIRO: Справочное руководство по API".

2.6.5. Работа с пользователями

На простых сайтах авторизация пользователей не требуется практически никогда. Однако, стоит появиться форуму или закрытому разделу, то сразу встает вопрос о создании учетной записи пользователя и возможности каждого посетителя работать от своего имени.

2.6.5.1. Авторизация и аутентификация пользователей

Платформа Cairo предоставляет удобный интерфейс для управления пользователями, а также простой механизм авторизации и аутентификации посетителя на сайте. Интерфейс управления пользователями описан в документе "Система управления контентом Cairo: Интерфейс администратора". Здесь же будут рассмотрены функции авторизации и аутентификации пользователя в системе.

Подсказка

Следует отличать понятия аутентификации и авторизации. Под аутентификацией понимается проверка соответствия субъекта и того за кого он себя выдает. В нашем случае, это ввод логина и пароля в форме. А в функции авторизации входит предоставление идентифицированному пользователю доступа к различным ресурсам.

Можно выделить два основных способа аутентификации: аутентификация средствами протокола HTTP и аутентификация через форму. При стандартной HTTP-аутентификации пользователь видит диалоговое окно (см. рис. 2.4), которое предлагает ввести имя учетной записи и пароль. Однако, такой метод не всегда удобен при разработке на языке PHP. Например, если PHP работает через CGI-интерфейс, то HTTP-аутентификация не доступна. Подробнее об использовании HTTP-аутентификации на языке PHP можно прочитать в официальном руководстве.

Рисунок 2.4. Стандартный диалог HTTP-аутентификации

Стандартный диалог HTTP-аутентификации

Вне зависимости от того, какой метод аутентификации вы используете для получения данных от пользователя, их обработка в Cairo всегда идентична. Для того, чтобы авторизировать пользователя, необходимо методу CCore::login() передать его имя и пароль.

Пример 2.38. Аутентификация пользователя на сайте

  1. <?php
  2. // Инициализируем систему
  3. require_once($_SERVER["DOCUMENT_ROOT"]."/etc/init.php");
  4. ...
  5. //
  6. // Получение от пользователя значений
  7. // переменных $login и $passwd
  8. //
  9.  
  10. if ( CCore::login($login, $passwd) ) {
  11.     // Аутентификация прошла успешно
  12.     ....
  13. }
  14. else {
  15.     // Имя пользователя или пароль неправильные
  16.     ...
  17. }
  18. ...
  19. ?>

Затем, в начале каждого скрипта, который требует только авторизированного доступа, помещается вызов метода CCore::check_login(), который возвращает true, если пользователь прошел аутентификацию и false иначе.

Пример 2.39. Авторизация пользователя на странице

  1. <?php
  2. // Инициализируем систему
  3. require_once($_SERVER["DOCUMENT_ROOT"]."/etc/init.php");
  4. ...
  5. //
  6. // Получение от пользователя значений
  7. // переменных $login и $passwd
  8. //
  9.  
  10. if ( !CCore::check_login() ) {
  11.     // Пользователь не авторизирован
  12.     ....
  13. }
  14. ...
  15. ?>

Внимание

Метод CCore::check_login() проверяет только сам факт аутентификации. Проверка прав доступа пользователя к этой странице не происходит.

Последней не рассмотренной операцией пользователя является его выход из системы. Завершение сессии аутентификации пользователя осуществляется с помощью метода CCore::logout(). После вызова этого метода система рассматривает пользователя как анонима и доступ к закрытым страницам для него будет запрещен.

2.6.5.2. Работа с учетными записями пользователей

Для того, чтобы пользователь мог авторизироваться в системе, у него должна быть заведена учетная запись. Это можно сделать двумя способами: через интерфейс администратора и сделать форму регистрации на сайте. Регистрация пользователей в интерфейсе администратора не требует дополнительных усилий от программиста и может быть весьма успешной при небольшом числе учетных записей. Однако, если планируется создание сайта, на котором предполагается большое число зарегистрированных пользователей, то их регистрация "вручную" будет занимать все время администратора и значительно снизит удобство использования ресурса. Поэтому имеет смысл автоматизировать процесс регистрации пользователей и предоставить им возможность самостоятельно вводить данные, необходимые для создания учетной записи. Работа с интерфейсом администратора рассматривается в документе "Система управления контентом CAIRO: Интерфейс администратора", здесь же будет показано как организовать работу с аккаунтами пользователя на сайте.

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

Пример 2.40. Создание пользователя

  1. <?php
  2. // Инициализируем систему
  3. require_once($_SERVER["DOCUMENT_ROOT"]."/core/init.php");
  4. // Подключаем класс для работы с пользователями
  5. require_once($_CONFIG["general.coredir"]."/cuser.php");
  6.  
  7. // Определяем параметры нового пользователя
  8. $oUser = CUser::create( 0, array(
  9.     "nick"  => "foo",
  10.     "passw" => "123456",
  11.     "email" => "foo@sample.com",
  12. ) );
  13.  
  14. // Добавляем пользователя в группу с GID=2
  15. $oUser->add_group(2);
  16.  
  17. // Сохраняем данные
  18. if ( !$oUser->save() ) {
  19.     die("Ошибка создания пользователя");
  20. }
  21. ?>

Для создания учетной записи необходимо как минимум определить логин пользователя (nick), его пароль (passw), и адрес электронной почты (email). Все параметры пользователя приведены в таблице 2.4. Это множество параметров может быть расширено для отдельных ресурсов данных (см. "Настройка учетной записи пользователя"), тогда к обязательным полям по умолчанию могут добавиться новые.

Таблица 2.4. Параметры учетной записи пользователя

ПараметрОписание
uid

Идентификатор пользователя

Этот параметр хранит уникальный идентификатор пользователя в системе. После создания учетной записи ее идентификатор будет записан в свойство CUser::$params["uid"].

nick

Псевдоним пользователя

Традиционно в Интернет пользователи используют псевдонимы (ники). Они используются для идентификации пользователя на форумах, гостевых и т.п., а также на форме аутентификации.

passw

Пароль.

Пароль хранится в БД в виде MD5-хэша.

email

E-mail пользователя.

Адрес электронной почты может использоваться как для связи с пользователем, так и для его аутентификации на сайте вместо ника.

lastname

Фамилия.

firstname

Имя.

surname

Отчество.

sex

Пол.

Это перечисление (1-мужской, 2-женский).

birthday

Дата рождения

about

Дополнительная информация о пользователе.

Кроме набора параметров учетная запись пользователя характеризуется группами, в которые он входит. В примере мы показано как добавить пользователя в группу с идентификатором 2. Для этого мы использовали метод CUser::add_group(), он проверяет существование группы и регистрирует в ней пользователя. Однако следует помнить, что изменения в учетной записи будут сохранены в БД только после вызова метода CUser::save(). Для удаления пользователя из группы используется метод CUser::del_group(), он принимает идентификатор удаляемой группы и возвращает признак успешного завершения операции. Фактическое удаление пользователя из группы также произойдет после вызова метода CUser::save(). Покажем, как с помощью системы Cairo создать простейшую форму регистрации пользователей:

Пример 2.41. Форма регистрации пользователей

  1. <?php
  2. /**
  3. * Код добавления новости
  4. *
  5. * Этот файл форму добавления новости
  6. * и, если пользователь отправил форму,
  7. * добавляет их в систему.
  8. */
  9.  
  10. // Инициализируем систему
  11. require_once($_SERVER["DOCUMENT_ROOT"]."/etc/init.php");
  12.  
  13. // Подключаем класс cfPage
  14. require_once($GLOBALS["CFPARAMS"]["basedir"]."/interface/view/cfpage.class.php");
  15.  
  16. // Подключаем cfForm
  17. require_once($GLOBALS["CFPARAMS"]["basedir"]."/interface/view/cfform.class.php");
  18.  
  19. // Подключаем cUser
  20. require_once($_CONFIG["general.coredir"]."/cuser.php");
  21.  
  22.  
  23.  
  24. do {
  25.  
  26.     /*****************************
  27.      *
  28.      * Создаем страницу
  29.      *
  30.      *****************************/
  31.     if ( !$oPage = cfPage::create() ) {
  32.         CCore::error_push(CError::create(
  33.             CCore::lc("Page creation error"),
  34.             __FILE__,
  35.             __LINE__));
  36.         break;
  37.     }
  38.  
  39.  
  40.     /****************************************
  41.      *
  42.      * Работа с формой
  43.      *
  44.      ****************************************/
  45.     $oForm = cfForm::create();
  46.  
  47.     // Заполняем форму полями пользователя
  48.     $oForm->fields = CUser::get_fields();
  49.  
  50.     // Удаляем поле uid, так как его
  51.     // пользователь вводить не должен
  52.     $oForm->deleteField("uid");
  53.  
  54.     // Добавляем поле проверки пароля
  55.     $oForm->addField(fldAbstract::create(0, array(
  56.         "field_type"   => "password",
  57.         "field_name"   => "conf_passw",
  58.         "field_alias"  => "Подтверждение пароля",
  59.         "field_format" => "",
  60.     )));
  61.  
  62.  
  63.     // Определяем порядок вывода полей
  64.     $oForm->setFieldsOrder(array(
  65.         "nick", "passw", "conf_passw", "email", "lastname",
  66.         "firstname", "surname", "sex", "birthday", "about"
  67.     ));
  68.  
  69.  
  70.     // Добавляем проверку соответствия пароля и его подтверждения
  71.     $oForm->addCustomJs(
  72.     "    if (frm.fld_passw.value != frm.fld_conf_passw.value) {
  73.         alert('Введенные пароли не совпадают');
  74.         return false;
  75.     }", CFFORM_JSINSERT_AFTER);
  76.  
  77.     // Обнуляем массив ошибок формы
  78.     $errors = array();
  79.  
  80.     // Добавляем кнопку
  81.     $oForm->addButton("submit", "bt_submit", "Отправить");
  82.  
  83.     if ( $oForm->isSubmitted() ) {
  84.         // Обрабатываем данные, пришедшие от формы
  85.  
  86.         do {
  87.             // Проверяем данные
  88.             if ( !$oForm->checkData() ) {
  89.                 $errors = $oForm->getErrors();
  90.                 break;
  91.             }
  92.  
  93.             // Создаем пользователя
  94.             if ( !$oUser = CUser::create(0, $oForm->getData()) ) {
  95.                 // Делаем сообщение об ошибке
  96.                 $oForm->addError("Ошибка создания пользователя");
  97.                 break;
  98.             }
  99.  
  100.             // Помещаем пользователя в группу 2
  101.             $oUser->add_group(2);
  102.  
  103.             // Сохраняем информацию о пользователе
  104.             if ( !$oUser->save() ) {
  105.                 // Делаем сообщение об ошибке
  106.                 $oForm->addError("Ошибка сохранения информации о пользователе");
  107.                 CCore::print_errors();
  108.                 die(error);
  109.                 break;
  110.             }
  111.  
  112.             // Сохранение прошло успешно
  113.             // Переадресовываем на страницу "Новость добавлена"
  114.             header("Location: event-added.php");
  115.             exit;
  116.  
  117.         } while (false);
  118.  
  119.     }
  120.  
  121.  
  122.  
  123.     /****************************************
  124.      *
  125.      * Подключаем шаблон
  126.      *
  127.      ****************************************/
  128.  
  129.     // Определяем используемый шаблон
  130.     if (!$oPage->setTemplate($_CONFIG["general.tpldir"]."/bin/register.tpl.php")) {
  131.         CCore::error_push(CError::create(
  132.             CCore::lc("Page creation error"),
  133.             __FILE__,
  134.             __LINE__));
  135.         break;
  136.     }
  137.  
  138.     // Регистрируем данные для использования в шаблоне
  139.     $oPage->addData("Form", $oForm);
  140.  
  141.     // Генерируем страницу
  142.     $oPage->build();
  143.  
  144. } while (false);
  145.  
  146. // Произошла ошибка
  147. // Подключаем стандартный обработчик
  148. require_once($_CONFIG["general.basedir"]."/bin/error.php");
  149. ?>

В приведенном примере использовался класс cfForm для вывода формы и класс CUser для сохранения информации о пользователе. Для формы мы использовали два не рассмотренных ранее метода: cfForm::setFieldsOrder() и cfForm::addCustomJs(). Метод cfForm::setFieldsOrder() позволяет определить порядок, в котором поля будут выведены на форме. В примере он используется для того, чтобы вывести поле "Подтверждение пароля" сразу после поля "Пароль". Если этот метод не использовать, то поле подтверждения пароля будет выведено в самом низу формы. С помощью метода cfForm::addCustomJs() добавляется проверку соответствия введенного пароля с его подтверждением. И, если эти два значения не будут равны, то пользователь увидит соответствующее сообщение и данные отправлены не будут. Следует обратить внимание, что в JavaScript имена полей начинаются с префикса "fld_". Как уже упоминалось ранее, для исключения конфликтов имен ко всем именам полей на стороне браузера добавляется префикс, по умолчанию "fld_". Поэтому при добавлении JavaScript-кода в обработчик формы следует помнить про префикс в именах полей. Константа CFFORM_JSINSERT_AFTER во втором параметре означает, что код будет вставлен после основной проверки полей. Если код надо было бы вставить до этой проверки, то следовало бы использовать константу CFFORM_JSINSERT_BEFORE.

Этот пример нельзя рассматривать как готовое решение для рабочего проекта. В рабочем проекте желательно использовать защиту от роботов (CAPTCHA), а также подтверждение регистрации по E-mail. Все эти функции, а также многие другие легко добавить самостоятельно.

2.6.6. Работа со справочниками

Справочники представляют собой специальный тип поля, основной особенностью которого является возможность связывания двух справочников внутри одной формы (типа сущностей). Под связыванием понимается возможность в зависимости от выбранного значения одного справочника позволять выбирать только из ограниченное множества значений другого. Например, допустим наличие двух справочников: "Вид растения" и "Растение". Справочник "Вид растения" содержит следующие элементы: "Дерево", "Кустарник", "Корнеплод", а справочник растения - "Вишня", "Абрикос", "Смородина". При этом необходимо обеспечить невозможность выбора кустарника-вишни и дерева-смородины. Для этого и используются связи между справочниками.

Работа со справочниками и их связями осуществляется через раздел "Управление справочниками" универсального интерфейса администратора. Он позволяет создавать и удалять справочники, изменять их элементы, а также управлять связями между ними. Подробное описание работы со справочниками можно найти в документе "Система управления контентом CAIRO: Интерфейс администратора".

При работе с классом cfForm обработка полей справочников производится автоматически. Но в случае, если не используется стандартный механизм работы с формами, то доступ к возможностям справочников может быть получен с помощью класса modDict. Класс modDict подключается к скрипту страницы следующим образом:

  1. // Подключаем класс modDict
  2. require_once($_CONFIG["general.moddir"]."/moddict.php");

Для работы со справочниками достаточно трех методов:

  • modDict::get_dictionary_items() - извлечение элементов справочника

  • modDict::get_dictionary_params() - извлечение параметров справочника

  • modDict::linked_items() - извлечение связей элементов

Наиболее часто при работе со справочниками используется метод modDict::get_dictionary_items(). Он позволет получить элементы справочника в виде перечисления или ассоциативного массива. Например, запрос перечня деревьев будет выглядеть следующим образом:

  1. // Определяем идентификатор справочника
  2. define("DICID", 3);
  3.  
  4. // Извлекаем элементы справочника
  5. $items = modDict::get_dictionary_items(DICID);
  6.  
  7. var_dump($items);
  8.  
  9. // Результат работы скрипта:
  10. // array(3) {
  11. //   [0]=>
  12. //   array(5) {
  13. //     ["drecid"]=>
  14. //     string(1) "7"        // Внутренний идентификатор элемента справочника
  15. //     ["dicid"]=>
  16. //     string(1) "3"        // Идентификатор справочника
  17. //     ["value"]=>
  18. //     string(1) "1"        // Значение справочника
  19. //     ["alias"]=>
  20. //     string(6) "Дерево"   // Псевдоним элемента
  21. //     ["comment"]=>
  22. //     string(0) ""         // Комментарий к элементу
  23. //   }
  24. //   [1]=>
  25. //   array(5) {
  26. //     ["drecid"]=>
  27. //     string(1) "9"
  28. //     ["dicid"]=>
  29. //     string(1) "3"
  30. //     ["value"]=>
  31. //     string(1) "3"
  32. //     ["alias"]=>
  33. //     string(9) "Корнеплод"
  34. //     ["comment"]=>
  35. //     string(0) ""
  36. //   }
  37. //   [2]=>
  38. //   array(5) {
  39. //     ["drecid"]=>
  40. //     string(1) "8"
  41. //     ["dicid"]=>
  42. //     string(1) "3"
  43. //     ["value"]=>
  44. //     string(1) "2"
  45. //     ["alias"]=>
  46. //     string(9) "Кустарник"
  47. //     ["comment"]=>
  48. //     string(0) ""
  49. //   }
  50. // }

В примере элементы справочника возвращались в виде простого массива. Но, метод modDict::get_dictionary_items() позволяет получать информацию в виде ассоциативного массива с ключами по значению параметра value. Для этого вторым аргументом в вызове метода modDict::get_dictionary_items() необходимо передать константу MODDICT_ITEMS_ASSOC:

  1. // Определяем идентификатор справочника
  2. define("DICID", 3);
  3.  
  4. // Извлекаем элементы справочника
  5. $items = modDict::get_dictionary_items(DICID, MODDICT_ITEMS_ASSOC);
  6.  
  7. var_dump($items);
  8.  
  9. // Результат работы скрипта:
  10. // array(3) {
  11. //   [1]=>
  12. //   array(5) {
  13. //     ["drecid"]=>
  14. //     string(1) "7"
  15. //     ["dicid"]=>
  16. //     string(1) "3"
  17. //     ["value"]=>
  18. //     string(1) "1"
  19. //     ["alias"]=>
  20. //     string(6) "Дерево"
  21. //     ["comment"]=>
  22. //     string(0) ""
  23. //   }
  24. //   [3]=>
  25. //   array(5) {
  26. //     ["drecid"]=>
  27. //     string(1) "9"
  28. //     ["dicid"]=>
  29. //     string(1) "3"
  30. //     ["value"]=>
  31. //     string(1) "3"
  32. //     ["alias"]=>
  33. //     string(9) "Корнеплод"
  34. //     ["comment"]=>
  35. //     string(0) ""
  36. //   }
  37. //   [2]=>
  38. //   array(5) {
  39. //     ["drecid"]=>
  40. //     string(1) "8"
  41. //     ["dicid"]=>
  42. //     string(1) "3"
  43. //     ["value"]=>
  44. //     string(1) "2"
  45. //     ["alias"]=>
  46. //     string(9) "Кустарник"
  47. //     ["comment"]=>
  48. //     string(0) ""
  49. //   }
  50. // }

Перечня элементов справочника достаточно для реализации большинства задач при построении форм, но для работы со связанными справочниками необходимо иметь информацию о структуре связей. Для этого используется метод modDict::linked_items(). Ему передается идентификатор связи (идентификатор связи можно увидеть в Управлении справочниками в системе администрирования) и он возвращает ассоциативный массив связей элементов.

  1. // Определяем идентификатор связи
  2. define("DLINKID", 2);
  3.  
  4. // Извлекаем элементы справочника
  5. $items = modDict::get_linked_items(DLINKID);
  6.  
  7. var_dump($items);
  8.  
  9. // Результат работы скрипта:
  10. // array(2) {
  11. //   [7]=>
  12. //   array(2) {
  13. //     [0]=>
  14. //     array(8) {
  15. //       ["dlinkid"]=>
  16. //       string(1) "2"            // Идентификатор связи справочника
  17. //       ["parentdrecid"]=>
  18. //       string(1) "7"            // Идентификатор родительского элемента
  19. //       ["childdrecid"]=>
  20. //       string(2) "11"           // Идентификатор дочернего элемента
  21. //       ["drecid"]=>
  22. //       string(2) "11"           // Идентификатор дочернего элемента 2
  23. //       ["dicid"]=>
  24. //       string(1) "4"            // Идентификатор дочернего справочника
  25. //       ["value"]=>
  26. //       string(1) "2"            // Значение дочернего элемента
  27. //       ["alias"]=>
  28. //       string(7) "Абрикос"      // Псевдоним дочернего элемента
  29. //       ["comment"]=>
  30. //       string(0) ""             // Комментарий дочернего элемента
  31. //     }
  32. //     [1]=>
  33. //     array(8) {
  34. //       ["dlinkid"]=>
  35. //       string(1) "2"
  36. //       ["parentdrecid"]=>
  37. //       string(1) "7"
  38. //       ["childdrecid"]=>
  39. //       string(2) "10"
  40. //       ["drecid"]=>
  41. //       string(2) "10"
  42. //       ["dicid"]=>
  43. //       string(1) "4"
  44. //       ["value"]=>
  45. //       string(1) "1"
  46. //       ["alias"]=>
  47. //       string(5) "Вишня"
  48. //       ["comment"]=>
  49. //       string(0) ""
  50. //     }
  51. //   }
  52. //   [8]=>
  53. //   array(1) {
  54. //     [0]=>
  55. //     array(8) {
  56. //       ["dlinkid"]=>
  57. //       string(1) "2"
  58. //       ["parentdrecid"]=>
  59. //       string(1) "8"
  60. //       ["childdrecid"]=>
  61. //       string(2) "12"
  62. //       ["drecid"]=>
  63. //       string(2) "12"
  64. //       ["dicid"]=>
  65. //       string(1) "4"
  66. //       ["value"]=>
  67. //       string(1) "3"
  68. //       ["alias"]=>
  69. //       string(9) "Смородина"
  70. //       ["comment"]=>
  71. //       string(0) ""
  72. //     }
  73. //   }
  74. // }

Метод modDict::linked_items() возвращает ассоциативный массив, описывающий связи элементов родительского справочника с элементами дочернего. Ключами в нем служат внутренние идентификаторы элементов родительского справочника (drecid), а значениями - перечисление связанных с ними элементов дочернего. Каждый дочерний элемент описывается набором своих параметров, а также параметрами связи. Этих данных достаточно, чтобы организовать работу со связанными справочниками без использования стандартной формы.

Глава 3. Архитектура Cairo

3.1. Структура каталогов

3.1.1. Введение

В Cairo используется жесткая структура каталогов, обычная для UNIX-подобных систем. В данном разделе описано назначение ветвей данной структуры.

Рисунок 3.1. Структура каталогов системы

Структура каталогов системы

3.1.2. Каталоги ядра системы

Ядро системы представляет собой набор классов, которые обеспечивают взаимодействие с данными и другие сервисные функции. Классы ядра находятся в каталоге <DocumentRoot>/core. Классы, содержащиеся в этом каталоге, отвечают за доступ пользователя к данным, обеспечивают целостность данных и реализуют разделение прав пользователей. Подробнее о классах ядра можно узнать в документе "Система управления контентом CAIRO: Справочное руководство по API".

Классы ядра для своей работы используют классы доступа к данным, содержащиеся в каталоге <DocumentRoot>/usr/lib. Они реализуют дополнительную функциональность, такую как работа с БД, файловой системой и т.п.

Каталог <DocumentRoot>/usr/entities зарезервирован.

В каталоге <DocumentRoot>/usr/fields содержатся классы встроенных типов полей. Здесь же должны храниться и классы типов полей, созданные разработчиком.

3.1.3. Каталоги интерфейса

Каталоги интерфейса содержат файлы, реализующие работу с универсальной системой администрирования Cairo. К таким файлам относятся скрипты-контроллеры, классы элементов управления и шаблоны страниц.

Каталог <DocumentRoot>/bin содержит скрипты, которые занимаются построением страниц интерфейса администратора. В большинстве случаев, каждый скрипт отвечает за отдельную страницу, но допускается объединение нескольких страниц одним скриптом-обработчиком.

В каталоге <DocumentRoot>/usr/widgets содержатся классы элементов управления, которые используются при формировании страниц. При создании новых элементов управления можно наследовать свойства уже существующих и добавлять собственные надстройки. При этом все созданные классы должны храниться в данном каталоге.

3.1.4. Каталоги модулей

Модули в системе оформляются в виде классов, которые хранятся в каталоге <DocumentRoot>/usr/modules, а скрипты, которые реализуют визуальную часть работы с модулями, находятся в каталоге <DocumentRoot>/usr/bin.

Все файлы, касающиеся модулей, должны храниться именно в этих каталогах. Исключением являются только файлы шаблонов. Они содержатся в директории <DocumentRoot>/usr/skins.

3.1.5. Каталог настроек

Настройки системы описываются в каталоге <DocumentRoot>/etc. Здесь содержатся настройки ядра, модулей, полей и любые другие, которые пожелает ввести пользователь. В подкаталогах <DocumentRoot>/etc/interface и <DocumentRoot>/etc/menu содержатся дополнительные настройки модулей.

Подробнее о настройках системы можно узнать в документе "Система управления контентом CAIRO: Установка и настройка".

3.1.6. Каталоги данных пользователя

Пользователи в процессе работы с системой могут загружать на сервер файлы, изображения, документы. Они должны храниться в структурированном виде.

Все файлы, которые пользователь выгружает на сервер в рамках использования полей типа "Изображение" и "Файл", хранятся в каталоге <DocumentRoot>/var/uploaded. Имена файлов представляют собой случайные строки, поэтому отличить один файл от другого можно только по ссылкам на них в базе данных.

Изображения, выгруженные пользователем на сервер, могут преобразовываться, например, для получения уменьшенных эскизов. Результаты таких преобразований кэшируются, кэш изображений хранится в каталоге <DocumentRoot>/var/cache. Работа с кэшем происходит автоматически, но допускается и прямая очистка кэша.

3.1.7. Каталог стилей оформления

Стили оформления позволяют разрабатывать несколько дизайнерских решений для одного веб-ресурса. Используемый механизм позволит полностью изменить расположение элементов на странице, цветовую гамму и стиль оформления веб-ресурса.

Вся информация о стиле оформления хранится в каталоге <DocumentRoot>/usr/skins/<имя стиля>. Информация о стиле оформления разбивается на пять подгрупп:

  • bin - содержит шаблоны страниц интерфейса;

  • fields - содержит шаблоны полей;

  • media - содержит изображения, css-файлы и прочие элементы оформления страниц;

  • modules - содержит шаблоны страниц работы с модулями;

  • widgets - содержит шаблоны элементов управления.

3.1.8. Прочие каталоги

В этом разделе описаны каталоги, которые не вошли ни в одну из вышеприведенных групп.

Каталог временных файлов (<DocumentRoot>/tmp) используется системой для хранения промежуточных данных.

Каталог <DocumentRoot>/locale содержит переводы интерфейса ресурса, используемые при интернационализации.

При администрировании системы могут потребоваться некоторые дополнительные утилиты. Все они собраны в каталоге <DocumentRoot>/usr/utils. Кроме утилит управления и поддержания ссылочной целостности данных, каталог содержит такие вспомогательные скрипты, как создание эскизов изображений, организация загрузки файлов с возможностью восстановления процесса и пр.

3.2. Хранение данных

3.2.1. Введение

Хранение структурированных данных - одно из основных назначений системы. Большую часть информации можно хранить в СУБД, но некоторые данные (например, изображения, видеоролики и т.п.) предпочтительнее хранить в файловой системе, что позволит значительно разгрузить сервер.

3.2.2. Хранение сущностей

Сущность - это единица информационной структуры, которая полностью описывает какой-либо объект данных. Сущности объединены в строгую древовидную структуру, которая и представляет собой информационную модель ресурса. Если быть точным, то структура данных информационного ресурса представляет собой не дерево, а направленный граф, но, в большинстве случаев, этот граф вырождается в дерево.

Вся информация о сущностях, а также об их структуре и правилах наполнения этой структуры, хранится в БД и может быть получена с помощью соответствующих классов ядра.

3.2.3. Хранение файлов и изображений

Большая часть данных сущности хранится в БД. Это значения полей типа дата, строка, текст и т.п. Но хранение в БД значений полей типа файл и изображение неоправданно и создает дополнительные ограничения на хранимый контент.

Современные БД поддерживают хранение бинарных данных. Но такой подход связан с некоторыми трудностями. Во-первых, при доступе к большим изображениям можно столкнуться с проблемой нехватки лимита оперативной памяти. Во-вторых, пересылка больших объемов данных между приложениями - достаточно ресурсоемкий процесс. И, наконец, хранение больших объемов данных в БД может значительно снизить ее быстродействие.

В платформе Cairo файлы и изображения хранятся вне базы данных. Это позволяет значительно снизить нагрузку на БД, и, в некоторых случаях, для отдачи файлов пользователю ограничиться только функциональностью веб-сервера, что позволяет снизить нагрузку на сервер в общем.

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

Работа с файлами осуществляется с помощью типов данных "Файл" (fldFile) и "Изображение" (fldImage).

3.3. Модули

3.3.1. Введение

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

Модули могут нести в себе две функции: перехват событий ядра и расширение веб-интерфейса администратора Cairo. Каждый отдельный модуль может выполнять как обе функции сразу, так и какую-либо одну из них. В настоящий момент с помощью механизма модулей реализованы такие функции универсального веб-интерфейса, как управление пользователями, структурой данных, справочниками и импортом.

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

3.3.2. Архитектура модулей и способы их интеграции в систему

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

При создании модулей необходимо придерживаться определенных правил и требований. Все модули являются классами, порожденными от класса modAbstract, который содержит все методы, определяющие базовое поведение модуля.

Кроме основного класса, модуль может использовать скрипты вывода страниц веб-интерфейса управления модулем. Такие скрипты позволяют уменьшить объем самого класса модуля за счет выноса необязательных частей кода. Страницы используются только в случае, если модуль интегрируется в универсальный интерфейс администратора. Вся работа с интерфейсным модулем осуществляется через метод modAbstract::show().

Модуль можно использовать как обработчик событий. В настоящий момент система позволяет использовать модуль как обработчик четырех типов событий:

  • добавление потомка в сущность;

  • обновление сущности;

  • удаление потомка сущности;

  • чтение сущности.

Как только наступает событие, которое модуль должен обработать, вызывается метод modAbstract::do_event(). Если к событию прикреплены несколько модулей, то они будут выполняться поочередно.

Процесс создания нового модуля детально описан в разделе "Создание модулей".

Глава 4. Расширение возможностей системы

Существует два пути расширения функциональности системы:

  • создание модулей;

  • создание новых типов полей.

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

Создание нового типа поля может потребоваться, если для нужд веб-ресурса не хватает предустановленных полей. Например, потребуется номер телефона в формате определенной страны.

В данном разделе приводится два кратких руководства по созданию дополнительных модулей и типов полей.

4.1. Правила именования

4.1.1. Введение

Стандартизация именования объектов системы позволяет избежать путаницы и множества проблем, особенно в условиях коллективной разработки проекта. Подобные соглашения упрощают разработку и облегчают понимание исходного кода. Данный раздел посвящен описанию правил именования различных объектов системы, к которым относятся файлы, переменные и классы.

4.1.2. Имена классов

Используемые в системе классы можно разделить на несколько групп:

  • классы ядра;

  • библиотечные классы;

  • классы типов данных;

  • классы модулей;

  • классы элементов управления.

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

Классы ядра и библиотечные классы должны иметь префикс в виде заглавной буквы "C". Например, CCore, CEntity, CUtils.

Замечание

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

Имена классов типов полей должны начинаться с префикса "fld" (от "field" - поле). Например, fldDate, fldImage, fldFile.

Классы модулей имеют префикс "mod" (от "module" - модуль). Например, modPoll, modUsers, modImport.

Для классов элементов управления используется префикс "wg" (от "widget" - элемент управления). Примеры: wgButton, wgForm, wgItemsList.

При расширении системы и разработке собственных классов рекомендуется использовать предложенные правила и размещать классы в соответствующих каталогах.

4.1.3. Имена файлов

В системе используется несколько типов файлов. К ним относятся скрипты классов, а также шаблоны и скрипты веб-страниц. Для именования этих групп файлов используются определенные правила. Подробнее эти правила мы рассмотрим ниже.

4.1.3.1. Файлы классов

Ядро системы и большая часть сервисных библиотек оформлены в виде классов. Каждый класс хранится в отдельном файле. Файл, содержащий класс, должен именоваться следующим образом:

<имя_класса>.php

Где <имя_класса> есть полное имя класса в нижнем регистре. Файлы классов должны располагаться в каталогах, определенных в разделе Структура каталогов.

Файлы классов должны оформляться таким образом, чтобы было безопасно множественное подключение файла, содержащего класс.

Пример 4.1. Оформление файла, содержащего класс

  1. <?php
  2. /**
  3. * Пример оформления файла класса
  4. *
  5. * Этот пример показывает, как следует оформлять
  6. * файлы, содержащие классы таким образом,
  7. * чтобы обеспечить безопасное подключение таких
  8. * классов неограниченное количество раз.
  9. *
  10. * @package  Examples
  11. * @author   Victor Grischenko <victor.grischenko@cairo.com.ua>
  12. */
  13.  
  14. if (!class_exists("CFoo")) {
  15.  
  16. class CFoo {
  17.  
  18.     /*** Attributes: ***/
  19.  
  20.     /**
  21.      * Первый атрибут
  22.      *
  23.      * Так следует оформлять комментарии к
  24.      * атрибутам класса.
  25.      *
  26.      * @var    string   $_attr1
  27.      * @access private
  28.      */
  29.     var $_attr1;
  30.  
  31.  
  32.     /*** Methods: ***/
  33.  
  34.     /**
  35.      * Метод класса
  36.      *
  37.      * Здесь следует размещать описание
  38.      * функциональности метода.
  39.      *
  40.      * @access public
  41.      * @return void
  42.      */
  43.     function fooBar() {
  44.  
  45.         // Некоторый код
  46.  
  47.     } // end ob member function fooBar()
  48.  
  49.  
  50. } // end of CFoo
  51.  
  52. }
  53. ?>

4.1.3.2. Скрипты страниц и шаблоны

Скрипты страниц представляют собой те части веб-приложения, которые отвечают за логику ресурса. Особых требований к их именованию нет, но желательно, чтобы файлы имели семантически осмысленные названия.

Файлы страниц для визуализации данных используют шаблоны. Имена шаблонов страниц должны иметь следующий вид:

<имя_скрипта_страницы>.tpl.php

В случае если скрипт страницы поддерживает подключение нескольких шаблонов, следует файл шаблона называть следующим образом:

<имя_скрипта_страницы>_<расширение_названия>.tpl.php

При этом <расширение_названия> должно в достаточной мере описывать назначение именно этого шаблона. По подобному принципу работают шаблоны типов данных. Каждый тип данных имеет несколько шаблонов для вывода в различных ситуациях (просмотр, редактирование и т.д.). Имя файла шаблона страницы имеет вид:

<имя_класса_типа_данных>_<действие>.tpl.php

Где <действие> описывает конкретный случай, в котором будет применяться данный шаблон.

4.1.4. Таблицы сущностей

При разработке ресурса необходимо определять типы используемых сущностей и давать названия таблицам, в которых будет храниться информация о сущностях.

Для удобства работы с базой данных рекомендуется все имена таблиц сущностей снабжать префиксом "class_".

4.2. Создание нового модуля

Модули могут выполнять две роли: обрабатывать события ядра системы и расширять возможности универсального веб-интерфейса администратора. Эти две функции могут объединяться в одном модуле, позволяя одновременно расширить функциональность системы и создать веб-интерфейс для управления новой функциональностью.

Существует четыре типа обрабатываемых событий:

  • добавление ссылки на сущность (здесь же рассматривается и создание сущности);

  • удаление ссылки на сущность;

  • обновление сущности;

  • доступ к сущности.

Процесс создания модуля состоит из нескольких этапов:

  • создание класса модуля;

  • создание обработчиков событий в модуле (если модулю необходимо обрабатывать события ядра);

  • создание блоков интерфейса модулей (если модуль будет расширять стандартный веб-интерфейс администратора);

  • регистрация модуля в системе.

Далее в этом разделе мы рассмотрим подробно каждый этап создания модулей.

4.2.1. Создание класса модуля

Все модули реализуются в виде классов, которые, фактически, являются библиотеками функций. Все модули порождаются от класса modAbstract, который содержит абстрактные методы, переопределяемые в каждом новом модуле. Рассмотрим подробнее каждый метод, который должен переопределяться при создании модуля.

4.2.1.1. Метод get_title()

Метод get_title() возвращает название модуля. Название модуля может запрашиваться в двух видах: кратком и полном. Чаще всего, название используется при встраивании модуля в универсальный веб-интерфейс администратора Cairo. Краткое название используется в закладках, а полное - в главном меню.

При запросе краткого имени параметром метода передается значение константы MOD_TITLE_SHORT, при получении полного имени - MOD_TITLE_MENU.

В общем случае реализация методе get_title() может выглядеть следующим образом:

  1. function get_title($_title_type) {
  2.     switch ($_title_type) {
  3.         case MOD_TITLE_SHORT:
  4.             return "Краткое имя";
  5.         case MOD_TITLE_MENU:
  6.             return "Полное имя модуля";
  7.     }
  8. }

4.2.1.2. Метод get_submenu()

Этот метод используется для генерации подменю модуля. Подменю модуля выводится в главном меню универсального веб-интерфейса, если пользователь работает с этим модулем.

Метод возвращает список элементов подменю в следующем формате:

  1.     "title"  => <Заголовок подменю>,
  2.     "hint"   => <Всплывающая подсказка>,
  3.     "url"    => <URL, на который указывает пункт>,
  4.     "active" => <Флаг активности пункта подменю>
  5. )

В метод передается один аргумент - признак активного пункта. В различных модулях могут использоваться различные способы идентификации активного пункта. Это может быть как индекс элемента в массиве подменю, так и некий строковый идентификатор при динамическом формировании подменю.

Реализация данного метода может быть продемонстрирована на примере модуля управления пользователями, который входит в поставку системы Cairo.

  1. function get_submenu($active_item = 0) {
  2.     $ar_items = array(
  3.         array(
  4.             "title"  => CCore::lc("Users", "modusers"),
  5.             "hint"   => CCore::lc("Users", "modusers"),
  6.             "url"    => "/modules/modusers/users/",
  7.             "active" => ($active_item === 1),
  8.         ),
  9.         array(
  10.             "title"  => CCore::lc("Groups", "modusers"),
  11.             "hint"   => CCore::lc("Groups", "modusers"),
  12.             "url"    => "/modules/modusers/groups/",
  13.             "active" => ($active_item === 2),
  14.         ),
  15.     );
  16.  
  17.     return $ar_items;
  18. }

4.2.1.3. Метод show()

Метод show() используется при работе с графическим интерфейсом модуля.

Методу передается массив параметров, которые, в большинстве случаев, являются разобранными параметрами из запроса GET. В массиве параметров есть обязательный элемент с ключом "path". В этом элементе хранится путь, который запросил пользователь (без каталога modules и имени модуля). Анализируя этот путь можно определить, какую информацию необходимо возвращать пользователю.

Параметры, переданные пользователем, также получаются при разборе строки "path". В зависимости от запроса пользователя, подключается тот или иной блок интерфейса, который обрабатывает переданные пользователем параметры, получает необходимые данные и формирует вывод страницы.

  1. function show($_ar_params) {
  2.     if (empty($_ar_params['path'])) {
  3.         // Перенаправляем на страницу по умолчанию
  4.         header("Location: /modules/".$_ar_params['module']."/default.html");
  5.         exit;
  6.     }
  7.  
  8.     // Вызываем обработчик страницы по умолчанию
  9.     if (preg_match('/^default\.html$/', $_ar_params['path'])) {
  10.         return include($_CONFIG['general.basedir'].'/usr/bin/module_default.php');
  11.     }
  12. }

4.2.1.4. Метод do_event()

Метод do_event() вызывается в качестве обработчика системных событий. В метод передается идентификатор типа события и массив параметров. Метод анализирует тип события и вызывает соответствующий обработчик.

Подсказка

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

Итак, основная задача данного метода - определить имя обработчика и вызвать его. Ниже приведен общий вид реализации метода do_event(). В различных реализациях могут меняться только имена фалов-обработчиков либо их набор. Например, при рассылке уведомлений о появлении новых сообщений в форуме, необходим только обработчик события EVENT_ADD_CHILD, все остальные можно опустить.

  1. function do_event($_event, $_params) {
  2. global $_CONFIG;
  3.  
  4.     switch ($_event) {
  5.         case EVENT_ADD_CHILD:
  6.             $s_fname = $_CONFIG['general.basedir']."/usr/bin/module_update.php";
  7.         break;
  8.         case EVENT_READ_ENTITY:
  9.             $s_fname = $_CONFIG['general.basedir']."/usr/bin/module_update.php";
  10.         break;
  11.         case EVENT_UNLINK_ENTITY:
  12.             $s_fname = $_CONFIG['general.basedir']."/usr/bin/module_update.php";
  13.         break;
  14.         case EVENT_UPDATE_ENTITY:
  15.             $s_fname = $_CONFIG['general.basedir']."/usr/bin/module_update.php";
  16.         break;
  17.     }
  18.  
  19.     if (isset($s_fname)) {
  20.         $_EVENT  = $_event;
  21.         $_PARAMS = $_params;
  22.  
  23.         if (!include($s_fname)) {
  24.             CCore::error_push(CError::create(CCore::lc("Can't include file"),
  25.                                              __FILE__,
  26.                                              __LINE__,
  27.                                              "",
  28.                                              $s_fname));
  29.             return false;
  30.         }
  31.  
  32.     }
  33.     return true;
  34. }

Замечание

Следует учесть, что модуль будет обрабатывать события только для тех типов, для которых он зарегистрирован как обработчик. Подробную информацию о регистрации модулей можно найти в разделе "Регистрация модуля в системе".

4.2.2. Создание обработчиков событий

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

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

Поэтому рекомендуется все необязательные части модуля, которые не будут отрабатывать каждый раз при загрузке модуля, выносить в отдельный файл в каталоге <DocumetRoot>/usr/bin/.

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

При написании файла-обработчика возврат ошибок необходимо производить способом, приведенным ниже.

  1. if (/*Error*/) {
  2.     CCore::error_push(CError::create("Error description",
  3.                                      __FILE__,
  4.                                      __LINE__,
  5.                                      "",
  6.                                      $s_fname));
  7.     return false;
  8. }

Следует учитывать, что файл-обрабртчик является частью функции и вызывается из нее, поэтому при работе с глобальными переменными их необходимо явно указывать в инструкции global.

Так как файл рассматривается как вызов функции, то завершаться он должен командой

  1. return true;

4.2.3. Создание блоков интерфейса модулей

Модули могут расширять возможности веб-интерфейса администратора Cairo. С одной стороны, модули могут появляться в виде пунктов главного меню и закладок на страницах сущностей, с другой, предоставлять пользователю страницы, на которые эти элементы управления указывают.

Метод show() является основным при выводе страниц модуля. Он определяет по запросу пользователя, какую именно страницу ему выводить, и перенаправляет на выполнение скрипта страницы. Имеет смысл общие операции (построение главного меню, получение информации о пользователе, получение списка типов сущностей и т.п.) производить в методе show(), а в скриптах страниц реализовывать только индивидуальную для каждого блока функциональность.

Как и в случае с обработчиками событий, файлы блоков интерфейсов модуля, фактически, являются функциями, и работать с ними следует аналогично. Единственное отличие в том, что перед возвратом, необходимо подключить шаблон выводимой страницы. Используемые шаблоны должны подключать стандартные шаблоны page_top.tpl.php и page_footer.tpl.php.

4.2.4. Регистрация модуля в системе

После того, как модуль описан, его необходимо зарегистрировать в системе. Регистрация в системе означает указание в файлах настройки, где этот модуль выводится, к каким типам сущностей относится и какие события обрабатывает. Кроме того, определяется, кто имеет права на доступ к веб-интерфейсу модуля.

Настройка прав доступа к модулю производится в файле /etc/login.ini. Каждая категория этого файла описывает один модуль. Имя категории должно соответствовать имени модуля в нижнем регистре. Каждая категория содержит два параметра: accept_users и accept_groups. accept_users содержит перечисление идентификаторов пользователей, имеющих доступ к веб-интерфейсу модуля. Параметр accept_groups содержит перечень идентификаторов групп, которые имеют доступ к модулю. Идентификаторы групп перечисляются через запятую.

Внимание

Если определен параметр accept_users, параметр accept_groups игнорируется.

Если модуль может выводиться в главном меню, это необходимо указать в файле /etc/interface/menu.ini. Формат файла достаточно прост. Необходимо перечислить в квадратных скобках имена модулей, выводимых в главном меню в том порядке, в котором они будут выводится.

Появление закладки модуля на странице сущностей определяется в файле /etc/interface/tabs.ini. Каждый раздел также соответствует одному модулю. Параметры accept_etypeid и accept_etid определяют список типов и подтипов, для которых на страницах сущностей должна выводится закладка.

Последний способ регистрации - регистрация модуля как обработчика системных событий. Настройки обработчиков событий хранятся в каталоге /etc/handlers. Этот каталог содержит четыре файла: add, read, unlink и updated, которые содержат настройки событий, соответственно: добавление, чтение, удаление и обновление сущностей.

Форматы всех файлов одинаковы. Каждый файл содержит категории, называемые именами модулей. Внутри категории может быть определен только один параметр etids, который содержит список идентификаторов подтипов, разделенных запятой. Обработчик модуля будет использоваться только на тех подтипах, которые определены в соответствующих файлах.

4.3. Создание нового типа поля

Поля используются для определения того, какая хранится информация о сущности. Комбинируя наборы полей, можно получить неограниченное количество типов сущностей для описания практически любых предметных областей.

При этом может возникнуть необходимость создания специализированного типа поля, функциональность которого невозможно реализовать, используя существующие типы.

Создание нового типа поля можно логически разделить на следующие этапы:

  1. Создание класса типа поля.

  2. Создание шаблонов поля.

  3. Регистрация поля в системе.

Остановимся подробнее на этих этапах.

4.3.1. Создание класса типа поля

Классы всех полей порождаются от класса fldAbstract. При создании нового класса поля, в первую очередь, необходимо перегрузить виртуальные методы этого класса, которые определяют поведение поля. В некоторых случаях может понадобиться создание дополнительных методов и свойств, не определенных для абстрактного поля. Описание дополнительных свойств может потребоваться только при создании сложных полей, таких как справочники, пользователи и проч.

4.3.1.1. Конструктор

В первую очередь необходимо написать конструктор класса. У конструктора есть единственный аргумент - $params, также в конструкторе должно быть обязательно заполнено свойство $this->params. Кроме того, в конструкторе могут определяться глобальные переменные или константы, задаваться строки интернационализации и т.п.

4.3.1.2. Метод convert_form_data()

Данный метод конвертирует переданные из формы данные, преобразовывая их к виду, удобному для хранения в БД. Например, для поля "Дата-время" (fldDatetime), данные из формы передаются в виде двух полей <field_name>_time и <field_name>_date. В методе convert_form_data() эти два поля преобразовываются в формат БД "Y-m-d H:i:s". Для полей "Файл" (fldFile) и "Изображение" (fldImage) метод подготавливает данные, описывающие файл и копирует сам файл во временный каталог.

У метода есть один аргумент - массив $params, который метод может изменить. Изменения возвращаются через этот массив.

4.3.1.3. Метод convert_import_data()

Формат данных, передаваемых в файле импорта, может отличаться как от формата данных в БД, так и от формата данных, используемого в формах. Поэтому используется метод, который преобразовывает данные из формата импорта во внутрисистемный формат.

4.3.1.4. Метод _get_template_data()

Метод преобразовывает данные из внутрисистемного формата в формат, используемый в шаблоне. Фактически, преобразования, совершаемые в этом методе, являются обратными по отношению к преобразованиям, совершаемым в методе convert_form_data().

4.3.1.5. Метод get_sql_order_str()

Получение подстроки запроса, отвечающей за сортировку по полю. Запрос к БД при получении списка сущностей, собирается из нескольких подстрок. Данный метод по указанному типу сортировки возвращает SQL-строку, используемую в инструкции "ORDER BY".

4.3.1.6. Метод get_sql_search_str()

Метод формирует строку поиска по фильтрам. В платформе реализован механизм фильтров для работы со списками сущностей. Для реализации этого механизма используется метод get_sql_search_str(). Он аналогичен методу conert_form_data, но позволяет преобразовать данные формы фильтрации в SQL-строку, которую можно использовать в запросе выбора списка сущностей.

4.3.1.7. Метод get_sql_field_list()

Сложные типы данных могут представляться несколькими полями в БД. В этом случае, метод get_sql_field_list() должен возвращать список всех используемых полей, разделенных запятыми.

4.3.1.8. Метод get_addon_fields()

Метод возвращает список дополнительных полей в виде массива. В список не включается основное поле.

4.3.1.9. Метод clear_trash()

Если при работе типа поля используются хранение некоторой части информации в файлах, то необходимо в методе clear_trash() производить очистку файлов при удалении сущности.

4.3.1.10. Метод get_sql_tables_list()

Некоторые поля (такие, как справочники, пользователи) используют подключение других таблиц при выборке. Если для работы нового типа поля необходимо подключение другой таблицы, то метод get_sql_tables_list() должен возвращать SQL-строку, используемую для получения списка сущностей.

4.3.1.11. Переопределение стандартных методов

Кроме описания вышеприведенных методов, может возникнуть необходимость в переопределении методов базового класса. Такое переопределение имеет смысл, если функциональность методов класса fldAbstract не достаточна для реализации задач, поставленных перед создаваемым типом данных.

4.3.2. Создание шаблонов поля

Шаблоны поля определяют то, каким образом будет выводиться поле в различных режимах. Кроме того, для каждого поля определяется шаблон клиентской проверки. Существует семь видов шаблонов. Четыре из них выводят поле в различных режимах и еще три содержат скрипты пользовательской проверки.

Поле может быть выведено в одном из трех состояний:

  • статическое;

  • редактирование;

  • фильтр;

Статический шаблон используется для вывода значения поля в интерфейсе администратора Cairo. Этот шаблон применяется для вывода данных на странице сущности и в списке сущностей. Имя файла шаблона статического поля имеет вид: <имя_типа>_show_static.tpl.php.

Шаблон редактирования используется для ввода данных через интерфейс администратора Cairo на страницах добавления и редактирования сущности. Имя файла шаблона редактирования имеет вид: <имя_типа>_show_edit.tpl.php.

Шаблон фильтра - это форма для редактирования условий фильтрации поля. В интерфейсе администратора Cairo данный шаблон выводится в строке "Фильтры" на странице перечня сущностей. Имя файла шаблона фильтра имеет вид: <имя_типа>_show_filter.tpl.php.

Следующие шаблоны позволяют описать правила проверки корректности введенных пользователем данных средствами JavaScript. К шаблонам проверки относятся:

  • проверка корректности параметра;

  • проверка корректности условия фильтрации;

Шаблон проверки корректности значений используется при выводе поля в режиме редактирования, когда необходимо осуществлять проверку введенных пользователем данных на стороне клиента. Имя файла шаблона имеет вид: <имя_типа>_check_client.tpl.php.

В режиме вывода фильтров следует использовать шаблон проверки корректности условия фильтрации (<имя_типа>_check_client_filter.tpl.php). Использование этого шаблона позволить проверять условие фильтрации до отправки его на сервер.

Важно

Если один или несколько шаблонов не определены, то будет использован шаблон по умолчанию.

4.3.3. Регистрация поля в системе

После создания класса поля и определения шаблонов новый тип поля необходимо прописать в системе. Все поля описываются в одном файле: /etc/fields.ini. Фактически, описание содержит настройки по умолчанию, которые используются при создании поля заданного типа.

Каждый тип поля описывается в отдельном разделе с именем, соответствующим имени типа поля. В разделе перечисляются все параметры, которые описывают поле.

Приложение A. Файлы настроек

Файлы настроек позволяют регулировать поведение системы. Все файлы настроек содержаться в каталоге /etc и представляют собой ini-файлы.

/etc/core.ini

/etc/core.ini — Настройки ядра системы

Описание

Файл содержит настройки ядра системы и веб-интерфейса администратора Cairo. К ним относятся настройки каталогов, режимов отладки, адресов электронной почты и др.

Общие настройки [general]

Общие настройки содержат настройки каталогов и часового пояса клиента.

Таблица A.1. Общие настройки

ПараметрОписание
general.basedirКаталог, в который установлена система. Обычно совпадает с установкой document_root web-сервера.
general.tmpdirКаталог временных файлов.
general.libdirКаталог библиотек функций.
general.coredirКаталог ядра системы.
general.moddirКаталог, содержащий модули веб-интерфейса.
general.fieldsdirКаталог, содержащий классы полей.
general.entdirКаталог, содержащий классы сущностей.
general.skindirКаталог, содержащий темы оформления.
general.widgetdirКаталог, содержащий классы элементов управления.
general.jsdirКаталог, содержащий библиотеки функций JavaScript. Всегда принимает значение "/media/javascript".
general.etcdirКаталог файлов настроек.
general.cachedirКаталог кэша.
general.skinИмя темы оформления по умолчанию.
general.pathsПути поиска файлов классов.
general.gmt.clientВременная зона клиента. Этот параметр будет использоваться для пересчета временных параметров в формат, удобный для использования клиентом.

Настройки языка интерфейса [locale]

Интерфейс может поддерживать несколько языков. Подробнее о том, как организуется многоязычный интерфейс можно узнать в разделе "Интернационализация веб-интерфейса".

Выбор активного языка интерфейса осуществляется с помощью параметра locale.LANG. В качестве значения параметр может принимать название любой локали, установленной на сервере. Например:

locale.LANG = ru_RU.CP1251

Таблица A.2. Настройки языка интерфейса

ПараметрОписание
locale.LANGЛокаль по умолчанию, используемая веб-интерфейсом. Может быть переопределена для каждого ресурса в файле login.ini.
locale.decimal.phpДесятичный разделитель, используемый в вещественных числах при вычислениях в PHP.
locale.decimal.viewДесятичный разделитель, используемый при выводе вещественных чисел.

Настройки режима отладки [debug]

Если система находится в режиме отладки, то внизу каждой страницы будет выведена отладочная информация. Тип выводимой информации настраивается в данном разделе. Все параметры принимают значение 0/1 или on/off.

Таблица A.3. Параметры режима отладки

ПараметрОписание
debug.show_php_errorsПоказывать ли ошибки PHP. Если опция отключена, то ошибки пользователю выводиться не будут.
debug.show_stat

Показывать ли статистику генерации страниц. К статистике генерации относятся:

  • время генерации страницы;

  • время выполнения SQL-запросов;

  • объем страницы до сжатия;

  • объем страницы после сжатия.

debug.sql_stat

Выводить ли расширенную статистику по SQL-запросам. Статистика SQL-запросов включает:

  • тело запроса;

  • время компиляции запроса;

  • время выполнения запроса;

  • общее время отработки запроса.

debug.show_runtime_errorsВыводить ли ошибки, если они появились при генерации страницы.

Параметр debug.sql_stat действует, только если включен параметр debug.show_stat.

Настройки изображений [image]

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

Замечание

Настройки действуют только в веб-интерфейсе администратора.

Ширина эскиза изображения определяется параметром image.thumb_width:

image.thumb_width = 76

Высота эскиза изображения определяется параметром image.thumb_height. Если необходимо, чтобы высота изображения настраивалась автоматически, параметр устанавливается в ноль:

image.thumb_height = 0

Эскизы изображений могут создаваться двумя способами: с использованием пакета ImageMagic и с использованием библиотеки работы с изображениями GD.

;# Способ, которым будут создаваться уменьшенные копии изображений
;# Допустимые значения:
;# - ImageMagic
;# - GD
;# По умолчанию используется GD
image.thumb_handler = <ImageEngine>

В случае использования пакета ImageMagic, системе необходимо указать полный путь к утилите convert.

image.convert = <PathToConvert>

Обычно этот параметр принимает значение /usr/bin/convert или /usr/local/bin/convert.

В случае, если используется библиотека GD, необходимо указать ее версию:

image.gd_ver = 1.6.2

Предпочтительным является использование пакета ImageMagic. Однако, он может быть не доступен на сервере, в то время как библиотека GD установлена в большинстве случаев.

Настройки файлов [file]

Данная группа настроек действует только в интерфейсе администратора.

Таблица A.4. Настройки файлов

ПараметрОписание
file.icon_widthШирина иконки MIME-типа файла.
file.icon_heightВысота иконки MIME-типа файла.

Настройки Утилит [utils]

В процессе работы система Cairo использует различные системные утилиты (zip, unzip, mysqldump). Для этого ей необходимо полные пути к утилитам.

utils.zip="/usr/bin/zip"
utils.unzip="<[PathToUnzip]>"
utils.cat="<[PathToCat]>"
utils.gzip="<[PathToGzip]>"
utils.zcat="<[PathToZcat]>"
utils.bzcat="<[PathToBzcat]>"
utils.mysql="<[PathToMySQL]>"
utils.mysqldump="<[PathToMySQLDump]>"

Настройки адресов электронной почты [mail]

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

Таблица A.5. Настройки адресов электронной почты

ПараметрОписание
mail.webmasterE-mail веб-мастера сайта.
mail.robotE-mail с которого будут отправляться письма, сгенерированные автоматически
mail.host

Адрес SMTP-сервера, который используется для отправки почты.

В некоторых версиях системы параметр может не использоваться.

Настройки административного интерфейса [webface]

Таблица A.6. Настройки интерфейса администратора

ПараметрОписание
webface.free_entitiesЗарезервировано.
webface.gzipОпределяет будут или нет сжиматься генерируемые HTML-страницы. Значение этого параметра on позволяет значительно сэкономить трафик.

/etc/fields.ini

/etc/fields.ini — Настройки типов полей.

Описание

В данном файле описываются все типы полей, которые используются в системе. Каждый тип описывается в отдельной секции, название которой соответствует английскому наименованию типа. Например, описание типа поля email:

[email]
handler = fldAbstract
alias = email
format =
template = email
sql_type = VARCHAR
size = 100
format_preg = "/^[a-z0-9_\-\.]+@([a-z0-9_\-]+\.)+[a-z]{2,5}$/i"
default_val =
condition =
er_msg =
ntf_msg =
required =
comment =
visible =
in_title =
is_shown =
order_type =
ent_title =

Параметр handler определяет класс, который используется для обработки полей указанного типа. Параметр alias определяет псевдоним, который появляется при редактировании типов. В параметре sql_type содержится тип данных SQL, который будет использоваться для хранения полей определяемого типа.

Остальные параметры описывают значения по умолчанию, которые выводятся для всех полей этого типа. Подробное описание полей можно найти в описании свойства fldAbstract::$params.


/etc/login.ini

/etc/login.ini — Настройки ресурсов

Описание

Файл содержит настройки ресурсов данных, доступных через веб-интерфейс администратора Cairo.

Замечание

Настройки из этого файла действуют только на интерфейс администратора.

Файл может содержать описание одного или нескольких ресурсов. Каждый ресурс описывается в отдельном разделе. Описание раздела имеет следующий формат:

[Some]
dbhost = <Хост БД>
dbuser = <Имя пользователя БД>
dbpasswd = <Пароль к БД>
dbname = <Имя БД>
dbcharset = <Кодировка>
dbversion = <Версия MySQL>
authhost = <Доменное имя, по которому доступен интерфейс администратора>
basedir = <Каталог установки ресурса>
uploaddir = <Каталог для загруженных файлов>
cachedir = <Каталог кэша>
frmtext_files = <Каталог файлов для полей типа "Форматированный текст">
httphost = <Доменное имя, по которому доступен ресурс>
auth_user = <Разрешенные пользователи>
auth_group = <Разрешенные группы>
<Настройки доступа к модулям>

Таблица A.7. Параметры ресурса

ПараметрОписание
dbhostАдрес сервера базы данных.
dbuserИмя пользователя для доступа к базе данных.
dbpasswdПароль пользователя базы данных.
dbnameИмя базы данных
dbcharset

Кодировка базы данных. Для MySQL версий ниже 4.0 может принимать два значения:

  • "нет значения" - перекодировка данных не происходит;

  • "cp1251_koi8" - используется, если данные в базе хранятся в кодировке KOI8-RU, а на ресурсе выводятся в кодировке WINDOWS-1251.

В MySQL старше версии 4.0 можно использовать различные кодировки для БД. В этом случае параметр может содержать любую кодировку, поддерживаемую сервером.

dbversionВерсия MySQL. Используется для корректной установки кодировки.
authhost

Доменное имя для доступа к данному ресурсу данных через интерфейс администратора.

Веб-интерфейс администратора Cairo может работать с несколькими ресурсами данных. Но может возникнуть ситуация, когда желательно, чтобы в списке ресурсов появлялись не все допустимые ресурсы, а только необходимые. Для этого можно использовать данный параметр.

Предположим, что веб-интерфейс доступен по адресу cairo.sample.com и нам необходимо выделить ресурс Test на отдельный URL. Для этого необходимо хосту cairo.sample.com прописать псевдоним, например, test.sample.com и задать его в параметре authhost.

basedirКаталог установки ресурса (полный путь).
uploaddirКаталог, в который будут загружаться пользовательские файлы для ресурса (полный путь).
cachedirКаталог кэша ресурса (полный путь).
frmtext_filesКаталог файлов, используемых в полях типа "Форматированный текст" (Путь относительно каталога установки).
httphostURL ресурса. Параметр используется для создания ссылок на основной ресурс.
auth_userСписок идентификаторов пользователей, разделенных запятой, которым разрешен доступ к веб-интерфейсу администратора.
auth_groupСписок идентификаторов групп, разделенных запятой, которым разрешен доступ к веб-интерфейсу администратора.

Для каждого ресурса можно ограничить доступ к различным модулям. По умолчанию, доступ к модулям имеет только пользователь root, но если необходимо дать доступ и другим пользователям или группам, то это необходимо указать явным образом.

Настройки доступа к модулям аналогичны настройкам доступа к ресурсам. Для каждого модуля задаются списки пользователей и групп, которые имеют к нему доступ. Формат описания имеет следующий вид:

<имя модуля>.accept_users = <UID 1>[, <UID 2>[, ...]]
<имя модуля>.accept_groups = <GID 1>[, <GID 2>[, ...]]

В качестве имени модуля можно использовать следующие значения:

  • modtypes - управление структурой данных;

  • moddict - управление справочниками;

  • modimport - импорт данных;

  • modusers - управление пользователями;


/etc/import.ini

/etc/import.ini — Настройка импорта для каждого отдельного ресурса данных

Описание

Веб-интерфейс администратора позволяет импортировать данные из XML-файлов специализированного формата. В настройках импорта должны быть описаны все типы сущностей, которые могут импортироваться. Описание каждого типа заключается в определении первичного ключа, по которому связываются сущности в файле импорта и в базе данных. Кроме того, для каждого ресурса данных определяется родитель по умолчанию для импортируемых сущностей.

Формат описания импортируемых типов для ресурса выглядит следующим образом:

[<Имя базы данных>]
uid = <Идентификатор пользователя, от которого будет производится импорт>
gid = <Идентификатор группы, от которой будет производится импорт>
set.parentid = <Идентификатор родителя по умолчанию>
<тип 1> = "<ключ в файле для типа 1>,<ключ в базе для типа 1>"
<тип 2> = "<ключ в файле для типа 2>,<ключ в базе для типа 2>"

Параметр set.parentid задает идентификатор (entid) родителя, в которого будут импортироваться сущности из файла. Если же необходимо импортировать сущность в другого родителя, то его идентификатор необходимо прописать явно.

Для каждого типа, сущности которого предполагается импортировать, необходимо определить ключевые поля для сопоставления сущностей в файле импорта и базе данных. Это необходимо для того, чтобы обеспечить целостность данных и исключить дублирование сущностей при импорте. В качестве ключа задается имя ключевого поля.


/etc/mimes.ini

/etc/mimes.ini — Настройки MIME-типов

Описание

Файл содержит описание MIME-типов, автоматически распознаваемых веб-интерфейсом администратора.

MIME-тип распознается по расширению. Список расширений имеет формат:

<расширение> = <MIME-тип, соответствующий этому расширению>

MIME-типы используются, например, при загрузке файла пользователем для отдачи ему соответствующих заголовков.


/etc/interface/menu.ini

/etc/interface/menu.ini — Перечень модулей, отображаемых в главном меню интерфейса администратора.

Описание

Файл содержит перечень модулей, которые могут выводиться в главном меню интерфейса администратора.

Замечание

Настройки из этого файла действуют только на интерфейс администратора.

Ссылка на указанный в файле модуль не будет отображаться, если у пользователя нет прав на работу с ним. Настройка прав на работу с модулем осуществляется в файле /etc/login.ini.

Имена модулей перечисляются в квадратных скобках в том порядке, в котором они будут выводиться в меню. Например:

[modDict]

[modTypes]

[modImport]

В приведенном примере модули будут выведены в главном меню интерфейса администратора в следующем порядке:

  1. модуль управления справочниками;

  2. модуль управления типами;

  3. модуль импорта.


/etc/interface/tabs.ini

/etc/interface/tabs.ini — Настройки закладок модулей.

Описание

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

Замечание

Настройки из этого файла действуют только на интерфейс администратора.

Пример настройки закладок:

[modSome]
accept_etypeid =
accept_etid = 12,45

В этом примере закладка модуля будет появляться на страницах сущностей с идентификатором подтипа равным 12 или 45.

Таблица A.8. Настройки закладок модулей

ПараметрОписание
accept_etypeidСписок идентификаторов типов сущностей, на страницах которых будет появляться закладка модуля. Элементы списка разделены запятыми.
accept_etidСписок идентификаторов подтипов сущностей, на страницах которых будет появляться закладка модуля. Элементы списка разделены запятыми.

Внимание

Интерфейс закладок является экспериментальным и применять его в настоящий момент не рекомендуется.