3
\$\begingroup\$

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.

\$\endgroup\$
  • \$\begingroup\$ This framework is now much more mature, and I have my first release candidate https://github.com/sbebbers/FrameWork.php/releases/tag/0.1.4-RC1 if anyone would like to test and give feedback etc... \$\endgroup\$ – Shaun Bebbers Oct 25 '17 at 13:29

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.