> 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/nachalo-raboty/obnovlenie-s-versii-9.9.md).

# Обновление с версии 9.9

{% hint style="warning" %}
Обновляться на 10.0 можно **только с версии 9.9**. Если у вас более старая версия, сначала обновитесь до 9.9 по инструкции в [документации ветки 9.9](https://github.com/johncms/documentation/tree/9.9), а затем возвращайтесь сюда.
{% endhint %}

## Главное изменение: корень сайта переехал в `public/`

Раньше корнем сайта (document root) была корневая папка JohnCMS. Это означало, что по прямому адресу были доступны и `config/`, и `system/`, и `vendor/`, и файл `.env` — от чтения их спасали только правила в `.htaccess`.

Теперь по HTTP доступна только папка **`public/`**. Код, конфигурация и шаблоны остались в корне и больше не отдаются веб-сервером.

**Адреса страниц и файлов при этом не изменились.** Структура внутри `public/` повторяет прежнюю, поэтому `/themes/default/assets/css/app.css`, `/upload/...` и `/install/` открываются по тем же адресам, что и раньше.

### Что нужно сделать при обновлении

#### 1. Переведите корень сайта на папку `public`

Это главный и обязательный шаг.

* **Обычный хостинг с панелью управления.** В настройках сайта найдите поле «Корневая директория сайта» (или «Document root») и укажите в нём папку `public` внутри каталога сайта. Например, было `/var/www/mysite`, стало `/var/www/mysite/public`.
* **Nginx.** В конфигурации сервера измените директиву `root`:

  ```nginx
  server {
      # было: root /var/www/mysite;
      root /var/www/mysite/public;
      index index.php;
  }
  ```
* **Apache.** Измените `DocumentRoot` в конфигурации виртуального хоста:

  ```apache
  DocumentRoot /var/www/mysite/public
  <Directory /var/www/mysite/public>
      AllowOverride All
      Require all granted
  </Directory>
  ```

#### 2. Перезапустите PHP

После обновления обязательно перезапустите **php-fpm** (или веб-сервер, если PHP работает как модуль Apache). PHP кэширует пути к файлам (realpath cache), и без перезапуска сайт может продолжать искать файлы по старым адресам.

```bash
sudo systemctl restart php8.2-fpm
```

#### 3. Обновите зависимости

```bash
composer install
```

{% hint style="danger" %}
Если корень сайта не перевести на `public/`, сайт перестанет открываться: в корневой папке больше нет файла `index.php`.
{% endhint %}

### Если корень сайта сменить нельзя

На некоторых дешёвых хостингах корневую папку сайта поменять невозможно. Для таких случаев в корне JohnCMS лежит файл `.htaccess`, который перенаправляет все запросы в `public/` и закрывает доступ к папкам с кодом. Он подхватится сам, и сайт продолжит работать.

{% hint style="warning" %}
Это запасной вариант, а не полноценная замена. Код при нём физически остаётся внутри веб-дерева, и защита держится на правилах `.htaccess`. Работает он **только на Apache**: если у вас nginx, единственный способ — перевести корень сайта на `public/`.
{% endhint %}

### Что это значит для авторов шаблонов

Шаблон теперь разложен по двум местам:

* исходники и шаблоны страниц остались в корне — `themes/<ваш шаблон>/src/` и `themes/<ваш шаблон>/templates/`;
* собранные стили, скрипты и картинки переехали в `public/themes/<ваш шаблон>/assets/`.

Сборка перешла с webpack на Vite. Точки входа тема объявляет сама — в разделе `entries` своего манифеста `theme.php`; `vite.config.js` собирает их из манифестов всех установленных тем, поэтому править конфигурацию сборки не нужно. Собранные файлы попадают в `public/build/`, а шаблон подключает их через `{{ vite('public') }}` — какой именно файл подставить, система берёт из того же раздела `entries`. Картинки, шрифты и прочая статика по-прежнему лежат в `public/themes/<ваш шаблон>/assets/`, и функция `asset()` в шаблонах работает как прежде.

## Шаблоны переведены на Twig

{% hint style="danger" %}
Это ломающее изменение для сторонних тем: шаблоны `.phtml` больше не работают, движок Plates из системы удалён.
{% endhint %}

Все шаблоны движка теперь написаны на [Twig](https://twig.symfony.com/), расширение файлов — `.twig`. Если у вас своя тема, её шаблоны нужно переписать. Что изменилось по сути:

* **Подключение макета.** Вместо `$this->layout('system::layout/default')` страница пишет `{% extends '@theme/layouts/default.twig' %}`, а её содержимое кладётся в `{% block content %}…{% endblock %}`.
* **Имена шаблонов.** Вместо `модуль::файл` — `@модуль/public/файл.twig`. Пространства имён: `@theme` — шаблоны темы, `@admin` — вёрстка админ-панели, `@имя_модуля` — шаблоны модуля.
* **Вывод данных.** Вместо `<?= $this->e($value) ?>` — `{{ value }}`. Twig экранирует всё, что печатает, поэтому экранировать вручную не нужно. Если значение — готовая разметка, она приходит из PHP как `Twig\Markup` и выводится так же; фильтр `|raw` нужен лишь в редких случаях.
* **Расположение шаблонов модуля.** Публичные страницы лежат в `templates/public`, админские — в `templates/admin`, поэтому и в теме путь стал длиннее: `themes/<ваша тема>/templates/homepage/public/index.twig`.
* **Письма** переехали из `templates/system/mail/` в `templates/emails/`, системные страницы (результат действия, ошибки 403 и 404) — в `templates/pages/`, общие блоки — в `templates/components/`.

Живой пример темы, состоящей из одного файла, лежит в `themes/example`: включите её в настройках и откройте главную страницу — там же рассказано, как устроена тема.

## Удалены скрипты обновления со старых версий

Из папки `install` удалены разовые скрипты обновления с версий ниже 9.9, конвертеры данных и скрипты доустановки модулей 9.9 (`install_collections.php`, `install_consent.php`, `install_contacts.php`, `update_counters_cookie_consent.php`). Теперь папка `install` содержит только веб-инсталлятор.

Если вы обновляетесь с версии ниже 9.9, эти скрипты и инструкции к ним остались в ветке `9.x` и в [документации ветки 9.9](https://github.com/johncms/documentation/tree/9.9). Выполните их **до** перехода на 10.0.

## Зависимости переехали в `vendor/`

Composer-зависимости, которые раньше лежали в `system/vendor`, теперь устанавливаются в стандартную папку `vendor/` в корне. При обновлении удалите старую папку и переустановите зависимости:

```bash
rm -rf system/vendor
composer install
```

## База данных: обновляется одной командой

Схема базы данных описана миграциями, и приводит её в порядок одна команда — выполните её после `composer install`:

```bash
php system/bin/console migrate
```

Она добавит всё, чего в вашей базе ещё нет: таблицы ролей и сессий, колонки повторной отправки писем. Уже существующее не трогается, повторный запуск безопасен. Прежние команды `auth:upgrade-schema` и `mail:upgrade-schema` удалены — их работу делает `migrate`.

Если доступа к консоли нет, запустите задачу «Update the database» в админ-панели, в разделе **Обслуживание** (`/admin/maintenance`). Пока обновление не выполнено, админ-панель напоминает об этом на каждой странице.

{% hint style="warning" %}
Перед обновлением сделайте резервную копию базы данных. MySQL не откатывает изменения структуры таблиц, поэтому прерванное обновление невозможно отменить автоматически.
{% endhint %}

Подробности — в разделе [Миграции базы данных](/10.0/baza-dannykh/migracii.md).

## Права доступа: должности стали ролями

Числовое поле `users.rights` (0 — пользователь, 3 — модератор форума, 9 — супервизор) и настройки доступа к модулям (`mod_forum`, `mod_guest`, `mod_lib`, `mod_down`, `mod_reg`, `active`) заменены ролями и правами. Подробности для авторов модулей — в разделе [Права доступа](/10.0/moduli/prava-dostupa.md).

Таблицы ролей и сессий создаёт `migrate`. После него перенесите данные:

```bash
php system/bin/console auth:migrate-legacy-access
```

Команда создаёт встроенные роли, выдаёт каждому сотруднику ту, которая соответствует его прежней должности, и превращает настройки `mod_*` в права ролей. Нужно ли что-то делать, она определяет по самой базе: пока колонка `users.rights` на месте, перенос не завершён; когда её не станет — команда скажет, что переносить нечего.

Дальше откройте `/admin/roles` и проверьте, что роли получились такими, как вы ожидаете. Сайт при этом уже работает. Когда убедитесь — удалите то, что осталось от старой системы:

```bash
php system/bin/console auth:drop-legacy-columns
```

Эта команда удаляет данные безвозвратно и отказывается работать, пока хоть у кого-то есть прежняя должность и нет роли. Отдельным шагом она осталась намеренно: решение выбросить старые колонки принимает человек, а не автоматическое обновление.

{% hint style="info" %}
Есть и третья команда — `auth:sync-roles`. Она не про переход с 9.9: её стоит запускать после **каждого** обновления CMS. Новая версия или новый модуль могут объявить роль или право, которых у вашего сайта ещё нет, — эта команда их добавит. Она только добавляет и никогда ничего не отнимает, поэтому запускать её повторно безопасно. Всё то же есть кнопками в админке, в разделе **Обслуживание**.
{% endhint %}

Экран «Права» (`/admin/modules-access`) удалён: кто может открывать модуль и писать в нём, настраивается в `/admin/roles`. Переключатели комментариев в библиотеке и загрузках и модерации регистрации переехали в «Системные настройки».

## Почта: письмо больше не теряется при сбое отправки

Письмо, которое не удалось отправить, раньше помечалось отправленным и пропадало. Теперь оно остаётся в очереди и отправляется повторно, а причина сбоя сохраняется. Нужные для этого колонки таблице `email_messages` добавляет `migrate` — отдельная команда для этого больше не нужна.

Настройка почты пополнилась параметром `dsn` — строкой подключения symfony/mailer, через которую доступны все её транспорты, включая провайдеров с собственным API. Прежняя настройка через `transport` и `options` продолжает работать. Подробности — в разделе [Отправка электронной почты](/10.0/obshie-svedeniya/otpravka-elektronnoi-pochty-email.md).

## Обработка картинок: новый сервис вместо Intervention Image

Библиотека обработки изображений обновлена со второй версии на четвёртую, и код CMS больше не обращается к ней напрямую. Вместо неё внедряется собственный сервис `Johncms\Image\ImageProcessorInterface`.

**Для сайта ничего делать не нужно** — достаточно `composer install`. Изменение касается авторов сторонних модулей: сервиса `Intervention\Image\ImageManager` в контейнере больше нет, и модуль, который его запрашивал, работать не будет.

Было:

```php
$this->imageManager->make($file->tmpPath)
    ->resize(400, 300, static function ($constraint): void {
        $constraint->aspectRatio();
        $constraint->upsize();
    })
    ->save($target, 100, 'jpg');
```

Стало:

```php
$this->imageProcessor->saveScaledDown($file->tmpPath, $target, 400, 300);
```

Подробности — в разделе [Обработка изображений](/10.0/obshie-svedeniya/images.md).

Заодно изменилось поведение при сохранении: снимки с телефона теперь разворачиваются по данным EXIF (раньше сохранялись лежащими на боку), а метаданные из сохранённого файла вырезаются — в публичный файл больше не попадают GPS-координаты места съёмки.

## Файлы хранятся через диски

Работа с файловой системой собрана за одним интерфейсом — `Johncms\Storage\StorageInterface`. Модули больше не собирают пути из `UPLOAD_PATH` и не создают папки сами: диск делает это за них, а какой именно диск используется, решает конфигурация. Появилась поддержка объектных хранилищ, совместимых с S3.

Что изменилось для авторов модулей:

| Было                                                         | Стало                                                     |
| ------------------------------------------------------------ | --------------------------------------------------------- |
| `di(Johncms\Files\Filesystem::class)->storage('local')`      | `Johncms\Storage\StorageInterface` через конструктор      |
| `Johncms\Files\FileStorage`                                  | `Johncms\Files\FileStore`                                 |
| `$storage->saveFromRequest($request, 'upload', 'my_module')` | `$files->storeUpload($uploadedFileDTO, 'my_module')`      |
| `Johncms\Files\Models\File`                                  | `Johncms\Files\StoredFileDTO` (модель наружу не выдаётся) |
| `UPLOAD_PATH . 'my_module/' . $name`                         | `$storage->store('my_module/' . $name, $contents)`        |
| `League\Flysystem\FilesystemException`                       | `Johncms\Storage\StorageException`                        |

Формат `config/autoload/filesystem.global.php` тоже изменился: вместо `storages` с ключами `type` и `root_dir` — `disks` с ключами `driver`, `root`, `url`, `visibility` и `permissions`. Файл входит в поставку и обновляется вместе с CMS; свои настройки держите в `filesystem.local.php`.

Подробности — в разделах [Хранилище файлов](/10.0/obshie-svedeniya/storage.md) и [Загруженные файлы](/10.0/obshie-svedeniya/files.md).

{% hint style="info" %}
Права на загруженные файлы теперь выставляются явно, по конфигурации диска. Раньше режим файла определялся значением `umask` на сервере, и при строгом `umask` загруженный файл мог оказаться недоступным для чтения веб-сервером — картинка загружалась, но не открывалась. Уже лежащих файлов это не касается, только новых.
{% endhint %}

## Капча выбирается в настройках

Раньше капча была одна: каждый модуль сам создавал картинку через `mobicms/captcha`, сам клал код в сессию под ключом `code` и сам рисовал поле ввода. Теперь между формой и проверкой стоит `Johncms\Captcha\CaptchaManager`, а какая капча используется, выбирают в админке: **Система → Капча**. В поставку входят картинка с кодом, hCaptcha, Yandex SmartCaptcha и Google reCAPTCHA v3; трём сервисам нужны только ключи, дополнительных пакетов они не требуют.

Что изменилось для авторов модулей:

| Было                                                                            | Стало                                            |
| ------------------------------------------------------------------------------- | ------------------------------------------------ |
| `new Mobicms\Captcha\Code()` и `new Mobicms\Captcha\Image($code)` в контроллере | `$captcha->challenge('имя_формы')`               |
| `$session->set('code', ...)` и `$session->remove('code')`                       | ничего: ответ гасит сама проверка                |
| `$request->body('code')`                                                        | `$request->body($captcha->fieldName())`          |
| `<img src="{{ captcha }}">` и своё поле ввода в шаблоне                         | `{% include '@theme/components/captcha.twig' %}` |
| `Johncms\Auth\Authentication\LoginCaptcha`                                      | `CaptchaManager` с областью `login`              |

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

Библиотека `mobicms/captcha` обновлена до 5.x — у неё изменился API: класс `Code` удалён, картинка сама создаёт код (`getCode()`, `getImage()`). Если модуль обращался к ней напрямую, его нужно перевести на `CaptchaManager`.

Подробности — в разделах [Капча](/10.0/obshie-svedeniya/captcha.md) и [Свой провайдер капчи](/10.0/moduli/svoi-provaider-kapchi.md).

## Превью картинок отдаются маршрутами, а не скриптами

Скрипты `public/assets/modules/downloads/preview.php` и `public/assets/modules/forum/thumbinal.php` удалены. Превью теперь строят обычные контроллеры, а результат кэшируется на диске в `data/cache/thumbnails` — раньше картинка декодировалась и размывалась заново при **каждом** показе списка.

Адреса превью изменились:

| Было                                                           | Стало                                              |
| -------------------------------------------------------------- | -------------------------------------------------- |
| `/assets/modules/downloads/preview.php?img=<путь>`             | `/downloads/preview/{id}` — превью самого файла    |
| `/assets/modules/downloads/preview.php?img=<путь к скриншоту>` | `/downloads/preview/{id}/{имя}` — превью скриншота |
| `/assets/modules/forum/thumbinal.php?file=<имя>`               | `/forum/file-preview/{id}`                         |

Стандартные шаблоны обновлены. Проверить нужно только собственные шаблоны и модули, если вы вставляли эти адреса вручную.

{% hint style="warning" %}
Старые скрипты принимали путь к картинке прямо из адреса. Имя файла с `../` внутри позволяло прочитать любое изображение на диске, в том числе за пределами папки загрузок. Новые маршруты принимают только идентификатор записи и строят путь сами, поэтому подставить в него что-либо нельзя.
{% endhint %}
