Controller using Services, Events, Jobs, Actions - Laravel
Tags: laravel, php, webdev, programming
Published: 2022-06-03 11:29:17

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
```php
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');
}
```

- Validation: Form Request classes

```php
php artisan make:request StoreUserRequest
```
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
```php
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()],
        ];
    }
}
```
Finally, in our Controller 
```php
use App\Http\Requests\StoreUserRequest;
 
class RegisteredUserController extends Controller
{
    public function store(StoreUserRequest $request)
    {
        // No $request->validate needed here
 
        // Create user
        $user = User::create([...]) // ...
    }
}
```
- 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
```php
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;
    }
}
```
Finally, in our Controller 
```php
use App\Services\UserService;
 
class RegisteredUserController extends Controller
{
    public function store(StoreUserRequest $request, UserService $userService)
    {
        $user = $userService->createUser($request);
 
        // Login and other operations...
```
Service Class with Single Responsibility Principle
```php
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']
        ]);
    }
}
```
Finally, in our Controller
```php
public function store(StoreUserRequest $request, UserService $userService)
{
    $avatar = $userService->uploadAvatar($request);
    $user = $userService->createUser($request->validated() + ['avatar' => $avatar]);
 
    // ...
```

- 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
```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
        ]);
    }
}
```
Finally, in our Controller
```php
public function store(StoreUserRequest $request, CreateNewUser $createNewUser)
{
    $user = $createNewUser->handle($request);
 
    // ...
```

- 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
```php
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;
    }
}
```
Finally, in our Controller
```php
public function store(StoreUserRequest $request, UserService $userService)
{
	// ...
 
    Auth::login($user);
 
    $voucherCode = $userService->createVoucherForUser($user->id);
    $user->notify(new NewUserWelcomeNotification($voucherCode));
```
Something like this
```php
class UserService
{
    public function sendWelcomeEmail(User $user)
    {
        $voucherCode = $this->createVoucherForUser($user->id);
        $user->notify(new NewUserWelcomeNotification($voucherCode));
    }
```
Then, Controller will have only one line of code for this
```php
$userService->sendWelcomeEmail($user);
```

- Notifying Admins: Queueable Jobs

In this case, Laravel provides the Artisan command for us
```php
php artisan make:job NewUserNotifyAdminsJob
```
app/Jobs/NewUserNotifyAdminsJob.php
```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));
        }
    }
}
```
Finally, in our Controller
```php
use App\Jobs\NewUserNotifyAdminsJob;
 
class RegisteredUserController extends Controller
{
    public function store(StoreUserRequest $request, UserService $userService)
    {
    	// ...
 
        NewUserNotifyAdminsJob::dispatch($user);
```
So, now, we've moved all the logic from the Controller to elsewhere, and let's recap what we have
```php
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);
}
```

- Events/Listeners

```php
php artisan make:event NewUserRegistered
php artisan make:listener NewUserWelcomeEmailListener --event=NewUserRegistered
php artisan make:listener NewUserNotifyAdminsListener --event=NewUserRegistered
```
app/Events/NewUserRegistered.php
```php
use App\Models\User;
 
class NewUserRegistered
{
    use Dispatchable, InteractsWithSockets, SerializesModels;
 
    public User $user;
 
    public function __construct(User $user)
    {
        $this->user = $user;
    }
}
```
Then, the Event is dispatched from the Controller, like this
```php
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);
}
```
And, in the Listener classes, we repeat the same logic
```php
use App\Events\NewUserRegistered;
use App\Services\UserService;
 
class NewUserWelcomeEmailListener
{
    public function handle(NewUserRegistered $event, UserService $userService)
    {
        $userService->sendWelcomeEmail($event->user);
    }
}
```
And, another one
```php
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));
        }
    }
}
```

- Observers: "Silent" Events/Listeners

```php
php artisan make:observer UserObserver --model=User
```
app/Observers/UserObserver.php
```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));
        }
    }
}
```

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