PearCMS Docs v2.4
Документация / Модули / Анатомия модуля

Анатомия модуля

Разбираем модуль по файлам на примере Banner. Поняв этот шаблон, можно писать свой модуль с нуля или править существующий.

Актуально для v2.4 Обновлено 1 июля 2026

Структура файлов #

modules/Banner/text
Banner/
├── module.json                  # манифест: name, version, icon, requires
├── Banner.php                   # публичный фасад (run() для /banner/*)
├── BannerRouter.php             # админ-роутер: match action → controller
├── migration.sql                # CREATE TABLE + INSERT routes/menu
├── uninstall.sql                # откат — на случай удаления модуля
├── Controllers/
│   ├── BaseController.php       # общий layout() + flash()
│   ├── BannerListController.php
│   ├── BannerFormController.php
│   └── BannerUploadController.php
├── Services/
│   └── BannerService.php        # SQL + logAction + notify
├── FlagsPanel/
│   └── BannerFlagsPanel.php     # декларация actions для /flags-settings
├── AdminApi/                   # опционально: REST для мобильного клиента
│   └── BannerAdminApi.php
├── Widget/                      # опционально: виджет на дашборде
│   └── BannerWidget.php
├── lang/
│   ├── ru/banner.php            # __('banner.title') и т.д.
│   └── en/banner.php
└── views/
    ├── admin/banner/           # шаблоны админки
    └── default/banner/         # фолбэк-шаблоны публичной части

Публичный фасад #

Точка входа для URL /banner/*. Ядро вызовет Banner::run($action, $request):

modules/Banner/Banner.phpphp
final class Banner
{
    public static function run(string $action, Request $r): Response
    {
        if (!ModuleManager::isEnabled('Banner')) return Response::redirect('/');

        return match ($action) {
            'click'  => (new ClickController())->handle($r),
            default => Response::error404(),
        };
    }
}

Админ-роутер #

Все URL /s-panel/banner/* попадают в один роутер. Он первым делом гасит запросы без прав через module_require():

modules/Banner/BannerRouter.phpphp
final class BannerRouter
{
    public function run(string $action, Request $r): Response
    {
        if (!Auth::check()) return Response::redirect('/s-panel/login');
        if ($resp = module_require('Banner', 'view', '/s-panel/')) return $resp;

        return match ($action) {
            'list'   => (new BannerListController())->index(),
            'create' => (new BannerFormController())->create($r),
            'edit'   => (new BannerFormController())->edit($r),
            'delete' => (new BannerFormController())->delete($r),
            default  => Response::error404(),
        };
    }
}

Service-слой #

Бизнес-логика и SQL — в Service'ах, а не в контроллерах. Контроллер валидирует и отдаёт ответ, а Service ходит в БД, пишет логи и шлёт уведомления:

modules/Banner/Services/BannerService.phpphp
public function create(array $data): int
{
    DB::insert('banner__items', $data);
    $id = (int)DB::lastInsertId();

    logAction('banner.create', "id={$id}");
    notify('Создан баннер', $data['title'], '/s-panel/banner', 'success', 'fa-image');
    return $id;
}

Миграция и сидинг #

migration.sql модуля выполняется при установке. В нём — DDL своих таблиц плюс INSERT в routes и settings__menu, чтобы URL и пункты меню сразу появились в админке.

modules/Banner/migration.sqlsql
CREATE TABLE banner__items (
    id          INT AUTO_INCREMENT PRIMARY KEY,
    title       VARCHAR(255) NOT NULL,
    image_path  VARCHAR(512),
    url         VARCHAR(512),
    is_active   TINYINT(1) NOT NULL DEFAULT 1,
    created_at  DATETIME DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

INSERT INTO routes (id, slug, module, action, method) VALUES
    (300, 's-panel/banner',        'banner', 'list',   'GET'),
    (301, 's-panel/banner-create', 'banner', 'create', 'POST');

INSERT INTO settings__menu (id, title, url, icon, parent_id, order_num, is_visible, module_name) VALUES
    (100, 'Баннеры', '/s-panel/banner', 'fa fa-image', NULL, 60, 1, 'Banner');

Важно: id'ы пишите вручную, потому что routes.id и settings__menu.id объявлены без AUTO_INCREMENT в схеме ядра.

Локализация #

Каталог lang/ модуля — это словари для __(). Имя файла даёт префикс ключа:

modules/Banner/lang/ru/banner.phpphp
return [
    'title'      => 'Баннеры',
    'create'     => 'Добавить баннер',
    'fields' => [
        'title'  => 'Заголовок',
        'image'  => 'Изображение',
    ],
];

// в шаблоне:
// __('banner.fields.title') → 'Заголовок'