Skip to content

Instantly share code, notes, and snippets.

@Rixafy
Last active February 23, 2025 02:22
Show Gist options
  • Save Rixafy/82d96712cf1f87ce26ce052ced2eba78 to your computer and use it in GitHub Desktop.
Save Rixafy/82d96712cf1f87ce26ce052ced2eba78 to your computer and use it in GitHub Desktop.
Deadlock-safe Doctrine ORM EntityManager
<?php
declare(strict_types=1);
namespace App\Model\Doctrine;
use Doctrine\DBAL\Exception\RetryableException;
use Doctrine\ORM\Decorator\EntityManagerDecorator;
use Doctrine\ORM\EntityManagerInterface;
final class EntityManager extends EntityManagerDecorator
{
public function __construct(
EntityManagerInterface $wrapped,
) {
parent::__construct($wrapped);
}
public function flush(): void
{
/** @noinspection PhpUnhandledExceptionInspection */
$this->withRetry(fn () => $this->wrapped->flush());
}
public function wrapInTransaction(callable $func): mixed
{
/** @noinspection PhpUnhandledExceptionInspection */
return $this->withRetry(fn () => $this->wrapped->wrapInTransaction($func));
}
/** @noinspection PhpUnhandledExceptionInspection */
private function withRetry(callable $problematic, int $attempt = 1): mixed
{
$unitOfWorkClone = clone $this->wrapped->getUnitOfWork();
try {
return $problematic();
} catch (RetryableException $e) {
if ($attempt >= 10) {
throw $e;
}
if (!$this->wrapped->isOpen()) {
(function () use ($unitOfWorkClone): void {
$this->closed = false; /** @phpstan-ignore-line */
$this->unitOfWork = $unitOfWorkClone; /** @phpstan-ignore-line */
})->bindTo($this->wrapped, $this->wrapped)();
}
usleep(10_000 * $attempt);
return $this->withRetry($problematic, ++$attempt);
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment