mtarld/apip-ddd

Add async commands

javaDeveloperKid opened this issue · 2 comments

Hi, I am using this repository as base for my new project. However this repo does not implement async commands and as a consequence it does not show how to consider async commands inside a command bus implementation.

I rewritten the MessengerCommandBus implementation however this does not satisfy PHPStan as returning null for async commands results in error

Method MessengerCommandBus::dispatch() should return T but returns null.                                
💡 Type null is not always the same as T. It breaks the contract for some argument types, typically subtypes.
final class MessengerCommandBus implements CommandBusInterface
{
    public function __construct(MessageBusInterface $commandBus)
    {
        $this->messageBus = $commandBus;
    }

    /**
     * @template T
     *
     * @param CommandInterface<T> $command
     *
     * @return T
     */
    public function dispatch(CommandInterface $command): mixed
    {
        try {
            $envelope = $this->messageBus->dispatch($message);
            /** @var HandledStamp[] $handledStamps */
            $handledStamps = $envelope->all(HandledStamp::class);

            if (!$handledStamps) {
                // async command
                return null;
            }
            
            if (\count($handledStamps) > 1) {
                $handlers = implode(', ', array_map(fn (HandledStamp $stamp): string => sprintf('"%s"', $stamp->getHandlerName()), $handledStamps));

                throw new LogicException(sprintf('Message of type "%s" was handled multiple times. Only one handler is expected when using "%s::%s()", got %d: %s.', get_debug_type($envelope->getMessage()), static::class, __FUNCTION__, \count($handledStamps), $handlers));
            }

            return $handledStamps[0]->getResult();
        } catch (HandlerFailedException $e) {
            if ($exception = current($e->getWrappedExceptions())) {
                throw $exception;
            }

            throw $e;
        }
    }
}
if (!$handledStamps) {
    // async command
    return null;
}

Note that still a normal sync command must meet a handler. By removing the logic exception you'll miss that rule.

The alternative to allow async without losing the sync validation would be:

if (!$handledStamps) {
    if ($envelope->all(SentStamp::class)) {
        // async command
        return null;
    }

    throw new LogicException(sprintf('Message of type "%s" was handled zero times. Exactly one handler is expected when using "%s::%s()".', get_debug_type($envelope->getMessage()), static::class, __FUNCTION__));
}

Checking by SentStamp::class you'll know that the command was at least sent through an async transport.

Thank you for your response 👍 However my problem described above is about PHPStan and generics :)

Note that still a normal sync command must meet a handler. By removing the logic exception you'll miss that rule.

I believe allow_no_handlers: false in the bus configuration (this is a default in SF Messenger) will take care of that.