Skip to content

Commit

Permalink
Add console command to manage legacy events (#26)
Browse files Browse the repository at this point in the history
  • Loading branch information
benr77 authored Mar 1, 2022
1 parent 3a22e38 commit e69a4e7
Show file tree
Hide file tree
Showing 5 changed files with 261 additions and 6 deletions.
37 changes: 34 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

DDD Domain Events for Symfony, with a Doctrine based event store.

This package allows you to dispatch domain events from within your domain
This package allows you to dispatch domain events from within your domain
model, so that they are persisted in the same transaction as your aggregate.

These events are then published using a Symfony event listener in the
Expand Down Expand Up @@ -149,7 +149,7 @@ be removed and superseded by the new _ReminderDue_ event.

By default only the DomainEvent is dispatched to the configured event bus.

You can overwrite the default event dispatcher with your own implementation to
You can overwrite the default event dispatcher with your own implementation to
annotate the message before dispatching it, e.g. to add an envelope with custom stamps.

Example:
Expand Down Expand Up @@ -224,12 +224,43 @@ automatically registered. This allows the StoredEvent entity to persist events
with microsecond accuracy. This ensures that events are published in the exact
same order they are recorded.

### Legacy Events Classes

During refactorings, you may well move or rename event classes. This will
result in legacy class names being stored in the database.

There is a console command, which will report on these legacy event classes
that do not match an existing, current class in the codebase (based on the
Composer autoloading).

```
bin/console headsnet:domain-events:name-check
```
You can then define the `legacy_map` configuration parameter, to map old,
legacy event class names to their new replacements.
```yaml
headsnet_domain_events:
legacy_map:
App\Namespace\Event\YourLegacyEvent1: App\Namespace\Event\YourNewEvent1
App\Namespace\Event\YourLegacyEvent2: App\Namespace\Event\YourNewEvent2
```

Then you can re-run the console command with the `--fix` option. This will
then update the legacy class names in the database with their new references.

There is also a `--delete` option which will remove all legacy events from
the database if they are not found in the legacy map. **THIS IS A DESTRUCTIVE
COMMAND PLEASE USE WITH CAUTION.**

### Default Configuration

```yaml
headsnet_domain_events:
message_bus:
name: messenger.bus.event
name: messenger.bus.event
legacy_map: []
```
### Contributing
Expand Down
205 changes: 205 additions & 0 deletions src/Console/EventNameCheckCommand.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
<?php

declare(strict_types=1);

namespace Headsnet\DomainEventsBundle\Console;

use Doctrine\Common\Collections\Criteria;
use Doctrine\ORM\EntityManagerInterface;
use Headsnet\DomainEventsBundle\Domain\Model\StoredEvent;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;

final class EventNameCheckCommand extends Command
{
/**
* @var string
*/
protected static $defaultDescription = 'Check and/or update legacy event class names stored in the database.';

/**
* @var EntityManagerInterface
*/
private $em;

/**
* @var array<string, string>
*/
private $legacyMap;

/**
* @var SymfonyStyle
*/
private $io;

/**
* @var bool
*/
private $deleteUnfixable;

public function __construct(
EntityManagerInterface $em,
array $legacyMap
) {
parent::__construct();
$this->em = $em;
$this->legacyMap = $legacyMap;
}

protected function configure(): void
{
$this
->addOption(
'fix',
'f',
InputOption::VALUE_NONE,
'Automatically fix any errors based on the legacy_map setting.'
)
->addOption(
'delete',
'd',
InputOption::VALUE_NONE,
'Remove events that cannot be fixed using the legacy_map. THIS IS A DESTRUCTIVE COMMAND!'
);
}

protected function execute(InputInterface $input, OutputInterface $output): int
{
$this->io = new SymfonyStyle($input, $output);
$this->deleteUnfixable = $input->getOption('delete');

if ($input->getOption('fix') && 0 === count($this->legacyMap)) {
$this->showDefineLegacyMapErrorMessage();

return Command::INVALID;
}

$legacyEvents = $this->findLegacyEventTypes();

$this->displayLegacyEventsFound($legacyEvents);

if ($input->getOption('fix')) {
$this->fixLegacyEvents($legacyEvents);
}

return Command::SUCCESS;
}

private function showDefineLegacyMapErrorMessage(): void
{
$this->io->error([
"You must define the legacy mappings before you can fix event class names.\n\n".
"In headsnet_domain_events.yaml, configure the 'legacy_map' option. E.g.\n\n".
"headsnet_domain_events:\n".
" legacy_map:\n".
" App\Namespace\Event\YourLegacyEvent1: App\Namespace\Event\YourNewEvent1\n".
" App\Namespace\Event\YourLegacyEvent2: App\Namespace\Event\YourNewEvent2\n",
]);
}

protected function loadEventTypesFromEventStore(): array
{
$eventTypes = $this->em->createQueryBuilder()
->select([
'event.typeName',
])
->from(StoredEvent::class, 'event')
->groupBy('event.typeName')
->orderBy('event.typeName', Criteria::ASC)
->getQuery()
->getResult();

return array_map(
function (array $type): string {
return $type['typeName'];
},
$eventTypes
);
}

protected function findLegacyEventTypes(): array
{
$legacyEvents = [];
foreach ($this->loadEventTypesFromEventStore() as $storedEventClass) {
if (!class_exists($storedEventClass)) {
$legacyEvents[] = $storedEventClass;
}
}

return $legacyEvents;
}

protected function displayLegacyEventsFound(array $legacyEvents): void
{
if (count($legacyEvents) > 0) {
$this->io->warning(
sprintf('Found %d legacy event classes found', count($legacyEvents))
);

array_map(
function (string $legacyEvent) {
$this->io->text($legacyEvent);
},
$legacyEvents
);
}
}

protected function fixLegacyEvents(array $legacyEvents): void
{
array_map(
function (string $legacyEvent) {
$this->tryFixingEventClass($legacyEvent);
},
$legacyEvents
);
}

private function tryFixingEventClass(string $eventClass): void
{
if (!array_key_exists($eventClass, $this->legacyMap)) {
$this->io->error(sprintf("Cannot fix - not found in legacy map:\n%s", $eventClass));

return;
}

if (null !== $this->legacyMap[$eventClass]) {
$this->fixLegacyEventName($eventClass);
} elseif ($this->deleteUnfixable) {
$this->removeLegacyEvent($eventClass);
}
}

private function fixLegacyEventName(string $eventClass): void
{
$this->io->success(
sprintf("Fixing legacy event\n%s =>\n%s", $eventClass, $this->legacyMap[$eventClass])
);

$this->em->createQueryBuilder()
->update(StoredEvent::class, 'event')
->set('event.typeName', ':new_name')
->where('event.typeName = :old_name')
->setParameter('new_name', $this->legacyMap[$eventClass])
->setParameter('old_name', $eventClass)
->getQuery()
->execute();
}

private function removeLegacyEvent(string $eventClass): void
{
$this->io->warning(
sprintf("Removing legacy event\n%s", $eventClass)
);

$this->em->createQueryBuilder()
->delete(StoredEvent::class, 'event')
->where('event.typeName = :event_to_delete')
->setParameter('event_to_delete', $eventClass)
->getQuery()
->execute();
}
}
4 changes: 4 additions & 0 deletions src/DependencyInjection/Configuration.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@ public function getConfigTreeBuilder()
->end()
->end()
->end() // message_bus
->arrayNode('legacy_map')
->normalizeKeys(false)
->scalarPrototype()->end()
->end() // legacy_map
->end()
;

Expand Down
6 changes: 4 additions & 2 deletions src/DependencyInjection/HeadsnetDomainEventsExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ class HeadsnetDomainEventsExtension extends Extension implements PrependExtensio
private const DBAL_MICROSECONDS_TYPE = 'datetime_immutable_microseconds';

/**
* @param array<mixed> $configs
* @param array<string, array> $configs
*/
public function load(array $configs, ContainerBuilder $container): void
{
Expand All @@ -33,11 +33,13 @@ public function load(array $configs, ContainerBuilder $container): void
$configuration = new Configuration();
$config = $this->processConfiguration($configuration, $configs);

$container->setParameter('headsnet_domain_events.legacy_map', $config['legacy_map']);

$this->useCustomMessageBusIfSpecified($config, $container);
}

/**
* @param array<mixed> $config
* @param array<string, array> $config
*/
private function useCustomMessageBusIfSpecified(array $config, ContainerBuilder $container): void
{
Expand Down
15 changes: 14 additions & 1 deletion src/Resources/config/services.xml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@
xsi:schemaLocation="http://symfony.com/schema/dic/services
https://symfony.com/schema/dic/services/services-1.0.xsd">

<parameters>
<parameter key="headsnet_domain_events.legacy_map" />
</parameters>

<services>

<!-- Clients can override this to change the default lock factory, or indeed
Expand Down Expand Up @@ -52,7 +56,16 @@
<argument type="service" id="event_dispatcher"/>
</service>

<service id="Headsnet\DomainEventsBundle\Domain\Model\EventStore" alias="headsnet_domain_events.repository.event_store_doctrine"/>
<service id="Headsnet\DomainEventsBundle\Domain\Model\EventStore"
alias="headsnet_domain_events.repository.event_store_doctrine"
/>

<service id="headsnet_domain_events.event_check_command"
class="Headsnet\DomainEventsBundle\Console\EventNameCheckCommand">
<argument type="service" id="doctrine.orm.default_entity_manager"/>
<argument>%headsnet_domain_events.legacy_map%</argument>
<tag name="console.command" command="headsnet:domain-events:name-check"/>
</service>

</services>

Expand Down

0 comments on commit e69a4e7

Please sign in to comment.