The PHPStage

How Do You Work in Laravel


Reading time: 5 min, 2 s

I recently read Shawn Mayzes Medium post, How Do You Work in Laravel, and wanted to take a few minutes to talk about my current workflow. This way, in 6 months or so, I can reflect back on how much I've changed. I think it's a good idea to do this once or twice a year to track my growth using the framework.

Overview

Lately, I've actually adopted some of the ideas from Paul Jones' ADR pattern. A full adoption doesn't seem feasible for some projects, but I guess that's true of any pattern, app structure, or architecture. The parts I've chosen to utilize include (1) using single action, invokable, controllers (mostly) and (2) controller specific responders. I've also incorporated Laravel's new API resources (>=5.5) and Laravel's FormRequests.

Routing

My routes are pretty ordinary. I do like to use route groups for everything, even though in some cases, the groups only serve an organizational purpose. This could possibly change in the future as Daniel Coulbourne has a pending pull request that would make route groups "first-class citizens."

Route::group(['namespace' => 'Users'], function($router) {
    $router->get('/users', UserIndex::class)->name('users.index');
    $router->get('/users/{user}', UserShow::class)->name('users.show');
});

Controllers

Here's an example of a simple controller. I'll break it down below:

<?php

namespace App\Http\Controllers\Users;

use App\Models\User;
use Illuminate\Http\Request;
use App\Http\Controllers\Users\UsersController;
use App\Http\Responders\Users\UserIndexResponder;

class UserIndex extends UsersController
{
    public function __invoke(Request $request, UserIndexResponder $responder)
    {
        return $responder->respond($request, $this->users->get());
    }
}

Earlier, I said "mostly" when I mentioned single action, invokable, controllers. Sometimes, I'll use the Laravel built-in auth, and I'll stick with the tradional controller setup in this case. As a matter of fact, that's usually true for my authentication component, whether I write it from scratch or use an existing implementation.

If you're wondering where "$this->users" comes from as a parameter to "respond", I have a parent controller for each resource. It is in this parent controller that I pull in any model or repository dependencies that will be needed in the "controllers" associated with that resource.

In this simple controller, we're only displaying a list of users. I simply delegate to the responder and pass along the request and the data. Next the responder will determine if this is an api call that needs JSON, or if a view will suffice:

<?php

namespace App\Http\Responders\Users;

use App\Http\Responders\Responder;
use App\Http\Resources\UserCollectionResource;

class UserIndexResponder extends Responder
{
    public function respond($request, $data)
    {
        if ($request->isApi()) {
            return new UserCollectionResource($data);
        }

        return $this->view->make('admin.users.index', ['users' => $data]);
    }
}

Again, each responder extends an abstract parent, where Illuminate\View\Factory is pulled in to be used via "$this->view". Also worth noting, "$request->isApi" is just a custom macro.

Validation and Sanitization

Lately, I'm seeing developers take advantage of how easy it is to handle validation inside the controller. Laravel 5.5 makes this simple by allowing you to call "validate" on the request object. This will also only return the fields that were validated. This is a great tool and is usually more than enough to handle validation in most web apps. However, I prefer consistency across my application. Since sometimes this validation can be a little much to leave in the controller, I like to always use FormRequests. This way, for every request that my app handles, I know where to look to see how the request is validated. For validation that isn't covered by Laravel's built-in rules, I reach for the new custom Rule objects. If it doesn't make sense as a Rule object, I simply utilize the "after" validator hook and finish validating.

Finally, I've incorporated sanitizers into my FormRequests. In the same way that the "rules" function returns an array of validation rules, I have a "sanitizers" function that returns an array of sanitizers. The request data is sanitized before being passed to the validator.

Now, if control goes to the controller action, I can be sure that the data is valid and in the format I require.

Here's an excerpt from a FormRequest class:

public function authorize()
{
    return $this->gate->allows('manage-users');
}

public function rules()
{
    return [
        'name' => ['required'],
        'email' => ['email', 'required'],
        'role' => ['required', 'in:user,admin,super_admin'],
    ];
}

public function sanitizers()
{
    return [
        'name' => ['strip_tags'],
        'email' => ['strip_tags', 'strtolower'],
        'role' => ['strip_tags', 'strtolower'],
    ];
}

Service Layer

Finally, for most of the actual "work", I delegate to a service layer that is comprised of synchronous jobs. I have the Bus and Event dispatcher available in my base Controller class and simply dispatch any work that needs to be done. The jobs themselves defer to methods located directly on the model. When needed, queued events/jobs are utilized. Again, for consistency and clarity, I prefer not to have some controllers that handle all of the work and some controllers that delegate to the service layer. My code is much more readable and understandable when there is consistency across the project.

In Conclusion

I don't believe I've found a solution that I'll stick with forever, however, it works for me right now. Hopefully, this time next year, I will have grown to have a much better understanding of some things. So in a year, I'll write this up again and see what may be different, or the same. I DO believe this is ONE way to bring clarity to your codebase. After being away from a project for a while, I can easily get in "sync" with my code. Starting with my routes, following through to my controllers, FormRequests, Jobs, Models and Resources, everything has its place and fits nicely in the app.

Thanks!

Links

Date: September 12th at 10:22am

PREVIOUS

Blog Comments powered by Disqus.