Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Understanding / improving a barebones MVC framework

I realize this topic has been asked and addressed repeatedly, and although I've read through countless similar questions and read through countless articles, I'm still failing to grasp a few key concerns... I'm attempting to build my own MVC framework for learning purposes and to better familiarize myself with OOP. This is for personal private use, not to imply that as an excuse for being lazy, but rather I'm not so concerned with having all the bells and whistles of more robust frameworks.

My directory structure is as follows:

public
- index.php
private
- framework
  - controllers
  - models
  - views
  - FrontController.php
  - ModelFactory.php
  - Router.php
  - View.php
- bootstrap.php

I have an .htaccess file with directs all requests to index.php, this file contains basic configuration settings such as timezone and global constants, and it then loads the bootstrap.php file. The bootstrap contains my autoloader for classes, starts the session, defines global functions to use throughout my project, and then calls the router. The router picks apart the request from the URL, validates it using a ReflectionClass, and executes the request in the form of example.com/controller/method/params.

All of my controllers extend the FrontController.php:

<?php
namespace framework;

class FrontController
{
    public $model;
    public $view;
    public $data = [];

    function __construct()
    {
        $this->model = new ModelFactory();
        $this->view = new View();
    }

    // validate user input
    public function validate() {}

    // determines whether or not a form is being submitted
    public function formSubmit() {}

    // check $_SESSION for preserved input errors
    public function formError() {}
}

This front controller loads the ModelFactory:

<?php
namespace framework;

class ModelFactory
{
    private $db       = null;
    private $host     = 'localhost';
    private $username = 'dev';
    private $password = '********';
    private $database = 'test';

    // connect to database
    public function connect() {}

    // instantiate a model with an optional database connection
    public function build($model, $database = false) {}
}

And base View:

<?php
namespace framework;

class View
{
    public function load($view, array $data = [])
    {
        // calls sanitize method for output
        // loads header, view, and footer
    }

    // sanitize output
    public function sanitize($output) {}

    // outputs a success message or list of errors
    // returns an array of failed input fields
    public function formStatus() {}
}

Finally, here is an example controller to demonstrate how a request is currently processed:

<?php
namespace framework\controllers;

use framework\FrontController,
    framework\Router;

class IndexController extends FrontController implements InterfaceController
{
    public function contact()
    {
        // process form if submitted
        if ($this->formSubmit()) {
            // validate input
            $name = isset($_POST['name']) && $this->validate($_POST['name'], 'raw') ? $_POST['name'] : null;
            $email = isset($_POST['email']) && $this->validate($_POST['email'], 'email') ? $_POST['email'] : null;
            $comments = isset($_POST['comments']) && $this->validate($_POST['comments'], 'raw') ? $_POST['comments'] : null;

            // proceed if required fields were validated
            if (isset($name, $email, $comments)) {
                // send message
                $mail = $this->model->build('mail');
                $to = WEBMASTER;
                $from = $email;
                $subject = $_SERVER['SERVER_NAME'] . ' - Contact Form';
                $body = $comments . '<br /><br />' . "\r\n\r\n";
                $body .= '-' . $name;

                if ($mail->send($to, $from, $subject, $body)) {
                    // status update
                    $_SESSION['success'] = 'Your message was sent successfully.';
                }
            } else {
                // preserve input
                $_SESSION['preserve'] = $_POST;

                // highlight errors
                if (!isset($name)) {
                    $_SESSION['failed']['name'] = 'Please enter your name.';
                }
                if (!isset($email)) {
                    $_SESSION['failed']['email'] = 'Please enter a valid e-mail address.';
                }
                if (!isset($comments)) {
                    $_SESSION['failed']['comments'] = 'Please enter your comments.';
                }
            }
            Router::redirect('contact');
        }

        // check for preserved input
        $this->data = $this->formError();

        $this->view->load('contact', $this->data);
    }
}

From what I'm able to understand, my logic is off for the following reasons:

  • Validation should be done in the Model, not the Controller. However, a Model should not have access to $_POST variables, so I am not entirely sure whether or not I'm doing this part correctly? I feel like this is what they call a "fat controller" which is bad, but I'm not sure what needs to change...
  • The controller should not send data to the View; instead, the View should have access to the Model to request its own data. So would moving the $data property out of the FrontController and into the ModelFactory, and then calling the View from the Controller without passing along data resolve this issue? Technically it would then adhere to the MVC flowchart, but the proposed solution seems like such an insignificant or even trivial detail, assuming it's that simple, which it probably isn't..
  • The part that has me questioning my whole implementation is that I have a User object that is instantiated with a users corresponding roles and permissions, and I've been trying to figure out how or more specifically where to create an isAllowed() method that can be called from both the Controller and View. Would it make sense then to put this method in the Model, since both the Controller and View should have access to the Model?

Overall, am I on the right track here or what glaring problems do I need to address in order to get on the right track? I'm really hoping for a personal response specific to my examples rather than a "go read this".. I appreciate any honest feedback and help.

like image 378
mister martin Avatar asked Nov 03 '22 21:11

mister martin


2 Answers

  • The $_POST superglobals should be abstracted by a request instance, as explained in this post.

  • Input validation is not the responsibility of the controllers. Instead it should be handles by domain objects within model layer.

  • Model factory is no a model.

  • Defining class parameter visibility as public breaks the object's encapsulation.

  • HTTP location header (redirect) is a form of response. Thus, it should be handled by view instance.

  • In its current form, your controllers are directly manipulating superglobals. This causes tight coupling to the globals state.

  • Authorization checks should be performed outside controller. Not inside of it.

  • Your "model factory" should instead be a service factory, that is injected in both controller and view. It would ensure that each service is instantiated only once and thus let your controllers work with same model layer's state.

like image 110
tereško Avatar answered Nov 10 '22 05:11

tereško


First, I think it's great you are trying to create you own framework. Many say that everyone should do this, if only for learning purposes.

Second, I would suggest you read this Wikipedia article on frameworks. Many people don't realize there are different patterns for routing (url dispatch, traversal), and views (push, pull).

Personally, I don't see a need to abstract out the super globals since they are already abstractions (by php) from the raw input (php://input) and can be modified. Just my opinion.

You are right that validation should be done by the Model. You don't validate forms, you validate data. As for the View accessing the data, that depends on the pattern you choose. You can push the data to the View, or the View can pull the data.

If you are curious, my attempt at a MVC basic framework is on github. Its 4 files, less 2K line of code (DB layer is 1K lines). It implements traversal (component) routing and pulls data, there were already plenty of frameworks that implement the alternate patterns.

like image 24
Brent Baisley Avatar answered Nov 10 '22 05:11

Brent Baisley