How to build a Laravel application using Lucid architecture

How to build a Laravel application using Lucid architecture

Exploring the micro variant in lucid architecture to build scalable laravel applications using a blog as an example

Table of contents

No heading

No headings in the article.

In this article, we'll build a blogging app using lucid architecture - micro variants.

Architecture is a gathering of connected structures or the art and processes of building something. In this tutorial's context, we're looking at the art and processes of building a laravel application.

Lucid is from Vinelab authored by @mulkave with Abed Halawi - The lucid architecture for building scalable applications - Laracon EU 2016 as the Tech lead.

"Lucid is a software architecture to build scalable Laravel projects.

It incorporates Command Bus and Domain Driven Design at the core, upon which it builds a stack of directories and classes to organize business logic. It also derives from SOA (Service Oriented Architecture) the notion of encapsulating functionality within a service and enriches the concept with more than the service being a class."

Use Lucid to:

  • Write clean code effortlessly

  • Protect your code from deteriorating over time

  • Review code in fractions of the time typically required

  • Incorporate proven practices and patterns in your applications

  • Navigate code and move between codebases source

Concept

This architecture is in an amalgamation of best practices, design patterns and proven methods that we decided to incorporate at the forefront of our code.

Lucid’s built-in patterns are Command Bus and Domain Driven Design, upon which it builds a stack of directories and units to organize business logic. It also derives from SOA (Service Oriented Architecture) the notion of encapsulating functionality within a “service” and enriches the concept with more than the service being a class.

  • Command Bus: to dispatch units of work. In Lucid terminology, these units will be a Feature, Job or Operation.

  • Domain Driven Design: to organize the units of work by categorizing them according to the topic they belong to.

  • Service Oriented Architecture: to encapsulate and manage functionalities of the same purpose with their required resources (routes, controllers, views, database migrations etc.)

Position

In a typical MVC application, Lucid will be the bond between the application’s entry points and the units that do the work, securing code from meandering in drastic directions:

image.png

The Stack

image.png

Read more on Lucid Concept or The Lucid Architecture — Concept by Abed Halawi

Philosophy

We wanted something simple [KISS] yet effective at scale. Enjoyable to work with by all levels of engineers alike. And elevates the level of understanding abstraction throughout its steep learning process. Simply put:

Lucid is a set of principles that depend on our discipline to preserve

More about Lucid Philosophy

Principles

  • Features shall serve a single purpose

  • Jobs shall perform a single task

  • Domains shouldn’t cross

  • Features shall not call other features

  • Jobs shall not call other jobs

  • Operations shall not call other operations

  • Controllers serve Features

  • Write code that humans can read

Micro

A single-purpose project that contains a moderate amount of functionalities that fit in a similar context.

It contains the fundamental units of the Lucid stack: Domains, Features, Operations and Data, complementing the Laravel framework:

image.png

When To Use Micro?

Micro is suitable for most projects, including quick prototypes that are ought to become actual products at some point which is where Lucid comes in to reduce the technical debt imposed over time. Or API projects that are meant to be organized for scale.

Also as the name suggests they are best suitable for Microservices, where you would have multiple instances of Laravel • Lucid Micro, each representing a microservice in your system.

Installation

The Lucid Architecture is delivered as a Composer package lucidarch/lucid. In a bit to get on board with how lucid microarchitecture is used to build laravel applications, we'll be building a simple blog application.

Let's start with a fresh laravel 9 app installation

laravel new lucid_architecture_laravel_app

Next, install Lucid

composer require lucidarch/lucid

Make sure to place Composer’s project vendor bin directory in your $PATH so the lucid executable can be located by your system. Usually, it’s done by running export PATH="$PATH:./vendor/bin" to have it available in your current Terminal session, or add it to your corresponding Terminal profile (e.g. ~/.bashrc, ~/.bash_profile, ~/.zshrc) to have it permanently loaded with every session.

export PATH="$PATH:./vendor/bin"

image.png

Next, Initialize a Micro instance:

Lucid Micro is the default variant for single-purpose applications.

lucid init:micro

This will generate an initial Micro structure:

image.png

Next, serve the application

php artisan serve

image.png

Lucid Stack

Controllers

In an effort to minimize the work of controllers, for they are not here to do work for the application but to point the request in the right direction. In Lucid terms, to serve the intended feature to the user.

Eventually, we will end up having one line within each controller method, achieving the thinnest form possible.

Responsibility: Serve the designated feature.

Features

Represents a human-readable app feature in a class, named the way you would describe it to your colleagues and clients. It contains the logic of implementing the feature with minimum friction and level of detail to remain concise and straight to the point.

It runs Lucid Units: Jobs and Operations to perform its tasks. They are thought of as the steps in the process of serving its purpose. A Feature can be served from anywhere, most commonly Controllers and Commands. Can also be queued to run asynchronously using Laravel’s powerful Queueing capabilities.

Technically, it is a class that encapsulates all the functionalities required for a single request/response lifecycle (or command), in which the handle method represents the task list when you want to implement it in your application.

Jobs

Jobs do the actual work by implementing the business logic. Being the smallest unit in Lucid, a Job should do one thing, and one thing only - that is: perform a single task. They are the snippets of code we wish we had sometimes, and are pluggable to use anywhere in our application.

Our objective with Jobs is to limit the scope of a single functionality so that we know where to look when finding it, and when we’re within its context we don’t tangle responsibility with other jobs to achieve the ultimate form of single responsibility principle.

Usually called by a Feature or an Operation, but can be called from anywhere by any other class once setup as a custom dispatcher; hence, being the most shareable pieces of code.

Domains

Inspired by Domain-Driven Development, this piece of the Lucid stack is merely directories that are all about organizing and categorizing code according to the topic they belong to.

There is no specific way of naming them because it differs per case, but according to our experience over the years we found that they’re usually of two types, the “internal” and the “external” type of domain. Here are a few examples to illustrate the difference

image.png

Request Similar to the Laravel Form Validation we can create a custom request using the lucid commands to keep them organized.

From the Lucid stack we’d be using Features, Jobs, Domains and Requests to implement the following:

  • create a post

  • list posts

  • update a post

  • delete a post

Next, set the environment variable for this application on the .env file

DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=lucid_architecture_laravel_app
DB_USERNAME=root
DB_PASSWORD=

Next, create the posts table

php artisan make:migration create_posts_table --create=posts

Add the following columns

            $table->foreignId('user_id');
            $table->string('title');
            $table->text('description');

Next, run the migration command

php artisan migrate

Next, use the lucid command to create the PostController to manage posts and have it ready for serving features.

lucid make:controller Post

image.png

Find it at app/Http/Controllers/PostController.php

<?php

namespace App\Http\Controllers;

use Lucid\Units\Controller;

class PostController extends Controller
{}

Notice the automatic addition of Controller suffix, used as a naming convention to match other suffixes in the Lucid stack such as Feature, Job and Operation.

Next, create the Post model using the lucid command

lucid make:model Post

image.png

Update the Post model

<?php

namespace App\Data\Models;

use App\Models\User;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Factories\HasFactory;

class Post extends Model
{
    use HasFactory;

    protected $fillable = [
        'title', 'description'
    ];

//setup the relationship with the users table
    public function user()
    {

//User model comes as default in laravel but you may wish to use the lucid command to create one
        return $this->belongsTo(User::class);
    }

//cast the created at attribute to be readable by humans
    public function created_at(): Attribute
    {
        return new Attribute(
            get: fn ($value) => $value->diffForHumans(),
        );
    }
}

Update the User Model

<?php

namespace App\Models;

//models created using lucid command are placed in the data folder
use App\Data\Models\Post;

use Laravel\Sanctum\HasApiTokens;
use Illuminate\Notifications\Notifiable;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable;

class User extends Authenticatable
{
    use HasApiTokens, HasFactory, Notifiable;

    protected $fillable = [
        'name',
        'email',
        'password',
    ];

    protected $hidden = [
        'password',
        'remember_token',
    ];

    protected $casts = [
        'email_verified_at' => 'datetime',
    ];

    public function posts()
    {
        return $this->hasMany(Post::class);
    }
}

Next, set up the routes for the different sections of the blog application in the routes/web.php file

<?php

use Illuminate\Support\Facades\Route;
use App\Http\Controllers\PostController;

Route::controller(PostController::class)->group(function () {
    Route::get('/', 'all_posts');
    Route::get('/home', 'index')->name('home');
});

Route::middleware(['auth', 'verified'])->group(function () {
    Route::resource('posts', PostController::class);
});

Next, let's create a feature using lucid command and return the create post form view

lucid make:feature CreatePostFeature

Feature class CreatePostFeature created successfully.

    public function create()
    {
        return $this->serve(CreatePostFeature::class);
    }

CreatePostFeature.php

<?php

namespace App\Features;

use Lucid\Units\Feature;

class CreatePostFeature extends Feature
{
    public function handle()
    {
        return view('posts.create');
    }
}

Next, store the post

lucid make:feature StorePostFeature

Feature class StorePostFeature created successfully.

    public function store()
    {
        return $this->serve(StorePostFeature::class);
    }

Create Request Validation for the form submission

The first step of receiving input is to validate it. We will be using Form Request validation where each Request belongs in a Domain representing the entity that’s being managed, in this case, it’s Post containing a StorePost Request class.

This will be the beginning of working with Domains in Lucid. They’re used to group Jobs and custom classes whose logic is associated with certain topics according to domain-driven design.

Starting with validation, Lucid places Request classes within their corresponding domains. Let’s generate a StorePost request:

lucid make:request StorePost Post

Request class created successfully.

Find it at app\Domains\Post\Requests\StorePost.php

In StorePost we’ll need to update the methods authorize() and rules() which are created by default to validate the request and its input:

<?php

namespace App\Domains\Post\Requests;
use Illuminate\Foundation\Http\FormRequest;

class StorePost extends FormRequest
{
    public function authorize()
    {
        return true;
    }

    public function rules()
    {
        return [
            'title' => ['required', 'string', 'max:255'],
            'description' => ['required', 'string'],
        ];
    }
}

With our request ready, now we need StorePostFeature to use that request class when served. We can do that by simply injecting the request class in the feature’s handle() method and every time the feature is served this validation will be applied.

    public function handle(StorePost $request)

Save Post

To save the post we’ll create a job that saves posts and run it in our feature, which will be added to our Post domain at app/Domains/Post/Jobs/SavePostJob.

lucid make:job SavePost Post

Job class SavePostJob created successfully.

Find it at app\Domains\Post\Jobs\SavePostJob.php

SavePostJob should define the parameters that are required in its constructor, a.k.a the job’s signature, rather than accessing the data from the request so that we can call this job from other places in our application (e.g. from a command or a custom class) and not be restricted by the protocol, in this case HTTP request.

We use this technique to increase the degree of job isolation and secure the single responsibility principle.

<?php

namespace App\Domains\Post\Jobs;

use Lucid\Units\Job;
use App\Data\Models\Post;
use Illuminate\Support\Facades\Auth;

class SavePostJob extends Job
{

    public function __construct(public $title, public $description)
    {}

    public function handle()
    {
        $attributes = [
            'title' => $this->title,
            'description' => $this->description,
        ];

        $post = new Post($attributes);
        $user = Auth::user();

        return $user->posts()->save($post);
    }
}

Then we’ll run this job from the feature to save posts when received:

<?php

namespace App\Features;

use Lucid\Units\Feature;
use App\Domains\Post\Jobs\SavePostJob;
use App\Domains\Post\Requests\StorePost;

class StorePostFeature extends Feature
{
    public function handle(StorePost $request)
    {
        $this->run(SavePostJob::class, [
            'title' => $request->input('title'),
            'description' => $request->input('description'),
        ]);
    }
}

Calling $this->run($unit, $params) in a feature causes the underlying dispatcher to run SavePostJob synchronously by calling its handle method after initializing it with the provided $params which are passed as an associative array where the keys must match the job’s constructor parameters in naming, but not the order. So this would still work the same:

        $this->run(SavePostJob::class, [
            'description' => $request->input('description'),
            'title' => $request->input('title'),
        ]);

The next step is to redirect a successful request back to the create form. To do that we’ll create a RedirectBackJob which will simply call back(). Even though it might seem like an overhead, but remember that we’re setting up for scale, and as we scale, the less free-form code we have the better; instead of having plenty of back() and back()->withInput() and other calls, we centralize them in a job so that in case we ever wanted to modify that functionality or add to it, the change will only need to happen in one place.

RedirectBackJob will reside in a new Http domain, a place for all our HTTP-related functionality that isn’t specific to a business entity of our application, fits the abstract type of domains instead of the entity type.

lucid make:job RedirectBackJob http

Job class RedirectBackJob created successfully.

Find it at app\Domains\Http\Jobs\RedirectBackJob.php

Our job will provide the option withMessage to determine whether message should be included in the redirection. This is a simple example of how such a simple job may later provide functionality that can be shared across the application.

<?php

namespace App\Domains\Http\Jobs;

use Lucid\Units\Job;

class RedirectBackJob extends Job
{

    //PHP 8 constructor
    public function __construct(private $withMessage){}

    /**
     * Execute the job.
     *
     * @return void
     */
    public function handle()
    {
        $back = back();

        if ($this->withMessage) {
            //custom success message
            $back->with('success', $this->withMessage);
        }
        return $back;
    }
}

Next, let's update the StorePostFeature to make use of the redirect job after a post have been created

<?php

namespace App\Features;

use Lucid\Units\Feature;
use App\Domains\Post\Jobs\SavePostJob;
use App\Domains\Post\Requests\StorePost;
use App\Domains\Http\Jobs\RedirectBackJob;

class StorePostFeature extends Feature
{
    public function handle(StorePost $request)
    {
        $this->run(SavePostJob::class, [
            'title' => $request->input('title'),
            'description' => $request->input('description'),
        ]);

        return $this->run(RedirectBackJob::class, [
            'withMessage' => 'Post created successfully.',
        ]);

    }
}

Next, create a method to handle the displaying of all posts by users, let’s call it all_posts:

    public function all_posts(){
        return $this->serve(AllPostsFeature::class);
    }

Create the AllPostsFeature to be served by the all_posts method to return all posts

lucid make:feature AllPostsFeature

AllPostsFeature.php

<?php

namespace App\Features;

use Lucid\Units\Feature;
use App\Data\Models\Post;

class AllPostsFeature extends Feature
{

//The handle method is automatically created by the Lucid command which we'll use to return all the posts 

    public function handle()
    {
            $posts = Post::latest()->get();

            return view('home', compact('posts'));

    }
}

Update the all_posts method in the PostController

    public function all_posts(){
        return $this->serve(AllPostsFeature::class);
    }

Next, create an index method to handle the listing of posts created by the user:

    public function index()
    {}

Feature

The index method will then serve the feature that will run the jobs required to display the posts. Generate a feature called PostsIndexFeature:

lucid make:feature PostsIndexFeature

Feature class PostsIndexFeature created successfully.

Find it at app\Features\PostsIndexFeature.php

Next update the index method to serve the posts index feature

<?php

namespace App\Http\Controllers;

use Lucid\Units\Controller;
use App\Features\PostsIndexFeature;

class PostController extends Controller
{
    public function index(){
        return $this->serve(PostsIndexFeature::class);
    }

}

The handle method in the PostsIndexFeature is automatically created by the Lucid command which we'll use to return all the posts

<?php

namespace App\Features;

use Lucid\Units\Feature;
use App\Data\Models\Post;
use Illuminate\Support\Facades\Auth;

class PostsIndexFeature extends Feature
{
    public function handle()
    {

        $posts = Post::where('user_id', Auth::user()->id)->latest()->get();

        return view('posts.index', compact('posts'));
    }
}

**The same approach to using features, jobs, requests mentioned above is used in the following sections: **

Show a post section:

 lucid make:feature ShowPostFeature

Feature class ShowPostFeature created successfully.

Find it at app\Features\ShowPostFeature.php

Update the ShowPostFeature.php

<?php

namespace App\Features;

use Lucid\Units\Feature;
use App\Data\Models\Post;

class ShowPostFeature extends Feature
{
    public function __construct(
        public Post $post,
    ) {
    }


    public function handle()
    {
        return view('posts.show')->with('post', $this->post);
    }
}

Show Method in the post controller

    public function show(Post $post)
    {
        return $this->serve(ShowPostFeature::class, [
            'post' => $post,
        ]);
    }

The Edit and Update Post section:

lucid make:feature EditPostFeature

Feature class EditPostFeature created successfully.

Find it at app\Features\EditPostFeature.php

Update the EditPostFeature.php

<?php

namespace App\Features;

use Lucid\Units\Feature;
use App\Data\Models\Post;

class EditPostFeature extends Feature
{
    public function __construct(
        public Post $post,
    ) {
    }

    public function handle()
    {
        return view('posts.edit')->with('post', $this->post);;
    }
}

The edit method in the PostController

    public function edit(Post $post)
    {
        return $this->serve(EditPostFeature::class, [
            'post' => $post,
        ]);
    }

The update method in the PostController

lucid make:feature UpdatePostFeature

Feature class UpdatePostFeature created successfully.

Find it at app\Features\UpdatePostFeature.php

Create an UpdatePost job

lucid make:job UpdatePost Post

Job class UpdatePostJob created successfully.

Find it at app\Domains\Post\Jobs\UpdatePostJob.php

UpdatePostJob.php

<?php

namespace App\Domains\Post\Jobs;

use Carbon\Carbon;
use Lucid\Units\Job;
use App\Data\Models\Post;

class UpdatePostJob extends Job
{

    public function __construct(public Post $post, public $title, public $description){}

    public function handle()
    {
        $attributes = [
            'title' => $this->title,
            'description' => $this->description,
            'created_at' => Carbon::now(),
            'deleted_at' => Carbon::now(),
        ];
        return $this->post->update($attributes);
    }
}

Update the UpdatePostFeature.php

<?php

namespace App\Features;

use Lucid\Units\Feature;
use App\Data\Models\Post;
use App\Domains\Post\Jobs\UpdatePostJob;
use App\Domains\Post\Requests\StorePost;
use App\Domains\Http\Jobs\RedirectBackJob;

class UpdatePostFeature extends Feature
{
    public function __construct(
        public Post $post,
    ) {
    }

    public function handle(StorePost $request)
    {

        $this->run(UpdatePostJob::class, [
            'post' => $this->post,
            'title' => $request->input('title'),
            'description' => $request->input('description'),
        ]);

        return $this->run(RedirectBackJob::class, [
            'withMessage' => 'Post updated successfully.',
        ]);
    }

}

update method in the PostController

    public function update(Post $post)
    {
        return $this->serve(UpdatePostFeature::class, [
            'post' => $post,
        ]);
    }

Delete a post section:

lucid make:feature DeletePostFeature

Feature class DeletePostFeature created successfully.

Find it at app\Features\DeletePostFeature.php

Update the DeletePostFeature.php file

<?php

namespace App\Features;

use Lucid\Units\Feature;
use App\Data\Models\Post;

class DeletePostFeature extends Feature
{
    public function __construct(
        public Post $post,
    ) {
    }

    public function handle()
    {

        $this->post->delete();

        return redirect()->route('posts.create')
        ->with('success', 'Post deleted!!!');
    }
}

To delete a post let's use the destroy method

Please note for real production app ensure to implement Laravel soft delete

    public function destroy(Post $post)
    {
        return $this->serve(DeletePostFeature::class, [
            'post' => $post,
        ]);
    }

Update the DeletePostFeature.php

<?php

namespace App\Features;

use Lucid\Units\Feature;
use App\Data\Models\Post;

class DeletePostFeature extends Feature
{
    public function __construct(
        public Post $post,
    ) {
    }

    public function handle()
    {

        $this->post->delete();

        return redirect()->route('posts.create')
        ->with('success', 'Post deleted!!!');
    }
}

Super cool!!! that's all for the micro section more information about lucid architecture check their documentation

Let's serve the application and use it

Create post

image.png

All posts

image.png

Show post with edit and delete feature

image.png

Tutorial Repository - Lucid-Micro branch

Research credits

More about Lucid architecture

Lucid documentation

Abed Halawi - The lucid architecture for building scalable applications - Laracon EU 2016

💡
Follow me on Hashnode: Alemsbaja X: Alemsbaja | Youtube: Tech with Alemsbaja to stay updated on more articles

Find this helpful or resourceful?? kindly share and feel free to use the comment section for questions, answers, and contributions.

Did you find this article valuable?

Support Alemoh Rapheal Baja by becoming a sponsor. Any amount is appreciated!