I'm working on my own "MVC" framework for learning purposes and for personal use. I use the "MVC" expression as a concept not a concrete design pattern. So I refer to MVC as a "separation concept" not a concrete thing since (mainly in the world of web) the implementation can be different in every framework.
Main
The htaccess rewrite rule says that everything (which is not a file or a directory) is passed to the index.php (bootstrap). A Router
instance is created and asked to process the URL and render the page.
Controller
The specific Controller
instance is instantiated by and its method is called by the Router
. The Router
extract the controller and action name from the URL.
Example:
localhost/user/viewall
is "translated to":
$controller = new UserController();
$controller->viewall();
So the Controller is something like an entry point.
View
I have a concrete View
class which is pretty simple, I can add, remove and get custom data which is used when the page is rendered, something like this:
public function setData($key, $value = null)
{
if (!is_string($key)) {
throw new Exceptions\TypeException("The \"key\" parameter must be a string");
}
$this->data[$key] = $value;
}
public function setFile($file)
{
if (!is_string($file)) {
throw new Exceptions\TypeException("The \"file\" parameter must be a string");
}
$this->file = $file;
}
public function render()
{
if (empty($this->file)) {
return "";
}
if (!empty($this->data)) {
extract($this->data);
}
// dirty-hack: use output buffering so we can easily check if a file can be included
// if not, simply reject the output and throw an exception (let the caller handle it)
// if the inclusion was successfull then return with the buffered content
ob_start();
if (!include($this->file)) {
ob_end_clean();
throw new Exceptions\FatalException("View file \"{$this->file}\" not found");
}
return ob_get_clean();
}
The "file" field is a "template file" which is currently a simple php file. This way it's possible to implement a custom template parser later on.
Model
The Model layer is the data representation and storage layer. I split up the persistence and representation. So I have a concrete class (like User) which represents a single user. I also have a UserService
class which is used to add/remove/modify/query user to/from the database or memory or anything (because it's an abstract class, different implementations can be created).
Because I want to write less boilerplate code I've created a simple "class inspector" class. It can be used to get (and parse) the doc comment of a class, its functions and fields. I use this inspector in the base Model class to "generate" automatic getters and setters. This place can be used to final-validate data.
Code example:
public function __call($name, $args)
{
// ...
$getMatches = StringHelper::match($name, "^get([a-zA-Z0-9]+)$");
if (!empty($getMatches)) {
$property = lcfirst($getMatches[0][1]);
if (!property_exists($this, $property)) {
throw new Exceptions\FatalException("Cannot execute {$name}: property {$property} not found");
}
$metadata = $this->inspector->getPropertyMeta($property);
if (!isset($metadata["@readonly"]) && !isset($metadata["@readwrite"])) {
throw new Exceptions\FatalException("Cannot execute getter on write-only property: {$property}");
}
return $this->$property;
}
// ...
}
Putting it all together
The Router
creates a new controller and call its action. The Controller
creates the corresponding view instance and pass the model layer's data to it as a function parameter. The View
instance then loads the required template file, set the data which is required by the template (strong coupling).
I also created a ServiceContainer
class which is used by the controllers. Because the Controller is instantiated by the Router I cannot use direct dependency injection and this was my solution for this problem.
Example
Here comes a simple example of a one-way data flow. The UserController
creates the UserView and call its viewUser(id)
function which loads the required template, sets the "presentable" data and done.
// input url: localhost/user/view/12
// =========================
// index.php
// =========================
// create services
$services = new Framework\ServiceContainer();
{
// TEST SERVICES
//$services->add("user", new UserServiceMock());
// REAL SERVICES
$services->add("user", new UserServiceDB($db));
}
// create routes
$router = new Framework\Router($services);
{
$router->addRoute("", "HomeController", "home");
}
// try to find a controller/action pair for this url and render the specified view
try {
$router->dispatchAndRender(new PageNotFoundView());
} catch (Exception $ex) {
die($ex->getMessage());
}
// =========================
// User.php
// =========================
class User extends Framework\Model
{
/**
* @readwrite
* @column
* @primary
* @type int
*/
protected $id;
/**
* @readwrite
* @column
* @type text
* @length 20
*/
protected $name;
// ...
}
// =========================
// UserService.php
// =========================
abstract class UserService
{
public abstract function findAll();
public abstract function findById($id);
// ...
}
// =========================
// UserController.php
// =========================
class UserController extends Framework\Controller
{
private $service;
private $view;
public function __construct($services)
{
$this->service = $services->get("user");
$this->view = new UserView();
$this->setView($this->view);
}
public function view($id)
{
$user = null;
if (is_numeric($id)) {
$user = $this->service->findById($id);
}
$this->view->viewUser($user);
}
}
// =========================
// MainLayoutView.php
// =========================
class MainLayoutView extends Framework\View
{
public function __construct()
{
$this->setFile("templates/MainLayout.php");
}
public function assignContent($contentView)
{
$this->setData("content", $contentView->render());
}
}
// =========================
// UserView.php
// =========================
class UserView extends MainLayoutView
{
public function viewUser($user)
{
$contentView = new Framework\View();
$contentView->setFile("templates/User_View.php");
if ($user) {
$userData = array();
$userData["id"] = $user->getId();
$userData["name"] = $user->getName();
// ... any other visible data
$contentView->setData("user", $userData);
}
$this->assignContent($contentView);
}
}
// =========================
// templates/User_View.php
// =========================
if (isset($user)) {
echo "id: {$user['id']}<br />";
echo "name: {$user['name']}<br />";
} else {
echo "<b>User not found</b>";
}
Questions
As you can see I totally separated the Model from the View Template, so the actual rendering does not know about the data in the Model layer. This looks good for me because of the separation. I also sometimes want to preprocess the data before sending it to rendering (like creating the correct date format and so on) and this can be done in the View. Is this a good idea?
The example shows a "one-way data flow" but what's about the forms? I have a
register()
function in theUserController
as well. It asks the UserView to read the form data (and check if it was sent at all). I've done this in the UserView because theUserController
does not know about the concrete form. Where should I validate the data? Before HTML5 the input fields of a form are sent as simple strings without restrictions (except the radio buttons and these of course).
I would put the validation in the Controller
. If I do that in the Controller
, I can check the incoming input with a pattern (eg. allow only letters and numbers for a username), check the length of the string (but this is determined by the model, isn't it?) and the most important aspect is that I can use the Model and Service directly to validate the data (eg. do not allow duplicated username).
You can see the skeleton of the "validation" in the following code:
public function register()
{
$formSent = $this->view->readRegisterForm($username, $password);
if ($formSent) {
$user = new User();
// validate input (using the User instance?)
// TODO
$valid = false;
if ($valid) {
// hash password
$password = password_hash($password, PASSWORD_DEFAULT);
$user->setName($username);
$user->setPassword($password);
$this->service->add($user);
$this->view->viewRegisterComplete($user);
return;
}
}
$this->view->viewRegisterForm();
}
Do you think it's a good idea to put the validation in the controller? My problem with the validation in the controller is that if the Model is (for some reason, so don't ask why) modified directly without using the Controller's function then the data wouldn't be validated.