luddinus's avatar

DTO, pass the user (or user id?) or not

Hello,

I have some doubts on how to make this approach.

Imagine a blog like an user create some post.

PostsController

public function store()
{
   // validate the request...

   $post = app(CreatePost::class)->execute(auth()->user(), PostDTO:fromRequest());

   return response()->json(...);
}

PostDTO

class PostDTO
{
   public function __construct(
      public readonly ?int $userId = null, // omit this because it does not "belongs" here
      public readonly string $title,
      public readonly string $body,
      // ...
   )
}

CreatePostAction

class CreatePostAction
{
   public function execute(User $user, PostDTO $dto)
   {
      return $user->posts()->create(['title' => $dto->title, 'body' => $dto->body]);
   }
}

My question is: would you pass the user (or the user Id) to the DTO or not?

0 likes
11 replies
rodrigo.pedra's avatar

If you are going to use the current user id to save it on the database: yes.

In your case, as you already send the user instance as an argument to the CreatePostAction's execute method, and ends not using the $dto->userId property: no.

Snapey's avatar

imo. Your DTO should work in all contexts so not just for authenticated web requests by the author. in this respect I would pass the Author into the dto

2 likes
luddinus's avatar

@rodrigo.pedra @snapey Thanks for the responses.

Not all the times I guess, but usually a dto "matches" the columns of the database table (with the relationships in some cases, like an array of ids as a parameter). But it feels "weird" to have almost all the actions with only one parameter (the DTO).

1 like
rodrigo.pedra's avatar

@luddinus you're welcome!

It is true that for most crud-y actions you are going to end with few parameters, most of the times just the DTO.

Well, I think you get used to it.

It is the same thing as passing an object around in javaScript to avoid listing each parameter individually.

But the advantage is type checking and grouping related data together.

The DTO does not need to match a database table column-by-column.

Your database table is a sort of storage, the idea of abstracting into entities, DTO, actions, repositories, is to decouple your business logic from implementation details, such where you store your data.

1 like
bugsysha's avatar

Always use objects. Less prone to mistakes if you type hint. And usually optional parameters should be declared last.

One note that I personally use: To me, app(CreatePost::class) doesn't read well. For that reason, I use resolve(CreatePost::class) since it is far more readable even though it is longer.

luddinus's avatar

@bugsysha

But in the action I already the user "model/object" as a function parameter. Let's take another example, like create a comment.

(an user posts a comment)

// action class
public function execute(User $user, Post $post, CommentData $data)
{
    // create the commet in the database, notify etc.
}
class CommenData
{
   // without user_id and post_id
   public function __construct(
      public readonly string $body
   ) {}

   // or with "all" the data
   public function __construct(
      public readonly int $user_id,
      public readonly int $post_id,
      public readonly string $body
   ) {}
}

(using "app" or "resolve" it's just an example, probably I would inject into the controller method)

Probably, for my understanding of the DTOs, the main benefit is that I will not have a "plain" parameter like "array $data" that I don't know what contains. As I see, in this last example, the user_id and the post_id are not part of the DTO because I prefer to pass them to as a paremeter to the Action method; however, its true that a comment without a post or without a user makes no sense... so in this case the action method would be like this:

public function execute(CommentData $data)
{
    $comment = User::find($data->user_id)->comments()->create(['post_id' => $data->post_id]);

     // ...
}
bugsysha's avatar

@luddinus even in that other example, I pass objects for the same reasons. It is passed by reference and it doesn't impact performance. It gives me greater confidence that the passed object is an appropriate argument and makes tests more understandable because it is not polluted by scalar types (magic numbers, strings, etc).

Correct, dependency injection is the best approach.

Yes, the problem with arrays is that the structure and data are not guaranteed. No way you can force that. Also, autocompletion is missing. All those things make code more prone to human error.

Execute method in the last example makes sense, but the problem it introduces is unnecessary queries. You will have a User object in the request, and you are doing another query to get the same data you already have. I remember on one app I've managed to get the response time from 40 seconds to under a second by just cleaning up duplicate queries. So it is obvious that it does matter.

luddinus's avatar

@bugsysha I don't understand. In both cases I pass objects to the execute methods. You mean the "user_id" and "post_id" of the DTO?

bugsysha's avatar

@luddinus on DTO you have int $user_id and int $post_id. Those are not objects.

1 like
luddinus's avatar

@bugsysha Ok, I get it. And what about with thinks like "string $body"? You would use ValueObjects...?

bugsysha's avatar

@luddinus one value object is enough per use case. For every CRUD action create one and you will have a cleaner code. Try to keep things DRY.

Please or to participate in this conversation.