Skip to content

Commit ecf1745

Browse files
committed
Add regression tests for Doctrine listener security/request context (#34, #90, #150, #151)
1 parent 5299106 commit ecf1745

11 files changed

Lines changed: 395 additions & 66 deletions

composer.lock

Lines changed: 63 additions & 51 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

config/routes.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,4 +92,12 @@
9292
$routes->add('send_message', '/send-message')
9393
->controller(App\Controller\SendMessageController::class)
9494
->methods(['GET']);
95+
96+
$routes->add('app_create_user', '/create-user')
97+
->controller(App\Controller\CreateUserController::class)
98+
->methods(['GET']);
99+
100+
$routes->add('app_create_user_with_confirmation', '/create-user-with-confirmation')
101+
->controller(App\Controller\CreateUserWithConfirmationController::class)
102+
->methods(['GET']);
95103
};

config/services.php

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,4 +26,37 @@
2626
'entity' => User::class,
2727
'lazy' => true,
2828
]);
29+
30+
// Doctrine listeners used by App\Tests\Functional\IssuesCest to reproduce the
31+
// security/request context regressions. Each is public so the tests can grab
32+
// it and read the state it captured during the request.
33+
34+
// Issue #34 (entity-listener path).
35+
$services->set(App\Doctrine\CurrentUserListener::class)
36+
->public()
37+
->tag('doctrine.orm.entity_listener', [
38+
'event' => Events::prePersist,
39+
'entity' => User::class,
40+
'lazy' => true,
41+
]);
42+
43+
// Issue #34 (event-listener path).
44+
$services->set(App\Doctrine\CurrentUserEventListener::class)
45+
->public();
46+
47+
// Issue #150 (request_stack inside a Doctrine listener).
48+
$services->set(App\Doctrine\RequestStackListener::class)
49+
->public();
50+
51+
// Issue #151 (shared listener instance across reboots).
52+
$services->set(App\Doctrine\FlushCounterListener::class)
53+
->public();
54+
55+
// Issue #90 (email sent from a Doctrine entity listener).
56+
$services->set(App\Doctrine\SendConfirmationListener::class)
57+
->tag('doctrine.orm.entity_listener', [
58+
'event' => Events::postPersist,
59+
'entity' => User::class,
60+
'lazy' => true,
61+
]);
2962
};
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace App\Controller;
6+
7+
use App\Entity\User;
8+
use Doctrine\ORM\EntityManagerInterface;
9+
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
10+
use Symfony\Component\HttpFoundation\Response;
11+
12+
/**
13+
* Persists a user (one prePersist + one flush) so the Doctrine listeners under
14+
* test fire within a real request. Shared trigger for issues #34, #150 and #151.
15+
*/
16+
final class CreateUserController extends AbstractController
17+
{
18+
public function __invoke(EntityManagerInterface $em): Response
19+
{
20+
$em->persist(User::create('jane_doe@gmail.com', '123456'));
21+
$em->flush();
22+
23+
return new Response('Created');
24+
}
25+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace App\Controller;
6+
7+
use App\Doctrine\SendConfirmationListener;
8+
use App\Entity\User;
9+
use Doctrine\ORM\EntityManagerInterface;
10+
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
11+
use Symfony\Component\HttpFoundation\Response;
12+
13+
/**
14+
* Persists the marker user that makes App\Doctrine\SendConfirmationListener send
15+
* a confirmation email from inside a Doctrine entity listener. Trigger for #90.
16+
*/
17+
final class CreateUserWithConfirmationController extends AbstractController
18+
{
19+
public function __invoke(EntityManagerInterface $em): Response
20+
{
21+
$em->persist(User::create(SendConfirmationListener::TRIGGER_EMAIL, '123456'));
22+
$em->flush();
23+
24+
return new Response('Created');
25+
}
26+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace App\Doctrine;
6+
7+
use App\Entity\User;
8+
use Doctrine\Bundle\DoctrineBundle\Attribute\AsDoctrineListener;
9+
use Doctrine\ORM\Event\PrePersistEventArgs;
10+
use Doctrine\ORM\Events;
11+
use Symfony\Component\Security\Core\Security;
12+
13+
/**
14+
* Issue #34, event-listener path: a Doctrine event listener registered on the
15+
* connection's event manager (not the ORM entity-listener resolver) must also
16+
* see the logged-in user. This is the case the issue thread describes as a
17+
* Doctrine "subscriber". Triggered by the `/create-user` route.
18+
*
19+
* @see https://github.com/Codeception/module-symfony/issues/34
20+
*/
21+
#[AsDoctrineListener(event: Events::prePersist)]
22+
final class CurrentUserEventListener
23+
{
24+
public ?string $currentUserIdentifier = null;
25+
26+
public function __construct(private readonly Security $security)
27+
{
28+
}
29+
30+
public function prePersist(PrePersistEventArgs $args): void
31+
{
32+
if (!$args->getObject() instanceof User) {
33+
return;
34+
}
35+
36+
$this->currentUserIdentifier = $this->security->getUser()?->getUserIdentifier();
37+
}
38+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace App\Doctrine;
6+
7+
use App\Entity\User;
8+
use Symfony\Component\Security\Core\Security;
9+
10+
/**
11+
* Issue #34, entity-listener path: the security token must be available inside a
12+
* Doctrine entity listener, exactly as it is inside a controller. Registered as a
13+
* lazy `doctrine.orm.entity_listener` in config/services.php and triggered by the
14+
* `/create-user` route (App\Controller\CreateUserController).
15+
*
16+
* @see https://github.com/Codeception/module-symfony/issues/34
17+
*/
18+
final class CurrentUserListener
19+
{
20+
public ?string $currentUserIdentifier = null;
21+
22+
public function __construct(private readonly Security $security)
23+
{
24+
}
25+
26+
public function prePersist(User $user): void
27+
{
28+
$this->currentUserIdentifier = $this->security->getUser()?->getUserIdentifier();
29+
}
30+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace App\Doctrine;
6+
7+
use Doctrine\Bundle\DoctrineBundle\Attribute\AsDoctrineListener;
8+
use Doctrine\ORM\Event\OnFlushEventArgs;
9+
use Doctrine\ORM\Events;
10+
11+
/**
12+
* Issue #151: when the EntityManager was persisted across kernel reboots, the
13+
* listener instance held by Doctrine diverged from the one returned by
14+
* grabService(), so state recorded during the request was invisible to the test.
15+
* Both must be the same shared instance. Triggered by the `/create-user` route,
16+
* which performs exactly one flush.
17+
*
18+
* @see https://github.com/Codeception/module-symfony/issues/151
19+
*/
20+
#[AsDoctrineListener(event: Events::onFlush)]
21+
final class FlushCounterListener
22+
{
23+
public int $flushes = 0;
24+
25+
public function onFlush(OnFlushEventArgs $args): void
26+
{
27+
++$this->flushes;
28+
}
29+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace App\Doctrine;
6+
7+
use App\Entity\User;
8+
use Doctrine\Bundle\DoctrineBundle\Attribute\AsDoctrineListener;
9+
use Doctrine\ORM\Event\PrePersistEventArgs;
10+
use Doctrine\ORM\Events;
11+
use Symfony\Component\HttpFoundation\RequestStack;
12+
13+
/**
14+
* Issue #150: the request_stack injected into a Doctrine listener must expose the
15+
* current request (and therefore its locale and session), instead of returning
16+
* null as it did when the EntityManager was persisted across kernel reboots.
17+
* Triggered by the `/create-user` route.
18+
*
19+
* @see https://github.com/Codeception/module-symfony/issues/150
20+
*/
21+
#[AsDoctrineListener(event: Events::prePersist)]
22+
final class RequestStackListener
23+
{
24+
public bool $hasRequest = false;
25+
public ?string $currentLocale = null;
26+
27+
public function __construct(private readonly RequestStack $requestStack)
28+
{
29+
}
30+
31+
public function prePersist(PrePersistEventArgs $args): void
32+
{
33+
if (!$args->getObject() instanceof User) {
34+
return;
35+
}
36+
37+
$request = $this->requestStack->getCurrentRequest();
38+
$this->hasRequest = $request !== null;
39+
$this->currentLocale = $request?->getLocale();
40+
}
41+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace App\Doctrine;
6+
7+
use App\Entity\User;
8+
use App\Utils\Mailer;
9+
10+
/**
11+
* Issue #90: an email sent from a Doctrine entity listener must be collected by
12+
* the profiler so that seeEmailIsSent() works. Registered as a lazy
13+
* `doctrine.orm.entity_listener` and triggered by the dedicated
14+
* `/create-user-with-confirmation` route.
15+
*
16+
* @see https://github.com/Codeception/module-symfony/issues/90
17+
*/
18+
final class SendConfirmationListener
19+
{
20+
/**
21+
* Marker address: only this user triggers the mail, so the listener
22+
* does not interfere with the email counts asserted in MailerCest.
23+
*/
24+
public const TRIGGER_EMAIL = 'issue90-listener@example.com';
25+
26+
public function __construct(private readonly Mailer $mailer)
27+
{
28+
}
29+
30+
public function postPersist(User $user): void
31+
{
32+
if ($user->getEmail() === self::TRIGGER_EMAIL) {
33+
$this->mailer->sendConfirmationEmail($user);
34+
}
35+
}
36+
}

0 commit comments

Comments
 (0)