I've been working on a simple MVC PHP framework on and off for the last nearly 12 months, so I thought that I'd put it out there.
The project began as a way to mentor and teach a junior (or a more junior than me) PHP developer, to show programming principles, OO methodologies and MVC-type file structures. The framework wasn't finished to my satisfaction as I moved on to a new employer in early 2017, but luckily I was allowed to continue its development, so I thought I'd put it out there.
It's on GitHub here - I've called it FrameWork.php as I hope it to be as simple and stupid as I am. A quickly written quick start guide is here.
I hope to finish the HTML builder so I can move on to making a MySQL/PDO query builder for the model core.
The file structure is as follows:
../application
./controller
./core
./library
./model
./view
../public_html
./css
./js
The entry point in the framework is via the Index.php class (i.e., mysite.local/home) as follows:
<?php
use Application\Core\Framework\Core;
require_once(serverPath('/core/FrameworkCore.php'));
require_once(serverPath('/core/GlobalHelpers.php'));
header('X-Content-Type-Options: nosniff');
header('X-XSS-Protection: 1');
header('Strict-Transport-Security: max-age=31536000; includeSubDomains');
session_set_cookie_params(0, '/', getDomain(host(), ($https = isHttps()) ), $https, true);
if(session_id() == ""){
session_start();
}
## filepath ../public_html
class Index
{
protected $core;
public $timeZone = "Europe/London";
/**
* This will initiate the core to load the view
* according to the uri path, one may also
* change the default timezone for the project
* by altering the public $timeZone string above
* for a list of valid timezones, see:
* http://php.net/manual/en/timezones.php
*
* @param na
* @author sbebbington
* @date 24 Jan 2017 - 09:49:15
* @version 0.0.2
* @return
* @todo
*/
public function __construct(){
$this->core = new \Application\Core\Framework\Core();
setTimeZone($this->timeZone);
$this->core->loadPage();
}
}
/**
* This will correctly route to the application
* directory on your server
*
* @param string
* @author Shaun
* @date 2 Feb 2017 - 13:01:13
* @version 0.0.2
* @return string
* @todo
*/
function serverPath(string $routeTo = ''){
$_x = str_replace("\\", "/", dirname(__FILE__)) . '/application';
$_x = str_replace("public_html/", "", $_x);
$_x .= $routeTo;
return str_replace("//", "/", $_x);
}
// Creates new instance and therefore initiates the controllers, models and views etc...
$page = new Index();
This will then call the core, which works out which view (page) to show, it works like this:
<?php
namespace Application\Core\Framework;
require_once(serverPath('/core/HtmlBuilder.php'));
## filepath ../application/core
class Core extends \Application\Core\Framework\HtmlBuilder
{
public $segment;
public $host;
public $allowedSegments = array(
'home',
);
public $pageController = array(
'home' => "HomeController",
);
public $partial;
public $controller;
public $title;
public $description;
public $serverPath;
public $root;
public $flash;
public $filePath;
public $uriPath;
public $http;
private $errorReporting = array(
"http://framework.php.local",
"https://framework.php.local"
);
private $allowedFileExts = array(
'htm', 'html', 'asp', 'aspx', 'js', 'php', 'phtml',
);
public $canonical = '';
public function __construct(){
parent::__construct();
$this->host = isHttps() ? "https://" : "http://";
$this->host .= host();
if(in_array($this->host, $this->errorReporting)){
error_reporting(-1);
ini_set('display_errors', '1');
}
$this->uriPath = '';
$page = array_filter(explode('/', $_SERVER['REQUEST_URI']), 'strlen');
if(count($page)>1){
foreach($page as $key => $data){
if($key != count($page)){
$this->uriPath .= "{$data}/";
}
}
}
$this->segment = !empty($page) ? strtolower($page[count($page)]) : "";
if(!empty($_GET)){
$segment = explode('?', $this->segment);
$this->segment = $segment[0];
if(isset($segment[1]) && strlen($segment[1])>0){
$_GET = array();
$get = explode("&", $segment[1]);
foreach($get as $data){
$_data = explode("=", $data);
$_GET[$_data[0]] = urldecode($_data[1]);
}
}
}
if(strpos($this->segment, ".") > 0){
$segments = explode(".", $this->segment);
if(in_array($segments[count($segments)-1], $this->allowedFileExts)){
$this->segment = $segments[count($segments)-2];
$this->canonical = "<link rel=\"canonical\" href=\"{$this->host}/{$this->segment}\">" . PHP_EOL;
}
}
$this->serverPath = serverPath();
$this->root = str_replace("\\", "/", $_SERVER['DOCUMENT_ROOT']);
$this->controller = new \stdClass();
$this->flash = new \stdClass();
$this->partial = array(
'header' => serverPath("/view/partial/header.phtml"),
'footer' => serverPath("/view/partial/footer.phtml"),
);
}
/**
* Sets the meta page titles in the views
*
* @param string
* @author sbebbington
* @date 2 Feb 2017 - 13:04:10
* @version 0.0.2
* @return string
* @todo
*/
public function setTitle(string $page = ''){
$titles = array(
'home' => "Example FrameWork.php skeleton site",
);
return $titles["{$page}"];
}
/**
* Sets the meta page descriptions in the views
*
* @param string
* @author sbebbington
* @date 2 Feb 2017 - 13:04:47
* @version 0.0.1
* @return string
* @todo
*/
public function setDescription(string $page = ''){
$descriptions = array(
'home' => "The Skeleton",
);
return $descriptions["{$page}"];
}
/**
* Bug fixed edition of the using ZF type view variables
*
* @param
* @author sbebbington
* @date 2 Feb 2017 - 13:05:41
* @version 0.0.3a
* @return
* @todo
*/
public function setView($instance, string $masterKey = ''){
foreach($instance as $key => $data){
if($masterKey == ''){
$this->$key = $data;
}else{
$this->$masterKey->$key = $data;
}
}
}
/**
* This will load the view and related controllers
* It has an added exception for Jamie's admin html
* template. This version should now allow Zend-alike
* view variables - so if you set an object in a page
* controller as $this->view->objName, you can use
* $this->objName in the PHP/HTML view or something.
*
* @param na
* @author sbebbington
* @date 6 Feb 2017 - 11:15:14
* @version 0.0.3
* @return void
* @todo
*/
public function loadPage(){
if($this->segment == ""){
$this->segment = 'home';
}
if(in_array($this->segment, $this->allowedSegments) == false || !file_exists(serverPath("/view/{$this->uriPath}{$this->segment}.phtml"))){
$this->setView(array("_404Error" => 1));
$this->title = '404 error - page not found, please try again';
$this->description = 'There\'s a Skeleton in the Sandbox';
require_once(serverPath("/view/404.phtml"));
exit;
}
if(in_array($this->segment, $this->allowedSegments) == true){
$this->title = $this->setTitle($this->segment);
$this->description = $this->setDescription($this->segment);
foreach($this->pageController as $instance => $controller){
if($this->segment == $instance){
require_once(serverPath("/controller/{$controller}.php"));
$this->controller->$instance = new $controller();
if(isset($this->controller->$instance->view)){
$this->setView($this->controller->$instance->view);
$this->controller->$instance->view = null;
}
}
}
if(isset($_SESSION['flashMessage']) && !empty($_SESSION['flashMessage'])){
$this->setView($_SESSION['flashMessage'], "flash");
}
require_once(serverPath("/view/{$this->uriPath}{$this->segment}.phtml"));
}
}
}
Each view will load a controller instance, routed in the ../application/controller
directory, by default there is a ControllerCore class and a page controller class (assumes HomeController.php by default), as follows:
<?php
namespace Application\Controller;
use Application\Library;
require_once(serverPath('/library/Library.php'));
## filepath ../application/controller
class ControllerCore
{
public $post;
public $view;
public $lib;
public $sql;
public $host;
public function __construct(){
$this->lib = new \Application\Library\Library();
if(!isset($_SESSION['flashMessage'])){
$_SESSION['flashMessage'] = array();
}
if(empty($this->post) || $this->post == null){
$this->post = array();
}
if(!empty($_POST)){
$this->setPost();
}
$this->view = new \stdClass();
$this->host = host();
}
/**
* Sanatizes posted data
*
* @param Array
* @author Linden && Shaun
* @date 7 Oct 2016 14:54:10
* @version 0.0.3
* @return na
* @todo
*/
public function setPost(){
foreach($_POST as $key => $data){
$this->post[$key] = is_string($data) ? trim($data): $data;
}
}
/**
* This should empty the super global $_POST and the controller $this->post
*
* @param na
* @author Shaun
* @date 16 Jun 2016 11:25:04
* @version 0.0.1
* @return array
* @todo
*/
public function emptyPost(){
$_POST = array();
$this->post = $_POST;
}
/**
* Clears PHP session cookies
*
* @param na
* @author Shaun
* @date 14 Sep 2016 14:29:23
* @version 0.0.2
* @return
* @todo
*/
public function emptySession(){
if(session_id() != ""){
session_destroy();
}
$this->session = null;
}
/**
* Sets flash messages (recommend using string for value param)
*
* @param string, string | int | boolean
* @author Shaun
* @date 14 Sep 2016 09:48:53
* @version 0.0.1
* @return na
* @todo
*/
public function setFlashMessage($key, $value){
$_SESSION['flashMessage'][$key] = $value;
}
/**
* Checks if flash message exists
*
* @param string
* @author Shaun
* @date 14 Sep 2016 10:18:04
* @version 0.0.1
* @return boolean
* @todo
*/
public function getFlashMessage($key){
return isset($_SESSION['flashMessage'][$key]) ? true : false;
}
/**
* Clears flash messages
*
* @param string|array
* @author Shaun
* @date 15 Sep 2016 10:40:49
* @version 0.0.1
* @return na
* @todo
*/
public function clearFlashMessage($key){
if(is_array($key)){
foreach($key as $_key => $_data){
$_SESSION['flashMessage'][$_data] = null;
}
}else{
$_SESSION['flashMessage'][$key] = null;
}
}
}
And the home controller:
<?php
require_once (serverPath('/controller/ControllerCore.php'));
require_once (serverPath('/model/HomeModel.php'));
## filepath ../application/controller
class HomeController extends \Application\Controller\ControllerCore
{
public function __construct(){
parent::__construct();
$this->sql = new HomeModel();
foreach($this->sql->getView() as $key => $data){
$key = $this->lib->convertSnakeCase($key);
$this->view->$key = htmlspecialchars_decode($data);
}
// if(isset($this->post['submit'])){
// Do something with the posted data here, but for now
// we'll simply see the contents of the posted data
// $this->lib->debug($this->post, true);
// }
}
}
Any feedback is appreciated.
https://github.com/sbebbers/FrameWork.php/releases/tag/0.1.4-RC1
if anyone would like to test and give feedback etc... \$\endgroup\$