cad background
Published 05/27/2019

How I Work in Laravel - May 2019 Edition

primary_post_image
This post is a follow-up to a post I made over a year ago detailing how I work in Laravel. See How Do You Work in Laravel.

WHAT IS THE SAME

I'm still a fan of the single-action invokable controller approach. I can go back and look at past projects and with a quick glance at my controllers (actions) folder, I can see every action that can be perfomed in my app.

The first problem that arose from this approach was the obvious need for more boilerplate than usual. Boilerplate code is something that comes along with common patterns. This is something you have to decide. It's the give and take of any pattern that you may implement in your code. Is the extra code worth the clarity it brings? Does the extra code actually make your code less clear?

REDUCING THE BOILERPLATE

To reduce this amount of time I was spending on boilerplate code, I decided to write a few small packages that could generate the necessary code while providing some extra functionality that I was looking for. I wrote a small package for each element of the ADR pattern, for use in Laravel apps:


Another package(perfect-oblivion/adr) wraps these three up and gives a single command for those cases when you want all three.

WORKFLOW

I have somewhat standardized the naming convention I use for my actions, services and responders, based on CRUD. So for a Post model, the actions would look like:

  • Index - ListPosts
  • Create - CreatePost
  • Edit - EditPost
  • Store - StorePost
  • Update - UpdatePost
  • Delete - DeletePost
  • Restore - RestorePost

The names of the corresponding services and responders would follow the same conventions:
ListPostsService, ListPostsResponder, etc.

THE ACTION

An action is generated as follows:

artisan adr:action Posts\\StorePost

The job of the action is to simply receive the request and pass the necessary data along to the service. There is no validation or domain logic in the action. It just gives the incoming data to the service. Once the service is finished, the result is passed back to the action. At this point, the action gives the resulting data to the responder to respond as needed.

<?php

namespace App\Http\Actions\Post;

use Illuminate\Http\Request;
use PerfectOblivion\Actions\Action;
use App\Services\Post\StorePostService;
use App\Http\Responders\Post\StorePostResponder;

class StorePost extends Action
{
    /** @var \App\Http\Responders\Post\StorePostResponder */
    private $responder;

    /**
     * Construct a new StorePost action.
     *
     * @param  \App\Http\Responders\Post\StorePostResponder  $responder
     */
    public function __construct(StorePostResponder $responder)
    {
        $this->responder = $responder;
    }

    /**
     * Execute the action.
     *
     * @param  \Illuminate\Http\Request
     *
     * @return \Illuminate\Http\Response
     */
    public function __invoke(Request $request)
    {
        return $this->responder->withPayload(
            StorePostService::call($request->only(['title', 'summary', 'body']))
        );
    }
}

This is the extent of an action in my apps. If there is any authorization that needs to be handled, I'll use middleware. I believe that authorization needs to take place asap once a request has been made. Authentication should also be handled as quickly as possible. The Services package has authentication built-in similarly to how Laravel's form requests work.

SERVICES

The next step is the service. Each service has a "run" method that receives the data that is passed when calling the service from the action. The constructor of the service resolves any dependencies needed by the service from Laravel's container. If a service requires validation, I create a validation service via an artisan command:

artisan adr:validation Posts\\StorePostValidationService

A service validator has the following structure:

<?php

namespace App\Services\Post\Validation;

use PerfectOblivion\Valid\ValidationService\ValidationService;

class StorePostValidationService extends ValidationService
{
    /**
     * Get the validation rules that apply to the data.
     *
     * @return array
     */
    public function rules()
    {
        return [
            'title' => ['required', 'min:3'],
            'body' => ['required'],
        ];
    }

    /**
     * Get the sanitization filters that apply to the data.
     *
     * @return array
     */
    public function filters()
    {
        return [
            'title' => ['strip_tags', 'trim'],
            'body' => ['trim'],
        ];
    }
}

The validation service handles the validation and any sanitization that needs to happen. The validator is type-hinted in the service's constructor, then called in the "run" method. (Currently calling the validator is a manual process. I'm hoping to automate this in much the same way that Laravel's form requests work).

My service class is below:

<?php

namespace App\Services\Post;

use App\Models\Post;
use PerfectOblivion\Services\Traits\SelfCallingService;
use App\Services\Post\Validation\StorePostValidationService;

class StorePostService
{
    use SelfCallingService;

    /** @var \App\Models\Post */
    private $posts;

    /** @var \App\Services\Post\Validation\StorePostValidationService */
    private $validator;

    /**
     * Construct a new StorePostService.
     *
     * @param  \App\Models\Post  $post
     * @param  \App\Services\Post\Validation\StorePostValidationService  $validator
     */
    public function __construct(Post $posts, StorePostValidationService $validator)
    {
        $this->posts = $posts;
        $this->validator = $validator;
    }

    /**
     * Handle the call to the service.
     *
     * @param  array  $params
     *
     * @return \App\Models\Post
     */
    public function run(array $params)
    {
        $this->validator->validate($params);

        return $this->posts->storePost(auth()->user(), $params);
    }
}

In the case that the validation fails, I am redirected to the previous page with the validation errors, again in the same way that Laravel's form requests handle failed validation. Otherwise, code execution proceeds in the "run" method. In the case above, I defer to the Post model to store this post. If that is successful, the post is returned, then sent back to the calling action.

Note: If needed, services can be queued. The "queue" method can be used instead of "call" to push the service to the background. I sometimes use a service within a service and will queue some of the work that isn't necessary to be sent in the response.

RESPONDERS

Responders can be generated with the following command:

artisan adr:responder Posts\\StorePostResponder

The responder has one job, to return the data to the requester. In situations where the request source may be expecting json sometimes and html at other times, I have a request macro that determines which is needed. The responder uses the macro to make this determination and returns the data. See a sample responder below. In following with the above "StorePost" examples, we simply redirect the user. If the request expects json, we send the json representation of the new post that was stored as the responder payload:

<?php

namespace App\Http\Responders\Post;

use PerfectOblivion\Responder\Responder;

class StorePostResponder extends Responder
{
    /**
     * Send a response.
     *
     * @return \Illuminate\Http\RedirectResponse
     */
    public function respond()
    {
        if (request()->isApi()) {
            return response()->json([
                'post' => $this->payload,
            ], 201);
        }

        return redirect('/')->with(['success' => 'Post created!']);
    }
}

It goes without saying that in not all cases, are each of these pieces necessary. Technically, none of them are necessary. We could handle the domain logic and validation inside the action and return directly. For some projects this works for me.  I don't like when people say "this/that is OK for 'small' projects". It completely depends on your style, your expectations, and what brings the most clarity to your projects. It's not necessarily about big or small. It's all about trade-offs and what works best for you. For me, I love this separation of concerns and the clarity it brings to my code. With these small packages that I use, along with a few editor snippets, I can spit out this boilerplate in the time it would take to write everything in the controller. So 'wasted' time really isn't a factor.

With the perfect-oblivion/adr package installed, all of these (action, service, and responder) can be generated via:

artisan adr:make Posts\\StorePost

This command will yield the following:

App\Http\Actions\Posts\StorePost
App\Services\Posts\StorePostService
App\Http\Responders\Posts\StorePostResponder

ENTER INERTIAJS

Recently I've been exploring inertiajs by Jonathan Reinink. So far I'm really enjoying it. Many times I've started a project with the intention of going full SPA, but I always end up ripping it out and going with a more server-centric approach. For me, I always get tripped up on SPA authentication. I've never been happy with the end result no matter which authentication method I choose to use. With inertiajs, I get the advantages that a SPA gives you but still get to utilize Laravel's routing and I get to write more PHP, which makes me happy. Check out the various libraries associated with inertiajs and see what you think.

A NOTE ON CSS

I would be remiss if I didn't mention tailwindcss by Adam Wathan. Tailwind is what led me to rewrite my blog and start back posting. In the past, I've started many projects that ended up in the trash, due to my lack of design skills. When I started using tailwind a year and a half ago, that all changed. I'm definitely still not a designer, but I can complete projects and be happy with the end design. Thank you to all of the creators out there in the PHP and general web development community that spend their time on projects that lead to an increase in overall developer productivity. You're truly leaving the community better than you found it.

CONCLUSION

Honestly, not a lot has changed over the past year. To bring more consistency, I wrote a few small packages to help with my implementation of this pattern and I've replaced form requests with the validation service from the "Services" package, but other than that, the workflow is basically the same.
If you'd like to check out some of the other practices being used, check out the laravelio repo on github. Where I use "services" Dries dispatches synchronous jobs to handle the domain logic. Reviewing this approach a couple of years ago is what started me down the path that led me to the "services" that I now use. For a completely different approach, check out some of the Laravel open source projects like Telescope or Horizon.