2

I'm refactoring a common web app (not spa but traditional server side html rendering). The design/code organization follows more or the less the Clean architecture concepts (ie: controller calls a use case layer then pass the UC response to a presentation layer). The technical environment is PHP/Symfony 6.

about vocabulary: in my environment uses cases are called services and presenter are called view but they covers the same responsibilities than their clean arch. equivalent.

Basically my controllers actions looks like this one:


    /**
     * @throws InternalErrorException
     */
    #[Route('/', name: 'item.front.new')]
    public function newItem(): Response
    {
        $response = $this->service->newItem();
        return $this->view->newItem($response);
    }

A service method is called then the service response object is passed to a View object (really its the presenter object which is called view here, the real view rendering is handled with Twig templates).

Based on this, i'm struggling to find were the handling of HTTP request inputs must be placed, in case of a form data submission for example. I've cases where i need to make some formating, data normalization etc. of the HTTP request inputs and build a Request object that the service expect as input, in other words: DTO stuff from HTTP Request to Service Request.

As of now i place this logic in the controller directly but something is saying to me this logic should not belong to the controller but in the presenter layer..

Here is what i do today (example of handling input from form submission before calling the service):


    /**
     * @throws Exception
     */
    #[Route('/item-update', name: 'item.front.update', methods: 'POST')]
    public function updateItem(Request $request): Response
    {
        $values = $request->request->all('item_form');
        if (empty($values)) {
            throw new BadRequestException('item_form values not found in request');
        }
    $updateItemRequest = new UpdateItemRequest();
    $updateItemRequest->title = $values['title'] ?? null;
    $updateItemRequest->qty = intval($values['qty']);
    [ ... more mapping stuff ... ]

    $response = $this->service->updateItem($updateItemRequest);
    return $this->view->updateItem($response);
}

From what i understand of the clean arch., I/Os from the "outside" should be handled by the presentation layer.. but because the presenter has to be called after the service it has no chances to take care of this responsibility.

Any advices ?

sgt-hartman
  • 149
  • 4

2 Answers2

2

From what i understand of the clean arch., I/Os from the "outside" should be handled by the presentation layer.. but because the presenter has to be called after the service it has no chances to take care of this responsibility.

Any advices ?

Well, nobody said that the presentation layer must consist of a single class, or that you can only call a single method from a class in the presentation layer.

Keep the code at the same level of abstraction. If you want to handle inputs in the presentation layer, then transform this

    /**
     * @throws Exception
     */
    #[Route('/item-update', name: 'item.front.update', methods: 'POST')]
    public function updateItem(Request $request): Response
    {
        $values = $request->request->all('item_form');
        if (empty($values)) {
            throw new BadRequestException('item_form values not found in request');
        }
    $updateItemRequest = new UpdateItemRequest();
    $updateItemRequest->title = $values['title'] ?? null;
    $updateItemRequest->qty = intval($values['qty']);
    [ ... more mapping stuff ... ]

    $response = $this->service->updateItem($updateItemRequest);
    return $this->view->updateItem($response);
}

into this

    /**
     * @throws Exception
     */
    #[Route('/item-update', name: 'item.front.update', methods: 'POST')]
    public function updateItem(Request $request): Response
    {
        $updateItemParams = $this->whatever->interpretUpdateItemRequest($request);
        $response = $this->service->updateItem($updateItemParams);
        return $this->view->updateItem($response);
    }

or some such thing, where whatever in the first line is your class in the presentation layer (might be view, might be something else).

Note that it now reads almost like a list of bullet points of tasks that are at the same "level":

  • interpret the UpdateItem request to obtain update parameters
  • ask the service to update the item
  • pass the response to the view

In the original code, the last two bullet points were preceded by a whole bunch of fine-grained operations. The updateItem endpoint doesn't need to know details like where to get the title from, or what to do if it is null, or that qty needs to be converted from a string, etc.

What I've described here is basically the "Extract Method" refactoring (you take a block of related code and extract it into a new method, turning the block into a named higher-level operation with well-defined inputs and outputs), followed by moving that method to a (new or existing) class.

You can also wrap this into a try-catch block, or alternatively return a more complex type from interpretUpdateItemRequest that indicates some sort of a status, then make a decision based on that.

1

Yes, If you had a more complex presentation layer like in a SPA, you could imagine that it would probably consume a client library which would handle the serialisation to HTTP and back when making calls to the backend.

The client and request/response models would be on an 'inner' circle and thus fine for the UI to reference and consume.

However, With your 'old skool' render html on server style its hard to cleanly separate out this serialisation code. Its built into the frameworks you are using, which expose the HTTP objects in the controllers.

I think its fine to put this logic in the controllers. I would try to minimise these request DTOs as much as possible and just send form or query data, which can be passed as primitives to your service layer. It cuts down on a lot of single use objects.

Ewan
  • 83,178