Lead Software Engineer at Mumsnet .

Published on

Laravel DDD - Using Data Objects; Domain Commands

In our last instalment we spoke about how we can use data objects and data object factories to escape what I call “array hell”, a place where we have no idea what is in something passed to a method and there is no context or strictness about them. Now that we know how we are going to create these let’s look at a way we can use them!

Domain commands, are a term that I use to describe actionable classes that perform write operations. This is something that I borrowed from the CQRS world, but without the added complexity. The purpose of these commands are to create single classes that have one purpose and one purpose only: to write data to any external service. In our case, we are using them to write to the database.

Following on from our original concept, we are going to create a command to write a new post into the database. To do this however we need to extend our data object a little. Make the following changes so your data object looks like the below:

5class PostDataObject implements PostDataObjectContract
7 public function __construct(
8 protected readonly string $title,
9 protected readonly string $content,
10 protected readonly bool $published = false,
11 protected readonly null|Carbon $publishedAt = null,
12 ) {}
14 public function toArray(): array
15 {
16 return [
17 'title' => $this->title,
18 'content' => $this->content,
19 'published' => $this->published,
20 'published_at' => $this->publishedAt
21 ];
22 }

Using this toArray method it allows us to turn our Data Object into something that Eloquent can understand and work with. As you can also see we are adding PostDataObjectContract to to data object as an implementation, so that we can ensure we are returning the correct data objects without binding it to a specific implementation.

Building our first Command

To build our first command, we realistically want to create an interface/contract to bind our implementation to in the application. This will allow us to use dependency injection to resolve the concrete implementation out of the container, meaning that our domain code is interoperable.

Let’s start with our interface/contract, what we need is something specific for each command so that for each command has the ability to be easily overriden.

5namespace Infrastructure\Blogging\Commands;
7interface CreatePostContract
9 public function handle(PostDataObjectContract $post, int $user): null|Model;

In the above contract we are creating one method for this command called handle, and we want to pass in the implementation for the PostDataObjectContract as well as the int for the user, in this case it is the users ID that we pass in, then we expect to return either null or an Eloquent Model. Now onto our implementation itself.

Our implementation is something that is interchangeable incase we rename something like our Post model to Article or anything that could be intrusive to our code base.

5namespace Domains\Blogging\Commands;
7class CreatePostCommand implements CreatePostContract
9 public function handle(PostDataObjectContract $post, int $user): null|Model
10 {
11 return Post::query()->create(
12 attributes: array_merge(
13 $post->toArray(),
14 ['user_id' => $user]
15 ),
16 );
17 }

From the above we can see that this commands only job is to start a query builder on the Post model, then create a new instance of Post using some attributes. We know that a post is usually required to have an author, so we pass in the foreign key as an integer and array merge the data object as an array and an array that sets the user_id to the value passed in. This allows us to not only use the authenticated user but it also allows us to pass in the ID of any user that this might be created for making the command more flexible.

Binding our Command

Now that we have the interface/contract and the implementation - we need to let the Laravel container know about this so that when we try to inject using dependency injection we get the right implementation. We are able to do this inside our domain service provider using the bindings property. This is a great way to attach these into any controller or anything else. However, as we might have quite a few commands we want to register to this domain it is sensible to look at registering these within a separate service provider.

When it comes to additional service providers we have a number of options available - in terms of naming conventions. Should we create a CommandsServiceProvider inside our Blogging domain? Or should we create a PostsServiceProvider so that all bindings relating to this part of the domain is all together? My preferred way here is to use something specific to what is going to be needed, so in this case creating a PostsServiceProvider. To do this, we can create a Service Provider in src/Domains/Blogging/Providers/PostsServiceProvider.php and this should look like:

5namespace Domains\Blogging\Providers;
7use Domains\Blogging\Commands\CreatePost;
8use Domains\Blogging\DataObjects\PostDataObject;
9use Domains\Blogging\Factories\PostDataObjectFactory;
10use Infrastructure\Blogging\Commands\CreatePostContract;
11use Infrastructure\Blogging\DataObjects\PostDataObjectContract;
12use Infrastructure\Blogging\Factories\PostDataObjectFactoryContract;
14class PostsServiceProvider extends ServiceProvider
16 /**
17 * @var array<class-string,class-string>
18 */
19 public array $bindings = [
20 PostDataObjectFactoryContract::class => PostDataObjectFactory::class,
21 PostDataObjectContract::class => PostDataObject::class,
22 CreatePostContract::class => CreatePost::class,
23 ];

This is a single service provider that will let us add all the bindings for the posts in our blogging domain, as we move on we will be able to register things such as Queries and more.

Now all that is left for us to do is to let our Blogging domains Service Provider know about this additional provider, and then we can start binding to our DI container within Laravel.

5namespace Domains\Blogging\Providers;
7use Illuminate\Support\ServiceProvider;
9class BloggingServiceProvider extends ServiceProvider
11 public function boot(): void
12 {
13 $this->app->register(
14 provider: PostsServiceProvider::class
15 );
16 }

By calling $this->app->register we are able to register any additional service providers from within our domain to load in nicely and easily. It also means that if we remove the domain service provider from config/app.php then we are also removing these bindings automatically.

Using our Command

So now we have our service providers all connected and registering correctly, we can now look to start our application implementation. In this example I am going to take the easiest route to explain this, by using an API.

To begin with we need a controller that will allow us to inject what we need, this controller will be nice and simple to use, and the purpose of this one will be to allow users to create their own posts.

5namespace App\Http\Controller\API\V1\Posts;
7class StoreController
9 public function __construct(
10 private readonly Authenticatable $user,
11 private readonly CreatePostContract $command,
12 private readonly PostDataObjectFactoryContract $factory,
13 ) {}
15 public function __invoke(StoreRequest $request)
16 {
17 $postDataObject = $this->factory->make(
18 attributes: $request->validated(),
19 );
21 $post = $this->command->handle(
22 post: $postDataObject,
23 user: $user->id,
24 );
26 return new JsonResponse(
27 data: new PostResource(
28 resource: $post,
29 ),
30 status: Http::CREATED,
31 );
32 }

In our Controllers constructor we are injecting:

  • Authenticatable: The currently authenticated user.
  • CreatePostContract: The Commands interface/contract to use.
  • PostDataObjectFactoryContract: The Data Object Factory interface/contract to use.

We then build up our data object, send it through to our command, and pass this instance through to a a JSON Response using a Laravel Resource. Some very simple steps we would be very used to taking in our controllers, but the benefit is that each injected class is interoperable through the container, and each part can be testing in isolation nicely as well as testing the controller in a Feature test.