How to specify custom message names on prooph messages?

How to specify custom message names on prooph messages?

I've been using FQCN as the names of command, queries and events in my prooph application. I felt it was time to change those long and ugly names to something more readable. In various places these names are used in configuration so I wanted to keep the ability to easily refactor and jump into classes.

To change the name of messages we should do two things for different circumstances. For whenever we instantiate messages ourselves we must override the $messageName property in out Message.

direct instantiation of messages

// done in some script, direct instantiation of messages
$this->commandBus->dispatch(new RegisterUser([
    'user_id'           => (string) UserId::generate(),
    'display_name'      => $dataToImport['display_name'],
    'email_address'     => $dataToImport['email_address'],
    'password_hash'     => $dataToImport['password_hash'],
    'registration_date' => $dataToImport['registration_date'],
    'account_status'    => $dataToImport['account_status'],
    'email_status'      => $dataToImport['email_status'],
]));

In my messages adding this protected $messageName = 'identity.registerUser'; would be sufficient but I created a special CommandName class with only constants representing the messages names. My Command therefore looks like this;

final class RegisterUser extends Command implements PayloadConstructable
{
    use PayloadTrait;

    protected $messageName = CommandName::registerUser;

    /**
     * @return UserId
     */
    public function userId(): UserId
    ...

instantiation of messages via factory

For these situations we should create a new message factory that is capable of mapping these names to classes.
I came up with this one where the following should be noted; It is almost a verbatim copy of the FQCNMessageFactory that ships with prooph and that it is still capable of creating messages by FQCN. This is important because you might not have control over every message. (eg the Prooph\Snapshotter\TakeSnapshot Command)

use My\Domain\Identity;

class CustomMessageFactory implements MessageFactory
{
    protected $messageMap = [
        Identity\CommandName::registerUser => Identity\Command\RegisterUser::class,
        ...
    ];

    /**
     * @param string $messageName
     * @param array $messageData
     * @throws \UnexpectedValueException
     * @return CustomMessageFactory
     */
    public function createMessageFromArray($messageName, array $messageData)
    {
        if (isset($this->messageMap[$messageName])) {
            $messageClass = $this->messageMap[$messageName];
        } else {
            $messageClass = $messageName;
        }

        if (! class_exists($messageClass)) {
            throw new \UnexpectedValueException('Given message name is not a valid class: ' . (string) $messageClass);
        }

        if (!is_subclass_of($messageClass, DomainMessage::class)) {
            throw new \UnexpectedValueException(sprintf(
                'Message class %s is not a sub class of %s',
                $messageClass,
                DomainMessage::class
            ));
        }

        if (!isset($messageData['message_name'])) {
            $messageData['message_name'] = $messageName;
        }

        if (!isset($messageData['uuid'])) {
            $messageData['uuid'] = Uuid::uuid4();
        }

        if (!isset($messageData['version'])) {
            $messageData['version'] = 0;
        }

        if (!isset($messageData['created_at'])) {
            $time = (string) microtime(true);
            if (false === strpos($time, '.')) {
                $time .= '.0000';
            }
            $messageData['created_at'] = \DateTimeImmutable::createFromFormat('U.u', $time);
        }

        if (!isset($messageData['metadata'])) {
            $messageData['metadata'] = [];
        }

        return $messageClass::fromArray($messageData);
    }

enable factory

To enable the factory in the prooph middleware we change the appropriate (don't need to do them all at once) message_factory to our newly created factory.

return [
    'prooph' => [
        'middleware'     => [
            'query'   => [
                'message_factory'   => \My\Messaging\CustomMessageFactory::class,
                ...
            ],
            'command' => [
                'message_factory'   => \My\Messaging\CustomMessageFactory::class,
                ...
            ],
            'event'   => [
                'message_factory'   => \My\Messaging\CustomMessageFactory::class,
                ...
            ],
            'message' => [
                'message_factory'   => \My\Messaging\CustomMessageFactory::class,
                ...
            ],
        ],

And finally we can change every occurrence in the application configuration from the My\Domain\Command\RegisterUser::class name to My\Domain\Identity\CommandName::registerUser.

I find this useful because it keeps the configuration files readable. Especially since there are many locations where message names are used in configuration, in routing, busses, rbac in-memory configuration, etc...

Additionally, recorded events names are now independent of the actual used class.

Update: I moved the $messageMap property from the CustomMessageFactory to the various CommandName classes (per bounded context) because now I can change commands in one location.

class CustomMessageFactory implements MessageFactory
{
    /**
     * @var array maps message names to FQCN per bounded context domain
     */
    protected $messageMap = [
        'identity'    => Identity\CommandName::MESSAGE_MAP,
        ...
    ];

    public function createMessageFromArray($messageName, array $messageData)
    {
        $domain = explode('.', '$messageName', 2)[0];

        if (isset($this->messageMap[$domain][$messageName])) {
            $messageClass = $this->messageMap[$domain][$messageName];
        } else {
            $messageClass = $messageName;
        }
        ...

final class CommandName
{
    const MESSAGE_MAP = [
        CommandName::registerUser => Command\RegisterUser::class,
    ];

    const registerUser = 'identity.registerUser';
    ....
}