# Гайд для ИИ-агентов: расширение Celena CMS

Этот документ — единственный источник правды для ИИ-агентов (Claude, Cursor, Copilot и т.п.), создающих плагины и темы для Celena CMS. Следуй ему буквально.

---

## Контекст проекта

- **Имя**: Celena CMS.
- **Стек**: PHP 8.3+, PDO, OOP, **без сторонних фреймворков** (Laravel, Symfony и т.п. запрещены).
- **БД**: PostgreSQL по умолчанию или MySQL. Все запросы — только через `Celena\Core\Database\QueryBuilder` или `Connection::run()` с prepared statements.
- **Шаблонизатор**: собственный, синтаксис в [TEMPLATES.md](TEMPLATES.md). PHP в `.tpl` запрещён.
- **Архитектура**: см. [ARCHITECTURE.md](ARCHITECTURE.md).

---

## Жёсткие запреты

1. **Не править код в `core/`** без явного запроса пользователя. Это ядро.
2. **Не вставлять PHP в `.tpl`-файлы** (`<?php`, `<?=`). Использовать только теги шаблонизатора.
3. **Не использовать сторонние CDN** (Google Fonts, jsDelivr, unpkg). Все ассеты — локально.
4. **Не подключать composer-пакеты** в плагины без согласования. Если нужен пакет — обосновать в PR.
5. **Не писать SQL-конкатенацию**. Только `QueryBuilder` или `prepare()` с параметрами.
6. **Не использовать `eval`, `unserialize` (на пользовательских данных), `shell_exec` без обоснования.**
7. **Не менять схему БД миграциями ядра.** Свои миграции — в `plugins/<slug>/migrations/`.
8. **Не оставлять `var_dump`, `dd()`, `console.log` в коммитах.**

---

## Стиль кода

- Везде `declare(strict_types=1);` в первой строке после `<?php`.
- Типы аргументов и возврата — обязательно (включая `void`, `never`, union-типы).
- Один класс — один файл. Имя файла = имя класса.
- Namespace:
  - `Celena\Core\*` → `core/`
  - `Celena\Module\<Name>\*` → `modules/<Name>/`
  - `Celena\Plugin\<Slug>\*` → `plugins/<slug>/src/`
  - `Celena\Theme\<Slug>\*` → `templates/<slug>/` (если требуется PHP-компаньон)
- Константы и enum'ы вместо «магических строк».
- `final class` по умолчанию, если не предполагается наследование.
- Минимум комментариев. Описывать только **почему**, а не **что**.

---

## Скелет нового плагина

```
plugins/<slug>/
├── manifest.json
├── plugin.php
├── README.md
├── migrations/
│   └── 0001_create_<slug>_tables.php
├── src/
│   ├── Controllers/
│   │   └── ExampleController.php
│   ├── Models/
│   └── Services/
├── templates/
│   ├── admin/
│   └── async/
├── languages/
│   ├── ru.json
│   └── en.json
└── assets/
    ├── plugin.css
    └── plugin.js
```

`manifest.json`:
```json
{
  "slug": "myplugin",
  "name": "My Plugin",
  "version": "1.0.0",
  "author": "Имя",
  "description": "Что делает плагин",
  "requires": {
    "php": ">=8.3",
    "celena": ">=1.0"
  },
  "permissions": ["news.read", "options.write"],
  "migrations": "migrations/"
}
```

`plugin.php` (точка входа — регистрация хуков):
```php
<?php
declare(strict_types=1);

use Celena\Core\Kernel;
use Celena\Core\Plugin\Hook;
use Celena\Core\Template\BlockRegistry;

$container = Kernel::container();

// Регистрация async-блока
$container->make(BlockRegistry::class)->register('myplugin_widget', function ($c, array $params): array {
    return [
        'template' => 'plugins/myplugin/widget',
        'vars' => ['greeting' => 'Hello from MyPlugin'],
    ];
});

// Подписка на хук
Hook::on('news.published', function (array $news): void {
    // ваша логика
});

// Изменение значений через filter
Hook::addFilter('news.title', function (string $title): string {
    return mb_convert_case($title, MB_CASE_TITLE, 'UTF-8');
});
```

---

## Скелет новой темы

См. подробный план в [TEMPLATES.md](TEMPLATES.md), раздел «Быстрый старт». Минимум:

```
templates/<slug>/
├── theme.json
├── home.tpl
├── news_list.tpl
├── news_full.tpl
├── page.tpl
├── search.tpl
├── 404.tpl
├── _partials/
│   ├── head.tpl
│   └── foot.tpl
└── assets/
    └── theme.css
```

---

## Как добавить свой тег шаблонизатора

В `plugin.php`:
```php
use Celena\Core\Template\TagRegistry;

$tags = $container->make(TagRegistry::class);

// Inline-тег
$tags->registerInline('my_price', function (array $params, array $vars): string {
    $amount = (float) ($params['amount'] ?? 0);
    return number_format($amount, 2, '.', ' ') . ' $';
});

// Блочный тег (с телом)
$tags->registerBlock('my_loop', function (array $params, array $vars, callable $renderBody): string {
    $out = '';
    foreach (range(1, (int) ($params['times'] ?? 1)) as $i) {
        $out .= $renderBody($vars + ['i' => $i]);
    }
    return $out;
});
```

В шаблоне:
```html
{my_price amount="1234.5"}            → 1 234.50 $
[my_loop times="3"]
  Итерация {i}<br>
[/my_loop]
```

---

## Как добавить async-блок

```php
$container->make(BlockRegistry::class)->register('shop_cart', function ($c, array $params): array {
    return [
        'template' => 'plugins/shop/async/cart',
        'vars' => ['items' => /* ... */],
    ];
});
```

В шаблоне:
```html
{async block="shop_cart" trigger="visible" placeholder="Загружаем корзину…"}
```

---

## Как добавить хук (action / filter)

В коде ядра, модуля или плагина — испускайте события:

```php
use Celena\Core\Plugin\Hook;

// «Что-то произошло»
Hook::action('news.published', $news);

// «Дай мне обработать значение»
$title = Hook::filter('news.title', $news['title']);
```

Подписываться можно из любого `plugin.php`:

```php
Hook::on('news.published', fn($n) => /* … */, priority: 20);
Hook::addFilter('news.title', fn(string $t) => trim($t));
```

Имена хуков — `<area>.<event>` в нижнем регистре с точкой. Документируйте свои хуки в `README.md` плагина.

---

## Миграции

Плагин кладёт миграции в `plugins/<slug>/migrations/`. Файлы — `NNNN_description.php`, лексикографический порядок:

```php
<?php
declare(strict_types=1);

use Celena\Core\Database\Blueprint;
use Celena\Core\Database\Connection;
use Celena\Core\Database\Schema;

return function (Schema $schema, Connection $conn): void {
    $schema->create('myplugin_items', function (Blueprint $t) {
        $t->id();
        $t->string('title', 255);
        $t->text('body')->nullable();
        $t->boolean('is_active')->default(true);
        $t->timestamps();
        $t->index('is_active');
    });
};
```

Доступные методы Blueprint: `id, string, text, integer, bigInteger, boolean, json, datetime, date, float, decimal, nullable, default, unique, index, timestamps`.

Миграции автоматически применяются при включении плагина и не повторяются благодаря таблице `celena_migrations`.

---

## Чек-лист безопасности перед отправкой PR

- [ ] Все запросы к БД — через QueryBuilder или prepared statements (нет конкатенации).
- [ ] Любой пользовательский ввод проходит `Celena\Core\Security\Filter`.
- [ ] Любой вывод в HTML — через `e()` (в PHP) или auto-escape шаблонизатора (в `.tpl`).
- [ ] Все non-GET формы содержат `{csrf}` или валидируют `X-CSRF-Token`.
- [ ] Загрузки файлов проверяют mime + extension + magic bytes, ренеймят файл.
- [ ] Нет `eval`, `unserialize` пользовательских данных.
- [ ] Нет PHP-кода в `.tpl`.
- [ ] Нет сторонних CDN.
- [ ] Тёмная тема не сломана (проверить с `data-theme="dark"`).
- [ ] Адаптивность не сломана (проверить ширину 360px и 1280px).

---

## Локальная разработка

```bash
# 1. Развернуть Docker-окружение или OSPanel
# 2. Поставить ядро
php bin/celena migrate

# 3. После правок шаблонов — сбросить кеш
php bin/celena cache:clear

# 4. Список маршрутов
php bin/celena route:list
```

Включать `APP_DEBUG=true` в `.env` для подробных стек-трейсов. **Никогда не оставлять debug=true в продакшене.**

---

## Готовые сервисы ядра

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

| Что нужно | Класс | Где брать |
|---|---|---|
| Чтение настроек | `Celena\Module\Settings\Options` | `$c->make(Options::class)` |
| Авторизация | `Celena\Core\Security\Auth` | `$c->make(Auth::class)` |
| Хеш паролей | `Celena\Core\Security\Password` | статически |
| CSRF-токены | `Celena\Core\Security\Csrf` | статически |
| Rate-limit | `Celena\Core\Security\RateLimiter` | `$c->make(RateLimiter::class)` |
| Санитайз ввода | `Celena\Core\Security\Filter` | статически |
| Проверка прав | `Celena\Core\Security\Permission` | `Permission::can($user, 'news.create')` |
| Markdown → HTML | `Celena\Core\Support\Markdown` | `Markdown::render($src)` |
| Логи | `Celena\Core\Logger` | `$c->make(Logger::class)` |
| Очередь задач | `Celena\Core\Queue\QueueManager` | `$c->make(QueueManager::class)` |
| Email | `Celena\Core\Mail\Mailer` | `$c->make(Mailer::class)` |
| Пагинация | `Celena\Core\Http\Pagination` | `new Pagination($page, $perPage, $total)` |
| Flash-сообщения | `Celena\Core\Http\Flash` | `Flash::success('...')` |
| ИИ-задачи | `Celena\Core\Ai\AiManager` | `$c->make(AiManager::class)->run('summarize', ['text'=>$t])` |
| Custom fields | `Celena\Module\CustomFields\FieldRepository` | создать с Connection |

## Базовый контроллер админки

Для страниц в админке наследуйте `Celena\Module\AdminBase\AdminController`:

```php
final class MyController extends AdminController
{
    public function index(Request $req): Response
    {
        return $this->view('admin/mymodule/index', ['items' => []], 'my-menu', 'Заголовок');
    }
}
```

Он сам подставит текущего пользователя, active_menu, flash-сообщения, название сайта.

## Что делать, если непонятно

1. Прочитать [ARCHITECTURE.md](ARCHITECTURE.md).
2. Открыть `core/` и посмотреть, как реализованы аналогичные вещи в ядре.
3. Если нашёл несоответствие документации и кода — кодекс — это код. Открой issue.
4. Если задача требует менять `core/` — запросить подтверждение пользователя.
