Symfony Backend (Mart)
Конвенции для apps/backend/ — Symfony 7 + API Platform + Doctrine ORM + PostgreSQL 18.
Схемы БД проекта описаны в docs/db/ (33 PostgreSQL-схемы: public, billing, marketplace, …).
Структура src/
src/
├── Api/
│ ├── Resource/ # DTO с #[ApiResource] — контракт HTTP /api/…
│ │ ├── Public/
│ │ ├── Billing/
│ │ └── …/
│ └── State/ # Provider, Processor для Api Resource
│ ├── Public/
│ └── …/
├── Controller/ # HTTP вне API Platform (health, webhooks, …)
├── Console/ # Symfony Console (bin/console app:…)
│ ├── Public/
│ └── …/
├── Infrastructure/
│ ├── Entity/ # Doctrine, «тупые» — без #[ApiResource]
│ │ ├── Public/
│ │ └── …/
│ └── Repository/
│ ├── Public/
│ └── …/
└── Kernel.php
Domain / Application (src/Domain/, src/Application/) — по мере роста, когда правил станет много. Persistence всегда в Infrastructure/.
Разделение слоёв
| Слой | Папка | Назначение |
|---|---|---|
| API | Api/Resource/ | JSON-контракт, #[ApiResource] |
| API | Api/State/ | откуда/куда данные для ресурса |
| Persistence | Infrastructure/Entity/ | таблицы PostgreSQL, Doctrine mapping |
| Persistence | Infrastructure/Repository/ | запросы к БД |
| CLI | Console/ | точка входа bin/console, без бизнес-логики |
Entity не экспонируется в API напрямую.
Console
Symfony-команды — src/Console/, namespace App\Console (подпапки по схеме/домену — опционально: Console/Public/).
<?php
declare(strict_types=1);
namespace App\Console;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
#[AsCommand(name: 'app:campaign:sync', description: 'Sync campaigns from external source')]
final class SyncCampaignsCommand extends Command
{
protected function execute(InputInterface $input, OutputInterface $output): int
{
// делегировать в сервис Application/ — не писать логику здесь
return Command::SUCCESS;
}
}
Правила:
- Класс команды — тонкий: parse args/options → вызов сервиса → exit code.
- Бизнес-логика — в
Application/(когда появится) или временно в dedicated service. - Имя команды:
app:<domain>:<action>(напримерapp:billing:recalculate). - Создание:
php bin/console make:command— namespaceApp\Console(Maker по умолчанию предлагаетApp\Command).
Console vs bin/ в корне репозитория
apps/backend/src/Console/ | bin/<script>/ (корень monorepo) | |
|---|---|---|
| Runtime | PHP, Symfony DI, Doctrine | чаще Python, автономно |
| Когда | cron, миграции данных, maintenance внутри приложения | ETL, разовые скрипты, не нужен Symfony container |
| Документация | docs/symfony.md | bin/<script>/README.md + just bin-* |
Entity (Infrastructure)
Расположение и namespace
| PostgreSQL schema | Папка | Namespace |
|---|---|---|
public | Infrastructure/Entity/Public/ | App\Infrastructure\Entity\Public |
billing | Infrastructure/Entity/Billing/ | App\Infrastructure\Entity\Billing |
| … | Infrastructure/Entity/<Schema>/ | App\Infrastructure\Entity\<Schema> |
Имя подпапки — PascalCase от имени схемы (marketplace → Marketplace, public → Public).
Пример
<?php
declare(strict_types=1);
namespace App\Infrastructure\Entity\Public;
use App\Infrastructure\Repository\Public\CampaignRepository;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity(repositoryClass: CampaignRepository::class)]
#[ORM\Table(name: 'campaign', schema: 'public')]
class Campaign
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;
#[ORM\Column(length: 255)]
private string $name = '';
// getters/setters — без бизнес-логики и без #[ApiResource]
}
Правила:
schemaв#[ORM\Table]обязан совпадать с подпапкой (Public/→schema: 'public').- Имена таблиц —
snake_case, как вdocs/db/<schema>/. - Без
#[ApiResource]на Entity.
Новая сущность
cd apps/backend
php bin/console make:entity
# namespace: App\Infrastructure\Entity\Public\Campaign
php bin/console make:migration
php bin/console doctrine:migrations:migrate
Maker по умолчанию предлагает App\Entity — менять на App\Infrastructure\Entity\<Schema>.
Repository
Зеркальная структура Entity:
| Entity | Repository |
|---|---|
Infrastructure/Entity/Public/Campaign.php | Infrastructure/Repository/Public/CampaignRepository.php |
App\Infrastructure\Entity\Public\Campaign | App\Infrastructure\Repository\Public\CampaignRepository |
- Класс final, один репозиторий на Entity.
- Кастомные запросы — методы репозитория, не в Entity.
Api Resource (DTO)
Расположение и namespace
| PostgreSQL schema | Папка | Namespace |
|---|---|---|
public | Api/Resource/Public/ | App\Api\Resource\Public |
billing | Api/Resource/Billing/ | App\Api\Resource\Billing |
| … | Api/Resource/<Schema>/ | App\Api\Resource\<Schema> |
Provider / Processor — в Api/State/<Schema>/, namespace App\Api\State\<Schema>.
Простой CRUD (форма ≈ таблица)
DTO + маппинг на Entity через stateOptions и #[Map] (API Platform 4):
<?php
declare(strict_types=1);
namespace App\Api\Resource\Public;
use ApiPlatform\Doctrine\Orm\State\Options;
use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\Get;
use ApiPlatform\Metadata\GetCollection;
use ApiPlatform\Metadata\Patch;
use ApiPlatform\Metadata\Post;
use App\Infrastructure\Entity\Public\Campaign as CampaignEntity;
use Symfony\Component\ObjectMapper\Attribute\Map;
#[ApiResource(
shortName: 'Campaign',
stateOptions: new Options(entityClass: CampaignEntity::class),
operations: [
new GetCollection(),
new Get(),
new Post(),
new Patch(),
],
)]
#[Map(source: CampaignEntity::class)]
final class Campaign
{
public ?int $id = null;
public string $name = '';
}
Platform сам читает/пишет Entity, наружу отдаёт DTO.
Кастомное чтение (агрегаты, join'ы)
Отдельный DTO + Provider:
#[ApiResource(
operations: [
new GetCollection(provider: CampaignSummaryProvider::class),
new Get(provider: CampaignSummaryProvider::class),
],
)]
final class CampaignSummary
{
public function __construct(
public int $id,
public string $name,
public int $ordersCount,
) {}
}
// App\Api\State\Public\CampaignSummaryProvider
final class CampaignSummaryProvider implements ProviderInterface
{
public function provide(Operation $operation, array $uriVariables = [], array $context = []): object|array|null
{
// Repository / Query → new CampaignSummary(...)
}
}
Запись с правилами
Input-DTO + Processor → сервис или фабрика → Entity → flush().
API Platform
Ресурсы сканируются только из src/Api/Resource/ (не Entity).
| URL | Назначение |
|---|---|
/api | Swagger UI |
/api/docs.json | OpenAPI |
/health | Symfony Controller (не API Platform) |
Конфигурация
| Файл | Что задаёт |
|---|---|
config/packages/doctrine.yaml | mapping → Infrastructure/Entity |
config/packages/api_platform.yaml | scan path → Api/Resource |
.env / .env.local | DATABASE_URL, APP_SECRET |
Команды
just back-dev # symfony serve :8080
just back-check # psalm + php-cs-fixer + rector
just back-test # phpunit
composer lint:fix # автофикс стиля
Линтинг
Psalm (level 1), PHP-CS-Fixer (@Symfony), Rector — см. .claude/rules/linting-php.md.