DEV Community

Morcos Gad
Morcos Gad

Posted on

Controller using Services, Events, Jobs, Actions - Laravel

I found this wonderful and useful article https://laravel-news.com/controller-refactor that explains how to use Services, Events, Jobs, Actions with Controller and the difference between them and before that I researched a lot about their importance and it is better to use them in my projects better and in the following I will explain the idea in general

Imagine you have a Controller method for registering users that does a lot of things

public function store(Request $request)
{
    // 1. Validation
    $request->validate([
        'name' => ['required', 'string', 'max:255'],
        'email' => ['required', 'string', 'email', 'max:255', 'unique:users'],
        'password' => ['required', 'confirmed', Rules\Password::defaults()],
    ]);

    // 2. Create user
    $user = User::create([
        'name' => $request->name,
        'email' => $request->email,
        'password' => Hash::make($request->password),
    ]);

    // 3. Upload the avatar file and update the user
    if ($request->hasFile('avatar')) {
        $avatar = $request->file('avatar')->store('avatars');
        $user->update(['avatar' => $avatar]);
    }

    // 4. Login
    Auth::login($user);

    // 5. Generate a personal voucher
    $voucher = Voucher::create([
        'code' => Str::random(8),
        'discount_percent' => 10,
        'user_id' => $user->id
    ]);

    // 6. Send that voucher with a welcome email
    $user->notify(new NewUserWelcomeNotification($voucher->code));

    // 7. Notify administrators about the new user
    foreach (config('app.admin_emails') as $adminEmail) {
        Notification::route('mail', $adminEmail)
            ->notify(new NewUserAdminNotification($user));
    }

    return redirect()->route('dashboard');
}
Enter fullscreen mode Exit fullscreen mode
  • Validation: Form Request classes
php artisan make:request StoreUserRequest
Enter fullscreen mode Exit fullscreen mode

We move our validation rules from the controller to that class. Also, we need to add the Password class on top and change the authorize() method to return true

use Illuminate\Validation\Rules\Password;

class StoreUserRequest 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()],
        ];
    }
}
Enter fullscreen mode Exit fullscreen mode

Finally, in our Controller

use App\Http\Requests\StoreUserRequest;

class RegisteredUserController extends Controller
{
    public function store(StoreUserRequest $request)
    {
        // No $request->validate needed here

        // Create user
        $user = User::create([...]) // ...
    }
}
Enter fullscreen mode Exit fullscreen mode
  • Create User: Service Class

Next, we need to create a user and upload the avatar for them
In my case, I will create the app/Services/UserService.php class, with one method, for now

namespace App\Services;

use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;

class UserService
{
    public function createUser(Request $request): User
    {
        // Create user
        $user = User::create([
            'name' => $request->name,
            'email' => $request->email,
            'password' => Hash::make($request->password),
        ]);

        // Avatar upload and update user
        if ($request->hasFile('avatar')) {
            $avatar = $request->file('avatar')->store('avatars');
            $user->update(['avatar' => $avatar]);
        }

        return $user;
    }
}
Enter fullscreen mode Exit fullscreen mode

Finally, in our Controller

use App\Services\UserService;

class RegisteredUserController extends Controller
{
    public function store(StoreUserRequest $request, UserService $userService)
    {
        $user = $userService->createUser($request);

        // Login and other operations...
Enter fullscreen mode Exit fullscreen mode

Service Class with Single Responsibility Principle

class UserService
{
    public function uploadAvatar(Request $request): ?string
    {
        return ($request->hasFile('avatar'))
            ? $request->file('avatar')->store('avatars')
            : NULL;
    }

    public function createUser(array $userData): User
    {
        return User::create([
            'name' => $userData['name'],
            'email' => $userData['email'],
            'password' => Hash::make($userData['password']),
            'avatar' => $userData['avatar']
        ]);
    }
}
Enter fullscreen mode Exit fullscreen mode

Finally, in our Controller

public function store(StoreUserRequest $request, UserService $userService)
{
    $avatar = $userService->uploadAvatar($request);
    $user = $userService->createUser($request->validated() + ['avatar' => $avatar]);

    // ...
Enter fullscreen mode Exit fullscreen mode
  • Maybe Action Instead of Service?

Now, let's take a look at how our code would look in the case of the Action class.
Again, there's no php artisan make:action, you just create a PHP class. For example, I will create app/Actions/CreateNewUser.php

namespace App\Actions;

use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;

class CreateNewUser
{
    public function handle(Request $request)
    {
        $avatar = ($request->hasFile('avatar'))
            ? $request->file('avatar')->store('avatars')
            : NULL;

        return User::create([
            'name' => $request->name,
            'email' => $request->email,
            'password' => Hash::make($request->password),
            'avatar' => $avatar
        ]);
    }
}
Enter fullscreen mode Exit fullscreen mode

Finally, in our Controller

public function store(StoreUserRequest $request, CreateNewUser $createNewUser)
{
    $user = $createNewUser->handle($request);

    // ...
Enter fullscreen mode Exit fullscreen mode
  • Voucher Creation: Same or Different Service?

Since one of the features of Services is to contain multiple methods, I decided to not create a "lonely" VoucherService with one method. We'll do it in the UserService

use App\Models\Voucher;
use Illuminate\Support\Str;

class UserService
{
    // public function uploadAvatar() ...
    // public function createUser() ...

    public function createVoucherForUser(int $userId): string
    {
        $voucher = Voucher::create([
            'code' => Str::random(8),
            'discount_percent' => 10,
            'user_id' => $userId
        ]);

        return $voucher->code;
    }
}
Enter fullscreen mode Exit fullscreen mode

Finally, in our Controller

public function store(StoreUserRequest $request, UserService $userService)
{
    // ...

    Auth::login($user);

    $voucherCode = $userService->createVoucherForUser($user->id);
    $user->notify(new NewUserWelcomeNotification($voucherCode));
Enter fullscreen mode Exit fullscreen mode

Something like this

class UserService
{
    public function sendWelcomeEmail(User $user)
    {
        $voucherCode = $this->createVoucherForUser($user->id);
        $user->notify(new NewUserWelcomeNotification($voucherCode));
    }
Enter fullscreen mode Exit fullscreen mode

Then, Controller will have only one line of code for this

$userService->sendWelcomeEmail($user);
Enter fullscreen mode Exit fullscreen mode
  • Notifying Admins: Queueable Jobs

In this case, Laravel provides the Artisan command for us

php artisan make:job NewUserNotifyAdminsJob
Enter fullscreen mode Exit fullscreen mode

app/Jobs/NewUserNotifyAdminsJob.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));
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Finally, in our Controller

use App\Jobs\NewUserNotifyAdminsJob;

class RegisteredUserController extends Controller
{
    public function store(StoreUserRequest $request, UserService $userService)
    {
        // ...

        NewUserNotifyAdminsJob::dispatch($user);
Enter fullscreen mode Exit fullscreen mode

So, now, we've moved all the logic from the Controller to elsewhere, and let's recap what we have

public function store(StoreUserRequest $request, UserService $userService)
{
    $avatar = $userService->uploadAvatar($request);
    $user = $userService->createUser($request->validated() + ['avatar' => $avatar]);
    Auth::login($user);
    $userService->sendWelcomeEmail($user);
    NewUserNotifyAdminsJob::dispatch($user);

    return redirect(RouteServiceProvider::HOME);
}
Enter fullscreen mode Exit fullscreen mode
  • Events/Listeners
php artisan make:event NewUserRegistered
php artisan make:listener NewUserWelcomeEmailListener --event=NewUserRegistered
php artisan make:listener NewUserNotifyAdminsListener --event=NewUserRegistered
Enter fullscreen mode Exit fullscreen mode

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;
    }
}
Enter fullscreen mode Exit fullscreen mode

Then, the Event is dispatched from the Controller, like this

public function store(StoreUserRequest $request, UserService $userService)
{
    $avatar = $userService->uploadAvatar($request);
    $user = $userService->createUser($request->validated() + ['avatar' => $avatar]);
    Auth::login($user);

    NewUserRegistered::dispatch($user);

    return redirect(RouteServiceProvider::HOME);
}
Enter fullscreen mode Exit fullscreen mode

And, in the Listener classes, we repeat the same logic

use App\Events\NewUserRegistered;
use App\Services\UserService;

class NewUserWelcomeEmailListener
{
    public function handle(NewUserRegistered $event, UserService $userService)
    {
        $userService->sendWelcomeEmail($event->user);
    }
}
Enter fullscreen mode Exit fullscreen mode

And, another one

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));
        }
    }
}
Enter fullscreen mode Exit fullscreen mode
  • Observers: "Silent" Events/Listeners
php artisan make:observer UserObserver --model=User
Enter fullscreen mode Exit fullscreen mode

app/Observers/UserObserver.php

use App\Models\User;
use App\Notifications\NewUserAdminNotification;
use App\Services\UserService;
use Illuminate\Support\Facades\Notification;

class UserObserver
{
    public function created(User $user, UserService $userService)
    {
        $userService->sendWelcomeEmail($event->user);

        foreach (config('app.admin_emails') as $adminEmail) {
            Notification::route('mail', $adminEmail)
                ->notify(new NewUserAdminNotification($event->user));
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

I hope you enjoyed the code and wish you happy code in your future projects.

Discussion (0)