# Створення плагіна

Пройдемо шлях від порожньої теки до робочого плагіна з маршрутом, контролером і пунктом в адмінці. За основу зручно тримати перед очима `plugins/leads/`.

## Швидкий старт: порожній плагін

Щоб не починати з нуля, завантажте мінімальний робочий плагін:

**[Завантажити порожній плагін (zip)](/public/assets/plugins/handbook/celena-starter-plugin.zip)**

Усередині: `manifest.json`, `plugin.php` (публічний маршрут `/starter`, сторінка адмінки `/admin/starter`, пункт меню, міграція), контролери, шаблон адмінки, мови та `docs/`. Встановіть через **Плагіни → Встановити із zip** і натисніть «Увімкнути» — або розпакуйте теку `starter/` у `plugins/`. Потім перейменуйте `starter` у свій slug.

> Slug плагіна **не повинен містити дефіс**: із нього будується PHP-namespace (`Celena\Plugin\<Slug>`), а дефіс у namespace неприпустимий. Використовуйте `myplugin` або `my_plugin`.

Нижче — як усе влаштовано, щоб зібрати своє з нуля.

## З чого почати

1. Придумайте `slug` (латиниця, цифри, дефіс), наприклад `reviews`.
2. Створіть теку `plugins/reviews/` і в ній — `manifest.json` та `plugin.php`.

### manifest.json

```json
{
  "slug": "reviews",
  "name": "Відгуки",
  "version": "1.0.0",
  "author": "Ви",
  "description": "Відгуки клієнтів: приймання з фронту і модерація в адмінці.",
  "requires": { "php": ">=8.3", "celena": ">=1.0" },
  "migrations": "migrations/"
}
```

### plugin.php — точка входу

`plugin.php` підключається під час завантаження активного плагіна. Тут реєструють маршрути, хуки й теги. Контейнер доступний через `Kernel::container()`.

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

use Celena\Core\Http\Router;
use Celena\Core\Kernel;
use Celena\Core\Database\Connection;
use Celena\Core\Plugin\Hook;
use Celena\Plugin\Reviews\Controllers\ReviewsController;
use Celena\Plugin\Reviews\Admin\ReviewsAdminController;

$container = Kernel::container();

// 1. Ідемпотентний прогін міграцій (якщо є).
try {
    $dir = __DIR__ . '/migrations';
    if (is_dir($dir)) {
        (new \Celena\Core\Database\MigrationRunner(
            $container->make(Connection::class),
            $container->make(\Celena\Core\Logger::class),
        ))->run($dir);
    }
} catch (\Throwable) {}

// 2. Маршрути.
$router = $container->make(Router::class);
$router->post('/api/reviews/submit', ReviewsController::class . '@submit');   // публічний
$router->get('/admin/reviews', ReviewsAdminController::class . '@index');      // адмінка

// 3. Пункт у бічному меню адмінки.
Hook::addFilter('admin.sidebar', function (array $items): array {
    $icon = new \Celena\Core\Template\Tags\IconTag();
    $items[] = [
        'svg'   => $icon(['name' => 'star', 'size' => 22, 'class' => 'sb-ico']),
        'label' => 'Відгуки',
        'url'   => '/admin/reviews',
        'slug'  => 'reviews',
    ];
    return $items;
});
```

> Маршрути `/admin/*` реєструються БЕЗ спільного middleware — авторизацію перевіряє сам контролер (див. нижче). Публічні `/api/*` доступні всім.

## Контролери

PSR-4: клас `Celena\Plugin\Reviews\Admin\ReviewsAdminController` лежить у `plugins/reviews/src/Admin/ReviewsAdminController.php`.

### Адмінський контролер

Успадковує `Celena\Module\AdminBase\AdminController` — він дає `view()`, `redirect()`, доступ до `$this->auth`, `$this->options`, і спільний layout адмінки:

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

namespace Celena\Plugin\Reviews\Admin;

use Celena\Core\Database\Connection;
use Celena\Core\Http\Request;
use Celena\Core\Http\Response;
use Celena\Core\Template\Engine;
use Celena\Core\Security\Auth;
use Celena\Module\Settings\Options;
use Celena\Module\AdminBase\AdminController;

final class ReviewsAdminController extends AdminController
{
    public function __construct(Engine $engine, Auth $auth, Options $options, private readonly Connection $conn)
    {
        parent::__construct($engine, $auth, $options);
    }

    public function index(Request $request): Response
    {
        if ($this->auth->user() === null) {
            return Response::redirect('/admin/login');
        }
        $items = $this->conn->builder('reviews')->orderBy('created_at', 'desc')->get();
        return $this->view('plugins/reviews/admin/index', ['items' => $items], 'reviews', 'Відгуки');
    }
}
```

`$this->view($template, $vars, $activeMenu, $title)` рендерить шаблон у темі адмінки з бічним меню, сповіщеннями та поточним користувачем.

### Публічний контролер

Фронт-контролер не успадковує `AdminController`; залежності приходять через конструктор (авто-впровадження):

```php
namespace Celena\Plugin\Reviews\Controllers;

use Celena\Core\Database\Connection;
use Celena\Core\Http\Request;
use Celena\Core\Http\Response;
use Celena\Core\Security\Csrf;

final class ReviewsController
{
    public function __construct(private readonly Connection $conn) {}

    public function submit(Request $request): Response
    {
        if (!Csrf::validate((string) $request->input('_csrf'))) {
            return Response::json(['error' => 'csrf'], 419);
        }
        $this->conn->builder('reviews')->insert([
            'author'     => \Celena\Core\Security\Filter::string((string) $request->input('author')),
            'text'       => \Celena\Core\Security\Filter::string((string) $request->input('text')),
            'created_at' => date('Y-m-d H:i:s'),
        ]);
        return Response::json(['ok' => true]);
    }
}
```

## Шаблони

Шаблони плагіна лежать у `plugins/reviews/templates/`. Рушій резолвить їх за префіксом `plugins/<slug>/`:

```php
$this->view('plugins/reviews/admin/index', …);
// → plugins/reviews/templates/admin/index.tpl
```

Активна тема може перебити шаблон, створивши файл `templates/<theme>/plugins/reviews/admin/index.tpl`.

## Асети, мови, документація

- `assets/reviews.css` і `assets/reviews.js` (імʼя = slug) підключаться автоматично тегом `{plugin_assets}`. Інші файли — прямим посиланням `/public/assets/plugins/reviews/...`.
- `languages/ru.json`, `languages/en.json` підхопляться автоматично; використовуйте `{lang key="reviews.x"}`.
- Покладіть `docs/README.md` — він відкриється в адмінці кнопкою документації плагіна.

## Увімкнення

Зайдіть у **Плагіни**, натисніть «Увімкнути» біля вашого плагіна — застосуються міграції, підключиться `plugin.php`, скопіюються асети.

Далі: [Хуки та події](/docs/code/hooks), [Міграції та база даних](/docs/code/migrations), [Безпека](/docs/code/security).
