There is no official "Laravel" DDD pattern, but let me try to help/guide you a little bit more just based on my experience. Again, there is no official Laravel DDD pattern or a "standard", so this is my experience and things I saw elsewhere.
Think like this:
- You receive something (input)
- You share that data (whatever it is, request, command (CLI), job, listener) with a "service" class that has the business logic you want to execute/run
- The "service" receives this info, usually by calling a method on it. Maybe you can set some data on the constructor, but most of the time you execute/call a method or static method and expect a result back
- This "service" should be as basic as possible and NOT related to anything from Laravel, like a request. Make it as decoupled from Laravel as possible (you can use collections, models, etc, just do not couple it to input/output from Laravel), you should be receiving scalar types of data (like strings, integers, floats, booleans, etc) or known objects, but not a Request, or similar, so other consumers that are not working on that context, can still use this business logic
- That result should NOT be a
true
or false
, or anything similar. The "best" case scenario would be to throw a known exception if something went wrong. Why? Because we now use try/catch
instead of $didItWork = $service->method(); if ($didItWork === false) { ... }
, that is the old way, more "basic/dumb", use an exception so you can extract and share more info about what went wrong instead of a false
or similar
- Once you get a result (nothing if it worked, because it is inside a
try/catch
or a value because it is an ID or whatever useful data, or an exception if it failed somewhere), you just process that on the executing actor (a controller, a command, a job, a listener, whoever is calling this "service"s method)
- Once processed, you should return something so the end user or consumer knows what was the final result (output)
See that I have written "service" every time, I did that because, for me, a "service" is either a ServiceProvider
or your own custom class inside \App\Services
namespace that literally wraps an API, for example, you want to communicate with a social app, with a payment gateway, with another Laravel project in another place (not this project) through REST requests or another way. In my mind, when you refer to "service" with your code, you want to say a "Domain" class (following DDD). Remember, Domain
class only has the business logic, which is why it is called "Domain".
So, to summarize, let me "refactor" your code into a more "DDD way" (what I would expect to see):
PostController
public function store(StorePostRequest $request): JsonResponse
{
DB::beginTransaction();
try {
\App\Domain\Post\Post::store(
$request->input('title'),
$request->input('description'),
$request->input('status_id'),
);
DB::commit();
return response()->json(['status' => true, __('service.the_operation_was_successful')]);
} catch (\Throwable) {
DB::rollBack();
return response()->json(['status' => false, __('service.error_occurred_during_operation')], 500);
}
}
Post
(namespace: \App\Domain\Post
, file: app\Domain\Post\Post.php
)
class Post
{
public static function store(
string $title,
string $description,
int $status_id
): void {
// You should validate, again, the input data
$post = new Post();
$post->uuid = Str::uuid();
$post->title = $request->input('title');
$post->description = $request->input('description');
$post->status_id = $request->input('status_id');
$post->created_by_user_id = Auth::id();
$this->getPostRepository->store($post);
}
protected static function getPostRepositry(): PostRepository
{
return new PostRepository;
}
}
See that I have totally decoupled the Post
(domain class) input (Post::store(title, description, status_id)
) from the controller. Now it knows nothing about a Request
, and just expects the right data. Of course, ou should (I would say you must) validate the data again.
Why double validation? Think like this, the controller is the one using the business logic, if the controller can validate the input data, then you can have better control over the possible errors returned, if everything is okay, it will validate and give the data to the domain class, that will validate again and it should also be correct.
If you later use this business logic in a command (for example), you validate or whatever in the command context, and send the data to the domain class again, and you had to change NOTHING on the business logic, so you can also test the domain class once, and then re-use it everywhere else (controller, command, job, listener, etc) and just validate that that context is working (feature test).
I am not a fan of the Repository
pattern, I do hate it, but I left it untouched here as that is my issue, not yours.
Finally, see that I have moved all the returns to the controller, as the controller is the one returning a JSON (or a response in this case), not the domain class. The domain class will just return nothing (in this case) or a validation error (if you add the validation using a Validator
) or a "general" exception if the database fails or similar (that is why I am using \Throwable
in the catch
).
You can also replace the $request->input(...)
with a known data object class (this class only holds the data that is expected, and in the domain class you asks for that data -> function store(PostData $data)
, and use that instead of $title
, $description
, and $status_id
.
I would replace int $status_id
with a literal Status
object if that is possible, so you reduce even further the need for validation (if you pass a Status
object, it is already valid (most of the time)).
Again, remember that this is my experience, but you will mostly expect to see this. There are a few more things to take into account (so you end up with better code), but that would just be too much for this.