Skip to content

[make:doctrine:listener] Add maker for Doctrine event/entity listeners #1704

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: 1.x
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions config/help/MakeDoctrineListener.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
The <info>%command.name%</info> command generates a new event or entity listener class.

<info>php %command.full_name% UserListener</info>

If the argument is missing, the command will ask for the class name interactively.
6 changes: 6 additions & 0 deletions config/makers.xml
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,12 @@
<argument type="service" id="maker.event_registry" />
</service>

<service id="maker.maker.make_doctrine_listener" class="Symfony\Bundle\MakerBundle\Maker\MakeDoctrineListener">
<tag name="maker.command" />
<argument type="service" id="maker.doctrine.event_registry" />
<argument type="service" id="maker.doctrine_helper" />
</service>

<service id="maker.maker.make_message" class="Symfony\Bundle\MakerBundle\Maker\MakeMessage">
<argument type="service" id="maker.file_manager" />
<tag name="maker.command" />
Expand Down
2 changes: 2 additions & 0 deletions config/services.xml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@
<argument type="service" id="event_dispatcher" />
</service>

<service id="maker.doctrine.event_registry" class="Symfony\Bundle\MakerBundle\Doctrine\DoctrineEventRegistry" />

<service id="maker.console_error_listener" class="Symfony\Bundle\MakerBundle\Event\ConsoleErrorSubscriber">
<tag name="kernel.event_subscriber" />
</service>
Expand Down
108 changes: 108 additions & 0 deletions src/Doctrine/DoctrineEventRegistry.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
<?php

/*
* This file is part of the Symfony MakerBundle package.
*
* (c) Fabien Potencier <[email protected]>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\Bundle\MakerBundle\Doctrine;

use Doctrine\Migrations\Event\MigrationsEventArgs;
use Doctrine\Migrations\Event\MigrationsVersionEventArgs;
use Doctrine\Migrations\Events as MigrationsEvents;
use Doctrine\ORM\Events;
use Doctrine\ORM\Tools\ToolEvents;

/**
* @internal
*/
class DoctrineEventRegistry
{
private array $lifecycleEvents;

private ?array $eventsMap = null;

public function __construct()
{
$this->lifecycleEvents = [
Events::prePersist => true,
Events::postPersist => true,
Events::preUpdate => true,
Events::postUpdate => true,
Events::preRemove => true,
Events::postRemove => true,
Events::preFlush => true,
Events::postLoad => true,
];
}

public function isLifecycleEvent(string $event): bool
{
return isset($this->lifecycleEvents[$event]);
}

/**
* Returns all known event names.
*/
public function getAllEvents(): array
{
return array_keys($this->getEventsMap());
}

/**
* Attempts to get the event class for a given event.
*/
public function getEventClassName(string $event): ?string
{
return $this->getEventsMap()[$event]['event_class'] ?? null;
}

/**
* Attempts to find the class that defines the given event name as a constant.
*/
public function getEventConstantClassName(string $event): ?string
{
return $this->getEventsMap()[$event]['const_class'] ?? null;
}

private function getEventsMap(): array
{
return $this->eventsMap ??= self::findEvents();
}

private static function findEvents(): array
{
$eventsMap = [];

foreach ((new \ReflectionClass(Events::class))->getConstants(\ReflectionClassConstant::IS_PUBLIC) as $event) {
$eventsMap[$event] = [
'const_class' => Events::class,
'event_class' => \sprintf('Doctrine\ORM\Event\%sEventArgs', ucfirst($event)),
];
}

foreach ((new \ReflectionClass(ToolEvents::class))->getConstants(\ReflectionClassConstant::IS_PUBLIC) as $event) {
$eventsMap[$event] = [
'const_class' => ToolEvents::class,
'event_class' => \sprintf('Doctrine\ORM\Tools\Event\%sEventArgs', substr($event, 4)),
];
}

if (class_exists(MigrationsEvents::class)) {
foreach ((new \ReflectionClass(MigrationsEvents::class))->getConstants(\ReflectionClassConstant::IS_PUBLIC) as $event) {
$eventsMap[$event] = [
'const_class' => MigrationsEvents::class,
'event_class' => str_contains($event, 'Version') ? MigrationsVersionEventArgs::class : MigrationsEventArgs::class,
];
}
}

ksort($eventsMap);

return $eventsMap;
}
}
198 changes: 198 additions & 0 deletions src/Maker/MakeDoctrineListener.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
<?php

/*
* This file is part of the Symfony MakerBundle package.
*
* (c) Fabien Potencier <[email protected]>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\Bundle\MakerBundle\Maker;

use Doctrine\Bundle\DoctrineBundle\Attribute\AsDoctrineListener;
use Doctrine\Bundle\DoctrineBundle\Attribute\AsEntityListener;
use Doctrine\Bundle\DoctrineBundle\DoctrineBundle;
use Doctrine\Common\EventArgs;
use Symfony\Bundle\MakerBundle\ConsoleStyle;
use Symfony\Bundle\MakerBundle\DependencyBuilder;
use Symfony\Bundle\MakerBundle\Doctrine\DoctrineEventRegistry;
use Symfony\Bundle\MakerBundle\Doctrine\DoctrineHelper;
use Symfony\Bundle\MakerBundle\Exception\RuntimeCommandException;
use Symfony\Bundle\MakerBundle\Generator;
use Symfony\Bundle\MakerBundle\InputConfiguration;
use Symfony\Bundle\MakerBundle\Str;
use Symfony\Bundle\MakerBundle\Util\UseStatementGenerator;
use Symfony\Bundle\MakerBundle\Validator;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Question\ConfirmationQuestion;
use Symfony\Component\Console\Question\Question;

final class MakeDoctrineListener extends AbstractMaker
{
public function __construct(
private readonly DoctrineEventRegistry $doctrineEventRegistry,
private readonly DoctrineHelper $doctrineHelper,
) {
}

public static function getCommandName(): string
{
return 'make:doctrine:listener';
}

public static function getCommandDescription(): string
{
return 'Creates a new doctrine event or entity listener class';
}

public function configureCommand(Command $command, InputConfiguration $inputConfig): void
{
$command
->addArgument('name', InputArgument::OPTIONAL, 'Choose a class name for your doctrine event or entity listener')
->addArgument('event', InputArgument::OPTIONAL, 'What event do you want to listen to?')
->addArgument('entity', InputArgument::OPTIONAL, 'What entity should the event be associate with?')
->setHelp($this->getHelpFileContents('MakeDoctrineListener.txt'));

$inputConfig->setArgumentAsNonInteractive('event');
$inputConfig->setArgumentAsNonInteractive('entity');
}

public function interact(InputInterface $input, ConsoleStyle $io, Command $command): void
{
$io->writeln('');

$event = $input->getArgument('event');

if (!$event) {
$events = $this->doctrineEventRegistry->getAllEvents();

$io->writeln(' <fg=green>Suggested Events:</>');
$io->listing(array_map(function (string $event): string {
if ($this->doctrineEventRegistry->isLifecycleEvent($event)) {
$event .= ' <fg=yellow>(Lifecycle)</>';
}

return $event;
}, $events));

$question = new Question($command->getDefinition()->getArgument('event')->getDescription());
$question->setAutocompleterValues($events);
$question->setValidator(Validator::notBlank(...));

$input->setArgument('event', $event = $io->askQuestion($question));

if ($this->doctrineEventRegistry->isLifecycleEvent($event) && !$input->getArgument('entity')) {
$question = new ConfirmationQuestion(\sprintf('The "%s" event is a lifecycle event, would you like to associate it with a specific entity (entity listener)?', $event));

if ($io->askQuestion($question)) {
$question = new Question($command->getDefinition()->getArgument('entity')->getDescription());
$question->setValidator(Validator::notBlank(...));
$question->setAutocompleterValues($this->doctrineHelper->getEntitiesForAutocomplete());

$input->setArgument('entity', $io->askQuestion($question));
}
}
}

if (!$this->doctrineEventRegistry->isLifecycleEvent($event) && $input->getArgument('entity')) {
throw new RuntimeCommandException(\sprintf('The "%s" event is not a lifecycle event and cannot be associated with a specific entity.', $event));
}
}

public function generate(InputInterface $input, ConsoleStyle $io, Generator $generator): void
{
$name = $input->getArgument('name');
$event = $input->getArgument('event');

$eventFullClassName = $this->doctrineEventRegistry->getEventClassName($event) ?? EventArgs::class;
$eventClassName = Str::getShortClassName($eventFullClassName);

$useStatements = new UseStatementGenerator([
$eventFullClassName,
]);

$eventConstFullClassName = $this->doctrineEventRegistry->getEventConstantClassName($event);
$eventConstClassName = $eventConstFullClassName ? Str::getShortClassName($eventConstFullClassName) : null;

if ($eventConstFullClassName) {
$useStatements->addUseStatement($eventConstFullClassName);
}

$className = $generator->createClassNameDetails(
$name,
'EventListener\\',
'Listener',
)->getFullName();

$templateVars = [
'use_statements' => $useStatements,
'method_name' => $event,
'event' => $eventConstClassName ? \sprintf('%s::%s', $eventConstClassName, $event) : "'$event'",
'event_arg' => \sprintf('%s $event', $eventClassName),
];

if ($input->getArgument('entity')) {
$this->generateEntityListenerClass($useStatements, $generator, $className, $templateVars, $input->getArgument('entity'));
} else {
$this->generateEventListenerClass($useStatements, $generator, $className, $templateVars);
}

$generator->writeChanges();

$this->writeSuccessMessage($io);

$io->text([
'Next: Open your new listener class and start customizing it.',
'Find the documentation at <fg=yellow>https://symfony.com/doc/current/doctrine/events.html</>',
]);
}

public function configureDependencies(DependencyBuilder $dependencies): void
{
$dependencies->addClassDependency(
DoctrineBundle::class,
'doctrine/doctrine-bundle',
);
}

/**
* @param array<string, mixed> $templateVars
*/
private function generateEntityListenerClass(UseStatementGenerator $useStatements, Generator $generator, string $className, array $templateVars, string $entityClassName): void
{
$entityClassDetails = $generator->createClassNameDetails(
$entityClassName,
'Entity\\',
);

$useStatements->addUseStatement(AsEntityListener::class);
$useStatements->addUseStatement($entityClassDetails->getFullName());

$generator->generateClass(
$className,
'doctrine/EntityListener.tpl.php',
$templateVars + [
'entity' => $entityClassName,
'entity_arg' => \sprintf('%s $entity', $entityClassName),
],
);
}

/**
* @param array<string, mixed> $templateVars
*/
private function generateEventListenerClass(UseStatementGenerator $useStatements, Generator $generator, string $className, array $templateVars): void
{
$useStatements->addUseStatement(AsDoctrineListener::class);

$generator->generateClass(
$className,
'doctrine/EventListener.tpl.php',
$templateVars,
);
}
}
14 changes: 14 additions & 0 deletions templates/doctrine/EntityListener.tpl.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?= "<?php\n" ?>

namespace <?= $namespace; ?>;

<?= $use_statements; ?>

#[AsEntityListener(event: <?= $event ?>, entity: <?= $entity ?>::class)]
final class <?= $class_name."\n" ?>
{
public function __invoke(<?= $entity_arg ?>, <?= $event_arg ?>): void
{
// ...
}
}
14 changes: 14 additions & 0 deletions templates/doctrine/EventListener.tpl.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?= "<?php\n" ?>

namespace <?= $namespace; ?>;

<?= $use_statements; ?>

#[AsDoctrineListener(event: <?= $event ?>)]
final class <?= $class_name."\n" ?>
{
public function <?= $method_name ?>(<?= $event_arg ?>): void
{
// ...
}
}
Loading