jorge07 / symfony-6-es-cqrs-boilerplate

Symfony 6 DDD ES CQRS backend boilerplate.
MIT License
1.07k stars 187 forks source link

Symfony commands to generate Event/Command/Handlers/ProjectionView #87

Closed blixit closed 5 years ago

blixit commented 5 years ago

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

  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
<?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']);
    }
}

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 ?
jorge07 commented 5 years ago

About the first one. Yes, single responsibility has this kind of thing when you want to persist and expose data and decouple a lot. About the second one. Maybe the symfony serializer component can help with this by having the configuration in yaml files in infrastructure so you don't need to deal with it. Something like: https://github.com/jorge07/ddd-playground/blob/master/src/Infrastructure/WalletBundle/Resources/config/serializer/wallet/Model.Wallet.yml Problem is that this intefaces are required by broadway, something I'm not very happy with.

blixit commented 5 years ago

cool ! (about the 1st one) should I close this issue or let it opened ?

... by having the configuration in yaml files in infrastructure ...

This is exactly what i'm trying to not have. I know my code will grow fast so I dont want lose my mind with config files. I'm looking for the best way to reduce maintenance operations. Thanks for your response