1

I'm struggling on designing a solid architecture for my project.
In particular, I don't know how to handle the communication between the models and the controllers.

My goals are:

  • Following the Separation of Concerns principle and have the models as independent as possible.
  • Having internationalization support for the result messages
  • Being able to interpolate result messages with arguments (just like for the logs described in PSR-3)
  • Being able to couple result messages with a severity level and a HTTP status code
  • Being able to use the same controller for HTML and JSON responses

I've read a LOT of code bases and none I've seen does properly all of the above.
Either because don't do error handling at all or because they just do all the stuff in the model.

So I've come to this approach, but I'm not sure if I'm doing it right or if I've done something terrible.

First of I'll list here the main aspects of my code:

  • The Model is decoupled from all other classes
  • The only thing that is "coupled" in the model is it's the structure of $results
  • The controller glues together the various layers of the application; it calls the controller and just sets things up in the right place, ready for building a response, may it be a raw JSON (for an API response) or may it be a HTML response built with a template engine.
  • The result messages are internationalized and can be further customized with some arguments. (e.g. "Yo, welcome back {user_name}" or "There was an error while processing the array named {name} at this key index: {key}")

Here's an example of a controller, UserController:

    class UserController {
    
        public function __construct(
            private User $user,
            private ResponseHandler $response_handler
        ) {}
    
        public function changeAvatar() {
            $this->user->changeAvatar( /* arguments */ );
            $this->handleResults($this->user);
        }
    
        private function handleResults( $model ) {
            // NOTE: this function *could also be put in the front controller / main caller
            // so that we don't have to repeat this in every controller.
            // *should/is, I'm placing it here so you can see how it works
    
            $map = array(
             'User' => array(
                User::ERROR_INVALID_AVATAR_EXTENSION => array(
                    400,
                    TEXT_INVALID_AVATAR_EXTENSION,
                    LogLevel::ERROR
                ),
                User::ERROR_OVERSIZED_AVATAR => array(
                    // 413 = payload too large, idk if it fits here
                    // anyway it's just an example
                    413,
                    TEXT_OVERSIZED_AVATAR,
                    LogLevel::ERROR
                ),
                User::ERROR_CANNOT_CREATE_AVATAR => array(
                    500,
                    TEXT_CANNOT_CREATE_AVATAR
                    LogLevel::ERROR
                )
                User::STATUS_AVATAR_CREATED => array(
                    // 201 = created
                    201,
                    TEXT_AVATAR_CREATED,
                    LogLevel::INFO
                ),
                User::STATUS_AVATAR_UPDATED => array(
                    200,
                    TEXT_AVATAR_UPDATED,
                    LogLevel::INFO
                )
              ),
              'SomeOtherModel' => array(
                SomeOtherModel::STATUS_IDK_SUCCESS => array(
                    200,
                    TEXT_BLAHBLAH,
                    LogLevel::INFO
                )
            );
    
            foreach($model->results as $result) { // of course before we check if the key exists in $map
                if(in_array($result[0], $map[get_class($model)])) {
                    $this->response_handler->setStatusCode($map[$result[0]][0]);
                    // addMessage( level, message, interpolation args )
                    $this->response_handler->addMessage($map[$result[0]][2], $map[$result[0]][1], $result[1]);
                } else {
                    $this->response_handler->setStatusCode(500);
                    $this->response_hanlder->addMessage(TEXT_UNDEFINED_RESULT);
                }
            }
    
            // Then in the caller we one of these:
            // $response_hanlder->buildJSONResponse();
            // $response_hanlder->buildHTMLResponse();
        }
    }

And here is the User model:

    class User {
        public $results = array();
        public const ERROR_INVALID_AVATAR_EXTENSION = 0;
        public const ERROR_OVERSIZED_AVATAR = 1;
        public const ERROR_CANNOT_CREATE_AVATAR = 2;
        public const STATUS_AVATAR_CREATED = 3;
        public const STATUS_AVATAR_UPDATED = 4;
    
        public function changeAvatar( /* arguments */ ) {
            if( /* invalid extension  */) {
                $this->results[] = array(
                    self::ERROR_INVALID_EXTENSION,
                    array( /* details for the interpolation message */)
                );
                return false;
            }
            if( /* oversized avatar  */) {
                $this->results[] = array(
                    self::ERROR_OVERSIZED_AVATAR,
                    array( /* details for the interpolation message */)
                );
                return false;
            }
            try {
                if( /* avatar exists */ ) {
                    /* update avatar */
                    $this->results[] = array(
                        self::STATUS_AVATAR_UPDATED,
                        array( /* interpolation args */)
                    );
                } else {
                    /* create avatar */
                    $this->results[] = array(
                        self::STATUS_AVATAR_CREATED,
                        array( /* interpolation args */)
                    );
                }
                return true;
            } catch( /* idk exception */ ) {
                if( /* cannot create avatar */) {
                    $this->results[] = array(
                        self::ERROR_CANNOT_CREATE_AVATAR,
                        array( /* details for the interpolation message */)
                    );
                }
                return false;
            }
        }
    }

So what do I feel bad about this approach?
The $results array. It's structure is "tied" to the models, and this could be considered a form of coupling although it relies on the language's primitives.
I could write a wrapper class for the results, like class Result{ public int $error_code; public array $interpolating_args; }, but that would mean tying the models to this new class and I don't like it.

What about trying different approaches?
For interpolating the strings we need the arguments, which are in the model, and all the ways to get out those arguments are kinda ugly.
The alternative would be to interpolate the result messages directly within the model, but this would couple the models with the string interpolation code.

I don't see any other way.
I'm a bit skeptical about adding more abstraction layers to the models; I could extract the error codes from the model class to build an ad-hoc error class for every model, though with this I would triplicate the models files and the core logic behind the whole thing would be the same.
See the code below:

class UserErrorType {
    public const ERROR_INVALID_AVATAR_EXTENSION = 0;
    public const ERROR_OVERSIZED_AVATAR = 1;
    public const ERROR_CANNOT_CREATE_AVATAR = 2;
    public const STATUS_AVATAR_CREATED = 3;
    public const STATUS_AVATAR_UPDATED = 4;
}
class UserError {
        public UserErrorType $type;
        public array $interpolation_args;
    public function __construct(UserErrorType $type, array $interpolation_args){
        $this->type = $type;
        $this->interpolation_arg = $interpolation_arg;
    }
}

TL-DR;
My issue ties down to this problem: given that I want my models to be modular (meaning that they could be pulled out as they are from my project and easily put into any other stuff) what would be a 'formal way' for them to 'communicate' outside?
The only ways for 'communicating outside' I can come up with are two: a $result array and a more neat Result class
the Result class though has to come with every module you want to plug-in/plug-out, therefore I don't like it

0

2 Answers 2

2

Quick Answer:

I Did a similar thing, years ago.

In case of the $results, go for the O.O. wrapper approach. And, yes it will be more code, but more controlled.

Specially, when you have too many parameters, and those parameters although displayed as text, are originally of different type, and the code of conversion can be handle in that new class, called by the UserController class, instead of the UserController class itself.

And also getting the localized text.

In case of the HTML & JSON responses, you would need also to have at least three classes: a base class plus each subclass.

1
  • The word Specially seems out of place. Did you mean especially instead?
    – Sam Onela
    Commented Mar 28 at 6:26
1

In MVC design pattern there are three components: (1) controller including a front controller that delegates request processing to specialised controllers, (2) model that includes data model used to transport information between layers composed of value objects, data transfer objects, entities and business model composed of helpers, services, use cases, (3) view including response formatters, mostly template processing libraries.

To respond to requests the information is some way composed by the business model (retrieved from a persistence environment or from web services, or computing the input sent to the controller with the request), wrapped in data model and sent to the controller that pass it to the view to be formatted in the format the client can work with. To have a neat data model define result classes per request type (that is per controller). The example from the description is UserController that should have a UserResult value object to describe its response; it could have an array or a map describing the response though the value object approach centralises the response description in one place while with an array or a map the response description is spread over the business model composing the data for the response. While an array or a map leverages a dynamic response a value object per response leverages maintainability.

You will thank yourself later if you decide to use value objects to describe the responses.

Not the answer you're looking for? Browse other questions tagged or ask your own question.