Actions, Services, Events, Listeners, Observers, Traits and Jobs in Laravel 9 — The Difference.

Chimeremze Prevail Ejimadu
8 min readJan 3, 2023

--

Photo by Mohammad Rahmani on Unsplash

The debate severally have been how to structure your project in Laravel. There are several schools of thought but frankly, everything is based on preference.

There is no right or wrong answer when it comes to structuring code in Laravel. This is because Laravel gives us the option to do what we want where we want, this is both a blessing and a curse.

Do we put our business logic inside the controller? This can be okay if we have only one point of entry, but usually there might be more than one point of entry and different endpoints sharing the same code.

Definitions

Actions are classes that take care of one very specific task. it helps to follow the Single Responsibility principle of programming.

Service is a class where you implement related and common functionality that you want to be used in various places. it provides a ‘service’ to the controller. They return something — that’s the service they provide, unlike jobs or actions where nothing is usually expected to be returned.

We can note that there is no “php artisan make:service” or “php artisan make:action” because the service class as well as the action class is a normal php class with a fancy name.

Events provide a simple observer pattern implementation, allowing you to subscribe and listen for various events that occur within your application. Event classes are typically stored in the app/Events directory, while their listeners are stored in app/Listeners (From Laravel Docs)

Observers are used to group event listeners for a model eloquent.

Traits are a mechanism for code reuse in single inheritance languages such as PHP

Jobs are a generic name for any job (piece of code you want to execute) you would create and dispatch that are usually queued.

Why should we use all these?
Why shouldn’t we put all our logic in a controller?
Is it necessary to have all these in one project?
Is there a standard way of using all these?

This is all better explained with an example. Let’s face a simple scenario when you validate and create a user, upload their photo, log them in, assign them to groups, add points to their referrers, send them notification mail, notify admins of new user.

I believe you’ll agree that this is too much for one controller. It goes against everything in the single responsibility principle. we need to separate the logic and move the parts somewhere. But where exactly?

  • Services?
  • Jobs?
  • Events/listeners?
  • Action classes?
  • Traits?
  • Observers
  • Somewhere else?

The trickiest part is that all of the above would be the correct answers. That’s probably the main message you should take home from this article. I will emphasize it for you, in bold and caps.

YOU ARE FREE TO STRUCTURE YOUR PROJECT HOWEVER YOU WANT. (PovilasKorop)

So let us see how we can make use of some of these wonderful features in laravel and make our controller less congested. We will do this with a couple targets in mind; first, the controller should only accept inputs, call a method and return a view or response.

1. Form Validation

We can keep our request validations separately and define our rules as we want, you must know that everything here is all about preference. So we can create a request with the command

php artisan make:request CreateUserRequest

Next, we take out our validation rules and move them to the Request class that extends the FormRequest class.

Remember to change the authorize() method to return true:

<?php
use Illuminate\Validation\Rules\Password;
class CreateUserRequest extends FormRequest
{
public function authorize(){
return true;
}
public function rules()
{
return [
'name' => ['required', 'string', 'max:255'],
'email' => ['required', 'string', 'email', 'max:255', 'unique:users'],
'password' => ['required', 'confirmed', Password::defaults()],
];
}
}

2. Upload Avatar using Trait

Personally, I prefer to use traits for uploading photos because in many other parts of the application, we’ll need to upload photos, maybe for KYC (Know Your Customer), POP (Proof Of Payment), posts and stories upload, even product uploads too. In my article on how to use traits in laravel, I explained this in detail. we’ll pick the piece of code there and use this trait to upload photos.

<?php
namespace App\Traits;

use Illuminate\Http\Request;

trait ImageUpload {

/**
* @param Request $request
* @return $this|false|string
*/
public function upload(Request $request, $fieldname = 'image', $directory = 'images' ) {

if( $request->hasFile( $fieldname ) ) {
if (!$request->file($fieldname)->isValid()) {
flash('Invalid Image!')->error()->important();
return redirect()->back()->withInput();
}

return $request->file($fieldname)->store($directory, 'public');
}

return null;
}
}

3. Create User Action

Next, we can decide to use an Action class to create a user. We can also use a Service class rather, it all boils down to preference. First, let us use the action class to do this because of Single Responsibility Principle. We will use a service class later in our code.

So to create an action, create an Actions folder inside app and create CreateNewUser class file.

namespace App\Actions;
use App\Models\User;
use Illuminate\Support\Facades\Hash;

class CreateNewUser
{
public function handle(array $userData): User
{
return User::create([
'name' => $userData['name'],
'email' => $userData['email'],
'password' => Hash::make($userData['password']),
'avatar' => $userData['avatar']
]);
}
}

How is our controller looking now?

From 46 to 32 lines, we have reduced the complexity of our controller and made it easier to resuse these functions some other places. If we look at the CreateNewUser action, we can call this from a route, event, even artisan commands without being dependent on $request.

4. Create User Service

So next, we create a service that will add user to group and add bonus to the user’s referrer if there’s one. We can do this by creating a folder Services and creating a class file called UserService.

<?php
namespace App\Services;
use App\Models\User;
use App\Models\Group;
class UserService
{
public function addUserToGroupAndAwardReferrer(User $user): User
{
$this->addUserToGroup($user);
$this->awardReferrer($user);
}

public function addUserToGroup(User $user)
{

$group = Group::find($request->group_id);
$group->users()->attach($user->id);
return $user;
}

public function awardReferrer(User $user)
{
$referrer = User::find($user->referrer_id);
if($referrer)
$referrer->points += 5;
$referrer->update();
return true;
else
return null;
}
}

The service above has three methods, one adds the user to a group, the other adds points to the user’s referrer. The addUserToGroupAndAwardReferrer() method is used to call both methods and this is the one we are going to call in our controller.

Note that we can simply call both methods separately in the controller, but like I keep saying, it’s all about preference and what works for you.

What Next?

Here, we are going to use Observers to send email to the user, and we will use Jobs to send to admins. Because we presume that the notification will be sent to multiple admins, this action should be queued, so that it can be done in the background.

5. Set Observer to send user Email after creation

Observers are a nice way to do certain actions. Create an observer with the command below:

php artisan make:observer UserObserver --model=User

Then add this code to the created file at app/Observers/UserObserver.php

<?php
use App\Models\User;
use App\Notifications\WelcomeNotification;
use Illuminate\Support\Facades\Notification;
class UserObserver
{
public function created(User $user)
{
$user->notify(new WelcomeNotification());
}
}

This sends the notification email to the user, and one good thing about observers is that you don’t even get to call them in the controller. They are a passive way of dealing with events. You can perform a whole lot of other actions here because it all boils down to preference and for the sake of this article, I am keeping it simple and straightforward.

I must note that observers can be tricky since they are not directly called from the controller, and when the project grows, or when new persons join the project, they might not figure out that there’s an action that takes place from an observer, (Like their name, the seat quietly and observe in secret) which might become a headache.

6. Create Job to send admin notifications

Let’s first create a job.

php artisan make:job NewUserNotifyAdminsJob

Inside the created file at app/Jobs/NewUserNotifyAdminsJob.php

Note: you create your notifications as you wish using the command php artisan make:notification InvoicePaid You can check out the Laravel Docs on Notifications.

<?php
class NewUserNotifyAdminsJob implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
private User $user;
public function __construct(User $user)
{
$this->user = $user;
}
public function handle()
{
foreach (config('app.admin_emails') as $adminEmail) {
Notification::route('mail', $adminEmail)
->notify(new NewUserAdminNotification($this->user));
}
}
}

This does it. We can now call the Job in our controller together with the parameter $user.

Let us see what our controller should be looking like now

We can see that our controller is now very simple, readable and easy. You noticed that we haven’t even used Events and Listeners and we have a very smart looking controller.

There are several places we can uses Events and Listeners in Laravel and I’m going to show you an example finally. Let us create one event and two listeners for it. we’ll call the event NewUserRegistered and the events would be NewUserWelcomeEmailListener and NewUserNotifyAdminsListener respectively. When we do this, we can delete the observer since one of the listeners are doing exactly the same duty.

Always be sure to use very clear naming methods.

php artisan make:event NewUserRegistered
php artisan make:listener NewUserWelcomeEmailListener --event=NewUserRegistered
php artisan make:listener NewUserNotifyAdminsListener --event=NewUserRegistered

What the event does is to collect the user and pass it on to the listeners. See the code below and add it at app/Events/NewUserRegistered.php.

use App\Models\User;
class NewUserRegistered
{
use Dispatchable, InteractsWithSockets, SerializesModels;
public User $user;
public function __construct(User $user)
{
$this->user = $user;
}
}

Then what happens in the listeners? Let us look at both of them

at

use App\Events\NewUserRegistered;
use App\Notifications\WelcomeNotification;
class NewUserWelcomeEmailListener
{
public function handle(NewUserRegistered $event)
{
$event->user->notify(new WelcomeNotification());
}
}

And in the other

use App\Events\NewUserRegistered;
use App\Notifications\NewUserAdminNotification;
use Illuminate\Support\Facades\Notification;
class NewUserNotifyAdminsListener
{
public function handle(NewUserRegistered $event)
{
foreach (config('app.admin_emails') as $adminEmail) {
Notification::route('mail', $adminEmail)
->notify(new NewUserAdminNotification($event->user));
}
}
}

And that’s it. Now, let us modify our Controller with this approach and see what it looks like:

I love events because for more actions that you want to perform, all you have to do is to create more listeners, because where you called the event won’t change, but all the listeners that listen for that event will be perform one after another in their order of creation.

Final Notes

There’s is a whole lot of things to talk about when it comes to these beautiful features, but I’ve done what I could here and hope that it helps you understand the difference between these terms and when to use them.

And you must have have noticed that in several points in this article, I mentioned that it all depends on preference. So if your projects are not crafted this way, it’s perfectly okay. You can easily try out different ways and see what fits you. You only have to consider the team and the future of that project, make sure that whatever pattern you follow is easy to understand and won’t be a huge problem later.

You must understand that we used a very simple scenario and in real life, there might be larger and much more complex codes, and that’s when you’ll truly grasp the beauty of keeping your code short and clean and separate as possible.

Stay tuned!!! I will be back with some more cool Laravel tutorials in the next article. I hope you liked the article. Don’t forget to follow me 😇 and give some clap 👏. And if you have any questions feel free to comment.

Thank you 🙏

Thanks a lot for reading till end 🙏 You can contact me in case if you need any assistance:
Email: prevailexcellent@gmail.com
Github: https://github.com/PrevailExcel
LinkedIn: https://www.linkedin.com/in/chimeremeze-prevail-ejimadu-3a3535219
Twitter: https://twitter.com/EjimaduPrevail

--

--

Chimeremze Prevail Ejimadu

Laravel Developer + Writer + Entrepreneur + Open source contributor + Founder + Open for projects & collaborations. Hit FOLLOW ⤵