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\$
2
  • \$\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\$ Commented Oct 25, 2017 at 13:29
  • \$\begingroup\$ Given that there haven’t been any answers yet, feel free to update the code in the post to the latest version. \$\endgroup\$ Commented May 21, 2022 at 22:11

0

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Browse other questions tagged or ask your own question.