dahromy / symfony-hexagonal-architecture

Example of a Symfony application using Domain-Driven Design (DDD) and Test Driver Development (TDD) principes keeping the code as simple as possible.
63 stars 16 forks source link

Issue in hexagonal implementation for the controller. #3

Open ArensMyzyri opened 1 year ago

ArensMyzyri commented 1 year ago

Hey there. In the UI web controller you are using symfony http response which goes against hexagonal architecture. I would suggest to build a service in infrastructure where you implement the http response and then use its interface on your controller. Example: In Domain create MyHttpResponseInterface and implement it in MySymfonyHttpResponse class (in infrastructure), then use this interface in your controller.

Here an example how I built a controller:

<?php

declare(strict_types=1);

namespace App\Product\Infrastructure\Controller;

use App\Product\Application\Query\GetProductQuery;
use App\Product\Domain\Dto\Product\ProductDto;
use App\Product\Domain\Dto\Uuid;
use App\Product\Domain\Exception\DomainExceptionInterface;
use App\Product\Domain\NormalizerAndSerializer\ModelNormalizerInterface;
use App\Product\Domain\Validator\ModelValidatorInterface;
use App\Product\Domain\Validator\JsonValidatorInterface;
use App\Shared\Domain\Bus\Query\QueryBus;
use App\Shared\Domain\Http\HttpResponseFactoryInterface;
use App\Shared\Domain\Http\HttpResponseInterface;
use App\Shared\Domain\Http\RequestInterface;
use App\Shared\Domain\Http\ResponseDataBuilderInterface;

class ProductController
{
    public function __construct(
        readonly HttpResponseFactoryInterface $httpResponseFactory,
        readonly ResponseDataBuilderInterface $responseDataBuilder,
        readonly QueryBus $queryBus,
        readonly ModelValidatorInterface $modelValidator,
        readonly ModelNormalizerInterface $normalizer,
        readonly JsonValidatorInterface $jsonValidator
    ) {}

    public function getSingle(string $productId, ?string $resourceType = null): HttpResponseInterface
    {
        try {
            $errors = $this->modelValidator->validate([new Uuid($productId)]);

            if ($errors) {
                throw new DomainBadRequestException($errors);
            }

            /** @var ProductDto $product */
            $product = $this->queryBus->ask(new GetProductQuery($productId));

            if (!$product) {
                throw new DomainNotFoundException([]);
            }
            return $this->httpResponseFactory->create(
                $this->responseDataBuilder->build($product, false, ''),
                200,
                $resourceType
            );
        } catch (DomainExceptionInterface $domainException) {
            return $this->httpResponseFactory->create(
                $this->responseDataBuilder->build($domainException->getData(), true, $domainException->getMessage()),
                $domainException->getStatusCode(),
                $resourceType
            );
        }
    }
    }

Let me know what you think :)

ArensMyzyri commented 1 year ago

The implementation of the service looks like:

<?php

declare(strict_types=1);

namespace App\Shared\Infrastructure\Http;

use App\Product\Domain\Dto\DtoInterface;
use App\Product\Domain\NormalizerAndSerializer\ModelSerializerServiceInterface;
use App\Shared\Domain\Http\HttpResponseInterface;
use Symfony\Component\HttpFoundation\Response;

class HttpJsonResponse extends Response implements HttpResponseInterface
{
    public function __construct(readonly ModelSerializerServiceInterface $serializer, readonly  DtoInterface $data, readonly  int $statusCode)
    {
        parent::__construct($this->serializer->serializeToJson($data), $statusCode, ['Content-Type' => 'application/json;charset=UTF-8']);
    }
}

As you see only here I use symfony Http Response