DoeJohn's avatar

Extracting code (logic) into service-class: additional checks & returning error messages to the user

I am creating some simple web application with the user system:

  • Regular users can edit their profile (avatar, name, timezone), change password and change their email. There are 3 separate pages for that, one for editing profile (e.g. www.example.com/edit_profile), one for changing password (e.g. www.example.com/edit_passowrd) and one for changing email (e.g. www.example.com/edit_email).
  • Admins can go to the /admin area and edit profile (avatar, name, timezone), password and email of other user accounts. There is only one page for doing that, for example www.example.com/admin/users/{id}/edit.

There are two controllers, one is UserController for regular users, and the other one is in the Admin subdirectory (Admin/UserController) and it is used when admins edit users.

Because of this I am having a situation where UserController and Admin/UserController actions use the same code/logic, so I decided to extract that logic into its own service-class in app/Services/UserService.

For editing email I have one method in the UserService which I then use in both UserController & Admin/UserController. Then I have one method for editing the name, then one for editing password and so on... and I'm using them like this:

   public function __construct(UserService $userService)
   {
       $this->userService = $userService;
   }
   
   // In `Admin/UserController`:
   public function update(Request $request, $id) 
   {
       // Some code
       
       $user = $this->userService->setAvatar(Request $request, $user);
       $user = $this->userService->setName($request->name, $user);
       $user = $this->userService->setTmezone($request->timezone, $user);
       $user = $this->userService->setEmail($request->email, $user);
       $user = $this->userService->setPassword($request->password, $user);
       . . .
       $user->save();
   
       return back()->with('success', 'Updated!');
   }

   // In `UserController`:
   public function updateProfile(Request $request)
   {
       // Some code
       
       $user = $this->userService->setAvatar(Request $request, auth()->user());
       $user = $this->userService->setName($request->name, auth()->user());
       $user = $this->userService->setTmezone($request->timezone, auth()->user());

       . . .
       $user->save();
   
       return back()->with('success', 'Updated!');
   }

The problem is with the setAvatar(Request $request, User $user) method - it has a lot of checks (if statements) and in many cases I need to return some specific error message to the user. For example:

    <?php
    
    namespace App\Services;
    
    use App\User;
    use DB;
    use Illuminate\Http\Request;
    use Intervention\Image\ImageManagerStatic as Image;
    
    class UserService
    {
        /**
         * Set the avatar for the specified user.
         *
         * @param Request $request
         * @param User $user
         * @return User|\Illuminate\Http\RedirectResponse
         */
        public function setAvatar(Request $request, User $user)
        {
            // SOME CODE . . .
    
            if ($something) {
                 return back()->withInput()->withErrors(['avatar' => 'Please upload an avatar']); // <=========
             }
    
            // SOME CODE . . .

            if ($something_else) {
                 return back()->withInput()->with(['error' => 'some specific error message']); // <=========
            }

            // SOME CODE . . .

            if ($foo === $bar) {
                 if (! $request->avatar && $something) 
                 return back()->withInput()->with(['error' => 'another specific error message']); // <=========
            }

            // SOME CODE . . .
    
            $user->avatar = $this->saveAvatar($request, $user);
    
            return $user;
        }
    }

As you can see - in many cases I need to return \Illuminate\Http\RedirectResponse with the specific error message:

            if ($something) {
                 return back()->withInput()->with(['error' => 'Some specific error message about $something']);
            }

Of course, all these redirects will not work when I use this method in the controller:

    public function updateProfile(Request $request)
    {
        // Some code
    
        $user = $this->userService->setAvatar(Request $request, auth()->user());
    
        // Some code
    
        $user->save();
    
        return back()->with('success', 'Updated!');
    }

... and I understand why (it's clear), but I don't know how to handle this. I am not sure what is the proper way do it.

I was thinking about returning false instead of returning back()->withInput()->with(['error' => 'Some specific error message about $something']); - but then I will not know which check (if statement) returned false and what error message to show to the user.

Other way would be to return something like:

    ['status' => 'error', 'message' => 'specific message'] 
    
    or
    
    ['status' => 'ok', 'user' => $user] 

... and then in my controller I would do something like this:

    $resonse = $this->userService->setAvatar($request, auth()->user());
    if ($resonse['status'] === 'error') return back()->withInput()->with('error', $resonse['message']);
    if ($resonse['status'] === 'ok') $user = $resonse['user'];

... but I'm not sure if that's the right way to do it. Or maybe my approach with the class-service is completely wrong?

0 likes
6 replies
bobbybouwmann's avatar

Well, your problem is pretty clear because your service is combining the avatar validation with the implementation of how to save or update the avatar. You should pull these two apart.

So the basic idea should be

  1. User submits form
  2. Application handles validation
  3. If validation successful application handles saving using controllers and services and redirects user to page.

Right now you are combining step 2 and 3 in the UserService for the avatar. So you have two options here

The first option would be to do all the validation before anything hits the controller. You can use the Request classes for that: https://laravel.com/docs/5.6/validation#form-request-validation

The second option is using exceptions. Instead of returning a response, you simply throw an exception which you can catch in your controller and then based on that you can give a response to the user.

Based on your needs the first option sounds like the option in my opinion ;)

martinbean's avatar

@DoeJohn If you had gone with a “resource” based approach to your application, then it would still be “simple” and you wouldn’t need to extract code into service classes. Service classes (i.e. classes where you’re just stuffing logic so it’s neither in your controller nor model) is a sign there’s probably a better way.

Taking a resource approach, “profile”, “password” and “avatar” would become their own resources and have their own controllers with familiar methods such as edit and update. When you say the operators out loud, it becomes apparent which controller action to use for an operation: edit a profile, update a password, and so on.

Because you’re back to using controllers, you can then use things like form request classes for validation. So in the case of updating an avatar:

class AvatarController extends Controller
{
    public function __construct()
    {
        $this->middleware('auth');
    }

    public function edit()
    {
        // Show form for uploading avatar
    }

    public function update(UpdateAvatarRequest $request)
    {
        $request->user()->setAvatar($request->file('avatar'));

        // Redirect with success message
    }
}
class UpdateAvatarRequest extends FormRequest
{
    public function authorize()
    {
        return true;
    }

    public function rules()
    {
        return [
            'avatar' => 'required|image',
        ];
    }
}

If you’re manipulating things, think of what it is you’re manipulating and you can probably extract a resource from it. Resources don’t have to be models with a corresponding database table. If you’re editing or updating something, that’s usually a sign that there’s a resource to be uncovered in your application.

DoeJohn's avatar

@bobbybouwmann Hello bobby :)

I am already using Form Requests Validation, but the problem is:

  • Every user can use "static" or "dynamic" avatar, and they can set this on the "profile settings" page:
    • If a user is using dynamic avatar - it can be retrieved from Gravatar or social media (when they login with Facebook or Twitter account)...
    • If a user chooses to use "static avatar" (on his "profile settings" page) - then he must select some image, crop it and then he can save the changes (update his profile). But I'm using Croppie which encodes the cropped image with base64, and that base64 code is stored in the hidden input. This means that there is no image being uploaded, there is only base64 in the hidden input.

So I have to do some checks concerning the user's choice of a static or dynamic avatar, based on whether he had previously used a static or dynamic avatar ... and I don't think it's possible to move all those checks into Form Request class. For example, here is my setAvatar method in UserService:

class UserService
{
    /**
     * Set the avatar for the specified user.
     *
     * @param Request $request
     * @param User $user
     * @return User|\Illuminate\Http\RedirectResponse
     */
    public function setAvatar(Request $request, User $user)
    {
        // If static (custom) avatar was selected:
        if (! $request->dynamic_avatar) {
            // If the user has changed from dynamic to static (custom) avatar, he or 
            // she has to select an image and crop it using Cropper.js, which will 
            // store encoded (base64) avatar in 'avatar_code' hidden input.
            // Therefore, we treat 'avatar_code' as required if the user has changed 
            // from dynamic to static (custom) avatar:
            if ($user->dynamic_avatar && ! $request->avatar_code) {
                return back()->withInput()->withErrors(['avatar_not_selected' => 'Please upload an avatar']);
            }

            if ($request->avatar_code) {
                // SOME CODE
                
                // Save new avatar in the filesystem and set new the path:
                $user->avatar = $this->saveAvatar($request, $user);
            }
            
        // SOME CODE

        }

        // SOME CODE

        $user->dynamic_avatar = $request->dynamic_avatar;

        return $user;
    }
    
    . . .

Even more tricky (for me) is the protected method saveAvatar($request, $user) (also in UserService), because it validates base64 image, then its type (checks the if the type is. jpg or .png), decodes it (checks if the decoding failed) - and as far as I know - it is not possible to do all that in Form Request class:

    protected function saveAvatar(Request $request, User $user)
    {
        $avatar_code = $request->avatar_code;

        if (preg_match('/^data:image\/(\w+);base64,/', $avatar_code, $type)) {
            $avatar_base64 = substr($avatar_code, strpos($avatar_code, ',') + 1);
            $type = strtolower($type[1]);

            if (! in_array($type, ['jpeg', 'png', 'jpg'])) {
                // invalid type
                return back()->withInput()->with('error', 'some message about invalid type...');
            }

            $avatar_image = base64_decode($avatar_base64);

            if ($avatar_image === false) {
                // base64_decode failed
                return back()->withInput()->with('error', 'some message...');
            }
        } else {
            // did not match data URI with image data
            return back()->withInput()->with('error', 'some message...');
        }

    // some code
        
        return $avatar_path;
    }
    
    . . .

Maybe throwing exceptions (as you mentioned as second option) is the only way to do it in this case, but I'm not sure :)

DoeJohn's avatar

@martinbean Hi,

In my previous reply to @bobbybouwmann I tried to explain why I think that it's not possible to move all those checks into Form Request class (especially in case of saveAvatar method which validates base64 image, type and checks whether the decoding was successful...

As I mentioned in the opening post, there is:

  • One page (one form) for editing profile: here authenticated users (auth()->user()) can edit their own profile. On that page there is one form where they can set their avatar, name, timezone and some other stuff;
  • One separate page (form) for editing email: here authenticated users (auth()->user()) can change their email;
  • One separate page (form) for editing password...

On the other side, admin users have access to the /admin area where they can edit other users. When they edit user (/admin/users/{id}/edit) - there is only one big form which includes everything: profile (avatar, name, timezone...) + email + passowrd.

So, as for the taking a resource approach, I'm still not sure how to apply it in this case in order to avoid repeating the same logic (code) in the controllers used by the regular users and controllers used by the admin users (controllers that are in app/Http/Controllers/Admin subdirectory)...

I usually did that with using service-classes, but again - I have some specific cases here (as I described in my post above).

Sorry, my English is not good :)

bobbybouwmann's avatar

An exception is an exception right ;) As long as you or the framework catches the exception you're good to go ;)

Please or to participate in this conversation.