I'd love some feedback about this code that I'm editing and shortening now.
<?php
namespace bbn\cls;
class mvc
{
use \bbn\traits\info;
// From \bbn\traits\info.
protected static $info=array();
protected static $cli=false;
// Is set to null while not routed, then false if routing was unsucessful, and 1 if routing was done.
private $is_routed;
// Is set to null while not controled, then false if controller was not found, and 1 otherwise.
private $is_controled;
// The name of the controller.
private $dest;
// The path to the controller.
private $path;
// The controller file (with full path)
private $controller;
// The mode of the output (dom, html, json, txt, xml...)
private $mode;
// The data model
public $data;
// The output object
public $obj;
// The file extension of the view
public $ext;
// The request sent to the server to get the first controller.
public static $original_request;
// The first controller to be called at the top of the script.
public static $original_controller;
// The list of used controllers with their corresponding request, so we don't have to look for them again.
public static $known_controllers = array();
// The list of views which have been loaded. We keep their content in an array to not have to include the file again. This is useful for loops.
private static $loaded_views = array();
// Mustage templating engine.
private static $mustache;
// Reference to $appui variable
private static $appui;
// List of possible outputs with their according file extension possibilities
private static $outputs=array('dom'=>'html','html'=>'html','image'=>'jpg,jpeg,gif,png,svg','json'=>'json','pdf'=>'pdf','text'=>'txt','xml'=>'xml','js'=>'js');
/**
* List of possible and existing universal controller.
* First every item is set to one, then if a universal controller is needed, self::universal_controller() will look for it and sets the according array element to the file name if it's found and to false otherwise.
* @var array
*/
private static $ucontrollers=array('dom'=>1,'html'=>1,'image'=>1,'json'=>1,'pdf'=>1,'text'=>1,'xml'=>1,'js'=>1);
// Path to the controllers.
private static $cpath='mvc/controllers/';
// Path to the models.
private static $mpath='mvc/models/';
// Path to the views.
private static $vpath='mvc/views/';
// Path to the outputs.
private static $opath='mvc/_output/';
/**
* @return void
* This function is called once and for all for each request and create a new mustache engine
*/
private static function init()
{
if ( !isset(self::$appui) )
{
global $appui;
self::$appui =& $appui;
self::$mustache = new \Mustache_Engine;
// Sets the mode, which is the unique (so static) way of final output
if ( count(self::$appui->params) > 0 && isset(self::$outputs[self::$appui->params[0]]) )
{
self::$appui->mode = self::$appui->params[0];
array_shift(self::$appui->params);
}
else if ( count(self::$appui->post) > 0 )
self::$appui->mode = 'json';
else
self::$appui->mode = 'dom';
self::$original_request = implode('/',self::$appui->params);
}
return self::$original_request;
}
/**
* This checks whether an argument used for getting controller, view or model - which are files - doesn't contain malicious content.
*
* @param string $p The request path <em>(e.g books/466565 or html/home)</em>
* @return bool
*/
private static function check_path($p)
{
return ( strpos($p,'./') === false && strpos($p,'../') === false && strpos($p,'/') !== 0 );
}
/**
* This fetches the universal controller for the according mode if it exists.
*
* @param string $c The mode (dom, html, json, txt, xml...)
* @return string controller full name
*/
private static function universal_controller($c)
{
if ( !isset(self::$ucontrollers[$c]) )
return false;
if ( self::$ucontrollers[$c] === 1 )
self::$ucontrollers[$c] = @file_exists(self::$cpath.$c.'.php') ? self::$cpath.$c.'.php' : false;
return self::$ucontrollers[$c];
}
/**
* Adds the newly found controller to the known controllers array, and sets the original controller if it has not been set yet
*
* @param string $c The name of the request or how set by the controller
* @param file $c The actual controller file ($this->controller)
* @return void
*/
private static function set_controller($c, $f)
{
if ( !isset(self::$known_controllers[$c]) )
self::$known_controllers[$c] = $f;
if ( is_null(self::$original_controller) && !empty($c) )
self::$original_controller = $c;
}
/**
* This directly renders content with arbitrary values using the existing Mustache engine.
*
* @param string $view The view to be rendered
* @param array $model The data model to fill the view with
* @return void
*/
public static function render($view, $model)
{
self::init();
return self::$mustache->render($view,$model);
}
/**
* This will call the initial routing with the path in appui->params.
* This constructor is chainable
*
* @param string $path
* @return void
*/
public function __construct($path='')
{
$this->route(empty($path) ? self::init() : $path);
return $this;
}
/**
* This looks for a given controller in the file system if it has not been already done and returns it if it finds it, false otherwise.
*
* @param string $p
* @return void
*/
private function get_controller($p)
{
if ( !$this->controller )
{
if ( !is_string($p) || !is_dir(self::$cpath.$this->mode) )
return false;
if ( isset(self::$known_controllers[$p]) )
{
$this->dest = $p;
$this->controller = self::$known_controllers[$p];
}
else if ( file_exists(self::$cpath.$this->mode.'/'.$p.'.php') )
{
$this->dest = $p;
$this->controller = self::$cpath.$this->mode.'/'.$p.'.php';
self::set_controller($p,$this->controller);
}
else if ( is_dir(self::$cpath.$p) && file_exists(self::$cpath.$p.'/'.$this->mode.'.php') )
{
$this->dest = $p;
$this->controller = self::$cpath.$p.'/'.$this->mode.'.php';
self::set_controller($p);
}
else
return false;
}
return 1;
}
/**
* This looks for a given controller in the database
*
* @param string $path The request path <em>(e.g books/466565 or xml/books/48465)</em>
* @return void
*/
private function fetch_route($path='default')
{
if ( self::check_path($path) )
{
global $bbn;
return $bbn->db->query("
SELECT route
FROM controllers
WHERE path LIKE ?
AND output LIKE ?",
$path,
$this->mode)->fetchColumn();
}
return false;
}
/**
* This will fetch the route to the controller for a given path, using fetch_route() and get_controller()
*
* @param string $path The request path <em>(e.g books/466565 or xml/books/48465)</em>
* @return void
*/
private function route($path='')
{
if ( !$this->is_routed && self::check_path($path) )
{
$this->is_routed = 1;
if ( strpos($path,'/') !== false )
{
$t = explode('/',$path);
if ( isset(self::$outputs[$t[0]]) )
{
$this->mode = array_shift($t);
$path = implode('/',$t);
}
}
$this->path = $path;
if ( !$this->mode )
$this->mode = self::$appui->mode;
$this->ext = explode(',',self::$outputs[$this->mode]);
$p = false;
$fpath = $path;
while ( strlen($fpath) > 0 && !$p )
{
if ( $this->get_controller($fpath) )
return;
else if ( $this->get_controller($this->fetch_route($fpath)) )
return;
else if ( strpos($fpath,'/') === false )
$fpath = '';
else
$fpath = substr($this->path,0,strrpos($fpath,'/'));
}
$this->get_controller($this->fetch_route());
}
return false;
}
/**
* This will launch the controller in a new process.
* It is publicly launched through check(). In between
*
* @return void
*/
private function process()
{
if ( $this->controller && is_null($this->is_controled) )
{
$this->obj = new \stdClass();
$this->is_controled = 0;
$mvc =& $this;
$appui =& self::$appui;
call_user_func(
function() use ($mvc, $appui)
{
ob_start();
require($mvc->controller);
$output = ob_get_contents();
ob_end_clean();
if ( isset($mvc->obj->error) )
die($mvc->obj->error);
else if ( !isset($mvc->obj->output) )
$mvc->obj->output = $output;
}
);
if ( $this->data && is_array($this->data) && isset($this->obj->output) )
$this->obj->output = self::render($this->obj->output,$this->data);
if ( $this->obj )
$this->is_controled = 1;
}
return $this;
}
/**
* This will get a view.
*
* @param string $path
* @param string $mode
* @return string|false
*/
private function get_view($path='', $mode='')
{
if ( $this->mode && !is_null($this->dest) && self::check_path($path) && self::check_path($this->mode) )
{
if ( empty($mode) )
$mode = $this->mode;
if ( empty($path) )
$path = $this->dest;
if ( isset(self::$outputs[$mode]) )
{
$ext = explode(',',self::$outputs[$mode]);
foreach ( $ext as $e )
{
if ( @file_exists(self::$vpath.$mode.'/'.$path.'.'.$e) )
return file_get_contents(self::$vpath.$mode.'/'.$path.'.'.$e);
else
{
$t = explode('/',$path);
$last = array_pop($t);
if ( @file_exists(self::$vpath.$mode.'/'.$path.'/'.$last.'.'.$e) )
return file_get_contents(self::$vpath.$mode.'/'.$path.'/'.$last.'.'.$e);
}
}
}
}
return false;
}
/**
* This will get the model.
*
* @params There is no order, but one can put a path (string) or data (array)
* @return array|false A data model
*/
private function get_model()
{
if ( $this->dest )
{
$args = func_get_args();
foreach ( $args as $a )
{
if ( is_array($a) )
$d = $a;
else if ( is_string($a) && self::check_path($a) )
$path = $a;
}
if ( !isset($path) )
$path = $this->dest;
if ( !isset($d) )
$d = array();
if ( strpos($path,'..') === false && @file_exists(self::$mpath.$path.'.php') )
{
$appui =& self::$appui;
$file = self::$mpath.$path.'.php';
$data = $d;
return call_user_func(
function() use ($appui, $file, $data)
{
include($file);
if ( isset($model) )
return $model;
}
);
}
}
return false;
}
/**
* Processes the controller and checks whether it has been routed or not.
*
* @return bool
*/
public function check()
{
$this->process();
return $this->is_routed;
}
/**
* Returns the output object.
*
* @return object|false
*/
public function get()
{
if ( $this->check() && $this->is_controled )
return $this->obj;
return false;
}
/**
* Checks if data exists
*
* @return bool
*/
public function has_data()
{
return ( isset($this->data) && is_array($this->data) ) ? 1 : false;
}
/**
* Returns the rendered result from the current mvc if successufully processed
* process() (or check()) must have been called before.
*
* @return string|false
*/
public function get_rendered()
{
if ( isset($this->obj->output) )
return $this->obj->output;
return false;
}
/**
* Sets the data. Chainable. Should be useless as $this->data is public.
*
* @param array $data
* @return void
*/
public function set_data(array $data)
{
$this->data = $data;
return $this;
}
/**
* Merges the existing data if there is with this one. Chainable.
*
* @return void
*/
public function add_data($data)
{
if ( !is_array($this->data) )
$this->data = $data;
else
$this->data = array_merge($this->data,$data);
return $this;
}
/**
* Outputs the result.
*
* @return void
*/
public function output()
{
if ( $this->check() && $this->obj )
{
$obj = $this->obj;
$path = self::$opath;
$mode = $this->mode;
call_user_func(
function() use ($obj,$path,$mode)
{
include_once($path.$mode.'.php');
}
);
}
}
}
?>
Here is the router (index.php
), where constants, $bbn
and $appui
are defined in the includes.
<?php
include_once('config/cfg.php');
include_once('config/env.php');
include_once('config/vars.php');
include_once('config/custom.php');
if ( defined('BBN_SESS_NAME') && $appui->db )
{
if ( !isset($_SESSION[BBN_SESS_NAME]) )
include_once('config/session.php');
$bbn->mvc = new \bbn\cls\mvc();
if ( !$bbn->mvc->check() )
die('No controller has been found for this request');
$bbn->mvc->output();
}
?>
Well, and here's an example on how it works on a whole HTML document. Two views are used: the DOM structure, and a list element that is a part of a multi-level menu with no depth limit.
The DOM view:
...
</head>
<body itemscope itemtype="http://schema.org/WebPage">
<div id="example" class="k-content">
<div id="vertical">
<div id="top-pane" style="overflow:visible; width:100%">
<ul id="menu">{{{menu_content}}}</ul>
...
The HTML list element view:
{{#menus}}
<li{{specs}}>
{{#icon}}
<i class="icon-{{icon}}"></i>
{{/icon}}
{{{title}}}
{{#has_menus}}
<ul>
{{{content}}}
</ul>
{{/has_menus}}
</li>
{{/menus}}
And I have a nested model used by the controller for displaying the menu:
array (
'menus' => array (
0 => array (
'title' => 'Hello',
'icon' => 'cloud',
'has_menus' => false
),
1 => array (
'title' => '1',
'icon' => 'user',
'has_menus' => 1,
'menus' => array (
0 => array (
'title' => '11',
'icon' => 'cloud',
'has_menus' => false
),
1 => array (
'title' => '12',
'icon' => 'wrench',
'has_menus' => false
),
2 => array (
'title' => '13',
'icon' => 'remove',
'has_menus' => 1,
'menus' => array (
0 => array (
'title' => '131',
'icon' => 'cloud',
'has_menus' => false
),
1 =>
...
And now here is my controller for the DOM:
<?php
$mvc->data = array(
'site_url' => BBN_URL,
'is_dev' => BBN_IS_DEV ? 1 : "false",
'shared_path' => BBN_SHARED_PATH,
'static_path' => BBN_STATIC_PATH,
'year' => date('Y'),
'javascript_onload' => $mvc->get_view('init','js'),
'theme' => isset($_SESSION['atl']['cfg']['theme']) ? $_SESSION['atl']['cfg']['theme'] : false
);
$tmp = new \bbn\cls\mvc("html/menu");
if ( $tmp->check() )
$mvc->data['menu_content'] = $tmp->get_rendered();
echo $mvc->get_view('_structure','dom');
?>
Which is calling the controller for the nested menu:
<?php
if ( !$mvc->has_data() )
$mvc->data = $mvc->get_model();
if ( isset($mvc->data['menus']) && is_array($mvc->data['menus']) )
{
foreach ( $mvc->data['menus'] as $i => $m )
{
$tmp = (new \bbn\cls\mvc("html/menu"))->set_data($m);
if ( $tmp->check() )
$mvc->data['menus'][$i]['content'] = $tmp->get_rendered();
}
}
echo $mvc->get_view();
?>
It is working great now! Please let me know the flaws or whatever outrageous mistake or even any advice you might come with about this code.
As you can see - if ever you've reached this part! - all the menus look alike and they all use the third menu of the first children. I have tried many combinations, the array is fine at start. It is transformed at some stage when the MVC functions are called within the controller, but I can't figure out now.