---
name: coffee-cms-module-developer
description: Эксперт по созданию и редактированию модулей для Coffee CMS.
version: 1.1.1
tokens: 3000-4000
---
# Основной индекс навыка

Вы — специалист по разработке модулей для Coffee CMS. Ваша задача — создавать расширения, которые полностью соответствуют архитектуре системы (процедурный PHP, отсутствие ООП, использование глобального массива `$cms` и системы хуков).

## Как работать с этим скиллом

Используйте этот файл как единый источник знаний: раздел **Подробное руководство** содержит инструкции по каждому этапу разработки, а раздел **Примеры кода** — готовые фрагменты.

**Алгоритм разработки модуля:**

1.  **Планирование**: Определите таблицы БД, хуки и элементы админки.
2.  **Регистрация (`01_registration`)**: Создайте `.mod.php`, опишите метаданные и **обязательно** укажите все файлы в массиве `files`.
3.  **Логика и Хуки (`02_hooks_and_logic`)**: Реализуйте бизнес-логику через подписку на события (`query`, `template`, `echo` и т.д.).
4.  **База данных (`03_database`)**: Если нужны таблицы, используйте хук `create_tables` с проверкой совместимости MySQL/PostgreSQL.
5.  **Маршрутизация (`04_routing`)**: Добавьте свои URL через `$cms["urls"]`.
6.  **Интерфейс админки (`05_admin_ui`)**: Создайте пункты меню и HTML-страницы настроек.
7.  **API & AJAX (`06_api_ajax`)**: Реализуйте взаимодействие с клиентом через хук `api` в PHP и функцию `api()` в JS.
8.  **Ресурсы и Перевод (`07_assets_localization`)**: Подключите CSS/JS и настройте локализацию строк через `__()`.

---

## Важные правила кодинга Coffee CMS (Reminder)
- **НИКАКОГО ООП**. Только функции и массивы.
- Всегда используйте `array()` вместо `[]` для совместимости с PHP 5.2+.
- SQL запросы должны быть универсальными (MySQL/PostgreSQL).
- Передача параметров в хуках — строго **по ссылке** (`&$params`).
- Все файлы модуля должны быть перечислены в массиве `"files"`.

---

# Подробное руководство

## 1. Регистрация модуля и структура файлов

Чтобы модуль был распознан ядром Coffee CMS, необходимо создать файл `.cms/mod/{name}.mod.php`.

### Основные правила:
1. **Метаданные**: Заполните массив `$cms["modules"]["{name}.mod.php"]` с полями `name`, `description` и `files`.
2. **Список файлов (`files`)**: Это критически важный параметр. В него нужно включить **все** файлы модуля (сам `.mod.php`, JS, CSS, PHP-скрипты, файлы перевода). Если файла нет в списке, он может быть удален системой при обновлении.
3. **Проверка отключения**: Всегда добавляйте проверку на `disabled` в конфиге в начале файла, чтобы не выполнять лишний код.

### Шаблон регистрации:
```php
<?php
$cms["modules"]["{name}.mod.php"] = array(
    "name"        => "Название модуля",
    "description" => "Описание функционала",
    "files"       => array(
        ".cms/mod/{name}.mod.php",
        ".cms/js/{name}.js",
        ".cms/css/{name}.css",
        ".cms/lang/ru_RU.UTF-8/{name}.mod.php",
    ),
);

if ( ! empty( $cms["config"]["{name}.mod.php"]["disabled"] ) ) {
    return;
}
```

## 2. Внедрение логики через хуки

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

### Ключевые концепции:
1. **Регистрация**: Используйте `hook_add_fn( $hook, $function, $priority )`. 
   - `$priority`: Чем меньше число, тем раньше выполнится функция.
2. **Параметры по ссылке**: Всегда принимайте параметры в функции через `&$params`. Это позволяет изменять данные, проходящие через цепочку (например, содержимое страницы).
3. **Основные хуки**:
   - `query`: Самый ранний этап. Подходит для сбора статистики, логирования или подготовки данных.
   - `template`: Этап сборки страницы. Здесь можно модифицировать HTML или добавлять переменные в шаблон.
   - `echo`: Этап вывода контента. Позволяет внедрять (inject) элементы прямо в HTML-код страницы.
   - `write`: Этап после завершения вывода. Используется для кеширования или записи логов на диск.

### Шаблон функции хука:
```php
function {module_name}_{hook_name}_handler( &$params ) {
    global $cms;
    // Ваша логика здесь
}

hook_add_fn( "{hook}", "{module_name}_{hook_name}_handler", 10 );
```

## 3. Работа с базой данных и миграции

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

### Основные правила:
1. **Инициализация**: Проверяйте наличие `$cms["pdo"]` перед выполнением запросов.
2. **Совместимость (MySQL vs PostgreSQL)**: Разные типы БД требуют разных определений первичных ключей и типов данных для текста. Всегда делайте проверку типа драйвера через `PDO::ATTR_DRIVER_NAME`.
3. **Обработка ошибок**: Оборачивайте выполнение SQL в `try...catch`, чтобы ошибки базы не "клали" весь процесс загрузки страницы.

### Шаблон миграции:
```php
if ( is_admin() ) {
    hook_add_fn( "create_tables", "my_module_init_db" );
}

function my_module_init_db() {
    global $cms;
    if ( empty( $cms["pdo"] ) ) return;

    $type = $cms["pdo"]->getAttribute( PDO::ATTR_DRIVER_NAME );
    
    // Определяем специфичные для БД части SQL
    if ( $type === "mysql" ) {
        $id_col = "id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY";
        $text_col = "text LONGTEXT DEFAULT NULL";
    } elseif ( $type === "pgsql" ) {
        $id_col = "id BIGSERIAL PRIMARY KEY";
        $text_col = "text TEXT DEFAULT NULL";
    } else {
        return; // Не поддерживается
    }

    $sql = "CREATE TABLE IF NOT EXISTS my_table (
        {$id_col},
        name VARCHAR(255) NOT NULL,
        {$text_col}
    );";

    try {
        $cms["pdo"]->exec( $sql );
    } catch ( PDOException $e ) {
        error_log( "DB Error: " . $e->getMessage() );
    }
}
```

## 4. Маршрутизация (Routing)

Если модулю нужны собственные URL, они регистрируются в глобальном массиве `$cms["urls"]`.

### Правила:
1. **Регулярные выражения**: Ключом массива является регулярное выражение для сопоставления пути.
2. **Привязка к хуку**: Значением является имя функции (хука), которая должна быть вызвана при совпадении URL.
3. **Base Path**: Помните, что роутинг работает относительно `base_path`. Регулярное выражение должно учитывать это или ожидать путь без него (в зависимости от того, как вызывается `preg_match` в ядре). В Coffee CMS `$cms["url"]["path"]` уже очищен.

### Шаблон регистрации маршрута:
```php
// При создании пути используйте символ ^ для начала строки и $ для конца
$cms["urls"]["^/my-custom-path$"] = "my_module_custom_hook";
```

## 5. Интеграция в админ-панель

Интеграция состоит из трех уровней: Секции -> Пункты меню -> Страницы настроек.

### 1. Регистрация пункта меню
Пункт добавляется через конфиг модуля `$cms["config"]["{name}.mod.php"]`.

**Поля:**
- `title`: Заголовок (используйте `__()` для перевода).
- `sort`: Порядок сортировки.
- `section`: Секция админки (`settings`, `content`, `help`).

```php
$cms["config"]["my_module.mod.php"]["menu"]["my_mod_anchor"] = array(
    "title"   => __( "Настройки модуля", "my_module" ),
    "sort"    => 10,
    "section" => "settings",
);
```
*Примечание: `my_mod_anchor` — это идентификатор (якорь), который будет использоваться в URL админки (`#-admin#my_mod_anchor`).*

### 2. Создание страницы настроек
Содержимое страницы записывается в массив `$cms["admin_pages"]`. Это просто HTML-строка.

```php
$cms["admin_pages"]["my_mod_anchor"] = "
    <div class='panel'>
        <h3>Параметры модуля</h3>
        <form method='post' action='{$cms['base_path']}api/my_module_save'>
            <input type='text' name='setting_val' value='...'>
            <button class='button save'>Сохранить</button>
        </form>
    </div>
";
```

### 3. Условная загрузка ресурсов (Assets)
Чтобы не нагружать фронтенд, подключайте CSS/JS модуля только тогда, когда активна страница вашего модуля в админке. Для этого используется хук `admin_header` и функция `is_admin_header_on()`.

```php
if ( is_admin() ) {
    hook_add_fn( "admin_header", "my_module_load_assets" );
}

function my_module_load_assets() {
    global $cms;
    if ( is_admin_header_on( "my_module.mod.php" ) ) {
        echo "<link rel='stylesheet' href='{$cms['base_path']}css/my_module.css'>\n";
        echo "<script src='{$cms['base_path']}js/my_module.js'></script>\n";
    }
}
```

## 6. API и AJAX взаимодействие

Для динамического обновления данных в админке используется паттерн "один обработчик — много функций". Все запросы приходят на хук `api`.

### Клиентская часть (JavaScript)
Используйте встроенную функцию `api()`. Она выполняет POST-запрос и возвращает результат в callback.

```javascript
// Вызов функции сохранения
api({ 
    fn: 'save_settings', // Имя функции для обработки
    new_value: 'some_data' 
}, function(response) {
    if (response.result) {
        notify_answer({ result: { notify_html: "Успешно сохранено!" } });
    } else if (response.error) {
        notify_answer(response.error);
    }
});
```

### Серверная часть (PHP)
Подпишитесь на хук `api` и используйте `switch($_POST['fn'])` для распределения задач.

**Важно**: Все ответы должны быть в формате JSON. Используйте `echo json_encode(...)`.

```php
if ( is_admin() ) {
    hook_add_fn( "api", "my_module_api_handler" );
}

function my_module_api_handler( &$params ) {
    global $cms;

    // Проверяем, что запрос пришел именно на нашу функцию
    if ( ! isset($_POST["fn"]) ) return;

    switch ( $_POST["fn"] ) {
        case "save_settings":
            $val = trim($_POST["new_value"]);
            
            // Сохраняем в конфиг CMS
            $cms["config"]["my_module.mod.php"]["api_key"] = $val;

            if ( cms_save_config() ) {
                echo json_encode( array( "result" => array( "notify_html" => "Настройки обновлены!" ) ) );
            } else {
                echo json_encode( array( "error" => array( "notify_html" => "Ошибка сохранения в файл конфигурации!" ) ) );
            }
            return; // Важно прервать выполнение, чтобы не идти дальше

        case "delete_item":
            // Логика удаления...
            break;

        default:
            echo json_encode( array( "error" => array( "notify_html" => "Неизвестная функция API" ) ) );
            break;
    }
}
```

## 7. Ресурсы и Локализация

### 1. Управление ресурсами (CSS/JS)
Все файлы должны быть зарегистрированы в массиве `files` вашего модуля. Если вы хотите, чтобы стили или скрипты подгружались только в админке, используйте проверку `is_admin_header_on()`.

```php
// В .mod.php файле
if ( is_admin() ) {
    hook_add_fn( "admin_header", "my_module_assets" );
}

function my_module_assets() {
    global $cms;
    if ( is_admin_header_on( "my_module.mod.php" ) ) {
        echo "<link rel='stylesheet' href='{$cms['base_path']}css/my_module.css'>\n";
        echo "<script src='{$cms['base_path']}js/my_module.js'></script>\n";
    }
}
```

### 2. Локализация (Перевод)
Используйте функцию `__()`. Второй аргумент — имя вашего модуля. Это позволяет избежать конфликтов имен ключей в разных модулях.

### Использование:
```php
echo __( "welcome_message", "my_module" ); 
```

### Структура файлов перевода:
Файлы должны располагаться в `.cms/lang/{locale}/{module}.mod.php`.

**Пример файла `.cms/lang/ru_RU.UTF-8/my_module.mod.php`:**
```php
<?php
$cms["lang"]["my_module"]["ru_RU.UTF-8"] = array(
    "welcome_message" => "Добро пожаловать в мой модуль!",
);
```

---

# Примеры кода

## Регистрация модуля (`simple_module_registration.php`)
```php
<?php
// .cms/mod/favorites.mod.php

$cms["modules"]["favorites.mod.php"] = array(
    "name"        => "Избранное",
    "description" => "Позволяет пользователям добавлять контент в избранное",
    "files"       => array(
        ".cms/mod/favorites.mod.php",
        ".cms/js/favorites.js",
        ".cms/css/favorites.css",
        ".cms/lang/ru_RU.UTF-8/favorites.mod.php",
    ),
);

if ( ! empty( $cms["config"]["favorites.mod.php"]["disabled"] ) ) {
    return;
}
```

## Интеграция в админку (`admin_menu_and_page.php`)
```php
<?php
// .cms/mod/my_module.mod.php (фрагмент интеграции в админку)

// 1. Регистрация меню
$cms["config"]["my_module.mod.php"]["menu"]["my_settings"] = array(
    "title"   => __( "Мои настройки", "my_module" ),
    "sort"    => 20,
    "section" => "settings",
);

// 2. Определение страницы
$cms["admin_pages"]["my_settings"] = "
    <div class='panel'>
        <h2>Настройки модуля</h2>
        <form method='post' action='{$cms['base_path']}api/my_module_update'>
            <label>API Ключ:</label>
            <input type='text' name='api_key' value='12345'>
            <button class='button save'>Сохранить</button>
        </form>
    </div>
";

// 3. Загрузка стилей только в админке при открытии страницы модуля
if ( is_admin() ) {
    hook_add_fn( "admin_header", "my_module_assets" );
}

function my_module_assets() {
    global $cms;
    if ( is_admin_header_on( "my_module.mod.php" ) ) {
        echo "<link rel='stylesheet' href='{$cms['base_path']}css/my_module.css'>\n";
        echo "<script src='{$cms['base_path']}js/my_module.js'></script>\n";
    }
}
```

## Обработчик API (`api_handler.php`)
```php
<?php
// .cms/mod/my_module.mod.php (обработчик API)

if ( is_admin() ) {
    hook_add_fn( "api", "my_module_api_handler" );
}

function my_module_api_handler( &$params ) {
    global $cms;

    if ( empty( $_POST["fn"] ) ) return;

    switch ( $_POST["fn"] ) {
        case "update_counter":
            $count = (int) $_POST["count"];
            $cms["config"]["my_module.mod.php"]["counter"] = $count;

            if ( cms_save_config() ) {
                echo json_encode( array( "result" => array( "notify_html" => "Счетчик обновлен: {$count}" ) ) );
            } else {
                echo json_encode( array( "error" => array( "notify_html" => "Не удалось сохранить конфиг" ) ) );
            }
            return;

        default:
            echo json_encode( array( "error" => array( "notify_html" => "Unknown action" ) ) );
            break;
    }
}
```

## Миграция БД (`database_migration_example.php`)
```php
<?php
// .cms/mod/stat1.mod.php (фрагмент инициализации таблиц)

function stat1_create_tables() {
    global $cms;
    if ( empty( $cms["pdo"] ) ) return;

    $type = $cms["pdo"]->getAttribute( PDO::ATTR_DRIVER_NAME );
    
    if ( $type === "mysql" ) {
        $id_col = "id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY";
        $time_col = "visited_at DATETIME NOT NULL";
        $ip_col = "ip VARCHAR(45) NOT NULL";
    } elseif ( $type === "pgsql" ) {
        $id_col = "id BIGSERIAL PRIMARY KEY";
        $time_col = "visited_at TIMESTAMP NOT NULL";
        $ip_col = "ip VARCHAR(45) NOT NULL";
    } else {
        return;
    }

    $sql = "CREATE TABLE IF NOT EXISTS stat1_logs (
        {$id_col},
        {$time_col},
        {$ip_col},
        page_path TEXT NOT NULL,
        memory_used BIGINT NOT NULL
    );";

    try {
        $cms["pdo"]->exec( $sql );
    } catch ( PDOException $e ) {
        error_log( "Stat1 DB Error: " . $e->getMessage() );
    }
}

if ( is_admin() ) {
    hook_add_fn( "create_tables", "stat1_create_tables" );
}
```

## Внедрение кнопки через 'echo' (`echo_hook_example.php`)
```php
<?php
// Пример: Внедрение кнопки в контент на хуке 'echo'

function favorites_inject_button( &$params ) {
    // Проверяем, есть ли в параметрах контент для модификации
    if ( isset( $params["content"] ) && isset( $params["id"] ) ) {
        $btn = "<button class='fav-btn' data-id='{$params['id']}'>❤</button>";
        // Добавляем кнопку к существующему контенту
        $params["content"] .= $btn;
    }
}

// Регистрируем с низким приоритетом, чтобы кнопка была в конце контента
hook_add_fn( "echo", "favorites_inject_button", 100 );
```

## AJAX вызов (`js_api_call.js`)
```javascript
// .cms/js/my_module.js

/**
 * Функция для инкремента счетчика через API
 */
function incrementCounter() {
    const currentCount = parseInt(document.querySelector('#counter-val').innerText);
    const nextCount = currentCount + 1;

    api({ 
        fn: 'update_counter', 
        count: nextCount 
    }, function(response) {
        if (response.result) {
            // Обновляем UI при успешном ответе
            document.querySelector('#counter-val').innerText = nextCount;
            notify_answer({ result: { notify_html: response.result.notify_html } });
        } else if (response.error) {
            notify_answer(response.error);
        }
    });
}
```

## Локализация (`localization_example.php`)
```php
<?php
// .cms/lang/ru_RU.UTF-8/my_module.mod.php

$cms["lang"]["my_module"]["ru_RU.UTF-8"] = array(
    "settings_title" => "Настройки модуля",
    "save_success"   => "Изменения успешно применены!",
    "error_save"     => "Произошла ошибка при сохранении.",
);
```

## Логирование запроса (`query_hook_example.php`)
```php
<?php
// Пример: Логирование запроса на хуке 'query'

function my_logger_log( &$params ) {
    global $cms;
    $log_entry = "{$cms['url']['path']} | IP: {$_SERVER['REMOTE_ADDR']}\n";
    file_put_contents( $cms["site_dir"] . "/my_module.log", $log_entry, FILE_APPEND );
}

hook_add_fn( "query", "my_logger_log", 1 );
```

## Роутинг (`routing_example.php`)
```php
<?php
// .cms/mod/my_route.mod.php

$cms["urls"]["^/status-report$"] = "hook_report";
hook_add_fn( "hook_report", "my_route_status_handler" );

function my_route_status_handler( &$params ) {
    echo "<h1>Отчет о состоянии системы</h1>";
    echo "<p>Система работает нормально.</p>";
    // Чтобы прервать стандартный цикл и не пытаться рендерить шаблоны, 
    // можно использовать механизмы управления состоянием, если они предусмотрены.
}
```
