juststeveking.uk

Lead Software Engineer at Mumsnet .

Published on
Reading time
2 minute read

Laravel DDD - Routing within our Domain

In a normal Laravel application we store all of our routes within web.php or api.php typically, however how does this work with our Domain Driven Design approach? Do we create a routes/{domain}.php file, or do we want to put this somewhere else?

My typical approach with this scenario is to create a routes file per domain, and store this within the domain itself. We can then use our Domains Service Provider to register our routes, and they will then be enabled and disabled simply by removing the domain service provider. This makes each domain able to be completely removed simply by using the service providers.

Let us start with the Blogging Domain API again, and create the routes file, create the following file src/Domains/Blogging/Routes/Api/v1.php and add the following:

1<?php
2 
3declare(strict_types=1);
4 
5use Illuminate\Support\Facades\Route;
6 
7Route::prefix('api/v1')->as('api:v1:')->group(function () {
8 
9 Route::prefix('posts')->as('posts:')->group(function () {
10 Route::get('/', App\Http\Controllers\API\V1\Posts\IndexHandler::class)->name('index');
11 });
12 
13});

What we are doing here is creating a prefix of api/v1 for all of our routes, setting the naming strategy to all begin with api:v1: so that we have a consistent naming convention within our application. Soon we will add the logic into our Request Handler, which is a naming convention I use for Controllers. Feel free to rename this to Controllers if this makes you more comfortable.

Our next step is to start registering our routes, so we will return to our Service Provider and add the following bit of code:

1<?php
2 
3declare(strict_types=1);
4 
5namespace Domains\Blogging\Providers;
6 
7use Illuminate\Support\ServiceProvider;
8 
9class BloggingServiceProvider extends ServiceProvider
10{
11 /**
12 * @return void
13 */
14 public function boot(): void
15 {
16 $this->app->register(
17 provider: PostsServiceProvider::class,
18 );
19 
20 $this->registerRoutes();
21 }
22 
23 /**
24 * @return void
25 */
26 protected function registerRoutes(): void
27 {
28 $this->loadRoutesFrom(
29 path: __DIR__ . '/../Routes/API/v1.php',
30 );
31 }
32}

What we can do is start to add more routes inside the registerRoutes method as and when we need to add them. If we run php artisan route:list then we will see the following route listed:

1GET|HEAD api/v1/posts .................................................................. api:v1:posts:index › API\V1\Posts\IndexHandler

If we disable the BloggingServiceProvider inside config/app.php then we will no longer see this route registered. This allows us to have a really clean way to register and deregister our domains.

The logic for this Request Handler is very similar to what we did in a previous blog post, however I will walk through it again for the purpose of understanding the entire workflow.

To begin with, as usual we create an interface/contract for the query we want to perform:

1<?php
2 
3declare(strict_types=1);
4 
5namespace Infrastructure\Blogging\Queries;
6 
7use Illuminate\Database\Eloquent\Collection;
8 
9interface FindAllPostsContract
10{
11 /**
12 * @return Collection
13 */
14 public function handle(): Collection;
15}

Then we can create the implementation:

1<?php
2 
3declare(strict_types=1);
4 
5namespace Domains\Blogging\Queries;
6 
7use App\Models\Post;
8use Illuminate\Database\Eloquent\Collection;
9use Infrastructure\Blogging\Queries\FindAllPostsContract;
10 
11class FindAllPosts implements FindAllPostsContract
12{
13 /**
14 * @return Collection
15 */
16 public function handle(): Collection
17 {
18 return Post::query()->get();
19 }
20}

Then we can continue to add this into our PostsServiceProvider:

1<?php
2 
3declare(strict_types=1);
4 
5namespace Domains\Blogging\Providers;
6 
7use Domains\Blogging\Commands\CreatePostCommand;
8use Domains\Blogging\DataObjects\PostDataObject;
9use Domains\Blogging\Factories\PostDataObjectFactory;
10use Domains\Blogging\Queries\FindAllPosts;
11use Illuminate\Support\ServiceProvider;
12use Infrastructure\Blogging\Commands\CreatePostContract;
13use Infrastructure\Blogging\DataObjects\PostDataObjectContract;
14use Infrastructure\Blogging\Factories\PostDataObjectFactoryContract;
15use Infrastructure\Blogging\Queries\FindAllPostsContract;
16 
17class PostsServiceProvider extends ServiceProvider
18{
19 /**
20 * @var array<class-string,class-string>
21 */
22 public array $bindings = [
23 PostDataObjectFactoryContract::class => PostDataObjectFactory::class,
24 PostDataObjectContract::class => PostDataObject::class,
25 CreatePostContract::class => CreatePostCommand::class,
26 FindAllPostsContract::class => FindAllPosts::class,
27 ];
28}

Finally we want to add be able to access this logic within our Request Handler:

1<?php
2 
3declare(strict_types=1);
4 
5namespace App\Http\Controllers\API\V1\Posts;
6 
7use App\Http\Resources\API\V1\PostResource;
8use Illuminate\Http\JsonResponse;
9use Illuminate\Http\Request;
10use Infrastructure\Blogging\Queries\FindAllPostsContract;
11use JustSteveKing\StatusCode\Http;
12 
13final class IndexHandler
14{
15 /**
16 * @param FindAllPostsContract $query
17 */
18 public function __construct(
19 private readonly FindAllPostsContract $query,
20 ) {}
21 
22 /**
23 * @param Request $request
24 * @return JsonResponse
25 */
26 public function __invoke(Request $request): JsonResponse
27 {
28 return new JsonResponse(
29 data: PostResource::collection(
30 resource: $this->query->handle(),
31 ),
32 status: Http::OK,
33 );
34 }
35}

And we have come full circle, our routes are being registered and working and now we can handle these routes in our request handlers using a nice and simple query.