Abstracting to Service Classes for Cleanliness and Reusability

Written By Logan Fox
Posted on
Share

As a Junior Engineer at Zaengle, I’ve learned that writing good clean code is one of the most important aspects of being a developer. There are many places in code where logic can get out of hand quickly. One such place is in the controllers. We sometimes think that the controller is simply a place to dump any logic we don’t quite know what to do with. While it may seem like a good solution at first, those stray bits of code can get long and messy. Generally, code should be so clean and readable, that it reads like a sentence.

TL;DR

Service classes are a great approach for small bits of reusable code. But for a larger more specific chunk of code, check out this article, which talks about using Laravel pipelines.

The Approach

One way to combat long, smelly controller methods is to abstract the logic into service classes. A benefit of abstracting into dedicated service classes is that it allows you to use the same logic in numerous places throughout your application. Also, it makes the controller much easier to read for developers that might work on the application in the future. Let’s take a look at a simple example below.

The Controller

This controller is responsible for storing a post into the database. There are a few things going on here. In the request, we have an array with the post data and a string of tags. We are also binding a model to the route called UserGroup. We then parse and attach the tags from the request. Finally, we notify all users belonging to the given group and return with a 200 status.

public function __invoke(Request $request, UserGroup $userGroup)
{
   $postData = $request->get('post');

   $post = Post::create([
       'title' => $postData['title'],
       'body' => $postData['body'],
       'group_id' => $userGroup->getKey(),
   ]);

   collect(explode(',', $request->get('tags')))->map(function ($tag) {
       return strtolower(str_replace(' ', '-', trim($tag)));
   })->each(function ($tag) use ($post) {
       $post->tags()->attach(Tag::firstOrCreate(['name' => $tag]));
   });

   $userGroup->users->each(function ($user) use ($post, $userGroup) {
       $user->notify(new PostCreatedNotification($post, $userGroup));
   });

   return response([
       'status' => 200,
   ]);
}

Refactor

We'll extract a couple of small reusable service classes for our bits of logic in order to make this controller more readable and our snippets much easier to reuse.

Tags

We'll start with the functionality of parsing and attaching tags to the post. First, we'll create a service class called TagManager with a private $tags property and two public methods called parse() and attachTo().

The parse() method will accept a string of tags waiting to be parsed and an optional delimiter that defaults to a comma. It will then parse the string of tags into a collection and set the $tags property to that collection and return $this to stay fluid.

The attachTo() method will take a model, in this case, our post, and an optional collection of tags. It will then run a few checks to make sure our data is set properly. If the checks pass, it will iterate through the tags and attach each one to the model we provide.

<?php

namespace App\Services;

use App\Tag;
use Exception;

class TagManager
{
    private $tags;

    public function parse($tags, $delimiter = ',')
    {
        $parsedTags = collect(explode($delimiter, $tags))->map(function ($tag) {
            return strtolower(str_replace(' ', '-', trim($tag)));
        });

        $this->tags = $parsedTags;

        return $this;
    }

    public function attachTo($model, $tags = null)
    {
        if (is_null($tags) && is_null($this->tags)) {
            throw new Exception('Missing tags');
        }

        if (is_null($tags)) {
            $tags = $this->tags;
        }

        $tags->each(function ($tag) use ($model) {
            $model->tags()->attach(Tag::firstOrCreate(['name' => $tag]));
        });

        return $this;
    }
}

We can then come back and update our controller with a much more readable approach as we swap out our existing code for the new service class we built.

public function __invoke(Request $request, UserGroup $userGroup)
{
    $postData = $request->get('post');

    $post = Post::create([
        'title' => $postData['title'],
        'body' => $postData['body'],
        'group_id' => $userGroup->getKey(),
    ]);

    (new TagManager())
        ->parse($request->get('tags'))
        ->attachTo($post);

    $userGroup->users->each(function ($user) use ($post, $userGroup) {
        $user->notify(new PostCreatedNotification($post, $userGroup));
    });

    return response([
        'status' => 200,
    ]);
}

Notify Users

We will create one more service class to handle notifying the users in the given group. We'll title this service class NotifyUserGroup. This will be a very simple class with a constructor method that initializes a private $group property and a notify() method which gets passed to the notification class we want to send.

Notice how passing the notification class, we can reuse this anywhere we want. We simply initialize it with the group and pass the notification we want to send.

<?php

namespace App\Services;

class NotifyUserGroup
{
    private $group;

    public function __construct($group)
    {
        $this->group = $group;
    }

    public function notify($notification)
    {
        $this->group->users->each(function ($user) use ($notification) {
            $user->notify($notification);
        });
    }
}

Final controller

After abstracting out to a couple of simple reusable service classes, we have a much simpler, easier to read controller with bits of logic that can be used in other locations.

Another example would be creating a video for a user group. We could take the TagManager class and apply it to a Video model. Then we could use the NotifyUserGroup and call a VideoCreatedNotification.

public function __invoke(Request $request, UserGroup $userGroup)
{
    $postData = $request->get('post');

    $post = Post::create([
        'title' => $postData['title'],
        'body' => $postData['body'],
        'group_id' => $userGroup->getKey(),
    ]);

    (new TagManager())
        ->parse($request->get('tags'))
        ->attachTo($post);

    (new NotifyUserGroup($userGroup))
        ->notify(new PostCreatedNotification($post, $userGroup))

    return response([
        'status' => 200,
    ]);
}

Conclusion

You can see that by taking a little time to extract your controller logic into service classes, it makes your controller much cleaner and a lot easier for devs coming in later to decipher. This is a simple example but the possibilities are endless. One thing to note though is that when your controllers get much larger and more specific, this approach is not going to stand as strong. You might give this article a read for abstracting larger more specific chunks of logic out of a controller.

Whatever method you choose to clean up your controllers, the biggest takeaway from this example is that we should be putting 110% effort into making sure our code is written clean and readable. Trust me, your future self will thank you.

  1. Header photo from the NASA Flickr Commons