One of the most common questions I see about Laravel is how to structure the project. Or, in other words, where to put the logic out of the Controllers? In this article, I will try to show the options, trying to shorten one Controller method as an example.
This is a text-form tutorial based on the section of my video course How To Structure Laravel Projects
We will discuss how to move the logic from the Controller, to...:
- Form Requests
- Eloquent Mutators
- Eloquent Observers
- Service Classes
- Action Classes
- Jobs
- Events and Listeners
- Global Helpers
- Traits
- Base Controllers
- Repository Classes
A lot to cover, huh? So let's get started.
Initial Controller code
Before starting to cleanup the Controller, here's the code which we will try to make shorter:
public function store(Request $request){ $this->authorize('user_create'); $userData = $request->validate([ 'name' => 'required', 'email' => 'required|unique:users', 'password' => 'required', ]); $userData['start_at'] = Carbon::createFromFormat('m/d/Y', $request->start_at)->format('Y-m-d'); $userData['password'] = bcrypt($request->password); $user = User::create($userData); $user->roles()->sync($request->input('roles', [])); Project::create([ 'user_id' => $user->id, 'name' => 'Demo project 1', ]); Category::create([ 'user_id' => $user->id, 'name' => 'Demo category 1', ]); Category::create([ 'user_id' => $user->id, 'name' => 'Demo category 2', ]); MonthlyReport::where('month', now()->format('Y-m'))->increment('users_count'); $user->sendEmailVerificationNotification(); $admins = User::where('is_admin', 1)->get(); Notification::send($admins, new AdminNewUserNotification($user)); return response()->json([ 'result' => 'success', 'data' => $user, ], 200);}
Quite a big method, right? Now, let's walk through the options to shorten it.
Notice: at the end of the day, it's your personal preference where to move the code, you MAY choose any option listed below.
Validation to Form Request
We will start by extracting validation into Form Request. In this example, validation is simple, with three fields, but in real life, you could have 10+ fields.
Actually, we have two parts of the validation:
- permissions
- input data validation
Both of them CAN be moved to the Form Request class.
Let's start by creating a Form Request:
php artisan make:request StoreUserRequest
Now we have the app\Http\Requests\StoreUserRequest.php
file which has two methods inside: authorize()
for permissions and rules()
for data validation. So, Form Request would look like this:
app\Http\Requests\StoreUserRequest.php:
class StoreUserRequest extends FormRequest{ public function authorize() { return Gate::allows('user_create'); } public function rules() { return [ 'name' => 'required', 'email' => 'required|unique:users', 'password' => 'required', ]; }}
In the Controller, instead of the default Request
class, we need to inject our StoreUserRequest
, and validated data can be accessed using the validated()
method from the Request. Now, the Controller will look like this:
public function store(StoreUserRequest $request){ $userData = $request->validated(); $userData['start_at'] = Carbon::createFromFormat('m/d/Y', $request->start_at)->format('Y-m-d'); $userData['password'] = bcrypt($request->password); $user = User::create($userData); $user->roles()->sync($request->input('roles', [])); //}
A few first lines of the Controller shortened. Let's move on.
Data Change to Mutators or Observers
Let's say that you want to transform some data before saving it into the database.
Two examples here formatting the date and encrypting the password.
Now, we do it in the Controller, but let's use Laravel Eloquent features for that. I will show you two methods, one using Mutators and the another using Observers.
Mutators
In Eloquent models, you can define Mutators. There are two ways how you can define them, the "old" way, and the "new" way. Below are examples in both ways:
Laravel 9 and below:
public function setStartAtAttribute($value){ $this->attributes['start_at'] = Carbon::createFromFormat('m/d/Y', $value)->format('Y-m-d');} public function setPasswordAttribute($value){ $this->attributes['password'] = bcrypt($value);}
Since Laravel 9:
protected function startAt(): Attribute{ return Attribute::make( set: fn ($value) => Carbon::createFromFormat('m/d/Y', $value)->format('Y-m-d'); )} protected function password(): Attribute{ return Attribute::make( set: fn ($value) => bcrypt($value)); )}
Observers
You can create the Observer by running command:
php artisan make:observer UserObserver --model=User
If you will open app/Observers/UserObserver.php
our created observer, you will see there are generated methods about events that already happened like created()
or updated()
. But you can define creating()
which will be called before creating a record.
app/Observers/UserObserver.php:
class UserObserver{ public function creating(User $user) { $user->start_at = Carbon::createFromFormat('m/d/Y', $user->start_at)->format('Y-m-d'); $user->password = bcrypt($user->password); }}
But this method isn't mentioned in Laravel documentation so probably it's not officially recommended. So if you do want to shorten the controller and move that logic somewhere I probably would recommend using Mutators.
So now, we don't need those two lines in the Controller, and we don't need the $userData
variable, we can pass validated data directly into the User create method.
public function store(StoreUserRequest $request){ $user = User::create($request->validated()); $user->roles()->sync($request->input('roles', [])); //}
Adding a Service Class
We continue transforming our Controller method and moving logic elsewhere. Now we get to lines which are probably the main logic of saving the data in the DB. For this, we will create a Service class.
There's no make:service
Artisan command, so manually create a PHP app/Services/UserService.php
file which would look like that:
namespace App\Services; class UserService{ public function create(array $userData): User { $user = User::create($userData); $user->roles()->sync($userData['roles']); return $user; }}
Here we make the create()
method which accepts an array of validated data. In this method, we create a user and sync roles, and then return the created user.
Now, how to call this service in the controller? There are at least two ways.
The first one is to initialize the service by doing the (new UserService())->create($request->validated())
and passing validated data to the create()
method.
The second is injecting into a method, our case store()
, type-hinting that and assigning a variable. So now our controller would like this:
public function store(StoreUserRequest $request, UserService $userService){ $user = $userService->create($request->validated()); //}
If you want to find out how that type-hinting magic works, I have this article: Laravel Service Container: What Beginners Need to Know
Service into Action: What's the Difference?
Another option instead of a Service class, is called Action class. Again, there's no make:action
Artisan command, so manually create a file app/Actions/CreateUserAction.php
. Inside, typically there's one method called handle()
or execute()
.
namespace App\Actions; class CreateUserAction{ public function execute(array $userData): User { $user = User::create($userData); $user->roles()->sync($userData['roles']); return $user; }}
To call this Action class in the Controller, you would just initialize the action and call the execute()
method by passing data to it.
public function store(StoreUserRequest $request){ $user = (new CreateUserAction())->execute($request->validated()); //}
As you can see, there's not much difference when using a Service class and an Action class. The difference is more like do you divide your logic:
- Either into model-related entities like
UserService
orTaskService
, with many methods inside - Or, each operation as an Action class like
CreateUserAction
orUpdateUserAction
, with one method
Of course, inside of that Action class, you may also have some private methods for more logic, if you have something more complicated.
Jobs and Queues
Imagine there is some kind of sequence of actions to be performed "in the background", in addition to creating the main logic. For example, after user registration, you want to prepare some demo data for the dashboard. You may want to put it into the background queue, so the user wouldn't wait for the operation to finish.
Jobs are very similar to Actions, but Jobs may be put in a Queue. And they have Artisan command to create them:
php artisan make:job NewUserDataJob
And our job would be like this:
class NewUserDataJob implements ShouldQueue{ use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; public function __construct(public User $user) {} public function handle() { Project::create([ 'user_id' => $this->user->id, 'name' => 'Demo project 1', ]); Category::create([ 'user_id' => $this->user->id, 'name' => 'Demo category 1', ]); Category::create([ 'user_id' => $this->user->id, 'name' => 'Demo category 2', ]); }}
All that's left is to dispatch the Job in the Controller:
public function store(StoreUserRequest $request){ $user = (new CreateUserAction())->execute($request->validated()); NewUserDataJob::dispatch($user); //}
And then, you need to separately configure everything around the queue. If you want to learn about that, I have a separate course Queues in Laravel
Events and Listeners
Another possible option for "tasks in the background" is to call the Event in the Controller, and allow different classes (current ones or the future ones) to "listen" to that event.
Imagine the scenario in which you need to inform some other classes that the new user is registered. For example, we want to update a Monthly Report. So first, let's make an Event class:
php artisan make:event NewUserRegistered
And a Listener:
php artisan make:listener MonthlyReportNewUserListener
Now we can dispatch the Event in the Controller, similar to a job:
public function store(StoreUserRequest $request){ $user = (new CreateUserAction())->execute($request->validated()); NewUserDataJob::dispatch($user); NewUserRegistered::dispatch($user); //}
Inside the Event, we need to accept a User, so every Listener class would have access to that parameter:
class NewUserRegistered{ public function __construct(public User $user) {}}
And then in the EventServiceProvider
, we register our Event should be listened to:
class EventServiceProvider extends ServiceProvider{ protected $listen = [ Registered::class => [ SendEmailVerificationNotification::class, ], NewUserRegistered::class => [ MonthlyReportNewUserListener::class, ] ];}
Inside the MonthlyReportNewUserListener
listener class, we have an $event
parameter in the handle()
method. Inside that we move the code from the Controller:
class MonthlyReportNewUserListener{ public function handle(NewUserRegistered $event) { MonthlyReport::where('month', now()->format('Y-m'))->increment('users_count'); }}
Another example of event-listeners come from Laravel itself.
In the Controller, we don't need to send email verification notifications, because it is already handled by Laravel's Registered
event and SendEmailVerificationNotification
listener.
But we can create another Listener to send more notifications: for example, notify admins about something.
First, create a Listener and register it in the EventServiceProvider
:
php artisan make:listener NewUserSendAdminNotifications
app/Providers/EventServiceProvider.php:
class EventServiceProvider extends ServiceProvider{ protected $listen = [ Registered::class => [ SendEmailVerificationNotification::class, ], NewUserRegistered::class => [ MonthlyReportNewUserListener::class, NewUserSendAdminNotifications::class, ] ];}
Now in the NewUserSendAdminNotifications
listener, in the handle()
method, move the code from the Controller. And you can access the User
from the $event
using $user->event
:
class NewUserSendAdminNotifications{ public function handle(NewUserRegistered $event) { $admins = User::where('is_admin', 1)->get(); Notification::send($admins, new AdminNewUserNotification($event->user)); }}
So now, the full Controller looks just like this, from 37 to just 10 lines of code:
public function store(StoreUserRequest $request){ $user = (new CreateUserAction())->execute($request->validated()); NewUserDataJob::dispatch($user); NewUserRegistered::dispatch($user); return response()->json([ 'result' => 'success', 'data' => $user, ], 200);}
Additionally, let's talk about a few more options that we didn't directly use here.
Global Helpers
Helper classes have been around for a while. It's any Class that has some helper methods related to some topic. For example, DateHelper
, or CurrencyHelper
. For example, earlier we created the Attribute startAt
in the User
model, which has some data manipulation that can be added to a helper.
protected function startAt(): Attribute{ return Attribute::make( set: fn ($value) => Carbon::createFromFormat('m/d/Y', $value)->format('Y-m-d'); )}
Create a new file app/Helpers/DateHelper.php
(there's no Artisan command for this), and inside the DateHelper
class, add the convertToDB()
method which will have the code from the attribute.
namespace App\Helpers; class DateHelper{ public static function convertToDB($date) { return Carbon::createFromFormat('m/d/Y', $date)->format('Y-m-d'); }}
Now you can change Attribute to use this helper:
protected function startAt(): Attribute{ return Attribute::make( set: fn ($value) => DateHelper::convertToDB($value); )}
And whenever you need to convert the date from the same format before saving it to DB, now you can just use this helper.
Repeating parts: Traits or Base Controller?
Returning the result may be a repeating part of various controllers, especially in API controllers.
One option is to create a separate logic for responses from API. There are at least two ways how you can do that.
First, you can create the logic in the Base Controller which is app/Http/Controllers/Controller.php
:
class Controller extends BaseController{ use AuthorizesRequests, DispatchesJobs, ValidatesRequests; public function respondOk($data) { return response()->json([ 'result' => 'success', 'data' => $data, ], 200); }}
And then in your "regular" Controller, the return would be changed to $this->respondOk()
.
public function store(StoreUserRequest $request){ $user = (new CreateUserAction())->execute($request->validated()); NewUserDataJob::dispatch($user); NewUserRegistered::dispatch($user); return response()->json([ 'result' => 'success', 'data' => $user, ], 200); $this->respondOk($user); }
Another option is to use Traits. For example, make the app/Traits/APIResponsesTrait.php
file and inside this trait create the same method respondOk()
:
namespace App\Traits; trait APIResponsesTrait{ public function respondOk($data) { return response()->json([ 'result' => 'success', 'data' => $data, ], 200); }}
Now you just need to add this Trait either to each controller or the Base Controller.
class Controller extends BaseController{ use AuthorizesRequests, DispatchesJobs, ValidatesRequests; use APIResponsesTrait; }
After adding a trait, you can use $this->respondOk()
method the same way.
Why not Repository Class?
Similarly to the Services and Actions, one of the options is moving logic to Repositories.
It was a very popular pattern in Laravel 4 and 5 days. But times have changed and not many people use it now.
Repository idea has been an extra layer on top of Eloquent, between Eloquent and Controller. But the idea of repositories comes from general programming theory that a repository is a layer between the Controller and the Database. It makes sense when you use programming languages or frameworks that doesn't have an Eloquent ORM mechanism.
In the case of Laravel, Eloquent itself is the layer between the Controller and Database, so it kinda acts as Repository Pattern.
Instead of
SELECT * FROM USERS;
you do
User::all();
That's why adding a Repository as another layer on top of already a repository-like layer doesn't make much sense, in my opinion, and doesn't give many benefits.
That's it in this tutorial. Again, I will repeat: you are free to choose whichever of the options or patterns I mentioned above, there are no strict rules. Your goal is for the code to be easily readable and maintainable by future developers, including yourself.
If you want more examples of Laravel structure, join my 2-hour course How to Structure Laravel Projects.
thanks for sharing detailed on the above topics
this post is so well done! thank you Povilas! i learned a lot!
Amazing.
Thank you for this very informative post! It makes learning Laravel so much easier.
Thank you for this very clear article. Love your courses and learned so much.
Thanks for this article. A little typo in the 'Observer' section. app/Observevrs/UserObserver.php
Thanks for filling out such a comprehensive list of possibilities.
Please let me add that Responsable interface introduced in Laravel 5.5 is also a viable approach for fat Laravel controllers reorganization. Basically a Responsable object has to implement
toResponse()
method which will be called by frameworkRouter
as shown here: https://github.com/laravel/framework/blob/5.7/src/Illuminate/Routing/Router.php#L734-L736In Laravel,
the Illuminate\Contracts\Support\Responsable
interface provides a convenient way to return a HTTP response which reduces the controller method body to only one line of code mostly. The response layer could also contain transformation code or content negotiation instead of using these features directly in the controller. Elaborating on your example that illustrates a user CRUD use-case, I would like to share with you a POC of the idea here hosting it on "replit.com": https://replit.com/@garata/Laravel-10-Responsable-Interface. This example returns four custom responses you can find hosted withinApp\Http\Responses
namespace. Even if the implementation technique is different here, I also like a lot yourAPIResponsesTrait
approach which is more Laravel style explotingtrait
PHP language feature.In order to start my replit shared project, just press on the large play button and then use Show code button placed on the right just in case you're curious about the usage of Laravel's Responsable interface to create view model objects or dedicated HTTP response objects.
Best regards.