> For the complete documentation index, see [llms.txt](https://docs.johncms.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.johncms.com/10.0/moduli/sozdanie-modulya.md).

# Создание модуля

Давайте создадим свой первый простой модуль.\
Это будет обычная простая страница со списком наших партнёров.

Как нам уже известно, модули располагаются в папке **modules**, каждый — внутри папки своего вендора.

Вендор — это ваше имя или имя вашей студии; оно же станет первой частью имени пакета, если вы решите его публиковать. Возьмём для примера **mysite**: путь к папке модуля получится таким — **modules/mysite/partners**.

Пока создадим простой модуль без мультиязычности.

Исходный код модуля располагается в папке **src** и делится по слоям (Application, Domain, Infrastructure) — подробнее в [структуре модуля](/10.0/moduli/struktura-modulya.md). Для нашей простой страницы понадобится только прикладной слой (Application), конфигурация и шаблоны.

После выполнения всех действий из этой статьи у нас получится такая структура:

* modules
  * mysite
    * partners
      * module.php
      * config
        * routes.php
        * services.php
      * src
        * Application
          * Controllers
            * PartnersController.php
      * templates
        * public
          * index.twig

Контроллеры позволяют избавиться от большого количества базового кода, который необходимо написать для начала работы, а также упрощают настройку маршрутов: не нужно самостоятельно писать логику по определению страницы, которую необходимо показать пользователю.

Контроллеры являются обычными PHP-классами. В JohnCMS используется автозагрузка классов модулей по стандарту [PSR-4](https://www.php-fig.org/psr/psr-4/). Чтобы она работала, нужно придерживаться некоторых правил:

1. Классы модуля должны располагаться в папке **src** внутри модуля, а [пространство имён](https://www.php.net/manual/ru/language.namespaces.rationale.php) должно соответствовать структуре папок. Разрешено использовать любые директории внутри **src** для логического разделения классов.
2. Пространство имён объявляется в манифесте модуля — файле **module.php**. Корневой `composer.json` при этом трогать не нужно: он принадлежит поставке CMS и будет перезаписан при её обновлении.

## Манифест модуля

Папка становится модулем, когда в ней есть файл **module.php** — манифест. Создайте его в корне папки модуля:

{% code title="modules/mysite/partners/module.php" %}

```php
<?php

declare(strict_types=1);

return [
    // Ключ: вендор и название модуля. Должен совпадать с путём к папке.
    'key'      => 'mysite/partners',

    // Короткое имя: пространство имён шаблонов (@partners), домен переводов и
    // источник миграций. Менять его после выпуска модуля нельзя.
    'alias'    => 'partners',

    // Название модуля для панели администратора.
    'name'     => 'Партнёры',

    // Версия модуля. Сравнивается при обновлении и подставляется в адреса ассетов.
    'version'  => '1.0.0',

    // Пространство имён классов модуля и папка внутри модуля, которой оно соответствует.
    'autoload' => ['psr-4' => ['Mysite\\Partners\\' => 'src/']],
];
```

{% endcode %}

Таким образом, пространством имён нашего модуля будет **Mysite\Partners**, и оно указывает на папку **modules/mysite/partners/src**. Классы регистрируются при загрузке модуля, поэтому `composer dump-autoload` выполнять не нужно — и выключённый модуль перестаёт быть видимым в том числе для автозагрузчика.

{% hint style="info" %}
Пространство имён `Johncms\Modules\…` занято модулями поставки — для своего модуля берите собственное, чтобы классы не столкнулись.
{% endhint %}

Если `alias` не указать, он будет собран из ключа — `mysite.partners`. Короткое имя удобнее, но оно должно быть свободно: два модуля не могут занимать одно. Заняты и два имени, которые не принадлежат ни одному модулю: `system` — так называются миграции и переводы самой CMS, `theme` — пространство имён шаблонов темы. Модуль с таким алиасом система пометит как сломанный и устанавливать откажется. Совпадать с названием папки алиас не обязан — модуль `mysite/partner-list` с алиасом `partners` точно так же будет отвечать на `@partners`, `d__('partners', …)` и `migrate --source=partners`.

Остальные поля манифеста (`requires`, `assets`, `system`) описаны в статье [Публикация модуля](/10.0/moduli/publikaciya-modulya.md).

## Создание контроллера

Создадим наш первый контроллер, который будет отвечать за отображение страницы партнёров.

Исходя из типовой [структуры модуля](/10.0/moduli/struktura-modulya.md), классы контроллеров располагаются в папке **src/Application/Controllers**. С учётом объявленного пространства имён полное пространство имён для нашего контроллера будет таким: `Mysite\Partners\Application\Controllers`.

Давайте создадим файл **PartnersController.php** со следующим содержимым:

{% code title="modules/mysite/partners/src/Application/Controllers/PartnersController.php" %}

```php
<?php

declare(strict_types=1);

namespace Mysite\Partners\Application\Controllers;

use Johncms\Http\View\ViewResponse;
use Johncms\NavChain;

final readonly class PartnersController
{
    public function __construct(
        private NavChain $navChain,
    ) {
    }

    public function __invoke(): ViewResponse
    {
    }
}
```

{% endcode %}

Разберёмся с этим кодом:

* Контроллер объявлен как `final readonly class` — это рекомендуемый стиль для новых классов.
* Зависимости передаются через конструктор (constructor injection) и разрешаются автоматически контейнером зависимостей. Нам понадобятся:
  * `NavChain` — сервис для работы с цепочкой навигации (хлебными крошками).
* Метод `__invoke()` делает контроллер «вызываемым»: именно он выполняется при обращении к маршруту. Он возвращает `ViewResponse` — имя шаблона и данные для него. Сам контроллер страницу не рендерит: этим занимается ядро.
* Подключать локализацию модуля не нужно: маршруты, объявленные в `modules/mysite/partners/config/routes.php`, помечены этим модулем, и ядро подключает его домен переводов на каждый запрос. Как только в модуле появится папка **locale**, `__()` в контроллере и в его шаблонах начнёт брать строки оттуда.

Теперь дополним метод `__invoke()`.

Добавим нашу страницу в цепочку навигации:

```php
$this->navChain->add('Партнёры', '/partners/');
```

Подготовим данные для шаблона. Наполним массив нашими партнёрами и передадим его в шаблон:

```php
// Собираем массив данных, который будет передан в шаблон
$data = [
    'partners' => [
        [
            'name' => 'JohnCMS', // Название партнёра
            'url'  => 'https://johncms.com', // Ссылка на сайт партнёра
        ],
        [
            'name' => 'Партнёр 2',
            'url'  => 'https://example.com',
        ],
        [
            'name' => 'Партнёр 3',
            'url'  => 'https://example.org',
        ],
    ],
];

return new ViewResponse(
    '@partners/public/index.twig',
    [
        // Заголовок в теге title и заголовок страницы (h1)
        'title'      => 'Партнёры',
        'page_title' => 'Наши партнёры',
        'partners'   => $data['partners'],
    ]
);
```

Обратите внимание на последние строки. У каждого модуля есть своё пространство имён для шаблонов — это его `alias`; оно появляется само, как только модуль установлен. В имени `'@partners/public/index.twig'` после `@` — алиас модуля, дальше — путь к файлу внутри папки **templates**.

Вторым аргументом передаётся массив данных: его ключи становятся именами переменных шаблона. В нашем примере в шаблоне будут доступны `title`, `page_title` и `partners`.

### Полный код файла контроллера

{% code title="modules/mysite/partners/src/Application/Controllers/PartnersController.php" %}

```php
<?php

declare(strict_types=1);

namespace Mysite\Partners\Application\Controllers;

use Johncms\Http\View\ViewResponse;
use Johncms\NavChain;

final readonly class PartnersController
{
    public function __construct(
        private NavChain $navChain,
    ) {
    }

    public function __invoke(): ViewResponse
    {
        // Добавляем страницу в цепочку навигации
        $this->navChain->add('Партнёры', '/partners/');

        // Собираем массив данных, который будет передан в шаблон
        $data = [
            'partners' => [
                [
                    'name' => 'JohnCMS', // Название партнёра
                    'url'  => 'https://johncms.com', // Ссылка на сайт партнёра
                ],
                [
                    'name' => 'Партнёр 2',
                    'url'  => 'https://example.com',
                ],
                [
                    'name' => 'Партнёр 3',
                    'url'  => 'https://example.org',
                ],
            ],
        ];

        // Контроллер не рендерит страницу сам, а возвращает имя шаблона и данные для него.
        // Заголовок в теге title и заголовок страницы (h1) передаются там же.
        return new ViewResponse(
            '@partners/public/index.twig',
            [
                'title'      => 'Партнёры',
                'page_title' => 'Наши партнёры',
                'partners'   => $data['partners'],
            ]
        );
    }
}
```

{% endcode %}

## Регистрация сервисов

Контроллер создаёт не маршрутизатор, а контейнер зависимостей — тот самый, который подставляет `NavChain` в конструктор. Значит, классы модуля нужно контейнеру объявить, иначе обращение к странице закончится ошибкой о неизвестном сервисе.

Объявление лежит в файле **config/services.php** внутри модуля и подхватывается автоматически:

{% code title="modules/mysite/partners/config/services.php" %}

```php
<?php

declare(strict_types=1);

namespace Symfony\Component\DependencyInjection\Loader\Configurator;

return static function (ContainerConfigurator $container): void {
    $services = $container->services();

    // Все классы прикладного слоя: контроллеры, сценарии использования, сервисы.
    $services->load('Mysite\\Partners\\Application\\', dirname(__DIR__) . '/src/Application')
        ->autowire()
        ->autoconfigure()
        ->public();
};
```

{% endcode %}

* `autowire()` — зависимости конструкторов подставляются по типам аргументов;
* `autoconfigure()` — классы, реализующие точки расширения (права доступа, пункты меню, провайдер капчи и т.д.), подхватываются без отдельной регистрации;
* `public()` — контроллеры разрешено получать из контейнера по имени класса, именно это делает ядро при обработке маршрута.

Путь к папке указан относительно файла (`dirname(__DIR__)`), а не через константу `MODULES_PATH`: так модуль будет работать и в том случае, если его установят composer'ом — такой пакет остаётся в папке `vendor/`.

## Создание шаблона

Шаблоны пишутся на Twig. Публичные страницы модуля лежат в папке **templates/public**, админские — в **templates/admin**. Наша страница будет называться **index.twig**.

Имя шаблона в контроллере складывается из алиаса модуля и пути к файлу: `@partners/public/index.twig`. Регистрировать пространство имён не нужно — оно появляется само, как только модуль установлен.

{% code title="modules/mysite/partners/templates/public/index.twig" %}

```twig
{#
    Список партнёров.

    @var partners array
#}
{% extends '@theme/layouts/default.twig' %}

{% block content %}
    <div>
        Мы сотрудничаем со следующими партнёрами:
    </div>

    <ul>
        {# Перебираем массив партнёров и выводим название со ссылкой на сайт #}
        {% for partner in partners %}
            <li><a href="{{ partner.url }}">{{ partner.name }}</a></li>
        {% endfor %}
    </ul>
{% endblock %}
```

{% endcode %}

{% hint style="info" %}
Twig экранирует всё, что печатает, поэтому отдельно вызывать `htmlspecialchars()` не нужно — и наоборот, не стоит экранировать данные при сохранении в базу.
{% endhint %}

## Добавление маршрута

Наш модуль почти готов, но пока ещё не доступен в браузере. Давайте это исправим.\
Чтобы у модуля появились адреса страниц, нужно создать файл `config/routes.php` внутри папки модуля. Система подхватит его автоматически.

{% code title="modules/mysite/partners/config/routes.php" %}

```php
<?php

declare(strict_types=1);

use Johncms\Router\RouteCollection;
use Mysite\Partners\Application\Controllers\PartnersController;

return static function (RouteCollection $router): void {
    /*
     * /partners - Это адрес страницы, по которому будет доступен наш модуль.
     *
     * Вторым параметром передаётся класс контроллера. Так как контроллер
     * является вызываемым (реализует метод __invoke), название метода указывать не нужно.
     */
    $router->get('/partners', PartnersController::class)->name('partners.index');
};
```

{% endcode %}

Маршрут объявлен без завершающего слэша, а в ссылках слэш ставится (`/partners/`) — это общая [конвенция](/10.0/moduli/marshrutizaciya-routing.md), работают оба варианта.

## Установка модуля

Файлы на месте, но система пока считает наш модуль только **обнаруженным**: установка — отдельный шаг, именно она выполняет миграции модуля и его установщик. Выполните:

```bash
php system/bin/console module:install mysite/partners
```

Проверить, что система видит модуль и включила его, можно командой:

```bash
php system/bin/console module:list
```

То же самое делается в панели администратора, в разделе **Модули** (`/admin/modules`). Что происходит при установке, выключении и удалении, описано в статье [Установка и удаление модулей](/10.0/moduli/ustanovka-modulei.md).

Теперь наш модуль доступен по адресу **ваш.сайт/partners/**

## Отметка в списке онлайн

Теперь давайте сообщим модулю online, что у нас появился модуль партнёров и нужно в списке пользователей онлайн отображать тех, кто смотрит эту страницу.\
Для этого перейдём в папку **config** и создадим в ней файл **places.local.php**, если его ещё нет.

{% code title="/config/places.local.php" %}

```php
<?php

declare(strict_types=1);

return [
    '/partners' => '<a href="/partners/">Смотрит партнёров</a>',
];
```

{% endcode %}

Ключ — это путь страницы без завершающего слэша, таким его видит система. Файл дополняет `places.global.php`, где лежат отметки модулей поставки.

Отлично, наш модуль теперь полностью работоспособен, вам останется только добавить на него ссылку в основном шаблоне или на любой другой странице на ваше усмотрение.
