lmc/cqrs-types

A library containing types (interfaces and value objects) to help with Queries and Commands

3.2.0 2024-03-06 12:41 UTC

This package is auto-updated.

Last update: 2024-12-06 14:29:58 UTC


README

cqrs-types Latest Stable Version Tests and linting Coverage Status

This library contains types, value objects, interfaces and base implementation for CQRS library. So anyone can easily write an extension for it.

Table of contents

Installation

composer require lmc/cqrs-types

Interfaces (types)

There are many interfaces and types here, which are implemented in other CQRS libraries

Query Interface

The main interface for all Queries. Query is a request which fetch a data without changing anything. It is responsible for declaring and creating a request, which will be handled by QueryHandlerInterface. An object implementing Query Interface may implement feature and it should be handled by a QueryFetcher.

Available features:

  • caching
  • profiling

Query Fetcher Interface

An interface for a Query Fetcher (see Handler/QueryFetcher).

It is responsible for

  • finding a Query Handler based on Query request type
  • handle all Query features
    • caching
      • requires an instance of Psr\Cache\CacheItemPoolInterface
    • profiling
      • requires an instance of Lmc\Cqrs\Handler\ProfilerBag
  • decoding a response from the Query Handler

Query Handler Interface

It is responsible for handling a specific Query request and passing a result into OnSuccess callback. It must say which request it supports and it must not be able to handle a different request. When unsupported request is passed to handle method, it must pass UnsupportedRequestException into OnError callback. It must not throw any exception, all exception must be passed into OnError callback.

If necessary it may prepare a Query (for example inject a Client) - yet prepare method should not change a Query type or its content. It should prepare only supported queries. It should not throw any exception either.

Command Interface

The main interface for all Commands. Command is a request which change a data and may return result data. It is responsible for declaring and creating a request, which will be handled by SendCommandHandlerInterface. An object implementing Command Interface may implement feature and it should be handled by a CommandSender.

Available features:

  • profiling

Command Sender Interface

An interface for a Command Sender (see Handler/CommandSender).

It is responsible for

  • finding a Send Command Handler based on Command request type
  • handle all Command features
    • profiling
      • requires an instance of Lmc\Cqrs\Handler\ProfilerBag
  • decoding a response from the Send Command Handler

Send Command Handler Interface

It is responsible for handling a specific Command request and passing a result into OnSuccess callback. It must say which request it supports and it must not be able to handle a different request. When unsupported request is passed to handle method, it must pass UnsupportedRequestException into OnError callback. It must not throw any exception, all exception must be passed into OnError callback.

If necessary it may prepare a Command (for example inject a Client) - yet prepare method should not change a Command type or its content. It should prepare only supported commands. It should not throw any exception either.

Response Decoder Interface

It is meant to decode a response (a result of either QueryHandlerInterface or a SendCommandHandlerInterface). Decoder itself should be as small as possible and it should only support the one type to decode. Decoders should (and are in a default implementation) be used by a priority one by one.

If you need a decoder to be final and no other decoding to be done, you must return a DecodedValue object.

  • QueryFetcher and CommandSender is responsible not to send a DecodedValue to any other decoder

It should be pure.

The response should be decoded in the same way everytime, in that case it can be safely cached.

If you need to do some impure operations in your decoder, you should use ImpureResponseDecoderInterface instead. It works exactly the same as a pure decoder but in the addition the QueryFetcher knows, that a decoding process is not pure, so it may do some extra stuff. For example QueryFetcher should cache the result before the first impure decoding, so it won't cache the wrong result.

If an unsupported response is passed to decode method, it should return it untouched. It must not throw an exception.

There is also one predefined Response Decoder to decode a json string into an array - JsonResponseDecoder.

Profiler Formatter Interface

When a Command or a Query implements Feature\ProfileableInterface QueryFetcher/CommandSender will create a ProfilerItem with some information. The ProfilerItem contains a raw data about a duration, request type, response, error and more and it is meant to be shown in Symfony Profiler (if you use a CQRS bundle). Profiler Formatter format those items to provide a better experience than just a raw data, which might be lazy or unreadable without formatting. It is responsible to format a ProfilerItem to the ProfilerItem again, so it can get/set all of ProfilerItem properties.

There is a FormattedValue Value object to help with a formatting, it can be passed to the most of the ProfilerItem properties as a value. It contains both original and formatted value.

Profiler formatter can even further format already FormattedValue. Multiple Formatters should be called by priority on the ProfilerItem one by one to create the most readable form of a ProfilerItem.

There is also one predefined Profiler Formatter to format (decode) a json string into an array - JsonProfilerFormatter.

Features

Cacheable Interface

It allows to store and load an object implementing this interface to Psr\Cache\CacheItemPoolInterface. It uses a CacheKey which should be as unique as possible.

NOTE: It is also required to set up Psr\Cache\CacheItemPoolInterface implementation to QueryFetcher.

Profileable Interface

It allows a profiling the object implementing this interface. ProfilerId is a string, which does not have to be unique.

Base Implementations

Base implementations offers a method(s) which will be mostly needed in implementing Handlers etc.

Handlers

AbstractQueryHandler and AbstractSendCommandHandler

Offers a base implementation for asserting a supported Command/Query given to the handle method and base prepare method, which does nothing to the query/command (as most of handlers won't need it).

It supposed to be used like follows

class MyQueryHandler extends AbstractQueryHandler
{
    public function supports(QueryInterface $query): bool
    {
        return $query->getRequestType() === ExpectedRequestClass::class;
    }

    public function handle(QueryInterface $query, OnSuccessInterface $onSuccess, OnErrorInterface $onError): void
    {
        if (!$this->assertIsSupported(ExpectedRequestClass::class, $query, $onError)) {
            return;
        }

        try {
            $response = ...;    // handle query/command ...
            $onSuccess($response);
        } catch (\Throwable $e) {
            $onError($e);
        }
    }
}

Decoders

CallbackResponseDecoder

If you need a quick way of decoding a response, you can use this Callback Response Decoder, which allows you to pass a function to do decoding.

$decoder = new CallbackResponseDecoder(
    fn (string $response, $initiator) => is_string($response),
    fn (string $response) => sprintf('decoded:%s', $response),
);

JsonResponseDecoder

It decodes a string which contains a json into decoded array.

Other CQRS Libraries