MVC pattern from scratch
Sebastian Rapetti Nov 06, 2017
Before starting, the reader considers this my personal opinion, how I see the things and not the only way for implement or for use the MVC Pattern.
On internet, it's possible find a lot of implementations and MVC frameworks ready to use, I decided to start from scratch because this is the best method for know deeply a thing.
After hours of theorical and pratical study, for understand what a Model, a View, a Controller and what they do, questions multiply.
Often, I saw that the Pattern components' workload is unbalanced, Controller do the most of the job: receive and filter user input data, manipulate the Model, call the View etc.
Why if the Pattern has three components, the Controller should do the most of the job? Controller means that this component control others or the Pattern lifecycle? It not was better call it Controller Pattern?
After others studies, a theoretical MVC Pattern where components have responsibility equally divided, begins to take shape in my mind.
I have the components:
- Model -> Business Logic;
- View -> Show Informations;
- Controller -> Take User Input;
How to create a relation between thems without overload the Controller?
Considering that the Model should receive input data from Controller, process it, do all action needed; The View should be informed by the Model if data was updated for generate new well formatted output; The Controller should filter input data and activate the Model;
A possible solution could be...
- Model and View can speak through the Observer Pattern;
- Controller can pass data to the Model, invoking directly the proper Model' method;
- Controller and Model could also linked through the Observer Pattern, but this way is a bit more complicated because Model should be Subject and Observer at the same time.
And the View compared to Controller?
- Interaction between View and Controller could be unnecessary!
It's time, with previous "axioms" in mind, to write a lot of PHP code. :)
The Code
Is organized in three parents classes: Model, View, Controller that define the role and the relation of every component. After three classes for the concrete example: CalulatorModel, CalculatorView, CalculatorController. They inherit from parents classes. Finally the MVC pattern running.
Parents classes
Could be also abstract classes but not today.
Model
Contains methods for the Subject role (remind Observer Pattern), notify state change to the Observer.
<?php
/**
* Parent class for custom model classes.
*
* This class was implemented like part of Observer pattern
* https://en.wikipedia.org/wiki/Observer_pattern
* http://php.net/manual/en/class.splsubject.php
*/
class Model implements SplSubject {
/**
* @var object List of attached observerer
*/
private $observers;
/**
* @var array Data for notify to observerer
*/
private $updates = [];
/**
* Class Constructor.
*/
public function __construct() {
$this->observers = new SplObjectStorage();
}
/**
* Attach an Observer class to this Subject for updates
* when occour a subject state change.
*
* @param SplObserver $observer
*/
public function attach(SplObserver $observer) {
if ($observer instanceof View) {
$this->observers->attach($observer);
}
}
/**
* Detach an Observer class from this Subject.
*
* @param SplObserver $observer
*/
public function detach(SplObserver $observer) {
if ($observer instanceof View) {
$this->observers->detach($observer);
}
}
/**
* Notify a state change of Subject to all registered Observeres.
*/
public function notify() {
foreach ($this->observers as $value) {
$value->update($this);
}
}
/**
* Set the data to notify to all registered Observeres.
*
* @param array $data
*/
public function set(array $data) {
$this->updates = array_merge_recursive($this->updates, $data);
}
/**
* Get the data to notify to all registered Observeres.
*
* @return array
*/
public function get(): array {
return $this->updates;
}
}
View
Contains methods for the Observer role (remind Observer Pattern), retrive changes from the Subject.
<?php
/**
* Parent class for custom view classes.
*
* This class was implemented like part of Observer pattern
* https://en.wikipedia.org/wiki/Observer_pattern
* http://php.net/manual/en/class.splobserver.php
*/
class View implements SplObserver {
/**
* @var array Data for the dynamic view
*/
protected $data = [];
/**
* @var string Output data
*/
protected $output = '';
/**
* @var Model Model for access data
*/
protected $model;
/**
* Class Constructor.
*
* @param Model $model
*/
public function __construct(Model $model) {
$this->model = $model;
}
/**
* Render a template.
*/
public function render(): string {
return $this->output;
}
/**
* Update Observer data.
*
* @param SplSubject $subject
*/
public function update(SplSubject $subject) {
if ($subject instanceof Model) {
$this->data = array_merge($this->data, $subject->get());
}
}
}
Controller
On Controller there isn't a lot of say.
<?php
/**
* Parent class for custom controller classes.
*/
class Controller {
/**
* @var object The model object for current controller
*/
protected $model = null;
/**
* Class Constructor.
*
* @param object $model
*/
public function __construct(Model $model) {
$this->model = $model;
}
}
Concrete example classes, a basic calculator
I chose as example a basic calculator, it provide the possibility to add, subtract, multiply and divide, one or more number. I have prefered write a method for every math operation because is simpler follow the code.
Comparing this concrete example with the reasoning at the start of the article, I can say:
- Model: Perform mathematical operation and notify result to the View;
- View: Show operation and result as well formatted output;
- Controller: Filter user provided numbers and invoke the Model;
Calculator Model
<?php
/**
* Our Custom Model
*/
class CalculatorModel extends Model {
/**
* Class Constructor.
*/
public function __construct() {
parent::__construct();
}
/**
* Mutiply business logic.
*
* @param array $numbers
*/
public function multiply(array $numbers) {
$this->set([
'result' => $this->operation('*', $numbers),
'operands' => $numbers,
]);
}
/**
* Divide business logic.
*
* @param array $numbers
*/
public function divide(array $numbers) {
$this->set([
'result' => $this->operation('/', $numbers),
'operands' => $numbers
]);
}
/**
* Subtraction business logic.
*
* @param array $numbers
*/
public function sub(array $numbers) {
$this->set([
'result' => $this->operation('-', $numbers),
'operands' => $numbers
]);
}
/**
* Addition business logic.
*
* @param array $numbers
*/
public function add(array $numbers) {
$this->set([
'result' => $this->operation('+', $numbers),
'operands' => $numbers
]);
}
/**
* Do math operation.
* This is an example, division by 0 and other problematic things not checked.
* Danger - Very Horrible Code - need refactor.
*
* @param string $operator
* @param array $numbers
* @return int|float
*/
private function operation(string $operator, array $numbers) {
$temp = null;
foreach ($numbers as $n) {
if ($temp === null) {
$temp = $n;
continue;
}
//example, division by 0 and other not checked
switch ($operator) {
case '*':
$temp = $temp * $n;
break;
case '/':
$temp = $temp / $n;
break;
case '-':
$temp = $temp - $n;
break;
case '+':
$temp = $temp + $n;
break;
}
}
return $temp;
}
}
Calculator View
<?php
/**
* Our Custom Model
*/
class CalculatorView extends View {
/**
* Class Constructor.
*
* @param CalculatorModel $model
*/
public function __construct(CalculatorModel $model) {
parent::__construct($model);
}
/**
* Build output for multiply.
*/
public function multiply() {
$this->output = $this->generateOutput(
'Multiplication',
$this->data['operands'],
$this->data['result']
);
}
/**
* Build output for divide.
*/
public function divide() {
$this->output = $this->generateOutput(
'Division',
$this->data['operands'],
$this->data['result']
);
}
/**
* Build output for add.
*/
public function add() {
$this->output = $this->generateOutput(
'Addiction',
$this->data['operands'],
$this->data['result']
);
}
/**
* Build output for sub.
*/
public function sub() {
$this->output = $this->generateOutput(
'Subtraction',
$this->data['operands'],
$this->data['result']
);
}
/**
* Generate the output.
* Danger - Very Horrible Code - need refactor.
*
* @param string $operation
* @param array $operand
* @param type $result
*
* @return string
*/
private function generateOutput(string $operation, array $operands, $result): string {
$string = $operation . ': ';
foreach ($operands as $value) {
$string .= $value . ' * ';
}
$string = substr($string, 0, strlen($string) - 2);
$string .= '= ' . $result;
return $string;
}
}
Calculator Controller
<?php
/**
* Our Custom Controller
*/
class CalculatorController extends Controller {
/**
* Class Constructor.
*
* @param CalculatorModel $model
*/
public function __construct(CalculatorModel $model) {
parent::__construct($model);
}
/**
* Multiply input point.
*
* @param mixed $numbers
*/
public function multiply(... $numbers) {
//check user input
$this->checkOperands($numbers);
$this->filter($numbers);
//manipulate the model
$this->model->multiply($numbers);
}
/**
* Divide input point.
*
* @param mixed $numbers
*/
public function divide(... $numbers) {
//check user input
$this->checkOperands($numbers);
$this->filter($numbers);
//manipulate the model
$this->model->divide($numbers);
}
/**
* Sub input point.
*
* @param mixed $numbers
*/
public function sub(... $numbers) {
//check user input
$this->checkOperands($numbers);
$this->filter($numbers);
//manipulate the model
$this->model->sub($numbers);
}
/**
* Add input point.
*
* @param mixed $numbers
*/
public function add(... $numbers) {
//check user input
$this->checkOperands($numbers);
$this->filter($numbers);
//manipulate the model
$this->model->add($numbers);
}
/**
* Filter user supplied numbers.
*
* @param mixed $numbers
* @throws InvalidArgumentException
*/
private function filter(&$numbers) {
foreach ($numbers as $key => $number) {
switch (gettype($number)) {
case 'string':
$number[$key] = strtonum($number);
break;
case 'integer':
break;
case 'double':
break;
default:
throw new InvalidArgumentException('Not a number');
}
}
}
/**
* Convert a number given as string in the proper type (int or float).
* https://secure.php.net/manual/en/language.types.type-juggling.php
*
* @param string $string Number as string ex '1.0', '0.9' etc
* @return int|float
*/
private function strtonum(string $string) {
if (fmod((float) $string, 1.0) === 0.0) {
return (int) $string;
}
return (float) $string;
}
/**
* Check minimum numbers required for operations.
*
* @param array $numbers
* @throws ArgumentCountError
*/
private function checkOperands(array &$numbers) {
if (count($numbers) < 2) {
throw new ArgumentCountError('At least two numbers needed for operation');
}
}
}
Running
I wish multiply two numbers (2 and 3) with the basic calculator.
<?php
//create components
$model = new CalculatorModel();
$view = new CalculatorView($model);
$controller = new CalculatorController($model);
//attach the observer to the subject
$model->attach($view);
//call controller
$controller->multiply(2, 3);
//notify changes to observer
$model->notify();
//call view
$view->multiply();
//get the output
echo $view->render();
This code show as output:
Multiplication: 2 * 3 = 6
Finally
It was not easy, the Pattern seem very complicate and there isn't only one way for implement it, but after a bit of practice everything becomes simple, it is very flexible, is a great aids for the organization of code. It's Worth!
A final consideration about the runnig, at first look, code should not work because the View, after notify, is called outside the Model.
How the View (outside the Model) receive notifies if the scope of the object is different?
Model notify in local scope inside the instance, View calls method ($view->multiply()) in global scope. I think the solution could be found in "how php pass the objects to a function", according to the official documentation objects are passed by references by default but I'm not still sure it works for this.
If you wish try the code 3v4l.org can help you, code is on my Gist Account and require PHP7 for run.