# Symfony Workflow без фреймворка: 9 практических опытов

Проект для PHP >=8.2 с PDO SQLite и Composer 2. Symfony Workflow 7.4.20 (ветка LTS), EventDispatcher 7.4.17; точные зависимости в composer.lock. PHP нужен для исполнения опытов. Браузерная мастерская показывает **запись настоящего PHP/SQLite**, а не исполняет PHP и не имитирует Symfony отдельным JS-автоматом. Для чтения уже готовой записи сервер PHP не обязателен: подойдёт любой локальный HTTP-сервер со статическими файлами.

Все заявки вымышлены. Это воспроизводимая учебная модель процесса, а не готовая CRM с пользователями, авторизацией и доставкой сообщений.

## Windows / PowerShell

Откройте PowerShell внутри распакованного `symfony-workflow-project`:

```powershell
php -v
php --ini
php -r "print_r(PDO::getAvailableDrivers());"
composer install --no-interaction --prefer-dist
composer check-platform-reqs
composer test
php -S 127.0.0.1:8511 -t public
```

Откройте http://127.0.0.1:8511/. Остановка — Ctrl+C в том же терминале. Если 8511 занят, выберите свободный порт. Требуются PHP и Composer в PATH, расширение `pdo_sqlite`. Установите совместимые сборки PHP/Composer обычным для вашей среды способом; не используйте `--ignore-platform-reqs`.

Для собственной записи после изменения исходников:

```powershell
composer capture
composer build-public
composer test
```

`capture` заново создаёт **только** `var/persistence.sqlite`, `var/concurrency.sqlite`, `var/idempotency.sqlite` и перезаписывает `public/recording.json`. Если вы вручную редактировали эти учебные базы, сначала сохраните копии. Другие файлы, внешние БД и пользовательские пути команда не принимает. `test` использует отдельный случайно названный файл `var/test-*.sqlite` и удаляет его после проверки.

## Linux / Ubuntu 24.04 LTS

```bash
sudo apt update
sudo apt install php-cli php-sqlite3 php-mbstring php-xml php-curl php-zip unzip composer
php -v
php -r 'print_r(PDO::getAvailableDrivers());'
cd symfony-workflow-project
composer install --no-interaction --prefer-dist
composer check-platform-reqs
composer test
php -S 127.0.0.1:8511 -t public
```

Для новой записи выполните `composer capture`, `composer build-public`, `composer test`. Все команды проекта работают на обеих ОС; различия касаются установки PHP и синтаксиса оболочки. В других дистрибутивах проверьте названия пакетов и версии. Сервер `php -S` предназначен для разработки, не для открытого VPS.

## Что проверяется

1. Черновик нельзя согласовать; submit и approve меняют marking.
2. Operator/manager/director и граница суммы возвращают конкретные blockers.
3. Reject требует причины; reopen возвращает в draft.
4. Параллельные ветки требуют двух отметок перед finish.
5. Повторные can вызывают guards, но не меняют состояние; порядок lifecycle events проверен.
6. Отдельный PHP-процесс сохраняет SQLite, другой читает version=2.
7. Два читателя получают version=1, первый писатель побеждает, второй получает конфликт; outbox остаётся один.
8. Повтор того же operation key возвращает исходный результат; другой payload с тем же ключом отклоняется.
9. Карточка оператора показывает реальные allowed и blockers для трёх ролей.

Конкурентный пример — **контролируемое чередование устаревших снимков независимых процессов**, не одновременная нагрузка. Он проверяет потерянное обновление. Для проверки блокировок под нагрузкой нужен отдельный тест с синхронным стартом нескольких писателей; эта мастерская не заявляет измерений пропускной способности.

## Структура

```text
src/Ticket.php          Одно текущее состояние заявки
src/Review.php          Несколько мест параллельного workflow
src/Processes.php       Определения, guards, события, inspect
src/Repository.php      SQLite, version check, receipts, outbox
bin/child.php           Отдельный PHP-процесс, фиксированные учебные БД
bin/capture.php         Исполняет 9 сценариев и 30 шагов
bin/build-public.php    Копирует README, исходники, лицензии
tests/run.php           Проверки поведения и привязки записи к исходникам
public/                Единственный каталог для HTTP
composer.json / lock    Зависимости и точные версии
var/                   Учебные SQLite, не включены в архив
```

`source_hashes` в JSON связывает запись с `src/*.php` и lock. Если изменили доменный код и тест сообщает несоответствие, выполните capture заново. Даты, ОС и версия SQLite будут вашими. Не подменяйте новые исходники старой записью.

## Отдельные CLI-вызовы

Сначала `composer capture` создаст фиксированные учебные базы. Затем:

```bash
php bin/child.php persistence read
php bin/child.php concurrency read
php bin/child.php idempotency read
```

После capture `persistence` имеет submitted/version=2. Чтобы подтвердить текущую заявку:

```bash
php bin/child.php persistence apply 2 approve my-approval-002
php bin/child.php persistence apply 2 approve my-approval-002
```

Второй вызов возвращает тот же receipt с `replayed=true`. Ключ нельзя повторно использовать для другого действия. Эти вызовы изменяют только выбранную учебную БД; веб-мастерская обновится после нового capture, который снова создаст исходные сценарии. Для отдельного интерактивного продукта создайте HTTP-слой вокруг repository и не запускайте capture на production-данных.

## Linux/VPS: разместить мастерскую

Соберите запись на машине разработки или CI. Размещайте только `public/`, отдельно ZIP для скачивания. Никакого PHP backend для публичного replay не требуется. При использовании Caddy:

```caddyfile
workflow.example.com {
    root * /srv/workflow/public
    encode zstd gzip
    file_server
}
```

Замените домен, укажите DNS сервера и откройте порты 80/443, если используете автоматический HTTPS. Проверка: `caddy validate --config /etc/caddy/Caddyfile`, затем штатный reload сервиса. `vendor/`, `src/`, `bin/`, SQLite и конфигурация остаются за пределами корня сайта. Ссылка ZIP в мастерской относительная (`../symfony-workflow-project.zip`): положите архив в соответствующий разрешённый публичный URL либо измените ссылку под своё размещение.

## Linux/VPS: встроить в PHP-приложение

Установите зависимости из lock под отдельным deployment-пользователем. Веб-сервер должен обслуживать только `public/` приложения; PHP-FPM запускается от отдельного пользователя с минимальными правами. Сам проект не включает публичный HTTP endpoint — он остаётся CLI-стендом.

Постоянную БД храните вне webroot, например `/var/lib/service-workflow/workflow.sqlite`. Пользователь PHP должен иметь право на файл **и каталог** для SQLite journal/WAL. Каталог не должен быть доступен всем на запись. Не переносите `var/` из демо поверх production. Для создания каталога при существующем пользователе сервиса:

```bash
sudo install -d -m 0750 -o service-workflow -g service-workflow /var/lib/service-workflow
```

Создание системного пользователя и настройка FPM зависят от сервера; не меняйте владельца чужого приложения без проверки. В нашем repository путь передаётся конструктору. В реальном приложении берите его из серверной конфигурации, не из HTTP-параметра.

Доверенную роль и доступ к конкретной заявке проверяет аутентифицированный сервер. Проверка объекта и применение перехода выполняются в одной короткой транзакции. Для cookie-сессии нужны CSRF-токен и обычная защита сессии. `operation_key` в учебном проекте глобален; в multi-tenant приложении ключ нужно ограничить пользователем/организацией, проверять права при повторном чтении receipt, хранить отпечаток значимых полей и вводить срок хранения.

## Outbox и доставка

Таблица outbox здесь только фиксирует событие в той же транзакции, что и новый статус. Внешний HTTP/почта не отправляются. Рабочий dispatcher должен выбирать необработанные записи, иметь попытки и backoff, хранить подтверждение доставки. Если отправка прошла, а процесс упал до отметки, сообщение повторится. Получатель должен уметь дедуплицировать событие по стабильному ID. Этот стенд не обещает exactly-once доставку.

## Диагностика

| Проблема | Что проверить |
|---|---|
| could not find driver | `php --ini`, `pdo_sqlite` у CLI, затем у FPM |
| Transition cannot apply | marking и `buildTransitionBlockerList`, не отключайте guard |
| version_conflict | Перечитайте карточку; не повторяйте бизнес-действие автоматически |
| database is locked | Длительность транзакций, второй писатель, права и журнал; HTTP вне транзакции |
| JSON не загружается | Открыта ли страница через HTTP, доступен ли recording.json |
| source hash mismatch | После правки PHP выполните capture и build-public |
| Нужна большая параллельность | Оцените PostgreSQL/MySQL и ту же проверку версии; перепроверьте транзакционную модель |

## Лицензии и источники

Наш код MIT. Symfony, PSR и Bootstrap — отдельные лицензии в `public/THIRD-PARTY-LICENSES.txt`. Bootstrap 5.3.9 включён локально, внешняя CDN для мастерской не нужна.

- https://symfony.com/doc/7.4/workflow.html
- https://symfony.com/doc/7.4/workflow/workflow-and-state-machine.html
- https://symfony.com/releases/7.4
- https://github.com/symfony/workflow
- https://sqlite.org/lang_transaction.html
- https://www.php.net/manual/en/pdo.prepared-statements.php
- https://getcomposer.org/doc/01-basic-usage.md
