Giter Site home page Giter Site logo

jorge07 / symfony-6-es-cqrs-boilerplate Goto Github PK

View Code? Open in Web Editor NEW
1.1K 51.0 184.0 5.69 MB

Symfony 6 DDD ES CQRS backend boilerplate.

License: MIT License

PHP 91.83% Makefile 2.29% Dockerfile 1.15% Twig 3.88% Mustache 0.85%
symfony event-sourcing cqrs cqrs-es ddd symfony-flex backend docker hacktoberfest

symfony-6-es-cqrs-boilerplate's Introduction

Symfony 6 ES CQRS Boilerplate

All Contributors

A boilerplate for DDD, CQRS, Event Sourcing applications using Symfony as framework and running with PHP 8.

push

This is a long living repository that started at v4 and was upgraded to each mayor since then. You can find v4 and v5 versions at the following branches:

symfony-5 branch

symfony-4 branch

Documentation

Buses

Creating an Application Use Case

Adding Projections

Async executions

UI workflow

Xdebug configuration

Kubernetes Deployment

Architecture

Architecture

Implementations

  • Environment in Docker
  • Symfony Messenger
  • Event Store
  • Read Model
  • Async Event subscribers
  • Rest API
  • Web UI (A Terrible UX/UI)
  • Event Store Rest API
  • Swagger API Doc

Use Cases

User

  • Sign up
  • Change Email
  • Sign in
  • Logout

API Doc

Stack

  • PHP 8+
  • Mysql 8.0
  • Elastic & Kibana 7.11.0
  • RabbitMQ 3

Project Setup

Action Command
Setup make start
Run Tests make phpunit
Static Analisys make style
Code Style make cs
Code style check make cs-check
PHP Shell make s=php sh
Xdebug make xoff/xon
Build Artifacts make artifact

PHPStorm integration

PHPSTORM has native integration with Docker compose. That's nice but will stop your php container after run the test scenario. That's not nice when using fpm. A solution could be use another container just for that purpose but is way slower and I don't want. For that reason I use ssh connection.

IMPORTANT

ssh in the container it's ONLY for that reason and ONLY in the DEV TAG, if you've ssh installed in your production container, you're doing it wrong...*

Click here for the detailed instructions about how to setup the PHP remote interpreter in PHPStorm.

If you're already familiar with it, here a quick configuration reference:

Host Direction
Docker 4 mac localhost
Dinghy $ dinghy ip

Port: 2323

Filesystem mapping: {PROJECT_PATH} -> /app

Xdebug

To ease your development process, you can use Xdebug with PHPSTORM.

  1. Add a Docker interpreter

    Docker PHP interpreter

  2. Enable Xdebug listenning. Don't forget to also activate Xdebug helper from your browser.

    Xdebug activation

    Additionally, you can check Break at first line in PHP scripts to ensure your debug is working.

  3. Make a request from you API at http://127.0.0.1/api/doc for example. You should see this popup:

    Xdebug mapping

    Click on Accept and you should be ready to debug ! Start placing breakpoints on your code and enjoy debugging !

Note for Windows users:

You might need to update docker-os= to docker-os=windows in Makefile or specify its value on command line like $ make start docker-os=windows.

Contributors โœจ

Thanks goes to these wonderful people (emoji key):


Luis

๐Ÿ’ป

Kajetan

๐Ÿ’ป

Krzysztof Kowalski

๐Ÿ’ป

Patryk Woziล„ski

๐Ÿ’ป

jon-ht

๐Ÿ’ป

This project follows the all-contributors specification. Contributions of any kind welcome!

symfony-6-es-cqrs-boilerplate's People

Contributors

aliance avatar allcontributors[bot] avatar andrew-demb avatar cobak78 avatar cv65kr avatar dependabot[bot] avatar ipehov avatar jon-ht avatar jorge07 avatar kowalk avatar lutacon avatar n-e-m-a-nj-a avatar pascal08 avatar patrykwozinski avatar shuraknows avatar warguns avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

symfony-6-es-cqrs-boilerplate's Issues

Building prod image

When I'm building the production image using docker-compose -f docker-compose.prod.yml build

I get the following error :

!!  PHP Fatal error:  Uncaught Error: Class 'Symfony\Bundle\TwigBundle\TwigBundle' not found in /build/src/Kernel.php:34
.....
ERROR: Service 'php' failed to build: The command '/bin/sh -c composer run-script post-install-cmd' returned a non-zero code: 255

Did you have this issue?

Event-replay

Add CLI command for triggering an event-replay.

Features:

  • Filter by event type
  • Load by aggregate ID
  • Load by aggregate ID from playhead
  • Load by aggregate ID from playhead to playhead

Symfony commands to generate Event/Command/Handlers/ProjectionView

I played a little bit with your project the last days and I really like it.
I just found 2 anoying points:

  • the number of files to write before to get a visible result
  • the number of lines for serialization/deserialization (in projection views or events)
  1. Regarding the first issue
    I know that DDD requires use of interface to avoid coupling between application and infra. So do you plan to add some commands to make it easy to create simple classes automatically ? I mean events, commands/handlers, query/handlers, projectionview, ...
    Or do you have some tricks to facilitate the transition from REST to ES-CQRS ?

  2. About the 2nd point, I find my own way to avoid to write many lines. I've created a dynamic serializer that only de/serializes required fields. It also comes with many traits and heavy classes. I'm not sure it's the best way, but I saved many useless lines and improved my productivity.

  • My aggregate:
class RecipientApplication extends EventSourcedAggregateRoot
{
    use Timeable;
    use HasIdentity;
    use HasName;
    use HasOwner;
...
  • My event (the projection view has the same look):
<?php

declare(strict_types=1);

namespace App\Domain\SmsBusiness\RecipientApplication\Event;

use App\Domain\Shared\Parts\DynamicSerializer;
use App\Domain\Shared\Parts\HasIdentity;
use App\Domain\Shared\Parts\HasName;
use App\Domain\Shared\Parts\HasOwner;
use App\Domain\Shared\Timeable\Timeable;
use App\Domain\SmsBusiness\RecipientApplication\ValueObject\ApplicationName;
use Broadway\Serializer\Serializable;
use DateTime;
use Ramsey\Uuid\UuidInterface;

final class RecipientApplicationWasCreated implements Serializable
{
    use Timeable;
    use HasIdentity;
    use HasName;
    use HasOwner;

    public function __construct(
        UuidInterface $uuid,
        ApplicationName $name,
        UuidInterface $ownerId,
        DateTime $dateTime
    ) {
        $this->uuid      = $uuid;
        $this->name      = $name;
        $this->ownerId   = $ownerId;
        $this->createdAt = $dateTime;
    }

    /**
     * @return mixed The object instance
     */
    public static function deserialize(array $data)
    {
        return DynamicSerializer::deserialize(
            $data,
            self::class,
            ['uuid', 'name', 'ownerId', 'createdAt']
        );
    }

    /**
     * @return array
     */
    public function serialize() : array
    {
        return DynamicSerializer::serialize($this, ['uuid', 'name', 'ownerId', 'createdAt']);
    }
}
  • a trait:
<?php

declare(strict_types=1);

namespace App\Domain\Shared\Parts;

use Assert\Assertion;
use Ramsey\Uuid\Uuid;
use Ramsey\Uuid\UuidInterface;

trait HasOwner
{
    /** @var UuidInterface $ownerId */
    private $ownerId;

    public function ownerId() : ?string
    {
        return $this->ownerId->toString();
    }

    public function getOwnerId() : UuidInterface
    {
        return $this->ownerId;
    }

    public function setOwnerId(UuidInterface $ownerId) : void
    {
        $this->ownerId = $ownerId;
    }

    /**
     * @param mixed $instance
     */
    public static function serializeOwnerId($instance) : string
    {
        return $instance->ownerId();
    }

    /**
     * @param array $data
     */
    public static function deserializeOwnerId(array $data) : UuidInterface
    {
        Assertion::keyExists($data, 'ownerId');
        return Uuid::fromString($data['ownerId']);
    }
}
  • dynamic serializer:
<?php

declare(strict_types=1);

namespace App\Domain\Shared\Parts;

use Assert\Assertion;
use ReflectionClass;
use function call_user_func;
use function ucfirst;

class DynamicSerializer
{
    /** @var string[]ย */
    private static $serializers = [
        'uuid'      => 'serializeUuid',
        'name'      => 'serializeName',
        'ownerId'   => 'serializeOwnerId',
        'createdAt' => 'serializeCreatedAt',
        'updatedAt' => 'serializeUpdatedAt',
    ];

    /**
     * @param array $fields
     * @return mixed
     */
    public static function deserialize(array $data, string $instanceClass, array $fields)
    {
        $reflection = new ReflectionClass($instanceClass);
        $instance   = $reflection->newInstanceWithoutConstructor();

        $array = [];
        foreach ($fields as $field) {
            Assertion::keyExists(
                self::$serializers,
                $field,
                'Deserializer not found for this field: ' . $field
            );

            $deserializer = self::getDeserializer($field);
            Assertion::true(
                $reflection->hasMethod($deserializer)
            );

            $setter = 'set' . ucfirst($field);
            $value  = call_user_func(
                [$reflection->getName(), $deserializer],
                $data
            );
            call_user_func([$instance, $setter], $value);
        }

        return $instance;
    }

    public static function serialize($instance, array $fields) : array
    {
        $reflection = new ReflectionClass($instance);

        $array = [];
        foreach ($fields as $field) {
            Assertion::keyExists(
                self::$serializers,
                $field,
                'Serializer not found for this field: ' . $field
            );

            $serializer = self::getSerializer($field);
            Assertion::true(
                $reflection->hasMethod($serializer)
            );
            $array[$field] = call_user_func(
                [$reflection->getName(), $serializer],
                $instance
            );
        }
        return $array;
    }

    private static function getSerializer(string $field) : string
    {
        return self::$serializers[$field];
    }

    private static function getDeserializer(string $field) : string
    {
        return 'de' . self::getSerializer($field);
    }
}

What do you think about that ? do you have some suggestions ?

Related objects

Hi,
I have some problems with save and fetch object with related field, but some example:

  1. User
  2. Stream

Stream has owner User, so mapping:

stream

    <entity name="App\Infrastructure\Stream\Query\Projections\StreamView" table="streams">
        <id name="uuid" type="uuid_binary" column="uuid"/>
        (...)
        <many-to-one field="user" target-entity="App\Infrastructure\User\Query\Projections\UserView" inversed-by="streams">
            <join-column nullable="false" referenced-column-name="uuid" />
        </many-to-one>
    </entity>

user

    <entity name="App\Infrastructure\User\Query\Projections\UserView" table="users">
        <id name="uuid" type="uuid_binary" column="uuid"/>
        (...)
        <one-to-many field="streams" target-entity="App\Infrastructure\Stream\Query\Projections\StreamView" mapped-by="user">
        </one-to-many>
    </entity>

I want to create new stream, related to User. So, I create CreateStreamCommand and Handler, like this:

    public function __invoke(CreateStreamCommand $command): void
    {
        $stream = $this->streamFactory->register($command->uuid, $command->user, $command->parameters);

        $this->streamRepository->store($stream);
    }

    public function __construct(StreamFactory $streamFactory, StreamRepositoryInterface $streamRepository)
    {
        $this->streamFactory    = $streamFactory;
        $this->streamRepository = $streamRepository;
    }

register method from StreamFactory

    public function register(UuidInterface $uuid, UserViewInterface $user, Parameters $parameters): Stream
    {
        return Stream::create($uuid, $user, $parameters);
    }

create method from Stream

    public static function create(UuidInterface $uuid, UserViewInterface $user, Parameters $parameters): self
    {
        $stream = new self();

        $stream->apply(new StreamWasCreated($uuid, $user, $parameters));

        return $stream;
    }

And StreamWasCreated Event

    public function __construct(UuidInterface $uuid, UserViewInterface $user, Parameters $parameters)
    {
        $this->uuid         = $uuid;
        $this->user         = $user;
        $this->parameters   = $parameters;
    }

    /**
     * @throws \Assert\AssertionFailedException
     */
    public static function deserialize(array $data): self
    {
        Assertion::keyExists($data, 'uuid');
        Assertion::keyExists($data, 'user');
        Assertion::keyExists($data, 'parameters');

        return new self(
            Uuid::fromString($data['uuid']),
            $data['user'],
            Parameters::fromArray($data['parameters'])
        );
    }

    public function serialize(): array
    {
        return [
            'uuid'          => $this->uuid->toString(),
            'user'          => $this->user,
            'parameters'    => $this->parameters->toArray()
        ];
    }

At this step I don't serialize user, and everything is fine. But... When I want to get this Stream record, I see error:

PHP Fatal error:  Uncaught Symfony\Component\Debug\Exception\FatalThrowableError: Argument 2 passed to App\Domain\Stream\Event\StreamWasCreated::__construct() must implement interface App\Domain\User\Query\Projections\UserViewInterface, array given, called in /app/src/Domain/Stream/Event/StreamWasCreated.php on line 51 in /app/src/Domain/Stream/Event/StreamWasCreated.php:32
Stack trace:
#0 /app/src/Domain/Stream/Event/StreamWasCreated.php(51): App\Domain\Stream\Event\StreamWasCreated->__construct(Object(Ramsey\Uuid\Uuid), Array, Object(App\Domain\Stream\ValueObject\Parameters))
#1 /app/vendor/broadway/broadway/src/Broadway/Serializer/SimpleInterfaceSerializer.php(58): App\Domain\Stream\Event\StreamWasCreated::deserialize(Array)
#2 /app/vendor/broadway/event-store-dbal/src/DBALEventStore.php(234): Broadway\Serializer\SimpleInterfaceSerializer->deserialize(Array)
#3 /app/vendor/broadway/event-store-dbal/src/DBALEventStore.php(93): Broadway\EventStore\Dbal\DBALEventStore->deserializeEvent(Array)
#4 /app/vendor/broadway/broadway/s in /app/src/Domain/Stream/Event/StreamWasCreated.php on line 32

I checked second argument, and it is empty array. So I think, problem is in unserialize event data.

So my second try was serialize User object, but then I can't save Stream object, because doctrine thinks user is new object and try to cascade persist.

What is the propper way to work with relationship?

Sorry my english ;)

UserReadProjectionFactory

Hi,

I have a question and in your case i don't understand the usage of class UserReadProjectionFactory.
It seems like it is never called. What's the point of this Factory?

In the query handler "FindByEmailHandler" you inject directly MysqlUserReadModelRepository.
But in my point is heavily coupled and when we want to change the way we get the data (Doctrine, Redis, etc) we must change this class.

Why don't we inject the factory here? Where the factory will handle the way to retrieve the data and then only working on our Infrastructure instead of working on Infrastructure and Application?

May be there's something here i don't understand well..

Thank you

Item and Collection class

Hi, i don't understand the purpose of Item class and Collection class. Why using this classes instead of passing directly the data from infrastructure to application ui, it's like you guys using this as middleware to have a presentable data but i don't get it, i mean it doesn't make sense.

Can someone explain the reason please?

Thank you

make style returns 5 errors

Hi,
My experience with event-sourcing, CQRS and DDDare pretty new so please be nice if my questions are stupid.

I take time to dig into your boilerplate and I run into the command make style. It comes up with those errors :

docker-compose exec php sh -lc './vendor/bin/phpstan analyse -l 5 -c phpstan.neon src tests'
 85/85 [โ–“โ–“โ–“โ–“โ–“โ–“โ–“โ–“โ–“โ–“โ–“โ–“โ–“โ–“โ–“โ–“โ–“โ–“โ–“โ–“โ–“โ–“โ–“โ–“โ–“โ–“โ–“โ–“] 100%

 ------ ------------------------------------------------------------------------- 
  Line   src/Infrastructure/User/Auth/Guard/LoginAuthenticator.php                
 ------ ------------------------------------------------------------------------- 
  163    Property App\Infrastructure\User\Auth\Guard\LoginAuthenticator::$router  
         (Symfony\Bundle\FrameworkBundle\Routing\Router) does not accept          
         Symfony\Component\Routing\Generator\UrlGeneratorInterface.               
 ------ ------------------------------------------------------------------------- 

 ------ --------------------------------------------------------------- 
  Line   src/Infrastructure/User/Query/UserReadProjectionFactory.php    
 ------ --------------------------------------------------------------- 
  26     Property App\Domain\User\ValueObject\Auth\Credentials::$email  
         (App\Domain\User\ValueObject\Email) does not accept string.    
 ------ --------------------------------------------------------------- 

 ------ ----------------------------------------------------------------- 
  Line   src/Infrastructure/Share/Query/Repository/ElasticRepository.php  
 ------ ----------------------------------------------------------------- 
  58     Array (array<string>) does not accept int.                       
 ------ ----------------------------------------------------------------- 

 ------ ------------------------------------------------------------ 
  Line   tests/UI/Http/Web/Controller/ProfileControllerTest.php      
 ------ ------------------------------------------------------------ 
  27     Call to an undefined method                                 
         Symfony\Component\HttpFoundation\Response::getTargetUrl().  
 ------ ------------------------------------------------------------ 

 ------ ------------------------------------------------------------------ 
  Line   tests/UI/Http/Rest/Controller/Events/GetEventsControllerTest.php  
 ------ ------------------------------------------------------------------ 
  37     Casting to string something that's already string.                
 ------ ------------------------------------------------------------------ 

                                                                                
 [ERROR] Found 5 errors                                                         
                                                                                

make: *** [style] Error 1

Are configuration errors on my laptotp ? Something else ?

For now I did'nt do anything, just installed your boilerplate

How to get started?

This project looks very promising. But sorry, I feel a little bit lost. How to get this installed correctly as dependency?

I tried to install it via composer:

composer.json:

{
    "repositories": [
        {
            "type": "vcs",
            "url": "https://github.com/jorge07/symfony-4-es-cqrs-boilerplate"
        }
    ],
    "require": {
        "jorge07/symfony-4-es-cqrs-boilerplate": "dev-master"
    }
}

Install via Composer:

$ composer  install
Loading composer repositories with package information
Updating dependencies (including require-dev)                                       
Your requirements could not be resolved to an installable set of packages.

  Problem 1
    - Installation request for jorge07/symfony-4-es-cqrs-boilerplate dev-master -> satisfiable by jorge07/symfony-4-es-cqrs-boilerplate[dev-master].
    - jorge07/symfony-4-es-cqrs-boilerplate dev-master requires symfony/lts ^4@dev -> satisfiable by symfony/lts[4.x-dev] but these conflict with your requirements or minimum-stability.

type mismatch when adding new field

I was learning more about CQRS and when learning added a name field and a NameChangeCommand, I can create a user with a name but using the CLI command we get the following error, if you could provide some insight as to why or when this is happening i would be very grateful. If you require any further information I can provide the files i have edited

In ReflectionEmbeddedProperty.php line 81:

Warning: ReflectionProperty::getValue() expects parameter 1 to be object, string given

No order property in entities

Every entity (special with no sequence ids) should have created time or smth else to sort them in lists or collections.

Question about Doctrine migrations

I'm new to DDD/Cqrs. I'm reading many differents implementations (prooph by example) and I'm searching the one will match the way I work. I tried to write my own but it requires so much work.

Actually I need to understand how you handle migrations. (I will probably ask more questions later).
I didn't find any doctrine entities. I guess you hard write your migrations. If it's true is it a good a practice ?

On clean install getting 6 errors on phpunit

On clean install i`m running phpunit tests and getting 6 errors (all about elastic docker container):

docker-compose exec php sh -lc "./bin/phpunit "
#!/usr/bin/env php
PHPUnit 6.5.14 by Sebastian Bergmann and contributors.

Testing Api Test Suite
......E................E...EEEE......................             53 / 53 (100%)

Time: 2.2 minutes, Memory: 20.00MB

There were 6 errors:

1) App\Tests\Application\Query\Event\GetEvents\GetEventsTest::processed_events_must_be_in_elastic_search
Elasticsearch\Common\Exceptions\NoNodesAvailableException: No alive nodes found in your cluster

...

2) App\Tests\Infrastructure\Share\Event\Query\EventElasticRepositoryTest::an_event_should_be_stored_in_elastic
Elasticsearch\Common\Exceptions\NoNodesAvailableException: No alive nodes found in your cluster

...

3) App\Tests\UI\Http\Rest\Controller\Events\GetEventsControllerTest::events_list_must_return_404_when_no_page_found
Elasticsearch\Common\Exceptions\NoNodesAvailableException: No alive nodes found in your cluster

...

4) App\Tests\UI\Http\Rest\Controller\Events\GetEventsControllerTest::events_should_be_present_in_elastic_search
Elasticsearch\Common\Exceptions\NoNodesAvailableException: No alive nodes found in your cluster

...

5) App\Tests\UI\Http\Rest\Controller\Events\GetEventsControllerTest::given_invalid_page_returns_400_status
Elasticsearch\Common\Exceptions\NoNodesAvailableException: No alive nodes found in your cluster

...

6) App\Tests\UI\Http\Rest\Controller\Events\GetEventsControllerTest::given_invalid_limit_returns_400_status
Elasticsearch\Common\Exceptions\NoNodesAvailableException: No alive nodes found in your cluster

...

ERRORS!
Tests: 53, Assertions: 92, Errors: 6.

Generating code coverage report in HTML format ... done

Legacy deprecation notices (1)
make: *** [phpunit] Error 2

Question: How to run the deployed artifact?

Hi @jorge07 ,

Me again :)

I was wondering how you run your deployed artifact as showed in this repository? I ask because nginx is not pre-packaged in the image. So I assume the only way currently is to mount the volume of the artifact into a nginx configuration?

This would be how a docker-compose would look like to run the artifact?

# non-tested simplified example

app:
  image: jorge07/cqrs

web:
  image: nginx
  volumes_from: app  

Wouldn't it be better to build an image with nginx incorporated and have then 2 images, e.g:

jorge07/cqrs-cli => would be used to run consumers or other commands
jorge07/cqrs-http => would be used for the web/api

Unless you do this differently? But then I'm very eager to know how ;)

Thanks in advance

Cheers

Question about value objects

IMHO value objects should carry more complex business logic than validation rules. Wrapping every scalar value in own class complicates simple things. The best way i think to use private setters with assertions inside.

ElasticSearch index is not populated

Hi everyone,

Everything is working great except i have nothing in ElasticSearch.

Well no index is created. Isn't it supposed to create/populate the index when you post a new user ? (as defined in the workflow doc)

Sorry if i missed something in the doc...

Cheers

Stateless API authentication

I think API authentication is a really nice feature to have included in this boilerplate.

This is what I had in mind:

  • A new endpoint for authentication
  • The endpoint returns a token that can be used for successive requests requiring the user to be authenticated

Regarding some people a command bus should not return a value in CQRS context. I think it would be complicated to return the token to the user without returning it directly from the command handler. This would be a worth a discussion I guess.

If we make use of, for example: LexikJWTAuthenticationBundle, then we need to create an abstraction to be able to create token using the User aggregate root (this bundle requires us to pass Symfony\Component\Security\Core\User\UserInterface).

I would like to hear your opinions about this.

Async event publisher not called

I've been struggling with the AsyncEventPublisher and events been publish to RabbitMQ.

For example, I create a new user. A new event would be sent to RabbitMQ and then to Elasticsearch.
The user is created perfectly, the event stored in Mysql table but the event is not sent to Rabbit.

Looking at the symfony profiler I'm seeing that the AsyncEventPublisher is not called on kernel.terminate.

image

Did you have that problem?

Thank you for your work & time !

Question: why not update read models asynchronously?

For example, when we want to keep our read models in a different data storage than MySQL, we maybe want to update our read model outside of the request cycle that triggers the domain event(s). I might be incorrect but I think this is ultimately how CQRS should be implemented. What do you think?

Remove "abandoned" dependency: Symfony/LTS

Both branch (master/symfony-4.2-upgrade) have that package

From symfony/LTS repository:

Use symfony/flex instead and configure its extra.symfony.require setting to the version range you'd like to have.
For example, composer config extra.symfony.require "~4.1.0" restricts the versions of all Symfony components to ~4.1.0.

Event-replay, snapshotting, etc?

So far I think this repository is a great resource for developers new to CQRS/ES/DDD. I don't know you had a certain intention when you created this repository, but personally I think it would be awesome if we add features like event-replay, snapshotting and maybe even a microservice architecture example connected using RabbitMQ (or maybe Kafka).

If you agree with me I'd be glad to assist implementing some of these features. What do you think?

Command Handler: persisting data

How are data persisted in write database from command handler ?i'm a beginner with es-cqrs and it's difficult for me to understand this step!

Wrong namespace for App\Domain\User\Query ?!

Hi @jorge07 ,

First of all many thanks for taking the time to demonstrate a DDD-es-cqrs example with sf4. I use it to start up a new project, and doing ddd for some time but not sf4, it does help a lot.

However, I'm a bit sceptical about the location of the classes in App\Domain\User\Query ... Imho they should not be in the Domain namespace. Generally what we will get for the "Query" part are projections and only data necessary for views, right?!

I guess the Read repository interface is acceptable to keep in the Domain layer, but the UserView class seems not right to me ...

What's your thought on this?

Thanks

Travis CI

Travis CI should be disable in pull request because used GitHub action.

SF 4.1 Update

I was updating to 4.1 to check compatibility. I have a few errors executing the tests.

First of all, I get :

PHP Fatal error:  Cannot redeclare static Symfony\Bundle\FrameworkBundle\Test\KernelTestCase::$container as non static App\Tests\Application\Command\ApplicationTestCase::$container in /app/tests/Application/Command/ApplicationTestCase.php on line 16

Refactoring the container property in ApplicationTestCase with another name solves the problem. After fixing that all tests passed but some Uncaught exception arise.

Testing Api Test Suite
........................2018-05-30T16:03:32+00:00 [critical] Uncaught PHP Exception Assert\InvalidArgumentException: "Not a valid email" at /app/vendor/beberlei/assert/lib/Assert/Assertion.php line 288
.2018-05-30T16:03:33+00:00 [critical] Uncaught PHP Exception Assert\InvalidArgumentException: "Not a valid email" at /app/vendor/beberlei/assert/lib/Assert/Assertion.php line 288
.2018-05-30T16:03:33+00:00 [critical] Uncaught PHP Exception App\Domain\Shared\Query\Exception\NotFoundException: "Resource not found" at /app/src/Infrastructure/Share/Query/Repository/MysqlRepository.php line 35
...2018-05-30T16:03:36+00:00 [critical] Uncaught PHP Exception Assert\InvalidArgumentException: "Password can\'t be null" at /app/vendor/beberlei/assert/lib/Assert/Assertion.php line 288
.....2018-05-30T16:03:38+00:00 [critical] Uncaught PHP Exception Symfony\Component\Security\Core\Exception\AccessDeniedException: "Access Denied." at /app/vendor/symfony/security/Http/Firewall/AccessListener.php line 68
..2018-05-30T16:03:47+00:00 [critical] Uncaught PHP Exception Symfony\Component\Security\Core\Exception\AccessDeniedException: "Access Denied." at /app/vendor/symfony/security/Http/Firewall/AccessListener.php line 68
.....                         41 / 41 (100%)

Time: 40.94 seconds, Memory: 22.00MB

OK (41 tests, 88 assertions)

This exceptions are from Api Test cases. I would check on that later and try to submit an PR if you want.

Why 'make phpunit' delete database ?

As the title expose the question :

.PHONY: phpunit
phpunit: db ## execute project unit tests
		docker-compose exec php sh -lc './bin/phpunit'
make phpunit
docker-compose exec php sh -lc './bin/phpunit'
#!/usr/bin/env php
stty: standard input
PHPUnit 6.5.6 by Sebastian Bergmann and contributors.

Testing Api Test Suite
.........................................                         41 / 41 (100%)

Time: 57.98 seconds, Memory: 22.00MB

OK (41 tests, 88 assertions)

Generating code coverage report in HTML format ... done

Why the database has to be deleted for running tests ? I tried to remove it, no errors showed up in the testsuite so I just need to understand if it's the best practice that I don't know, or something else.

Documentation

Coming from #47

Some people has requested documentation or faq about design decisions and also about how to continue adding things to the project and why that way.

So, to document:

Command, Query and Event bus

  • Why Tactician for Command/Query Bus? (A: because of the return at resolver level)
  • How to add a new use case (Command or Query)

Event Store

  • How to replay events?

Read Model

  • How to build a projection?

Async Event subscribers

  • How do Symfony/RabbitMQ/ElasticSearch work together?

Deptrac violation on FindByEmailHandler

With the addition of Deptrac to the pipeline the travis build isn't going to pass. I have the following violation when using make layer:

App\Application\Query\User\FindByEmail\FindByEmailHandler::8 must not depend on App\Infrastructure\User\Query\Projections\UserView (Application on Infrastructure)

What are the specs of your PHP7.1 image

Hi,

First of all great work, and really nice of you to share it.

I would like to know what the purpose of this image jorge07/alpine-php:7.1-dev-sf.

Can we replace by one more generic ? Is your project work the same way ?
What we need to achieve to do so ?

Thanks again for your amazing work.

phpunit droped database but not executed

When i run "make phpunit" i getting:

docker-compose exec php sh -lc './bin/console d:d:d --force'
Dropped database for connection named `api`
docker-compose exec php sh -lc './bin/console d:d:c'
Created database `api` for connection named default
docker-compose exec php sh -lc './bin/console d:m:m -n'

After that i'm getting:

  ++ 2 migrations executed
  ++ 2 sql queries
docker-compose exec php sh -lc "./bin/phpunit "
#!/usr/bin/env php
PHP Fatal error:  Uncaught RuntimeException: Could not find https://github.com/sebastianbergmann/phpunit/archive/6.5.zip in /app/vendor/symfony/phpunit-bridge/bin/simple-phpunit:99
Stack trace:
#0 /app/bin/phpunit(18): require()
#1 {main}
  thrown in /app/vendor/symfony/phpunit-bridge/bin/simple-phpunit on line 99
make: *** [phpunit] Error 255

StyleCI

I use locally PHP-CS-Fixer but we could use something like StyleCI for the repository.

If you want I can open a PR with a config file similar to your repository code.

New migrations try to remove 'events' table

(I hope this issue is not a stupid one)

When I try to compute the new migration based on a new mapping I noticed this line:
$this->addSql('DROP TABLE events');

here my migration file:

final class Version20190101165636 extends AbstractMigration
{
    public function up(Schema $schema) : void
    {
        // this up() migration is auto-generated, please modify it to your needs
        $this->abortIf($this->connection->getDatabasePlatform()->getName() !== 'mysql', 'Migration can only be executed safely on \'mysql\'.');

        $this->addSql('CREATE TABLE recipient_application (uuid BINARY(16) NOT NULL COMMENT \'(DC2Type:uuid_binary)\', name VARCHAR(255) NOT NULL, ownerId BINARY(16) NOT NULL COMMENT \'(DC2Type:uuid_binary)\', PRIMARY KEY(uuid)) DEFAULT CHARACTER SET UTF8 COLLATE UTF8_unicode_ci ENGINE = InnoDB');
        $this->addSql('DROP TABLE events');
    }

    public function down(Schema $schema) : void
    {
        // this down() migration is auto-generated, please modify it to your needs
        $this->abortIf($this->connection->getDatabasePlatform()->getName() !== 'mysql', 'Migration can only be executed safely on \'mysql\'.');

        $this->addSql('CREATE TABLE events (id INT AUTO_INCREMENT NOT NULL, uuid BINARY(16) NOT NULL, playhead INT UNSIGNED NOT NULL, payload LONGTEXT NOT NULL COLLATE utf8_unicode_ci, metadata LONGTEXT NOT NULL COLLATE utf8_unicode_ci, recorded_on VARCHAR(32) NOT NULL COLLATE utf8_unicode_ci, type VARCHAR(255) NOT NULL COLLATE utf8_unicode_ci, UNIQUE INDEX UNIQ_5387574AD17F50A634B91FA9 (uuid, playhead), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8 COLLATE utf8_unicode_ci ENGINE = InnoDB');
        $this->addSql('DROP TABLE recipient_application');
    }
}

Is it normal ? Did I do something wrong ? (probably)

the docker service (worker) is not working

I'm still getting this error:

workers_1        | rabbitmq:multiple-consumer [-m|--messages [MESSAGES]] [-r|--route [ROUTE]] [-l|--memory-limit [MEMORY-LIMIT]] [-d|--debug] [-w|--without-signals] [-h|--help] [-q|--quiet] [-v|vv|vvv|--verbose] [-V|--version] [--ansi] [--no-ansi] [-n|--no-interaction] [-e|--env ENV] [--no-debug] [--] <command> <name> [<context>]
workers_1        | 
workers_1        | 2018-12-30T10:38:00+00:00 [error] Error thrown while running command "rabbitmq:multiple-consumer events". Message: "stream_socket_client(): unable to connect to tcp://0.0.0.0:5672 (Connection refused)"
workers_1        | 
workers_1        | In StreamIO.php line 141:
workers_1        |                                                                                
workers_1        |   stream_socket_client(): unable to connect to tcp://0.0.0.0:5672 (Connection  
workers_1        |    refused)                                                                    
workers_1        |                                                                                
workers_1        | 
workers_1        | rabbitmq:multiple-consumer [-m|--messages [MESSAGES]] [-r|--route [ROUTE]] [-l|--memory-limit [MEMORY-LIMIT]] [-d|--debug] [-w|--without-signals] [-h|--help] [-q|--quiet] [-v|vv|vvv|--verbose] [-V|--version] [--ansi] [--no-ansi] [-n|--no-interaction] [-e|--env ENV] [--no-debug] [--] <command> <name> [<context>]

I tried to set many values like:

RABBITMQ_URL=amqp://guest:guest@rmq:5672 (default one)
RABBITMQ_URL=amqp://guest:[email protected]:5672
RABBITMQ_URL=amqp://guest:guest@rmq:15672
RABBITMQ_URL=amqp://guest:[email protected]:15672

but it doesnt work. Could you help ?

Should I install something else ? the documentation doesnt say anything about that.

Question: Why not replace Controllers by Actions ?

Hi,

First of all thanks for this repository, it's a great boilerplate implementing CQRS ๐Ÿ‘

I have a question about the UI especialy Controllers. I think CQRS is compatible with ADR pattern am I wrong ?
Here for exemple we could replace Controllers by Action and Response could manage the way we want to respond json, html ...

What do you think about it ?

Sign-in form returns a JSON response

When I try to log in with an invalid email (like foo), I'm getting an error "Not a valid email" but the response is in JSON

image

Do you have any idea why the AuthenticationException isn't caught by firewall or isn't a basic HttpResponse ?


Edit: I'm trying to log via http://127.0.0.1/sign-in in the web form

Update Elastic

Kibana and Elasticsearch needs an update now that 6.3 is live.

Dcoker deployment config not working

Hi @jorge07 ,

Wanted to know if you are aware the docker deployment config is not working. Several issues I encountered:

RabbitMQ env missing

Solved by adding ENV RABBITMQ_URL amqp://guest:guest@rmq:5672

Doctrine Mapping not found

Step 13/18 : RUN composer run-script post-install-cmd
 ---> Running in 94b153943c81
Do not run Composer as root/super user! See https://getcomposer.org/root for details
ocramius/package-versions:  Generating version class...
ocramius/package-versions: ...done generating version class
Executing script cache:clear [OK]
Executing script assets:install --symlink --relative public [OK]

Removing intermediate container 94b153943c81
 ---> eae8722c79cc
Step 14/18 : FROM jorge07/alpine-php:7.2
 ---> 36089338995f
Step 15/18 : WORKDIR /app
 ---> Using cache
 ---> ac4003a09ac1
Step 16/18 : ENV APP_ENV prod
 ---> Using cache
 ---> 24917303beef
Step 17/18 : COPY --from=builder /build /app
 ---> b7011ebd4c50
Step 18/18 : RUN php /app/bin/console c:w
 ---> Running in fe21b3a17f49

 // Warming up the cache for the prod environment with debug
 // false

2018-08-30T09:48:23+00:00 [error] Error thrown while running command "'c:w'". Message: "File mapping drivers must have a valid directory path, however the given path [/build/config/mapping/orm/domain] seems to be incorrect!"

My guess is that the generation is done in the "builder image" and keeps absolute paths, which is not the same when copying from /build to /app later on ...

Following changes solves the creation of the image:

FROM jorge07/alpine-php:7.2-dev-sf as builder

WORKDIR /app

ENV APP_ENV prod
ENV RABBITMQ_URL amqp://guest:guest@rmq:5672

COPY composer.json /app
COPY composer.lock /app
COPY symfony.lock /app

RUN composer install --no-ansi --no-scripts --no-dev --no-interaction --no-progress --optimize-autoloader

COPY bin /app/bin
COPY config /app/config
COPY src /app/src
COPY public /app/public

RUN composer run-script post-install-cmd

FROM jorge07/alpine-php:7.2

WORKDIR /app

ENV APP_ENV prod
ENV RABBITMQ_URL amqp://guest:guest@rmq:5672

COPY --from=builder /app /app

RUN php /app/bin/console c:w

Not sure though if previous Dockerfile worked before, if it was work in progress or which direction you wanted to go.

Cheers

Unique email validation

First of all, thank you for a great example!

We have an aggregate domain rule "user must have an unique email", but we have just one assertion in factory. This is not a defensive way. In DDD domain object is permanently valid and can be used in stand alone manner. You can use specification pattern for validating aggregate rules.

public function changeEmail(Email $email, UniqueEmailSpecification $uniqueEmailSpecification): void
{
        Assertion::notEq((string)$this->email, (string)$email, 'New email should be different');

	if (!$uniqueEmailSpecification->isSatisfiedBy($email)) {
		throw new EmailAlreadyExistException('Email already registered.');
	}

    $this->apply(new UserEmailChanged($this->uuid, $email));
}

The same in User::create

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.