|
4 | 4 |
|
5 | 5 | namespace Codeception\Module\Symfony; |
6 | 6 |
|
| 7 | +use Doctrine\ORM\EntityManagerInterface; |
7 | 8 | use Doctrine\ORM\EntityRepository; |
8 | 9 | use Doctrine\ORM\Tools\SchemaValidator; |
| 10 | +use Doctrine\Persistence\ManagerRegistry; |
9 | 11 | use PHPUnit\Framework\Assert; |
10 | 12 | use Symfony\Bridge\Doctrine\DataCollector\DoctrineDataCollector; |
| 13 | +use Throwable; |
11 | 14 |
|
12 | 15 | use function array_count_values; |
13 | 16 | use function array_filter; |
@@ -53,6 +56,28 @@ public function dontSeeDuplicateQueries(): void |
53 | 56 | ); |
54 | 57 | } |
55 | 58 |
|
| 59 | + /** |
| 60 | + * Returns the Doctrine EntityManager the module is configured to use: |
| 61 | + * the `em_service` option, `doctrine.orm.entity_manager` by default. |
| 62 | + * |
| 63 | + * The manager is resolved from the container on every call, so it always belongs to the |
| 64 | + * kernel that is currently booted. Don't keep it in a property across requests: |
| 65 | + * [`amOnPage()`](#amOnPage) and friends reboot the kernel, which builds a new manager. |
| 66 | + * |
| 67 | + * To reach a manager other than the configured one, grab it by service id: |
| 68 | + * `$I->grabService('doctrine.orm.other_entity_manager')`. |
| 69 | + * |
| 70 | + * ```php |
| 71 | + * <?php |
| 72 | + * $em = $I->grabEntityManager(); |
| 73 | + * $user = $em->getRepository(User::class)->findOneBy(['email' => 'john_doe@gmail.com']); |
| 74 | + * ``` |
| 75 | + */ |
| 76 | + public function grabEntityManager(): EntityManagerInterface |
| 77 | + { |
| 78 | + return $this->_getEntityManager(); |
| 79 | + } |
| 80 | + |
56 | 81 | /** |
57 | 82 | * Returns the number of rows that match the given criteria for the |
58 | 83 | * specified Doctrine entity. |
@@ -135,6 +160,53 @@ public function seeNumQueriesIsLessThan(int $expectedCount): void |
135 | 160 | ); |
136 | 161 | } |
137 | 162 |
|
| 163 | + /** |
| 164 | + * Resets the Doctrine EntityManager. |
| 165 | + * |
| 166 | + * Doctrine closes the EntityManager as soon as an exception escapes a `flush()`: |
| 167 | + * a unique constraint violation, a deadlock, a failed transaction. Every write after |
| 168 | + * that throws `EntityManagerClosed`, which usually surfaces as an unrelated failure |
| 169 | + * further down the test. Call this after deliberately provoking such an error to carry |
| 170 | + * on with a healthy manager. |
| 171 | + * |
| 172 | + * If the manager is still open it is only cleared, detaching every managed entity, |
| 173 | + * which is handy to prove that the next read really hits the database. |
| 174 | + * The open test transaction is preserved either way: the manager is rebuilt, |
| 175 | + * the DBAL connection underneath it is not. |
| 176 | + * |
| 177 | + * ```php |
| 178 | + * <?php |
| 179 | + * $I->amOnPage('/register'); |
| 180 | + * $I->resetDoctrineManager(); |
| 181 | + * $I->seeNumRecords(1, User::class); |
| 182 | + * ``` |
| 183 | + * |
| 184 | + * @param non-empty-string|null $name Manager name as registered in Doctrine's registry, |
| 185 | + * `null` for the default one. |
| 186 | + */ |
| 187 | + public function resetDoctrineManager(?string $name = null): void |
| 188 | + { |
| 189 | + $em = $this->_getEntityManager(); |
| 190 | + |
| 191 | + if ($em->isOpen()) { |
| 192 | + $em->clear(); |
| 193 | + |
| 194 | + return; |
| 195 | + } |
| 196 | + |
| 197 | + if (!$this->resetManagerThroughRegistry($name) || !$this->_getEntityManager()->isOpen()) { |
| 198 | + $this->rebootClientKernel(); |
| 199 | + } |
| 200 | + |
| 201 | + if (!$this->_getEntityManager()->isOpen()) { |
| 202 | + Assert::fail( |
| 203 | + "The Doctrine EntityManager is still closed after resetting it.\n" |
| 204 | + . "Check that the module's 'em_service' option points at your entity manager " |
| 205 | + . 'and that the container can rebuild it.' |
| 206 | + ); |
| 207 | + } |
| 208 | + } |
| 209 | + |
138 | 210 | /** |
139 | 211 | * Asserts that a given number of records exists for the entity. |
140 | 212 | * 'id' is the default search parameter. |
@@ -244,4 +316,55 @@ private function isTransactionStatement(string $sql): bool |
244 | 316 | { |
245 | 317 | return preg_match('/^\s*("|`)?(START\s+TRANSACTION|BEGIN|COMMIT|ROLLBACK|SAVEPOINT|RELEASE\s+SAVEPOINT)\b/i', $sql) === 1; |
246 | 318 | } |
| 319 | + |
| 320 | + /** |
| 321 | + * Rebuilds the manager through Doctrine's registry, which swaps the lazy service in |
| 322 | + * place: everything already holding the manager, the application and the Doctrine |
| 323 | + * module included, sees the reopened one. |
| 324 | + * |
| 325 | + * Returns false when the app has no registry, or when the manager service is not lazy |
| 326 | + * and therefore cannot be reset that way. |
| 327 | + * |
| 328 | + * @param non-empty-string|null $name |
| 329 | + */ |
| 330 | + private function resetManagerThroughRegistry(?string $name): bool |
| 331 | + { |
| 332 | + if (!interface_exists(ManagerRegistry::class)) { |
| 333 | + return false; |
| 334 | + } |
| 335 | + |
| 336 | + $registry = $this->getService('doctrine'); |
| 337 | + if (!$registry instanceof ManagerRegistry) { |
| 338 | + return false; |
| 339 | + } |
| 340 | + |
| 341 | + try { |
| 342 | + $registry->resetManager($name); |
| 343 | + } catch (Throwable) { |
| 344 | + return false; |
| 345 | + } |
| 346 | + |
| 347 | + return true; |
| 348 | + } |
| 349 | + |
| 350 | + /** |
| 351 | + * Resolves the configured entity manager service, failing with a hint |
| 352 | + * instead of a type error when Doctrine isn't wired up. |
| 353 | + * |
| 354 | + * @param non-empty-string $serviceId |
| 355 | + */ |
| 356 | + private function resolveEntityManager(string $serviceId): EntityManagerInterface |
| 357 | + { |
| 358 | + $em = $this->getService($serviceId); |
| 359 | + |
| 360 | + if (!$em instanceof EntityManagerInterface) { |
| 361 | + Assert::fail(sprintf( |
| 362 | + "The '%s' service is not a Doctrine EntityManager.\n" |
| 363 | + . "Install and enable doctrine/doctrine-bundle, or point the module's 'em_service' option at your entity manager.", |
| 364 | + $serviceId |
| 365 | + )); |
| 366 | + } |
| 367 | + |
| 368 | + return $em; |
| 369 | + } |
247 | 370 | } |
0 commit comments