symfony-cqrs-handlers — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited symfony-cqrs-handlers (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
You are an expert in CQRS (Command Query Responsibility Segregation) within Symfony hexagonal architecture.
Symfony's docs recommend a single bus unless you need different middleware. This architecture has that need: command.bus wraps handlers in doctrine_transaction, query.bus must never open write transactions, and event.bus must allow zero handlers. Mention this when users question the extra buses.
| Feature | Requires | Fallback |
|---|---|---|
#[AsMessageHandler(bus: '...')] | Symfony 6.0 | - |
#[AsMessage('async')] on a command | Symfony 7.2 | framework.messenger.routing in YAML |
DeduplicateStamp | Symfony 7.3 + symfony/lock | Idempotency key check in the handler |
#[AsMessage(serializedTypeName: ...)] | Symfony 8.1 | default FQCN type name |
Commands represent write operations (create, update, delete). They are messages dispatched to the command bus.
final readonly class, immutable after constructionRegisterUser, PlaceOrder, CancelSubscriptionvoid or a scalar identifier (string ID)namespace App\Application\{Module}\Command;
use Symfony\Component\Validator\Constraints as Assert;
final readonly class {ActionVerb}{Entity}
{
public function __construct(
#[Assert\NotBlank]
public string $param1,
public string $param2,
) {
}
}namespace App\Application\{Module}\Command;
use App\Domain\{Module}\Port\{Entity}RepositoryInterface;
use Symfony\Component\Messenger\Attribute\AsMessageHandler;
#[AsMessageHandler(bus: 'command.bus')]
final readonly class {ActionVerb}{Entity}Handler
{
public function __construct(
private {Entity}RepositoryInterface $repository,
) {
}
public function __invoke({ActionVerb}{Entity} $command): void
{
// 1. Load/create domain objects
// 2. Execute business logic (entity methods)
// 3. Persist via port (repository dispatches domain events)
// NO side-effects here, use domain events
}
}Queries represent read operations. They return DTOs, never domain entities.
final readonly class, immutableGetUserById, ListActiveOrders, SearchProductsPage of DTOs{Entity}ReadRepositoryInterface) backed by DBAL QueryBuildernamespace App\Application\{Module}\Query;
final readonly class Get{Entity}ById
{
public function __construct(
public string $id,
) {
}
}namespace App\Application\{Module}\Query;
use App\Application\{Module}\DTO\{Entity}DTO;
use App\Domain\{Module}\Port\{Entity}RepositoryInterface;
use App\Domain\{Module}\ValueObject\{Entity}Id;
use Symfony\Component\Messenger\Attribute\AsMessageHandler;
#[AsMessageHandler(bus: 'query.bus')]
final readonly class Get{Entity}ByIdHandler
{
public function __construct(
private {Entity}RepositoryInterface $repository,
) {
}
public function __invoke(Get{Entity}ById $query): ?{Entity}DTO
{
$entity = $this->repository->findById(new {Entity}Id($query->id));
return $entity === null ? null : {Entity}DTO::fromEntity($entity);
}
}namespace App\Application\{Module}\DTO;
use App\Domain\{Module}\Entity\{Entity};
final readonly class {Entity}DTO
{
public function __construct(
public string $id,
public string $name,
) {
}
public static function fromEntity({Entity} $entity): self
{
return new self(
id: $entity->id()->value,
name: $entity->name(),
);
}
}Symfony 7.3+ ObjectMapper (#[Map]) can replace hand-written fromX() factories for DTO-to-DTO mapping. Do not map directly into Domain entities with it: it bypasses constructors and therefore invariants.
Use the CommandBus / QueryBus wrappers from references/bus-configuration.md. They return the handler result and rethrow the original domain exception instead of Messenger's HandlerFailedException, so the API exception subscriber can map it to the right HTTP status.
See references/ for detailed guides:
command-patterns.md - Full command examples with validation, async commandsquery-patterns.md - Read ports, pagination, DBAL QueryBuilder read modelsbus-configuration.md - Messenger bus setup, middleware, bus wrappers~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.