Part One: Learning the Basic Patterns
Disclaimer: please pardon my spellogrammatico errors. I am not really a rough draft person. I type and write codes the split seconds as they crossed my mind, otherwise I will get extremely lazy and then will eventually hibernate like a tardigrade. I hope I do not add more confusion to this subject matter.
Today is the last day of my very short Spring Break and I pretty much did not do anything productive for days, except for learning the new programming language called hack from facebook. I thought it would be something that I would spend my entire Spring break, but my assumption was wrong. The language was pretty cool and easy to learn. I also believe that the Hack language is the next biggest innovation in Web development.
Brief history of the script and about me
Years ago, I was trying really hard to learn MVC design patterns and I came up with this MVC framework simulator script. The script will also include the template engines I added into it, mainly the smarty and TBS.
What I am trying to convey here is how easy it is to learn PHP MVC pattern, only if we know the basic foundations than just downloading a framework e.g. CI, CAke, symfony2 without knowing how they were built. If a 10th and 11th grader can hack it, I am definitely sure you can.
Special Thanks
My special thanks to Mr. Lorenzo De Leon Alipio for allowing me to present this script to him and making some corrections.
What you will learn in this Tutorial?
I am hoping that the readers will learn how the PHP MVC Framework operates. I am also hoping that by providing a miniature framework (less than 700 lines of codes excluding template files) script, people will be able to learn the mechanics behind the MVC design patterns.
What are the requirements?
1. Knowledge in OOPHP or OOP PHP.
2. Desire to learn and explore.
3. Ability to anlyze how the business logics are transported from one pattern to the next.
4. Xampp or equivalent.
5. Download the TBS(Tiny But Strong ) and Smarty template engines.
5. Coffee and lots of them. If coffee does not work, try sour gummy bears or worms.
What is MVC?
Please read here. Or, it can be simply defined as the model - view - controller design patterns. It is very important to realize is that MVC framework must be viewed like a miniature operating system designed to support the application within the scope of its domain.
What are the components of PHP MVC Framework?
System or core
System/libraries
System/Controller
system/Model
System/Routing
Application
Application/controller
Application/model
Application/View
Application/libraries
Template Engine
Request Processor and Bootstrap
Request Autoloader and Instantiator ( Singleton or Factory)
When I was 11th grader, I was able to create the logical graph shown below. Of course, I was not able to achieved this without the advice and mentoring of Mr. Lorenzo De Leon Alipio from Fullerton, CA. I use this graph as a guide for writing MVC framework based applications.
In real world application, PHP MVC framework must have a System or a Core. This system or core must include the 3 major components of the MVC. Mainly, the model, the view, and the controller.
Let's write our first system component called the Controller. Some framework refers to this as the parent controller. I will be writing this as an Abstract class with an abstract method to force our application to follow a certain pattern of doing things. Of course, once you become an expert, you really don't have to make it an abstract class.
Here is a classic parent controller found in CodeIgniter.
class CI_Controller {
private static $instance;
public function __construct()
{
self::$instance =& $this;
foreach (is_loaded() as $var => $class)
{
$this->$var =& load_class($class);
}
$this->load =& load_class('Loader', 'core');
$this->load->initialize();
log_message('debug', "Controller Class Initialized");
}
public static function &get_instance()
{
return self::$instance;
}
}
For the sake of simplicity, we will deviate from the above controller but with similar results. Below is the deviated system or parent controller. Please pay attention to the abstract function index(). The method Index() is mandatory requirement on all application controllers residing in this particular framework.
Structural form : System/Controller
Abstract Class Controller {
protected $model, $view;
function __construct() {
$this->view = Init_Object::get_Object('View');
$this->settings = Set::settings();
}
/*
* All controllers must have a mandatory index()method. This is for the purpose of strict pattern implementation
*/
abstract function index();
}
What we will noticed on the codes above is the constructor. The constructor is initiating an instance of the object view. I elected to do it this way because, I will not be creating a parent View class.
Let's create the Parent Model Class.
Structural form : System/Model
Abstract Class BaseModel{
public function __construct(){
$this->settings = Set::settings();
}
Abstract function Content();
}
Since, we will not be creating System/View on this tutorial, that is all we need for our basic MVC framework that can do lots of things. However, before we can use it, we need to write an application that will utilize this framework.
First, we need add a simple library for our framework.
System/Router : ** Warning!** this router is not intended for production server. This was written for the purpose of simulating and MVC framework and I have not tested this router in production server.
class Router{
public function __construct(){
$this->settings= Set::settings();
}
public function route(){
$pageinfo = pathinfo(basename($_SERVER['REQUEST_URI'],'.php'));
//$url = pathinfo(('http://domain.com/mymvc/title=hello_wolrd')); //just testing to make sure the request can be routed properly :)
if (strpos($pageinfo['filename'], 'title=') !== false){
$title = (explode('=',$pageinfo['filename']));
return array('cont'=>'read','title'=>$title[1],'status'=>TRUE);
}
else{
return $pageinfo['filename'];
}
}
}
The basic and simple router above is tailored to the type of application we will be creating later. So, if you are looking at it and it is making you confused, please don't be.. If you want an advance router, there are many router available from the packagist.org.
In this particular framework, we will have two options as an instance triggering mechanisms. It could be a singleton pattern or Factory pattern.
System/Singleton
final class Init_Object{
/*
* set for array, to handle multiple request at the same time
*/
private static $objects = array();
private function __construct(){}
public static function get_Object($class)
{
## instantiate the class to create an object
self::create($class);
## return the object
return self::$objects[$class];
}
private static function create($class)
{
##check if an instance of requested class exists
if (!array_key_exists($class , self::$objects))
{
self::$objects[$class] = new $class;
}
}
}
Second option System/Factory
final class Factory{
public function __construct(){}
public static function get_Object($class){
if(class_exists($class)){
return new $class();
}
else{
throw new Exception("Invalid Request.");
}
}
}
Both Singleton and Factory patterns are capable of returning any objects upon requests. Normally this will respond to uri request like http://localhost/simplemvc.php/read/title=some_title witht the help coming from the Router.
We can also add another library to our system to do the database connection. Although I will not be utilizing this, I will include it here to serve as sample of a simple PDO connector.
System/libraries/Database
class PdoConnect{
private $host,$db_name,$user,$pass;
public function __construct(){
$this->host ='';
$this->db_name = '';
$this->user = '';
$this->pass = '';
}
private function db_con(){
try{
$this->db = new PDO('mysql:host='.$this->host.';dbname='.$this->db_name.';charset=utf8', $this->user, $this->pass ,array(PDO::ATTR_PERSISTENT => true));
$this->db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_SILENT);
//echo 'connected';
return true;
}
catch(PDOException $e) {
return false;
die('There is a connection error: ' .$e->getMessage());
}
}
/*
* set as public because we want to use the persistent connection in the model
*/
public function db_cnx(){
return self::db_con();
}
}
Side NOtes
Important Notes About namespaces: As I have already mentioned on the attached screenshot, this script was written when I was about between 10th and 11th grade. At that time, PHP namespace was not introduced yet. Namespace was introduced sometime in version 5.3 ( I am just guessing here. It can be a different version).
Implementing a namespace on a Framework similar to the ones found in framework like symfony2 is pretty simple. For example, if we are to add namespace to our system controller, model, and to the libraries, we can easily add the namespace like this
<?php
namespace System\Controller
do the same on the model
namespace System\Model
and the libraries
namespace System\libraries
So for the singleton instance and for the objects it is trying to instantiate it can be as simple as this
use System\libraries as Singleton;
Singleton\Init_Object::get_Object('this_controller_object_name');
for the child controllers, namespace can be implemented just like this
use System\Controller as __sys;
Class ReadController extends __sys\Controller{
** end of side notes**
Let's move on..
Now, our simple MVC framework is pretty much complete. We can finally write our simple application. For this particular example, I will write the most hated example in MVC framework called the blog. I really don't know why a blog application example always come to mind? I guess the reason is that a blog utilizes a database for storage of the articles.
I also wanted to warn everyone for the second time, that this script was written long time ago and at that time, my data storage of choice was a text file and xml file. Later on, you will see how I did it.
Our first application
1. obviously it is a blog .... oh noooooo not again :). Sorry guys, that was the in-thing of the past.
2. We want to be able to write a simple article and store it as a text file.
3. We want a summary page with links to the page where the full article is shown.
4. We want our application to be two or three template engine capable. For this example, I will show you how to use TBS and Smarty. If given more energy, I will create the third template in PHP. Else, two template engine options should suffice.
Remember this is our application's general construct. I will be demonstrating how to use all these three variants.
in your htdocs directory (xampp), or www directory if using UService WAMP stack, create these directories and make sure they are writtable. If you just want to read and enjoy my ranting, don't worry I will be providing the files for you to experiment.
smarty/ mvctheme/theme_compile/ mvctheme/theme/ mvctheme/cache/ mvctheme/css/ mvctheme/config/ mvctheme/tbs/ simplemvcarticle/summary/ simplemvcarticle/menu/ simplemvcarticle/text/
Let's create our simple config class. This will be a final class which means I don't want any child class for this class.
final class Set{ public function __construct(){} public static function settings(){ //return $set_items; return(array( /* select template engine to use */ 'use_template' => TRUE, // always set to true 'use_smarty'=> FALSE, 'use_tbs_eng'=> TRUE, 'use_twig'=>FALSE, /* for testing the PHP as template file, set use_template to false */ 'no_tbs' => 'simplemvc_view', // this will be the template file the application will use. /* define smarty directories */ 'smarty_compile'=>'mvctheme/theme_compile/', 'smarty_theme'=>'mvctheme/theme/', 'smarty_cache'=>'mvctheme/cache/', 'smarty_config'=>'mvctheme/config/', /* define TBS theme dir */ 'tbs_theme'=> 'mvctheme/tbs/', /* define template engines */ 'template_eng_s'=> 'SMARTY', 'template_eng' => 'TBS', /* define template file extension */ 'tpl_ext' =>'.tpl', /* define directory and filenames for the application */ 'summary' =>'simplemvcarticle/summary/summary.txt', 'xml_menu' => 'simplemvcarticle/menu/menu.xml', 'article_dir'=>'simplemvcarticle/text/*.txt', /* some site settings */ 'site_name' => 'Learning MVC', 'copyright'=>'YourSite.Com 2007', 'default_con'=>'Main', // this is a default controller for falling back or must define an error page 'ex_error'=> 'Invalid Request.' )); } }
We will create our application's main landing page. This is the page responsible for handling requests and call the responsible controller if it exists, otherwise it will send the request to the default controller.
class SimpleMVC{
private $page , $router; public function __construct(){ $this->page = pathinfo(basename(__FILE__)); $this->router = Init_Object::get_Object('Router'); $this->settings = Set::settings(); $this->default_con = $this->settings['default_con']; if($this->router->route()){ $requested_controller = $this->router->route(); $requested_controller = (is_array($requested_controller)? $requested_controller['cont']: $requested_controller); } /* * we are making sure we are on this page */ if($requested_controller === $this->page['filename']){ $this->object = Init_Object::get_Object($this->default_con); return $this->object->index(); } /* * if are not on this script page, then we load the controller request */ elseif(class_exists($requested_controller)){ $this->object = Init_Object::get_Object($requested_controller); return $this->object->index(); } else{ /* * this is right place to load a modified error page or redirection */ $this->object = Init_Object::get_Object($this->default_con); return $this->object->index(); } } }
The class above will direct request like this http://localhost/simplemvc.php/read/title=about_metallica to the controller read and the application model will look for the article entitled about_metallica from the simplemvcarticle/text/ directory. The controller will then get the response from the model and assign it to the view. You will learn more about this data transport later.
- The Application Controllers. We need to write the main, add, article, and read controllers.
Main Controller
class Main extends Controller{
public function __construct(){
parent::__construct();
$this->main_model = Init_Object::get_Object('Main_Model');
}
public function index(){
$this->view->use_tbs($this->main_model->Content(),FileHelper::parse_menu(),'index',FALSE);
$this->view->use_smarty($this->main_model->Content(),null,'index',FALSE);
}
}
Add controller : responsible for adding articles to our boring blog example.
class Add extends Controller{
public function __construct(){
parent::__construct();
$this->validate = Init_Object::get_Object('Form_Validate');
$this->add_model = Init_Object::get_Object('Add_Model');
//$this->add_Post();
}
public function index(){
return $this->add_Post();
}
public function add_Post(){
if((isset($_POST['add']))&& ((!empty($_POST['title'])) ||(!empty($_POST['content']) || (!empty($_POST['poster']))))){
$user_input = $this->validate->check_input();
$this->add_model->add_to_db($user_input);
$showform = FALSE;
$data = (array(
'showform'=>$showform,
'copyright'=>$this->settings['copyright'],
'title' => $user_input['title'],
'article' => $user_input['content'],
'author'=> $user_input['poster']
)
);
$data_for_tbs[] = $data;
$this->view->use_tbs($data_for_tbs,null,'post',FALSE);
$this->view->use_smarty($data,null,'post',FALSE);
}
else{
$showform = TRUE;
$processor = 'Add';
$data = array('copyright'=>$this->settings['copyright'],'title'=>'register','showform' => $showform, 'processor' => $processor);
$data_for_tbs[] = $data;
$this->view->use_tbs($data_for_tbs,null,'add',FALSE);
$this->view->use_smarty($data,null,'add',FALSE);
}
}
}
Article Controller: Responsible for getting the articles from the database. I am not sure what came to my head, I bypassed the model here to demonstrate how a framework is capable of bypassing one pattern. I know, I should done this with the other controllers instead of this one. Oh well :)... what a 10th grader .
/*
* This is a demonstration of bypassing the model
*/
class Article extends Controller{
private $template = 'simplemvc_vew';
public function __construct(){
parent::__construct();
//$this->article_model = Init_Object::get_Object('Article_Model');
}
public function index(){
$ar_data = $this->article_model->content();
$data_for_tbs[] = $this->article_model->content();
return($this->view->set_content($ar_data,$this->template));
}
}
Read Controller: Responsible for getting the full version of the article as requested
class Read extends Controller{
private $title;
public function __construct(){
parent::__construct();
$this->router = Init_Object::get_Object('Router');
$this->title = $this->router->route()['title'];
}
public function index(){
$copyright = array('copyright'=>$this->settings['copyright']);
$art_array = FileHelper::parse_textfile('simplemvcarticle/text/'.$this->title.'.txt');
$data_for_tbs[] = array_merge($copyright,$art_array);
$this->view->use_tbs($data_for_tbs,FileHelper::parse_menu(),'read',FALSE);
$this->view->use_smarty(array_merge($copyright,$art_array),null,'read',FALSE);
}
}
Obeservations:
Noticed the public function index() on all of the controllers? This is the fullfillment of the abstract method index from our parent controller. In the real world application, specially with routers like toro router, the abstract method index() must be removed and replace it the method get().
Another thing that we should look at are these parts of the codes
$this->view->use_tbs($data_for_tbs,FileHelper::parse_menu(),'read',FALSE);
$this->view->use_smarty(array_merge($copyright,$art_array),null,'read',FALSE);
Those are two separate instances of the view object. One for Smarty and one for TBS template engines.
We create our application Models
Main Model
class Main_Model extends BaseModel{ public function __contruct(){ parent::__construct(); } public function Content(){ return(FileHelper::get_summary()); } }
Add Model : This is functioning as a true model as defined.
class Add_Model extends BaseModel{
public function __construct(){
parent::__construct();
}
public function Content(){}
/*
* @add_to_db
* creates text files
* return true
*/
public function add_to_db($items=array()){
## add to database function here... set an instance of the PDO;
/*
print_r($items);//proves that $items exists as reference
*/
## alternatively, we can create xml file for this article object. I will provide you with the scrpt
## just in case, you want to store article as an xml file.
$art_xml = $items['poster'].' | '.$items['title'].' | '.$items['content'].'\n';
$file_name = trim(mb_strtolower($items['title']));
$handle = @fopen(str_replace(" ", "_", 'simplemvcarticle/text/'.$file_name).'.txt','w');
fwrite($handle, $art_xml);
fclose($handle);
$filehandle=@fopen(Set::settings()['summary'],'a+');
$text_url = str_replace(" ","_", $file_name);
$content_sum = substr($items['content'], 0,200).'...';
$art_summary = $items['poster']." | ". $items['title']." | read/title=".$text_url." | ". $content_sum. "\n";
$content_art = substr($items['content'], 0,200).'... \n';
file_put_contents(Set::settings()['summary'], $art_summary, FILE_APPEND | LOCK_EX);
}
}
Article Model: Noticed I am trying to demostrate here the flexibility of not using the model at all. This will not do anything.
class Article_Model extends BaseModel{
public function __construct(){
//$this->main_model = Init_Object::get_Object('Main_Model');
parent::__construct();
}
public function Content(){}
}
Read Model
## We don't have read model, only for the reason of demonstrating that in some cases, model may not be necessary. However, if you are following the standards, you must create a model pair for every controllers in the application.
- The application's LONE View Class. Yes, most applications will only have ONE view. The reason is that View class is only responsible for the presentation logic. In the case of our framework in this tutorial, we will be using two template engines the TBS and the Smarty. The implementation of these template engines, the responsiblility of the View Class has been reduced to just forwarding which template engine to use based on the given definition of our Set Class above.
The View Class
class View{
public $tbs;
public function __construct(){
$this->settings = Set::settings();
if(file_exists('tbsclass.php') && ($this->settings['use_tbs_eng'])){
include_once('tbsclass.php');
$this->tbs = new clsTinyButStrong();
$this->tbs->SetOption( array('chr_open'=>'{% ', 'chr_close'=>' %}') );
return true;
}
elseif($this->settings['use_smarty'] && file_exists('smarty/Smarty.class.php')){
include_once('smarty/Smarty.class.php');
$this->smarty = Init_object::get_Object('Smarty');
$this->smarty->left_delimiter = '{% ';
$this->smarty->right_delimiter = ' %}';
$this->smarty->template_dir = $this->settings['smarty_theme'];
$this->smarty->compile_dir = $this->settings['smarty_compile'];
$this->smarty->caching = 0;
$this->smarty->setCompileCheck(true);
$this->smarty->cache_dir = $this->settings['smarty_cache'];
$this->smarty->config_dir = $this->settings['smarty_config'];
return true;
}
}
public function set_content(&$content=array(), $theme = null){
if(file_exists($theme.'.php')){
include_once($theme.'.php');
}
}
public function use_tbs($data=array(),$data2 = null,$theme=null,$cache=false){
if($this->tbs){
$this->tbs->LoadTemplate($this->settings['tbs_theme'].$theme.$this->settings['tpl_ext'],false);
$this->tbs->MergeBlock('content',$data);
if($data2){
$data3 = array_merge($data,$data2);
$this->tbs->MergeBlock('data',$data2);
}
if($cache === true){
//$this->tbs->PlugIn(TBS_CACHE, $theme, 3600,CACHE_DIR);
//$this->tbs->PlugIn(TBS_CACHE,$templateName,TBS_CACHELOAD,CACHE_DIR);
}
$this->tbs->Show();
return;
}
}
public function use_smarty($data=array(),$data2 = null,$theme=null,$cache=false){
if($this->smarty){
$this->smarty->assign('content', $data);
$this->smarty->assign('menu',FileHelper::parse_menu());
//print_r(FileHelper::parse_menu());
return $this->smarty->display($this->settings['smarty_theme'].$theme.$this->settings['tpl_ext']);
}
}
}
Observations: Noticed how I changed the delimiters for both the TBS and Smarty template engines? I changed it to the same delimiters found in TWIG, just in case someone will ask to do it in tWIG. At least, the transition of converting template files to twig compliant files will be extremely easy.
This conclude part one of this tutorial. Tomorrow, I will continue on the part two which is solely focusing on the application's template files for TBS and SMARTY and simple integration of the TinyMCE. I will be providing a Zip file for you to download.
I hope I did not bore you death reading this type of lengthy undrafted crapola (Cranberries, Apples and Granola mixed in one package).
Thanks for reading and I sincerely apologize for any typos and inconsistencies.
Veedeoo