Skip to content

Instantly share code, notes, and snippets.

@ryanlong1004
Created October 19, 2020 22:48
Show Gist options
  • Save ryanlong1004/c3b97e0174617f1768354079b735361f to your computer and use it in GitHub Desktop.
Save ryanlong1004/c3b97e0174617f1768354079b735361f to your computer and use it in GitHub Desktop.
Model/View/Controller/Service/Gateway

Model / View / Controller / Service / Gateway

View

Views provide the UI/UX. Their responsibility is to take requests from the user to send to the associated controller, and display the response results of that request.

PHP code should be put into views only when necessary, as excessive amounts of PHP code mixed with HTML and JS add more cognitive load to the developer trying to work on the code.

<h1> Tasks </h1>
<hr />
<?=
$props = [];
$props['tasks'] = null;
?>

<?= foreach($this.GET($props) as $task) { ?>
    <li>Task Description <?= echo $task.getDescription(); ?> </li>
<?= } ?>

Controller

Controllers acts as the intermediary between the view and the service layers. They typically route to functionality based upon the type of request and the parameters passed to it.

Typically, they will return data to the view to be rendered in HTML. However, it is acceptable to create functions which turn data into HTML if the only alternative would be to pollute the view with excess PHP code.

The optional iTaskService parameter in the controllers constructor allows us to mock the service used to process the requests. This way, we are able to focus on what the controller does best and only that, while mocking out any services or other parts of the app the controller may require but is not responsible for.

class TaskController() {

    private $service;

    public function __construct($request, iTaskService $service=null) {
        $this.setService($service);
        $this.processRequest($request);
    }

    private function setService(iTaskService $service=null) {
        if (!$service) {
            $this.service = new TaskService();
        }
    }

    private function processRequest($request) {
        switch ($request.method) {
            case "GET":
                return $this.processGet($request);
            case "POST":
                return $this.processPost($request);
            case "PUT":
                return $this.processPut($request);
            case "DELETE":
                return $this.processDelete($request);
            default:
                throw new RequestException("Unknown request method");
        }
    }

    private function processGet($request){
        if array_key_exists($ids) {
            return $service.fetchTasks($ids);
        }
    }

    private function processPost($request) {
        if array_key_exists($tasks) {
            return $service.createTasks($tasks);
        }
    }

    // remaining request methods
}

Model

Models represent a blue print for objects (much the sameway a class does). They primarily act as a collection of properties.

However, its limits are not just getters and setters.

Models provide validation (as seen in the set methods) and also can be used to provide some business logic that does not change the underly properties it represents (ie isPastDue()).

Models should typically never mutate or persist data. However, translating data based on values is not only accepted but encouraged.

class Task {
    private id;
    private description;
    private dueDate;
    private isCompleted;

    public function __construct(id, description, dueDate, isCompleted) {
        $this.setId(id);
        $this.setDescription(description);
        $this.setDueDate(dueDate);
        $this.setIsCompleted(isCompleted);
    }

    public function getId() {
        return $this.id;
    }

    public function setId($value) {
        if (!is_numeric(value)) {
            throw new Exception("ID must be numeric");
        }
        $this.id = value
    }

    public function getDescription() {
        return $this.description;
    }

    public function setDescription($value) {
        if (!is_string(value)) {
            throw new Exception("Description must be a string");
        }
    }

    public function isPastDue() {
        return $this.getDueDate() >  date();
    }

    // remaining getters and setters...
}

Service Interface

The service interface is a contract. More so, it allows developers to rely on a module or package before it is finished being written. In the example below, you can see that the interface for the task service will provide basic crud operations.

fetchTasks() will require a variadic number of ids to retrieve task objects, while all other CRUD operations will require an instantiated task in order to perform the operation. Now the developer using the TaskService and the developer writing the task service can both work simultaneously.

In addition, the underlying "concrete" implementation of these methods can change at any time, while the calling code is none the wiser that there have been any changes.

interface iTaskService {
    public function fetchTasks(...$ids);
    public function createTasks(Task ...$tasks);
    public function updateTasks(Task ...$tasks);
    public function deleteTasks(Task ...$tasks);
}

Service Implementation

Services are where the majority of business logic will be done.

This is the layer that sits between the request/response(controller) and persistence(gateway) layers.

Notice in the constructor, we allow the caller to pass in their own gateway, even though a default will be used in production.

This is so that we can mock out the gateway layer in unit tests. In other words, we don't actually need to connect to a database to test the functionality. We can create a dummy or mock of how the gateway will respond to methods that are called on it.

private gateway;

class TaskService implements iTaskService {
    public function __construct(TaskGateway $gateway=null) {
        if (!$gateway) {
            $this.gateway(new TaskGateway());
        } else {
            $this.gateway($gateway);
        }
    }

    public function fetchTasks(...$ids) {
        return $this.gateway.fetchTasks($ids);

    }

    public function createTasks(Task ...$tasks) {
        return $this.gateway.createTasks($tasks);
    }

    // remaining required concrete implementations...
}

Gateway

Gateways are used for interaction with anything outside the immediate application code base. API's, databases, serverless functions, files and file systems, etc. It's primary purpose is to take application objects and convert them to and from a form that the endpoint can consume/deliver.

As noted in the service layer, the gateway layer constructor also can accept a $gateway parameter in its signature. In production, no parameter would be passed which would default into a new gateway being created for us. However, in unit testing this layer, we can mock the actual endpoint based off of what we know should be returned, allowing us to test the gateway without actually making any connections outside of the application.

class TaskGateway {

    private $gateway;

    public function __construct($gateway=null) {
        if (!$gateway) {
            $this.gateway = connectToDateBase(); // sudocode based on what we use to connect
        } else {
            $this.gateway = $gateway;
        }
    }

    public function fetchTasks(...$ids) {

        query = new Query("FROM tasks SELECT id, description, due_date, isCompleted where id = ?", $ids ?: "*");

        $results = $this.gateway.execute($query);

        tasks = [];
        foreach($results as $result) {
            tasks[] = new Task($result);
        }

        return tasks;
    }

    public function createTasks(Task ...$tasks) {
        try {
            foreach($tasks as $task) {
                query = new Query("INSERT into tasks(id, description, due_date, isCompleted") VALUES ("?,?,?,?)", $task.getId(), $task.getDescription(), $task.getDueDate(), $task.getIsCompleted()));
            }
            $this.database.commit();
            $taskIds = [];
            foreach($tasks as $task) {
                $taskIds[] = $task.getId();
            }
            return $this.fetchTasks($taskIds);
        } catch (DatabaseException $e) {
            $this.database.rollback();
            throw($e);
        }
    }

    // remaining database operations...
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment