Контроллеры в Cotonti
Статья введение в понимание принципов использования в Cotonti CMF контроллеров и объекты классов объектно-ориентированного программирования. Примеры и разъяснения.
Контроллеры в Cotonti
Контроллеры являются важнейшей частью архитектуры MVC (Model–View–Controller), которая всё шире применяется в Cotonti начиная с версии Siena и особенно в Verona. Они представляют собой объекты классов, унаследованных от cot\controllers\BaseController. Основная задача контроллера — принять входящий HTTP-запрос, обработать его, взаимодействуя при необходимости с моделями (базой данных) и представлениями (шаблонами), и сформировать исходящий ответ. Иными словами, контроллер — это связующее звено, «диспетчер» вашего приложения.
До появления контроллеров разработчики Cotonti использовали хуки module и standalone. Это были процедурные точки входа, в которых логика обработки запроса и генерации ответа смешивалась в одном файле, что часто приводило к трудно поддерживаемому коду. Контроллеры же предлагают объектно-ориентированный, структурированный подход, позволяющий чётко разделить ответственность и упростить повторное использование кода.
В этом руководстве мы подробно рассмотрим, как создавать и использовать контроллеры в Cotonti, разберём все аспекты их работы — от маршрутизации до жизненного цикла .
1. Действия — сердце контроллера
Контроллер состоит из одного или нескольких действий (actions). Действие — это наименьшая функциональная единица, доступная конечному пользователю для выполнения. Например, просмотр статьи, создание комментария, удаление товара — всё это может быть реализовано как действия контроллера.
Действие представляет собой публичный метод класса контроллера, имя которого начинается с префикса action. Возвращаемое значение метода определяет тело ответа, которое будет отправлено пользователю. Это может быть HTML-код, JSON-строка, результат редиректа и т. д.
1.1. Простой пример контроллера с двумя действиями
Рассмотрим контроллер для работы с постами блога. Ниже код контроллера PostController, содержащего два действия: view (просмотр) и create (создание).
namespace cot\modules\blog\controllers;
use cot\controllers\BaseController;
use cot\exceptions\NotFoundHttpException;
use cot\modules\blog\models\PostRepository;
class PostController extends BaseController
{
public function actionView(): string
{
$postId = cot_import('id', 'G', 'INT');
$post = PostRepository::getById($postId);
if ($post === null) {
throw new NotFoundHttpException("Post not found");
}
$template = cot_tplfile('blog', 'post');
$t = new XTemplate($template);
$t->assign([
'ID' => $post['id'],
'TITLE' => htmlspecialchars($post['title']),
'TEXT' => $post['text'],
]);
$t->parse('MAIN');
return $t->text('MAIN');
}
public function actionCreate()
{
$title = cot_import('title', 'P', 'TXT');
$text = cot_import('text', 'P', 'TXT');
Cot::$db->insert(Cot::$db->posts, [
'title' => $title,
'text' => $text,
'created' => Cot::$sys['now']
]);
$id = Cot::$db->lastInsertId();
cot_redirect(cot_url('post', ['n' => 'post', 'a' => 'view', 'id' => $id], '', true));
}
}В действии view (метод actionView()) из GET-параметра id получается идентификатор поста. Затем через репозиторий PostRepository загружаются данные. Если пост не найден, выбрасывается исключение NotFoundHttpException, что приведёт к отдаче 404 страницы. Если данные есть, они передаются в шаблон, который парсится и возвращается как строка.
Действие create (метод actionCreate()) принимает POST-параметры title и text, сохраняет их в таблицу posts, получает идентификатор новой записи и перенаправляет браузер на просмотр созданного поста с помощью cot_redirect(). Обратите внимание: после редиректа выполнение скрипта прекращается, поэтому возвращаемого значения не требуется — метод объявлен без типа : string.
1.2. Аналогия: контроллер как банкомат
Представьте банкомат. Контроллер — это сам банкомат. У него есть несколько действий: «снять наличные», «проверить баланс», «оплатить услуги». Каждое действие запускается нажатием определённой кнопки. Действие выполняет нужные операции и выдаёт результат (чек, деньги, сообщение на экране). Также банкомат может перенаправить вас к другому действию, например, после снятия денег показать баланс. Так и контроллер: после выполнения действия можно вернуть HTML или перенаправить пользователя на другое действие.
2. Маршруты — как найти нужное действие
Чтобы обратиться к действию контроллера, пользователь (или другой код) формирует URL, который соответствует определённому маршруту. Маршрут — это способ идентификации расширения, контроллера и действия с помощью GET-параметров.
Стандартный маршрут Cotonti выглядит следующим образом:
https://ваш-сайт.tld?e=расширение&n=контроллер&a=действие
где:
- e (extension) — код расширения. Например,
blog,shop,forum. Этот параметр определяет, в каком расширении искать контроллер. - n (name) — идентификатор контроллера. Уникальное имя в рамках расширения. Например,
post,product,order. - a (action) — идентификатор действия. Уникальное имя в рамках контроллера. Например,
view,update,list.
Таким образом, URL https://example.com?e=blog&n=post&a=view&id=5 вызовет действие view контроллера post расширения blog, передав дополнительный параметр id=5.
Маршруты — это как почтовый адрес: расширение — город, контроллер — улица, действие — номер дома. Дополнительные параметры (id, page и др.) — это уже номер квартиры.
2.1. ЧПУ (Человекопонятные УРЛы) и контроллеры
Cotonti умеет преобразовывать GET-маршруты в красивые URL с помощью настроек URL-менеджера. Вы можете задать правила, по которым, например, blog/post/view/5 будет транслироваться в ?e=blog&n=post&a=view&id=5. Это делает ссылки более дружественными для пользователей и поисковых систем, не меняя логику работы контроллеров.
3. Создание контроллера
Контроллер — это класс, который должен быть унаследован от cot\controllers\BaseController (или его потомков). По умолчанию базовый класс предоставляет доступ к основным сервисам Cotonti, таким как Cot::$db, Cot::$cfg, Cot::$extrafields и т. д. Также он содержит методы beforeAction() и afterAction(), о которых мы поговорим позже.
3.1. Расположение файлов
Все контроллеры расширения размещаются в поддиректории controllers корневой папки расширения. Например, для модуля blog полный путь будет: modules/blog/controllers/. Контроллеры административной части (панели управления) располагаются в controllers/admin/ того же расширения.
3.2. Идентификатор контроллера и его имя класса
Идентификатор контроллера (то, что пишется в параметре n) обычно выбирается осмысленно, как имя существительное, указывающее на тип ресурса, которым управляет контроллер. Например, article, user, comment.
Чтобы Cotonti мог автоматически найти и загрузить класс контроллера, необходимо соблюдать соглашение об именовании. Оно простое:
- Берём идентификатор, например
post-comment. - Переводим первую букву каждого слова в верхний регистр:
Post-Comment. - Удаляем дефисы:
PostComment. - Добавляем суффикс
Controller:PostCommentController. - Предваряем пространством имён расширения, которое по умолчанию формируется как
cot\modules\имя_расширения\controllers. Для блога это будетcot\modules\blog\controllers\PostCommentController.
Файл с таким классом должен находиться по пути modules/blog/controllers/PostCommentController.php. Если идентификатор простой, например post, то класс будет PostController в файле PostController.php.
3.3. Контроллер по умолчанию
Если в маршруте не указан параметр n (т.е. контроллер не задан), Cotonti попытается вызвать контроллер с идентификатором index — то есть IndexController. Поэтому для создания главной страницы расширения достаточно создать класс IndexController с действием по умолчанию (обычно actionIndex()).
4. Создание действий подробнее
Действия — это методы контроллера, и мы можем создавать их двумя способами: встроенные (inline) действия и самостоятельные (standalone) действия. Рассмотрим оба.
4.1. Встроенные действия
Самый простой способ — объявить публичный метод с префиксом action. Имя метода получается из идентификатора действия по правилам, аналогичным именованию контроллера:
- Action ID:
hello-world→ методactionHelloWorld() - Action ID:
list→ методactionList()
Методы должны быть публичными; private и protected методы не будут восприняты как действия. Имена методов чувствительны к регистру (actionindex не равно actionIndex). Если метод не найден по правилам, будет выброшено исключение.
Встроенные действия удобны, когда логика действия специфична для данного контроллера и не будет переиспользоваться. Это самый частый вариант.
4.2. Самостоятельные действия (Standalone Actions)
Если вы планируете использовать одно и то же действие в разных контроллерах (например, действие «удалить» для разных сущностей), лучше вынести его в отдельный класс, унаследовав от cot\controllers\BaseAction. Такое действие называется самостоятельным.
Чтобы подключить самостоятельное действие к контроллеру, необходимо переопределить метод actions() в классе контроллера и вернуть массив соответствий «идентификатор действия → конфигурация». Пример:
public static function actions(): array
{
return [
'error' => cot\modules\blog\controllers\actions\ErrorAction::class,
'view' => [
'class' => cot\modules\blog\controllers\actions\ViewAction::class,
'viewPrefix' => 'blog.post',
],
];
}Класс самостоятельного действия должен реализовать публичный метод run(), который и будет содержать логику. Например:
namespace cot\modules\blog\controllers\actions;
use cot\controllers\BaseAction;
class HelloWorldAction extends BaseAction
{
public function run(): string
{
return "Привет, мир!";
}
}Преимущества: такие действия можно тестировать отдельно от контроллера, легко переиспользовать в других частях системы. Недостаток — требуется чуть больше кода для объявления.
4.3. Действие по умолчанию в контроллере
Каждый контроллер имеет свойство $defaultAction, которое определяет, какое действие вызывается, если в маршруте не передан параметр a. По умолчанию это 'index'. Изменить его легко:
class PostController extends BaseController
{
public static $defaultAction = 'list';
public function actionList()
{
// вывод списка постов
}
}4.4. Результаты действий и типы ответов
Действие может вернуть строку, которая станет телом HTTP-ответа. Однако это не единственный вариант. Контроллер может управлять ответом более гибко:
- Строка — возвращается HTML, JSON, XML или что угодно. По умолчанию Cotonti отправит заголовок
Content-Type: text/html. - Редирект — вызов
cot_redirect()внутри действия оборвёт выполнение и отправит заголовок Location. Возвращать строку после редиректа бессмысленно. - Исключение — если действие выбрасывает исключение (например,
NotFoundHttpException), Cotonti перехватит его и отобразит соответствующую страницу ошибки. - Прямая работа с HTTP — можно использовать
Cot::$out['headers']для установки произвольных заголовков иCot::$out['body']для тела ответа, если нужно.
Пример действия, возвращающего JSON:
public function actionApiGetPost(): string
{
$post = PostRepository::getById(5);
Cot::$out['headers']['Content-Type'] = 'application/json; charset=utf-8';
return json_encode($post);
}5. Жизненный цикл контроллера
Понимание жизненного цикла помогает правильно организовать код и избежать ошибок. Когда Cotonti получает запрос, адресованный контроллеру, выполняются следующие шаги:
- Создание экземпляра контроллера. Приложение определяет, какой контроллер и действие запрошены, загружает соответствующий класс и создаёт объект.
Вызов
beforeAction(). Этот метод вызывается перед выполнением любого действия. Он может быть переопределён в контроллере для проверки прав доступа, загрузки общих данных и т. д. Если метод возвращаетfalse, выполнение действия отменяется, и пользователь получает отказ (например, 403 Forbidden). Пример использования:public function beforeAction($action): bool { if (!Cot::$usr['isadmin'] && $action == 'delete') { cot_die_message(403, "Доступ запрещён"); return false; } return parent::beforeAction($action); }- Выполнение действия. Вызывается метод
actionXxx()илиrun()у standalone action. Результат сохраняется. - Вызов
afterAction(). Этот метод получает результат действия и может модифицировать его или выполнить завершающие операции (например, записать статистику). Возвращаемое значениеafterAction()заменит собой итоговый результат. - Отправка ответа. Приложение упаковывает результат (строку) в HTTP-ответ и отправляет клиенту.
Такая схема позволяет элегантно внедрять сквозную функциональность: логирование, проверку CSRF-токена, добавление обёртки HTML.
6. Практические примеры использования контроллеров
6.1. Контроллер для страниц (pages) с пагинацией
Рассмотрим контроллер, отвечающий за список страниц в категории с пагинацией. Он демонстрирует получение параметров, работу с моделью и передачу данных в шаблон.
namespace cot\modules\pages\controllers;
use cot\controllers\BaseController;
use cot\modules\pages\models\PageRepository;
class PageController extends BaseController
{
public function actionList(): string
{
$cat = cot_import('cat', 'G', 'ALP');
$page = cot_import('d', 'G', 'INT', 0);
$perPage = Cot::$cfg['maxrowsperpage'] ?? 20;
$total = PageRepository::countByCategory($cat);
$items = PageRepository::findByCategory($cat, $page * $perPage, $perPage);
$pagination = cot_generatePaginationTags(
cot_pagenav('pages', ['cat' => $cat], $page, $total, $perPage)
);
$t = new XTemplate(cot_tplfile('pages.list'));
$t->assign($pagination);
foreach ($items as $i => $item) {
$t->assign([
'LIST_ROW_NUM' => $i + 1,
'LIST_ROW_ID' => $item['id'],
'LIST_ROW_TITLE' => htmlspecialchars($item['title']),
'LIST_ROW_URL' => cot_url('page', ['id' => $item['id']]),
]);
$t->parse('MAIN.PAGE_ROW');
}
$t->parse('MAIN');
return $t->text('MAIN');
}
}6.2. Контроллер корзины интернет-магазина
Контроллер CartController иллюстрирует работу с сессией, добавление товара в корзину, изменение количества, удаление и отображение корзины.
namespace cot\modules\shop\controllers;
use cot\controllers\BaseController;
class CartController extends BaseController
{
public function actionAdd()
{
$productId = cot_import('pid', 'P', 'INT');
$qty = cot_import('qty', 'P', 'INT', 1);
$cart = $_SESSION['cart'] ?? [];
$cart[$productId] = ($cart[$productId] ?? 0) + $qty;
$_SESSION['cart'] = $cart;
cot_redirect(cot_url('shop', ['n' => 'cart', 'a' => 'view']));
}
public function actionRemove()
{
$productId = cot_import('pid', 'G', 'INT');
unset($_SESSION['cart'][$productId]);
cot_redirect(cot_url('shop', ['n' => 'cart', 'a' => 'view']));
}
public function actionView(): string
{
$cart = $_SESSION['cart'] ?? [];
$productIds = array_keys($cart);
$products = [];
if ($productIds) {
$ids = implode(',', array_map('intval', $productIds));
$res = Cot::$db->query("SELECT * FROM {$Cot::$db->products} WHERE id IN ($ids)");
while ($row = $res->fetch()) {
$row['qty'] = $cart[$row['id']];
$products[] = $row;
}
}
$t = new XTemplate(cot_tplfile('shop.cart'));
foreach ($products as $item) {
$t->assign([
'CART_ROW_ID' => $item['id'],
'CART_ROW_TITLE' => htmlspecialchars($item['title']),
'CART_ROW_PRICE' => $item['price'],
'CART_ROW_QTY' => $item['qty'],
'CART_ROW_SUM' => $item['price'] * $item['qty'],
]);
$t->parse('MAIN.CART_ROW');
}
$t->parse('MAIN');
return $t->text('MAIN');
}
}7. Роутинг и модульность
Контроллеры позволяют разделить крупное расширение на несколько небольших, логически связанных классов. Например, вместо одного огромного файла blog.standalone.php можно создать контроллеры PostController, CommentController, CategoryController. Каждый из них будет содержать только свои действия, что значительно упрощает поддержку и тестирование.
Кроме того, вы можете создавать вложенные пространства имён для действий, например controllers\admin\DashboardController для админки, controllers\api\PostController для REST API. Это делает структуру проекта более понятной.
8. Обработка ошибок и безопасность
Контроллеры позволяют централизованно управлять ошибками. Вместо разбросанных по коду вызовов cot_die_message() вы можете выбрасывать исключения, которые будут перехвачены ядром Cotonti. Это делает код чище и облегчает модульное тестирование.
- NotFoundHttpException — для 404 ошибок.
- ForbiddenHttpException — для 403.
- Собственные исключения, унаследованные от
\Exception.
Также настоятельно рекомендуется проверять права доступа в методе beforeAction(), чтобы не дублировать код в каждом действии. Например, для всех действий контроллера админки можно сделать общую проверку на администратора.
9. Совместная работа с моделями и представлениями
Хотя Cotonti не навязывает строгого разделения на модели, хорошей практикой является вынос логики доступа к данным в отдельные классы-репозитории (как в примерах выше). Это позволяет избежать дублирования SQL-запросов и упрощает тестирование. Контроллер же остаётся тонким, сосредоточенным на потоке управления.
Для представлений используйте шаблоны XTemplate с вызовом cot_tplfile(). Старайтесь не смешивать HTML внутри контроллера; исключение — простая отладка.
10. Миграция с хуков на контроллеры
Если у вас есть старое расширение, построенное на хуке module или standalone, вы можете постепенно переводить его на контроллеры. Достаточно создать контроллер, а в хуке вызывать перенаправление на нужный маршрут. Например, старый файл blog.standalone.php можно заменить контроллером и в системном файле расширения прописать:
Cot::$out['redirect'] = cot_url('blog', ['n' => 'post', 'a' => 'view', 'id' => $id]);Это позволит сохранить старые ссылки рабочими и постепенно отказаться от устаревшего кода.
11. Тестирование контроллеров
Благодаря тому, что контроллер — это обычный класс с внедрёнными зависимостями (через статические свойства Cot), его можно тестировать в изоляции. Вы можете подменить базу данных, конфигурацию, глобальные массивы $_GET, $_POST с помощью PHPUnit. Это огромный шаг вперёд по сравнению с процедурным кодом хуков, который практически невозможно тестировать автоматически.
12. Заключение
Контроллеры в Cotonti — мощный инструмент, позволяющий писать лучше структурированный, более тестируемый и легко расширяемый код. Они заменяют устаревшие хуки и выводят разработку на современный уровень. Мы рассмотрели создание контроллеров, действий, маршрутизацию, жизненный цикл, практические примеры для разных задач. Освоив эти принципы, вы сможете строить сложные расширения с минимальными затратами на поддержку.
Используйте контроллеры в новых проектах, а также для модернизации существующих — это инвестиция в будущее вашего сайта.
Комментарии (0)
Автор контента
Оффлайн
Sodium Carbonate
Последняя авторизация: 11.07.2026 19:44
- Страница размещена: 28.04.2026 18:15
- Последнее обновление: 28.04.2026 18:42
- Язык:
Связанные статьи
Cotonti Verona 1.0.0: ребрендинг, который важнее кода
Cotonti Verona 1.0.0 — это не столько технический релиз, сколько стратегический ребрендинг и попытка
Routing для standalone-плагинов Cotonti
Routing для standalone-плагинов Cotonti в публичной части (front-end)1. Что такое Hooks=standaloneВ
Загрузчик плагина (Plugin loader) в Cotonti Siena CMF
Обзор загрузчика и Введение в систему плагинов CotontiПодробный разбор самой концепции инициализации
English