0

The code examples are in PHP but the question is language agnostic.

Situation

I'm trying to figure out the best way to separate a service layer into multiple well defined layers.

In the example below I'm uploading a base64 encoded user avatar and showing how it would go through the layers. I'm using the decorator pattern to simulate the layers.

Important: The data passed into each layer is usually changed in some way before it is passed into the next layer which is exactly what I'm looking for. The one thing I don't like about this is that in order to update an avatar you must first talk to the ValidatedProfile object instead of say a Profile object. Something about it seems weird but I could always have a Profile object which delegates calls to the ValidatedProfile.

The Layers

  1. Validation: This is where you validate the data. As in the example below it is where you check the format of the $avatar string and ensure it is a valid image resource. During the validation process entity objects and resources are often created which are then passed to the next layer.
  2. Verification: Perform checks such as verifying if a supplied ID is real. As in the example below this is where I check if the supplied user ID is actually the real ID of a user.
  3. Commander: Where the action to be performed happens. By the time this layer is reached the data is thought to be fully validated and verified and no further checks need to be done on it. The commander delegates the action to other services(usually entity services) and can also call other services do more actions.
  4. Entity: This layer works with actions to be performed on an entity and/or its relations.

ValidatedProfile

class ValidatedProfile 
{
    private $verifiedProfile;

    /**
     * @param string $avatar Example: data:image/png;base64,AAAFBfj42Pj4
     */
    public function updateAvatar($userId, $avatar)
    {
        $pattern = '/^data:image\/(png|jpeg|gif);base64,([a-zA-Z0-9=\+\/]+)$/';
        if (!preg_match($pattern, $avatar, $matches)) {
            // error
        }

        $type = $matches[1]; // Type of image
        $data = $matches[2]; // Base64 encoded image data

        $image = imagecreatefromstring(base64_decode($data));
        // Check if the image is valid etc...

        // Everything went okay
        $this->verifiedProfile->updateAvatar($userId, $image);
    }
}

VerifiedProfile

class VerifiedProfile
{
    private $profileCommander;

    public function updateAvatar($userId, $image)
    {
        $user = // get user from persistence
        if ($user === null) {
            // error
        }

        // User does exist 
        $this->profileCommander->updateAvatar($user, $image);
    }
}

ProfileCommander

class ProfileCommander
{
    private $userService;

    public function updateAvatar($user, $image)
    {
        $this->userService->updateAvatar($user, $image);

        // If any processes need to be run after an avatar is updated
        // you can invoke them here.
    }

UserService

class UserService
{
    private $persistence;

    public function updateAvatar($user, $image)
    {
        $fileName = // generate file name

        // Save the image to disk.

        $user->setAvatar($fileName);

        $this->persistence->persist($user);
        $this->persistence->flush($user);
    }
}

You could then have a Profile class like the following:

class Profile
{
    private $validatedProfile;

    public function updateAvatar($userId, $avatar)
    {
        return $this->validatedProfile->updateAvatar($userId, $avatar);
    }
}

That way you just talk to an instance of Profile rather than ValidatedProfile which makes more sense I think.

Are there better and more widely accepted ways to achieve what I'm trying to do here?

ibanore
  • 1,500
  • 1
  • 12
  • 25

1 Answers1

0

I think you have too many layers. Two major objects should be enough for an operation like this. You need to validate the avatar input and persist it somehow.

Since you need to validate the user id and it is tied to persistence somehow, you can delegate that to the UserService object.

interface UserService {

    /**
     * @param string $userId
     * @param resource $imageResource
     */
    public function updateAvatar($userId, $imageResource);

    /**
     * @param string $userId
     * @return bool
     */
    public function isValidId($userId);
}

This checking for a valid user id should be a part of request validation. I would not make it a separate step like verification. So UserAvatarInput can handle that (validation implementation is just an example), and a little wrapper method to persist it all.

class UserAvatarInput {

    /**
     * @var UserService
     */
    private $userService;

    /**
     * @var string 
     */
    private $userId;

    /**
     * @var resource
     */
    private $imageResource;

    public function __construct(array $data, UserService $service) {
        $this->userService = $service; //we need it for save method
        $errorMessages = [];

        if (!array_key_exists('image', $data)) {
            $errorMessages['image'] = 'Mandatory field.';
        } else {
            //validate and create image and set error if not good
            $this->imageResource = imagecreatefromstring($base64);
        }

        if (!array_key_exists('userId', $data)) {
            $errorMessages['userId'] = 'Mandatory field.';
        } else {
            if ($this->userService->isValidId($data['userId'])) {
                $this->userId = $data['userId'];
            } else {
                $errorMessages['userId'] = 'Invalid user id.';
            }
        }

        if (!empty($errorMessages)) {
            throw new InputException('Input Error', 0, null, $errorMessages);
        }
    }

    public function save() {
        $this->userService->updateAvatar($this->userId, $this->imageResource);
    }

}

I used an exception object to pass validation messages around.

class InputException extends Exception {

    private $inputErrors;

    public function __construct($message, $code, $previous, $inputErrors) {
        parent::__construct($message, $code, $previous);
        $this->inputErrors = $inputErrors;
    }

    public function getErrors() {
        return $this->inputErrors;
    }

}

This is how a client would use it, for example:

class UserCtrl {

    public function postAvatar() {
        try {
            $input = new AvatarInput($this->requestData(), new DbUserService());
            $input->save();
        } catch (InputException $exc) {
            return new JsonResponse($exc->getErrors(), 403);
        }
    }

}
Weltschmerz
  • 2,166
  • 1
  • 15
  • 18