
Symfony:doctrine Transactions Skill
- 440 installs
- 190 repo stars
- Updated August 6, 2026
- makfly/superpowers-symfony
symfony:doctrine-transactions is a Claude Code skill that wraps multi-step Doctrine ORM 3 writes in Symfony services with explicit transaction boundaries, locking, and migration-safe schema evolution.
About
symfony:doctrine-transactions is one of 44 skills in the MakFly superpowers-symfony Claude Code plugin for Symfony 7.4 LTS and 8.x with Doctrine ORM 3. It guides modeling ownership and cardinality, defining transaction boundaries around use-case intents, applying mapping changes with reversible migrations, tuning fetch behavior to avoid N+1 queries, and verifying lifecycle events with targeted tests. The default workflow moves from boundary design through migration safety to hot-path query tuning and test validation. Developers reach for symfony:doctrine-transactions when entity updates, deletes, and event side effects must commit atomically or roll back on failure, or when optimistic and pessimistic locking is required in Symfony services.
- EntityManager transaction control
- Rollback on domain exceptions
- Unit-of-work flush timing
- Nested transaction pitfalls
- Testable service boundaries
Symfony:Doctrine Transactions by the numbers
- 440 all-time installs (skills.sh)
- Ranked #136 of 922 Databases skills by installs in the Skillselion catalog
- Data as of Aug 11, 2026 (Skillselion catalog sync)
npx skills add https://github.com/makfly/superpowers-symfony --skill symfonydoctrine-transactionsAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 440 |
|---|---|
| repo stars | ★ 190 |
| Last updated | August 6, 2026 |
| Repository | makfly/superpowers-symfony ↗ |
How do you handle Doctrine transactions in Symfony services?
Wrap multi-step Doctrine writes in Symfony services with explicit transactions so entity updates, deletes, and event side effects commit atomically or roll back on failure.
Who is it for?
Symfony backend engineers evolving Doctrine ORM 3 entities who need safe transaction boundaries, locking, and migration rollouts on Symfony 7.4+.
Skip if: Teams writing only read-only queries or pure HTTP functional tests without persistence changes should use doctrine-fetch-modes or functional-tests instead.
When should I use this skill?
The developer wraps multi-step Doctrine writes, designs transaction boundaries, adds optimistic locking, or needs migration-safe schema changes in Symfony.
What you get
Entity and migration diffs, transaction boundary decisions, integrity notes, and targeted test validation results
- Transaction-wrapped service methods
- Migration scripts with rollback notes
- Targeted integration test coverage
By the numbers
- One of 44 skills in the superpowers-symfony Claude Code plugin
- Targets Symfony 7.4 LTS and 8.x with Doctrine ORM 3
Files
Doctrine Transactions (Symfony)
Use when
- Designing entity relations or schema evolution.
- Improving Doctrine correctness/performance.
Default workflow
1. Model ownership/cardinality and transactional boundaries. 2. Apply mapping/schema changes with migration safety. 3. Tune fetch/query behavior for hot paths. 4. Verify lifecycle behavior with targeted tests.
Guardrails
- Keep owning/inverse sides coherent.
- Avoid destructive migration jumps in one release.
- Eliminate accidental N+1 and over-fetching.
Progressive disclosure
- Use this file for execution posture and risk controls.
- Open references when deep implementation details are needed.
Output contract
- Entity/migration changes.
- Integrity and performance decisions.
- Validation outcomes and rollback notes.
References
reference.mddocs/complexity-tiers.md
Reference
Doctrine Transactions
Basic Transactions
Implicit Transactions
By default, Doctrine wraps each flush() in a transaction:
$user = new User();
$user->setEmail('test@example.com');
$em->persist($user);
$em->flush(); // Auto-commits in transactionExplicit Transactions
For multiple operations that must succeed or fail together:
<?php
// src/Service/OrderService.php
class OrderService
{
public function __construct(
private EntityManagerInterface $em,
) {}
public function createOrderWithPayment(User $user, array $items): Order
{
$this->em->beginTransaction();
try {
// Create order
$order = new Order();
$order->setCustomer($user);
$order->setStatus(OrderStatus::PENDING);
foreach ($items as $item) {
$orderItem = new OrderItem();
$orderItem->setProduct($item['product']);
$orderItem->setQuantity($item['quantity']);
$order->addItem($orderItem);
}
$this->em->persist($order);
// Create payment
$payment = new Payment();
$payment->setOrder($order);
$payment->setAmount($order->getTotal());
$this->em->persist($payment);
$this->em->flush();
$this->em->commit();
return $order;
} catch (\Exception $e) {
$this->em->rollback();
throw $e;
}
}
}Using the wrapInTransaction Helper
Cleaner approach. wrapInTransaction() flushes before commit and closes the EntityManager if the callback throws:
public function createOrder(User $user, array $items): Order
{
return $this->em->wrapInTransaction(function () use ($user, $items) {
$order = new Order();
$order->setCustomer($user);
foreach ($items as $item) {
$order->addItem(new OrderItem($item));
}
$this->em->persist($order);
return $order;
});
}ORM 3 breaking change: EntityManager::transactional() was deprecated inORM 2.9 and removed in ORM 3.0. Use wrapInTransaction() (available sinceORM 2.9). They share the same signature, so the migration is a rename. Any
remaining $em->transactional(...) call will fatal on ORM 3.x.DBAL-level Transactions
On the DBAL Connection, transactional() is not affected by the ORM 3 removal — it remains valid for raw SQL / DML that does not go through the EntityManager:
use Doctrine\DBAL\Connection;
$connection->transactional(function (Connection $conn): void {
$conn->executeStatement('UPDATE product SET price = price * 0.9');
});Note this does not flush the ORM unit of work — use it only for DBAL-level work.
Flush Strategies
Single Flush (Recommended)
// Good: Single flush for all changes
$user = new User();
$user->setEmail('test@example.com');
$em->persist($user);
$profile = new Profile();
$profile->setUser($user);
$em->persist($profile);
$em->flush(); // One transaction, one commitAvoid Multiple Flushes
// Bad: Multiple flushes = multiple transactions
$user = new User();
$em->persist($user);
$em->flush(); // Transaction 1
$profile = new Profile();
$profile->setUser($user);
$em->persist($profile);
$em->flush(); // Transaction 2 - not atomic!Flush Only When Needed
// Service layer flushes
class UserService
{
public function register(string $email): User
{
$user = new User();
$user->setEmail($email);
$this->em->persist($user);
$this->em->flush(); // Service controls transaction boundary
return $user;
}
}
// Controller doesn't flush
class UserController
{
#[Route('/register', methods: ['POST'])]
public function register(Request $request, UserService $service): Response
{
$user = $service->register($request->get('email'));
return new Response('Created', 201);
}
}Optimistic Locking
Prevent concurrent modification conflicts with a #[ORM\Version] column (integer or datetime/datetime_immutable). Version numbers are preferred over timestamps in high-concurrency scenarios:
<?php
// src/Entity/Article.php
#[ORM\Entity]
class Article
{
#[ORM\Version]
#[ORM\Column(type: 'integer')]
private int $version = 1;
public function getVersion(): int
{
return $this->version;
}
}Usage:
use Doctrine\ORM\OptimisticLockException;
public function updateArticle(int $id, string $content, int $expectedVersion): void
{
$article = $this->em->find(Article::class, $id);
// Lock with expected version
$this->em->lock($article, LockMode::OPTIMISTIC, $expectedVersion);
$article->setContent($content);
try {
$this->em->flush();
} catch (OptimisticLockException $e) {
// Version mismatch - someone else modified it
throw new ConflictException('Article was modified by another user');
}
}Pessimistic Locking
Lock rows in database:
use Doctrine\DBAL\LockMode;
public function processPayment(int $orderId): void
{
$this->em->beginTransaction();
try {
// Lock the row for update
$order = $this->em->find(
Order::class,
$orderId,
LockMode::PESSIMISTIC_WRITE
);
if ($order->getStatus() !== OrderStatus::PENDING) {
throw new \Exception('Order already processed');
}
$order->setStatus(OrderStatus::PROCESSING);
$this->em->flush();
$this->em->commit();
} catch (\Exception $e) {
$this->em->rollback();
throw $e;
}
}Lock modes:
PESSIMISTIC_READ: Shared lock (SELECT ... FOR SHARE)PESSIMISTIC_WRITE: Exclusive lock (SELECT ... FOR UPDATE)
Error Handling
Connection Lost
use Doctrine\DBAL\Exception\ConnectionLost;
try {
$this->em->flush();
} catch (ConnectionLost $e) {
// Reconnect and retry
$this->em->getConnection()->connect();
$this->em->flush();
}Constraint Violations
use Doctrine\DBAL\Exception\UniqueConstraintViolationException;
try {
$user = new User();
$user->setEmail($email);
$this->em->persist($user);
$this->em->flush();
} catch (UniqueConstraintViolationException $e) {
throw new DuplicateEmailException('Email already exists');
}EntityManager State
After Exception
After a rollback, the EntityManager may be in an inconsistent state:
try {
$this->em->flush();
} catch (\Exception $e) {
$this->em->rollback();
// Clear the EntityManager
$this->em->clear();
// Re-fetch entities if needed
$user = $this->em->find(User::class, $userId);
}Clearing EntityManager
// Clear all managed entities
$this->em->clear();
// Clear specific entity type
$this->em->clear(User::class);Best Practices
1. Single flush per operation: Group related changes 2. Service layer transactions: Controllers don't manage transactions 3. Use `wrapInTransaction()`: Cleaner than try/catch (replaces the removed transactional()) 4. Optimistic locking: For concurrent editing scenarios 5. Clear after rollback: Reset EntityManager state 6. Short transactions: Don't hold locks too long
// Good pattern
class OrderService
{
public function createOrder(CreateOrderDTO $dto): Order
{
return $this->em->wrapInTransaction(function () use ($dto) {
$order = new Order();
// ... build order
$this->em->persist($order);
return $order;
});
}
}Applicability
- ORM 3.x / DBAL 4.x (target):
wrapInTransaction()only;
EntityManager::transactional() no longer exists.
- ORM 2.9+ (legacy): both
wrapInTransaction()andtransactional()exist;
prefer wrapInTransaction() so the code survives the 3.0 upgrade.
Connection::transactional()(DBAL) is valid across all versions.
Skill Operating Checklist
Design checklist
- Confirm operation boundaries and invariants first.
- Minimize scope while preserving contract correctness.
- Test both happy path and negative path behavior.
Validation commands
- php bin/console doctrine:migrations:diff
- php bin/console doctrine:migrations:migrate
- ./vendor/bin/phpunit --filter=Doctrine
Failure modes to test
- Invalid payload or forbidden actor.
- Boundary values / not-found cases.
- Retry or partial-failure behavior for async flows.
Related skills
How it compares
Pick symfony:doctrine-transactions over doctrine-relations when writes must commit atomically with locking and migration discipline—not just association mapping.
FAQ
What Symfony versions does symfony:doctrine-transactions support?
symfony:doctrine-transactions ships in superpowers-symfony, which targets Symfony 7.4 LTS and 8.x with Doctrine ORM 3 and API Platform v4. Legacy Symfony 6.4 LTS is also supported by the plugin.
What does symfony:doctrine-transactions output?
symfony:doctrine-transactions delivers entity and migration changes, integrity and performance decisions, validation outcomes from targeted tests, and rollback notes for destructive schema steps.
When should transaction boundaries be defined?
symfony:doctrine-transactions advises defining boundaries around use-case intents in Symfony services—not around low-level ORM flush calls—so entity updates, deletes, and side effects commit or roll back together.