Created
September 4, 2022 11:02
-
-
Save bizley/15f9be893ded796f4d0c68dea5e3efa5 to your computer and use it in GitHub Desktop.
Thread-safe business logic with Doctrine
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
namespace FavreBenjamin\Utils; | |
use Doctrine\Common\Persistence\ManagerRegistry; | |
use Doctrine\DBAL\Exception\RetryableException; | |
use Doctrine\ORM\EntityManagerInterface; | |
class CustomEntityManager | |
{ | |
private $em; | |
private $mr; | |
public function __construct(EntityManagerInterface $em, | |
ManagerRegistry $mr) | |
{ | |
$this->em = $em; | |
$this->mr = $mr; | |
} | |
public function transactional(callable $callback) | |
{ | |
$retries = 0; | |
do { | |
$this->beginTransaction(); | |
try { | |
$ret = $callback(); | |
$this->flush(); | |
$this->commit(); | |
return $ret; | |
} catch (RetryableException $e) { | |
$this->rollback(); | |
$this->close(); | |
$this->resetManager(); | |
++$retries; | |
} catch (\Exception $e) { | |
$this->rollback(); | |
throw $e; | |
} | |
} while ($retries < 10); | |
throw $e; | |
} | |
public function resetManager() | |
{ | |
$this->em = $this->mr->resetManager(); | |
} | |
public function __call($name, $args) { | |
return call_user_func_array([$this->em, $name], $args); | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
use FavreBenjamin\Utils\CustomEntityManager; | |
use MyProject\Entity\Account; | |
use Doctrine\DBAL\LockMode; | |
class MyBusinessLogicService | |
{ | |
private $em; | |
public function __construct(CustomEntityManager $em) | |
{ | |
$this->em = $em; | |
} | |
public function debit(int $accountId, int $amount): bool | |
{ | |
$callback = function() use ($accountId, $amount) { | |
// Get repository inside callable to make sure EntityManager is valid | |
$accounts = $this->em->getRepository(Account::class); | |
// Fetch account with FOR UPDATE write lock | |
$account = $accounts->find( | |
$accountId, | |
LockMode::PESSIMISTIC_WRITE | |
); | |
// We are protected from concurrent access here | |
if ($account->amount < $amount) | |
return false; | |
$account->amount -= $amount; | |
return true; | |
}); | |
return $this->em->transactional($callback); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment