Skip to content

Commit f15b44c

Browse files
committed
[make:doctrine:listener] Add maker for Doctrine event/entity listeners
1 parent 0624f13 commit f15b44c

File tree

14 files changed

+717
-0
lines changed

14 files changed

+717
-0
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
The <info>%command.name%</info> command generates a new event or entity listener class.
2+
3+
<info>php %command.full_name% UserListener</info>
4+
5+
If the argument is missing, the command will ask for the class name interactively.

config/makers.xml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,12 @@
6969
<argument type="service" id="maker.event_registry" />
7070
</service>
7171

72+
<service id="maker.maker.make_doctrine_listener" class="Symfony\Bundle\MakerBundle\Maker\MakeDoctrineListener">
73+
<tag name="maker.command" />
74+
<argument type="service" id="maker.doctrine.event_registry" />
75+
<argument type="service" id="maker.doctrine_helper" />
76+
</service>
77+
7278
<service id="maker.maker.make_message" class="Symfony\Bundle\MakerBundle\Maker\MakeMessage">
7379
<argument type="service" id="maker.file_manager" />
7480
<tag name="maker.command" />

config/services.xml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@
3131
<argument type="service" id="event_dispatcher" />
3232
</service>
3333

34+
<service id="maker.doctrine.event_registry" class="Symfony\Bundle\MakerBundle\Doctrine\DoctrineEventRegistry" />
35+
3436
<service id="maker.console_error_listener" class="Symfony\Bundle\MakerBundle\Event\ConsoleErrorSubscriber">
3537
<tag name="kernel.event_subscriber" />
3638
</service>
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony MakerBundle package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Bundle\MakerBundle\Doctrine;
13+
14+
use Doctrine\Migrations\Event\MigrationsEventArgs;
15+
use Doctrine\Migrations\Event\MigrationsVersionEventArgs;
16+
use Doctrine\Migrations\Events as MigrationsEvents;
17+
use Doctrine\ORM\Events;
18+
use Doctrine\ORM\Tools\ToolEvents;
19+
20+
/**
21+
* @internal
22+
*/
23+
class DoctrineEventRegistry
24+
{
25+
private array $lifecycleEvents;
26+
27+
private ?array $eventsMap = null;
28+
29+
public function __construct()
30+
{
31+
$this->lifecycleEvents = [
32+
Events::prePersist => true,
33+
Events::postPersist => true,
34+
Events::preUpdate => true,
35+
Events::postUpdate => true,
36+
Events::preRemove => true,
37+
Events::postRemove => true,
38+
Events::preFlush => true,
39+
Events::postLoad => true,
40+
];
41+
}
42+
43+
public function isLifecycleEvent(string $event): bool
44+
{
45+
return isset($this->lifecycleEvents[$event]);
46+
}
47+
48+
/**
49+
* Returns all known event names.
50+
*/
51+
public function getAllEvents(): array
52+
{
53+
return array_keys($this->getEventsMap());
54+
}
55+
56+
/**
57+
* Attempts to get the event class for a given event.
58+
*/
59+
public function getEventClassName(string $event): ?string
60+
{
61+
return $this->getEventsMap()[$event]['event_class'] ?? null;
62+
}
63+
64+
/**
65+
* Attempts to find the class that defines the given event name as a constant.
66+
*/
67+
public function getEventConstantClassName(string $event): ?string
68+
{
69+
return $this->getEventsMap()[$event]['const_class'] ?? null;
70+
}
71+
72+
private function getEventsMap(): array
73+
{
74+
return $this->eventsMap ??= self::findEvents();
75+
}
76+
77+
private static function findEvents(): array
78+
{
79+
$eventsMap = [];
80+
81+
foreach ((new \ReflectionClass(Events::class))->getConstants(\ReflectionClassConstant::IS_PUBLIC) as $event) {
82+
$eventsMap[$event] = [
83+
'const_class' => Events::class,
84+
'event_class' => \sprintf('Doctrine\ORM\Event\%sEventArgs', ucfirst($event)),
85+
];
86+
}
87+
88+
foreach ((new \ReflectionClass(ToolEvents::class))->getConstants(\ReflectionClassConstant::IS_PUBLIC) as $event) {
89+
$eventsMap[$event] = [
90+
'const_class' => ToolEvents::class,
91+
'event_class' => \sprintf('Doctrine\ORM\Tools\Event\%sEventArgs', substr($event, 4)),
92+
];
93+
}
94+
95+
if (class_exists(MigrationsEvents::class)) {
96+
foreach ((new \ReflectionClass(MigrationsEvents::class))->getConstants(\ReflectionClassConstant::IS_PUBLIC) as $event) {
97+
$eventsMap[$event] = [
98+
'const_class' => MigrationsEvents::class,
99+
'event_class' => str_contains($event, 'Version') ? MigrationsVersionEventArgs::class : MigrationsEventArgs::class,
100+
];
101+
}
102+
}
103+
104+
ksort($eventsMap);
105+
106+
return $eventsMap;
107+
}
108+
}

src/Maker/MakeDoctrineListener.php

Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony MakerBundle package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Bundle\MakerBundle\Maker;
13+
14+
use Doctrine\Bundle\DoctrineBundle\Attribute\AsDoctrineListener;
15+
use Doctrine\Bundle\DoctrineBundle\Attribute\AsEntityListener;
16+
use Doctrine\Bundle\DoctrineBundle\DoctrineBundle;
17+
use Doctrine\Common\EventArgs;
18+
use Symfony\Bundle\MakerBundle\ConsoleStyle;
19+
use Symfony\Bundle\MakerBundle\DependencyBuilder;
20+
use Symfony\Bundle\MakerBundle\Doctrine\DoctrineEventRegistry;
21+
use Symfony\Bundle\MakerBundle\Doctrine\DoctrineHelper;
22+
use Symfony\Bundle\MakerBundle\Exception\RuntimeCommandException;
23+
use Symfony\Bundle\MakerBundle\Generator;
24+
use Symfony\Bundle\MakerBundle\InputConfiguration;
25+
use Symfony\Bundle\MakerBundle\Str;
26+
use Symfony\Bundle\MakerBundle\Util\UseStatementGenerator;
27+
use Symfony\Bundle\MakerBundle\Validator;
28+
use Symfony\Component\Console\Command\Command;
29+
use Symfony\Component\Console\Input\InputArgument;
30+
use Symfony\Component\Console\Input\InputInterface;
31+
use Symfony\Component\Console\Question\ConfirmationQuestion;
32+
use Symfony\Component\Console\Question\Question;
33+
34+
final class MakeDoctrineListener extends AbstractMaker
35+
{
36+
public function __construct(
37+
private readonly DoctrineEventRegistry $doctrineEventRegistry,
38+
private readonly DoctrineHelper $doctrineHelper,
39+
) {
40+
}
41+
42+
public static function getCommandName(): string
43+
{
44+
return 'make:doctrine:listener';
45+
}
46+
47+
public static function getCommandDescription(): string
48+
{
49+
return 'Creates a new doctrine event or entity listener class';
50+
}
51+
52+
public function configureCommand(Command $command, InputConfiguration $inputConfig): void
53+
{
54+
$command
55+
->addArgument('name', InputArgument::OPTIONAL, 'Choose a class name for your doctrine event or entity listener')
56+
->addArgument('event', InputArgument::OPTIONAL, 'What event do you want to listen to?')
57+
->addArgument('entity', InputArgument::OPTIONAL, 'What entity should the event be associate with?')
58+
->setHelp($this->getHelpFileContents('MakeDoctrineListener.txt'));
59+
60+
$inputConfig->setArgumentAsNonInteractive('event');
61+
$inputConfig->setArgumentAsNonInteractive('entity');
62+
}
63+
64+
public function interact(InputInterface $input, ConsoleStyle $io, Command $command): void
65+
{
66+
$io->writeln('');
67+
68+
$event = $input->getArgument('event');
69+
70+
if (!$event) {
71+
$events = $this->doctrineEventRegistry->getAllEvents();
72+
73+
$io->writeln(' <fg=green>Suggested Events:</>');
74+
$io->listing(array_map(function (string $event): string {
75+
if ($this->doctrineEventRegistry->isLifecycleEvent($event)) {
76+
$event .= ' <fg=yellow>(Lifecycle)</>';
77+
}
78+
79+
return $event;
80+
}, $events));
81+
82+
$question = new Question($command->getDefinition()->getArgument('event')->getDescription());
83+
$question->setAutocompleterValues($events);
84+
$question->setValidator(Validator::notBlank(...));
85+
86+
$input->setArgument('event', $event = $io->askQuestion($question));
87+
88+
if ($this->doctrineEventRegistry->isLifecycleEvent($event) && !$input->getArgument('entity')) {
89+
$question = new ConfirmationQuestion(\sprintf('The "%s" event is a lifecycle event, would you like to associate it with a specific entity (entity listener)?', $event));
90+
91+
if ($io->askQuestion($question)) {
92+
$question = new Question($command->getDefinition()->getArgument('entity')->getDescription());
93+
$question->setValidator(Validator::notBlank(...));
94+
$question->setAutocompleterValues($this->doctrineHelper->getEntitiesForAutocomplete());
95+
96+
$input->setArgument('entity', $io->askQuestion($question));
97+
}
98+
}
99+
}
100+
101+
if (!$this->doctrineEventRegistry->isLifecycleEvent($event) && $input->getArgument('entity')) {
102+
throw new RuntimeCommandException(\sprintf('The "%s" event is not a lifecycle event and cannot be associated with a specific entity.', $event));
103+
}
104+
}
105+
106+
public function generate(InputInterface $input, ConsoleStyle $io, Generator $generator): void
107+
{
108+
$name = $input->getArgument('name');
109+
$event = $input->getArgument('event');
110+
111+
$eventFullClassName = $this->doctrineEventRegistry->getEventClassName($event) ?? EventArgs::class;
112+
$eventClassName = Str::getShortClassName($eventFullClassName);
113+
114+
$useStatements = new UseStatementGenerator([
115+
$eventFullClassName,
116+
]);
117+
118+
$eventConstFullClassName = $this->doctrineEventRegistry->getEventConstantClassName($event);
119+
$eventConstClassName = $eventConstFullClassName ? Str::getShortClassName($eventConstFullClassName) : null;
120+
121+
if ($eventConstFullClassName) {
122+
$useStatements->addUseStatement($eventConstFullClassName);
123+
}
124+
125+
$className = $generator->createClassNameDetails(
126+
$name,
127+
'EventListener\\',
128+
'Listener',
129+
)->getFullName();
130+
131+
$templateVars = [
132+
'use_statements' => $useStatements,
133+
'method_name' => $event,
134+
'event' => $eventConstClassName ? \sprintf('%s::%s', $eventConstClassName, $event) : "'$event'",
135+
'event_arg' => \sprintf('%s $event', $eventClassName),
136+
];
137+
138+
if ($input->getArgument('entity')) {
139+
$this->generateEntityListenerClass($useStatements, $generator, $className, $templateVars, $input->getArgument('entity'));
140+
} else {
141+
$this->generateEventListenerClass($useStatements, $generator, $className, $templateVars);
142+
}
143+
144+
$generator->writeChanges();
145+
146+
$this->writeSuccessMessage($io);
147+
148+
$io->text([
149+
'Next: Open your new listener class and start customizing it.',
150+
'Find the documentation at <fg=yellow>https://symfony.com/doc/current/doctrine/events.html</>',
151+
]);
152+
}
153+
154+
public function configureDependencies(DependencyBuilder $dependencies): void
155+
{
156+
$dependencies->addClassDependency(
157+
DoctrineBundle::class,
158+
'doctrine/doctrine-bundle',
159+
);
160+
}
161+
162+
/**
163+
* @param array<string, mixed> $templateVars
164+
*/
165+
private function generateEntityListenerClass(UseStatementGenerator $useStatements, Generator $generator, string $className, array $templateVars, string $entityClassName): void
166+
{
167+
$entityClassDetails = $generator->createClassNameDetails(
168+
$entityClassName,
169+
'Entity\\',
170+
);
171+
172+
$useStatements->addUseStatement(AsEntityListener::class);
173+
$useStatements->addUseStatement($entityClassDetails->getFullName());
174+
175+
$generator->generateClass(
176+
$className,
177+
'doctrine/EntityListener.tpl.php',
178+
$templateVars + [
179+
'entity' => $entityClassName,
180+
'entity_arg' => \sprintf('%s $entity', $entityClassName),
181+
],
182+
);
183+
}
184+
185+
/**
186+
* @param array<string, mixed> $templateVars
187+
*/
188+
private function generateEventListenerClass(UseStatementGenerator $useStatements, Generator $generator, string $className, array $templateVars): void
189+
{
190+
$useStatements->addUseStatement(AsDoctrineListener::class);
191+
192+
$generator->generateClass(
193+
$className,
194+
'doctrine/EventListener.tpl.php',
195+
$templateVars,
196+
);
197+
}
198+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<?= "<?php\n" ?>
2+
3+
namespace <?= $namespace; ?>;
4+
5+
<?= $use_statements; ?>
6+
7+
#[AsEntityListener(event: <?= $event ?>, entity: <?= $entity ?>::class)]
8+
final class <?= $class_name."\n" ?>
9+
{
10+
public function __invoke(<?= $entity_arg ?>, <?= $event_arg ?>): void
11+
{
12+
// ...
13+
}
14+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<?= "<?php\n" ?>
2+
3+
namespace <?= $namespace; ?>;
4+
5+
<?= $use_statements; ?>
6+
7+
#[AsDoctrineListener(event: <?= $event ?>)]
8+
final class <?= $class_name."\n" ?>
9+
{
10+
public function <?= $method_name ?>(<?= $event_arg ?>): void
11+
{
12+
// ...
13+
}
14+
}

0 commit comments

Comments
 (0)