Community Post

Service Layer in Laravel and lumen (Orchestrating Application Logic Using Jobs)

Ossaija ThankGod
👁️ 0 views
💬 comments

Building and managing complex applications can be daunting, more so when business requirements change regularly usually because companies are either trying to attain product market fit or scaling rapidly to meet user demands. In these cases, engineering teams are tasked with delivering robust architecture that is adaptable and can be scaled up fast to meet users demand without noticeable downtime to avoid lost of revenue.

At this stage, good software teams will realise that the skills, hacks and frameworks that were so useful at the MVP stage can no longer be relied upon. The giant monolith they have built can’t be held in place by hacky glues, and midnight patches anymore. and technical debts incurred at the early stages will have to be paid now. The system components need to become easy to test, cohesive, highly decoupled and maintainable. Good knowledge of design patterns, architecture patterns, antipatterns and SOLID design principles will be critical for the survival of the the business, else competitors with teams that have the required knowledge and skills will gain the larger market share.

In the rest of this post, I will demonstrate how software engineering teams can use jobs as a service layer to manage complexity in a big and growing application lumen/laravel application. Note (the example used is a mythical new application and not necessary an application that already exists. I will be writing another post on how to refactor a monolithic php application at a later date :)

What is a Service Layer?

A "service layer" exists between the UI and the backend system that store data. It is in charge of managing the business rules of transforming and translating data between those two layers. Cliff Gilley answer to a quora post.

Service layer, also called command handler, is the right place to compose and handle all the business logic responsible for fulfilling a user's ‘intent’ (end user's goals for taking an action in your application). it allows you represent user's actions, as intents that are understood by all stakeholders and can be passed to our application as commands that should be handled. It allows us have a single point where all business entities (domain models) that are responsible for fulfilling user’s goals can converge and fulfill those goals. In a laravel/lumen application, a service layer allows us respond to web, cli, api request in one place instead of repeating the same logic in three different controllers or closures. This makes our application easy to manage, maintain and test.

What we will be building?

Say we are asked to build an application that users can use to discover and read books, the application should have a community where users can talk about the books they are reading. It should also send notifications of relevant events that happens to users and admins. Our application is to be accessed via user's browser to begin with. It will have to support the following for now:

  • Admin users can post and publish new books
  • Users can discover published books
  • Users can add and delete books in their library
  • Users can read books in their library

After going through the following requirements we decided on the following:

  • A book will have author(s), chapters and tags. So there will be a book, author, chapter, and tags entities and associated data in our application.
  • A user can have an account, a library, and friends, so there will be a user, library, and account entities and associated data also in our application.
  • Our application should delegate long running tasks like processing file uploads to a background process so that user experience is not impacted by slow requests.
  • For maximum performance, our application should cache resources every now and then and be able to manage caches well.

Ok. Since we are aware of the danger of highly coupled codebases and what a horror managing them can be, we are careful not to make our future lives a living hell. We decide to use a service layer and adhere to other good patterns, as well as avoid as many antipatterns as possible.

Lets Get Started!!

In laravel/lumen having service layers means creating jobs. Laravel comes with a command bus implementation that can be accessed using the dispatch function and allows us process commands/jobs synchronously and asynchronously. ( new to command patterns see more here)

A typical job class in laravel looks like the following

<?php

namespace App\Jobs;

class ExampleJob extends Job
{
    /**
     * Create a new job instance.
     *
     * @return void
     */
    public function __construct()
    {
        //
    }

    /**
     * Execute the job.
     *
     * @return void
     */
    public function handle()
    {
        //
    }
}

The handle method is where we will write the majority of our code. More on that later.

Since our aim is to manage complexity, reduce coupling, and enhance testability, let's keep business logic away from the controllers as much as possible. Let our controllers be a passageway to our application. To do this, lets think of all user’s actions as intents and controllers methods as places to compose those intent and hand them over to our application to handle. That way whether a request is coming from a browser, a mobile phone, another application or the cli, our application will see them as jobs/commands to be processed.

Ok, how do we compose requests as jobs/commands?

Lets start with what our admin users can do; post and publish books:

  • To post a book, the system will have to save the book information into the books table, link the book to authors, process and save the chapters and link them to the book
  • To publish the book, the system will change a column in the book table and send a notification to users who subscribed to the topics the book is about.

There are two intents here, posting a book and publishing the posted book. Handling them can be done with the following code.

We will create a route for posting books in our web.php file

request/web.php
<?php

$router->get('/', function () use ($router) {
    return $router->app->version();
});

$router->post('/books', 'BooksController@store');

Next we create a BooksStore controller and in the store method, we will validate the request, compose it and dispatch a job to handle it.

app/controllers/BooksController.php
<?php

namespace App\Http\Controllers;
use App\Jobs\PostBook;

class BooksController extends Controller
{
    /**
     * Create a new controller instance.
     *
     * @return void
     */
    public function __construct()
    {
        //
    }

    public function store(Request $request)
    {
        $this->validate($request, [
            'title' => 'required|max:255',
            'chapters' => 'required|file',
            'authors' => 'required',
            'tags' => 'required'
        ]);
        dispatch(new PostBook($request->all()));
        return ['created' => true];
    }
}

See how simple our store method is? We just validated the request and dispatched a job. Jobs in laravel can be queued; queued jobs will not be handled by the same process as the request but will be handled by a worker process. Dispatching a job is telling laravel to handle it. The PostBook job in this case is not queued, so it will be handled in the same request-response cycle and the application will return a json response of created: true to the requester.

Note: The dispatch method in laravel uses an implementation of command bus to handle job dispatch.

Let see the code for PostBook job.

 <?php

namespace App\Jobs;

use App\Book;
use App\Chapter;
use App\Author;
use App\Tag;
class PostBook extends Job
{   
    protected $request;
    /**
     * Create a new job instance.
     *
     * @return void
     */
    public function __construct(Array $request)
    {
        $this->request = $request;
    }

    /**
     * Execute the job.
     *
     * @return void
     */
    public function handle(Book $book, Chapter $chapter, Author $author, Tag $tag)
    {   
        $chapters = $chapter->processFromFile($this->request['chapters']);
        $new_book = $book->create($this->request);
        $authors = $author->find($this->request['authors']);
        $tags = $tag->find($tags);

        $new_book->chapters()->createMany($chapters);
        $new_book->authors()->attache($authors);
        $new_book->tags()->attache($tags);
    }
}

Here, when an object of this class is instantiated we save the passed in request array in a property inside the object. In the handle method, we inject Book, Chapter, Author and Tag models. Instead of using models as shown, you can use a repository pattern as a data access layer for them. Next we tell the chapter model to process the file and return a collection of chapter instances. We create the book, find the authors and tags, then link the books to the authors and chapters and tags and our book is posted. Cool, right?

Now if the business wants us to support posting books from a mobile phone, a desktop, or cli, all we need to do is to create controllers or closures to accept the request, validate it and pass it to our PostBook job to handle. If we need to change how we handle posting books, we can do it in one place. How's that for low coupling and testability huh?

Now to the last part, we want to publish this book: Add an entry into our router for for accepting publish requests. Our request/web.php files should look like this now:

route/web.php
<?php

$router->get('/', function () use ($router) {
    return $router->app->version();
});

$router->post('/books', 'BooksController@store');
$router->patch('/books/{book}/publish', 'BooksController@publish');

We will have to create a publish method in our BooksController to respond to publish requests.

//app/controllers/BooksController
<?php
namespace App\Http\Controllers;
use App\Jobs\PostBook;
use app\Jobs\PublishBook;

class BooksController extends Controller
{
    /**
     * Create a new controller instance.
     *
     * @return void
     */
    public function __construct()
    {
        //
    }

    public function store(Request $request)
    {
        $this->validate($request, [
            'title' => 'required|max:255',
            'chapters' => 'required|file',
            'authors' => 'required',
            'tags' => 'required'
        ]);
        dispatch(new PostBook($request->all()));
        return ['created' => true];
    }

    public function publish(Request $requedst)
    {
        //validate request
        dispatch(new PublishBook($request->all(), $book));
    }
}

We dispatch another job called PublishBook in our publish method and pass it our request and the bookId we want to publish.

In the handle method of the PublishBook job, we find the book, publish it and emit a BookPublished event. Now we can put the logic to send notifications to anyone who is subscribed to the book category (tags) that there is a new book.

//app/jobs/PublishBook
<?php

namespace App\Jobs;

use App\Book;
class PublishBook extends Job
{   
    /**
     * user request 
     * @var [type]
     */
    protected $request;

    /**
     * id of book to be published
     */
    protected $bookId;

    /**
     * Create a new job instance.
     *
     * @return void
     */
    public function __construct(Array $request, $bookId)
    {
        $this->request = $request;

        $this->bookId = $bookId;
    }

    /**
     * Execute the job.
     *
     * @return void
     */
    public function handle(Book $bookEntity)
    {   
        $book = $bookEntity->find($this->bookId);
        $book->publish($this->request);
        event(new BookPublished($book));
    }
}

Notice how the logic for processing the chapter from a file and publishing books are resident in the entities themselves and not our handlers? That is single responsibility principle at play. If there is a need for any of that logic to change, we would go to one place and make those changes and our application works as normal.

Conclusion:

Building applications this way requires some clear understanding of the business needs before jumping into coding. Understanding the business goals will allow you to extract user intents that can be represented as jobs in your application. Also our application can be easily tested as we avoided coupling that makes testing nightmarish. Our application component can also be managed and changed easily as business requirements change.

I am working on another article that will continue the application from here. Stay tuned!!

Ossaija ThankGod

2 posts

A Software team lead at Imagine business services, Am obsesses with how things work and how machine can enhance human experience.