Over the years I have been learning PHP (big thanks to Sitepoint) and toying with the idea to make my own framework to further learn and maybe possibly help me build sites. I have a base that I have been slowly building and would like to move on building it - unless it’s broken, which is why I would like a review for bad practices or code smells. I would like to keep the code small, simple, clean, and modular, if at all possible.
The base code does some basic stuff:
a) Loads base/extension/template ini configs
b) Connects to the db (PDO)
c) Loads, sets, deletes options/extensions/templates/languages
- Extensions/Templates to require index.php/item.ini to load
d) Create an API for extensions/languages/validators/routes - Validator influenced by Zend
- Extensions/Templates influenced by WordPress
- Languages use strtr instead gettext
- Url parsing from rewrite & non rewrite (www.site.com/?/admin/login)
e) Sends the system to load the correct plugin controller (ala HMVC pattern)
My worries are:
- Static class or just plain procedural functions?
- Too much code or possible refactoring needed (Extensions for one)
- Is my interpretation of MVC/HMVC correct in the examples below?
- Could this be improved upon at this stage?
Please keep in mind, this is not finished at all.
Base Framework
<?php
error_reporting(-1);
#---------- Helper Functions ---------------------------------------------------
// Returns value from array[key]
// Based from https://wiki.php.net/rfc/functionarraydereferencing
// @return mixed on success, false on failure
function value($key, array $array) {
return isset($array[(string) $key])
? $array[(string) $key]
: false;
}
// Get values from an config (ini) file based on section/key
// @return mixed on success
function ini_value($key, $file = 'config.php') {
static $ini, $array;
if( $ini !== (string) $file ) {
$ini = (string) $file;
$array = parse_ini_file((string) $file, TRUE);
}
return value((string) $key, $array);
}
// PDO database connection wrapper
// @return object on success, dies on failure
function db() {
static $db;
if( !$db ) {
$db = FALSE;
try {
$db = new PDO(
ini_value('database.dsn'),
ini_value('database.user'),
ini_value('database.pass'),
(array) ini_value('database.options')
);
} catch( PDOException $e ) {
die( $e->getMessage() );
}
}
return $db;
}
// From http://www.php.net/manual/en/function.strip-tags.php#62705
function strip_tags_deep($value) {
return is_array($value)
? array_map('strip_tags_deep', $value)
: strip_tags($value);
}
// Modified version of http://php.net/manual/en/function.ksort.php#105399
function ksort_recursive(array &$array) {
ksort( $array );
foreach( $array as &$a ) {
if( is_array($a) ) {
ksort_recursive($a);
}
}
}
#---------- Registry Class -----------------------------------------------------
final class Registry {
private static
$_data = array();
static function get( $key ) {
if( isset(self::$_data[$key]) ) {
return self::$_data[$key];
}
}
static function set( $key, $value = '' ) {
self::$_data[$key] = $value;
}
}
#---------- Observable & Observer classes --------------------------------------
class Observable {
private
$_event,
$_observers = array();
final function attach( Observer $observer ) {
$i = array_search($observer, $this->_observers);
if( $i === false ) {
$this->_observers[] = $observer;
}
}
final function createEvent( $event ) {
$this->_event = $event;
$this->_notify();
}
final function getEvent() {
return $this->_event;
}
final private function _notify() {
foreach( $this->_observers as $observer ) {
$observer->update( $this );
}
}
}
interface Observer {
function update( Observable $subject );
}
#---------- AddOn Template -----------------------------------------------------
abstract class AddOn extends Observable {
protected
$_active,
$_list = array();
abstract function load();
abstract function get();
abstract function process($str);
abstract protected function set($addon);
}
// Procedural helpers
function addon_init(AddOn $addon) {
$addon->load();
}
function addon_get(AddOn $addon) {
return $addon->get();
}
function addon_process(AddOn $addon, $str) {
$addon->process($str);
}
#---------- Options CRUD Class ------------------------------------------------
final class Options {
private static
$_db,
$_data = array();
static function init( $db ) {
self::$_db = $db;
$sql = 'SELECT name, value
FROM options;';
foreach( self::$_db->query($sql) as $r ) {
self::$_data[$r['name']] = $r['value'];
}
}
static function get( $key ) {
if( self::exists($key) ) {
return self::$_data[$key];
}
}
static function set( $key, $value ) {
if( self::exists($key) ) {
$sql = "UPDATE options
SET value = :value
WHERE name = :key;";
} else {
$sql = "INSERT INTO options
VALUES(:key, :value);";
}
$sth = self::$_db->prepare($sql);
$sth->execute(
array(
':key' => $key,
':value' => $value
)
);
}
static function delete( $key ) {
if( self::exists($key) ) {
$sql = "DELETE
FROM options
WHERE name = :key;";
$sth = self::$_db->prepare($sql);
$sth->execute(
array(
':key' => $key
)
);
}
}
static function exists( $key ) {
return array_key_exists($key, self::$_data);
}
}
// Initialize options
Options::init(db());
// Procedural helpers
function get_option( $value ) {
return Options::get( $value );
}
function set_option( $key, $value ) {
Options::set( $key, $value );
}
function delete_option( $key ) {
Options::delete( $key );
}
#---------- Plugin Helper ------------------------------------------------------
define(
'EXT_PATH',
ini_value('path.extensions')
);
Registry::set('ext',
array(
'ext' => array(),
'hook'=> array()
)
);
function ext() {
return Registry::get('ext');
}
# Extension CRUD Class
final class Extension {
private static
$_db,
$_data = array();
static function init( $db ) {
self::$_db = $db;
$sql = 'SELECT name
FROM plugins;';
foreach ( self::$_db->query($sql) as $r ) {
self::$_data[] = $r['name'];
}
}
static function get() {
return self::$_data;
}
static function add( $name ) {
if( !self::exists($name) ) {
$sth = self::$_db->query("
INSERT INTO plugins
VALUES(:name);"
);
$sth->execute(
array(
':name' => $name
)
);
}
}
static function delete( $name ) {
if( self::exists($name) ) {
$sth = self::$_db->query("
DELETE
FROM plugins
WHERE name = :name;
");
$sth->execute(
array(
':name' => $name
)
);
}
}
static function exists( $name ) {
return in_array( $name, self::$_data );
}
}
final class Extensions extends AddOn {
function __construct() {
$this->_active = Extension::get();
}
function load() {
foreach( $this->_active as $p ) {
$file = EXT_PATH. '/' .$p. '/index.php';
$ini = EXT_PATH. '/' .$p. '/plugin.ini';
if( file_exists($file) && file_exists($ini) ) {
require( $file );
if( class_exists($p) ) {
$i = new $p;
$i->commit();
// Attach the observer
$this->attach($i);
}
}
}
}
function get() {
$dir = opendir(EXT_PATH);
if( $dir ) {
while( false !== ($file = readdir($dir)) ) {
if(
$file != '.'
&& $file != '..'
&& file_exists(EXT_PATH. '/' .$file. '/index.php')
&& file_exists(EXT_PATH. '/' .$file. '/plugin.ini')
) {
$this->_list[$file] = $this->data(
EXT_PATH. '/' .$file. '/plugin.ini'
);
if( in_array($file, $this->_active) ) {
$this->_list[$file]['active'] = TRUE;
}
}
}
}
return $this->_list;
}
protected function data( $file ) {
$array = parse_ini_file($file, TRUE);
return strip_tags_deep($array);
}
protected function set( $addon ) {
Extension::set( $addon );
$this->createEvent('activated_' . $addon);
}
protected function delete( $addon ) {
Extension::delete( $addon );
$this->createEvent('deactivated_' . $addon);
}
function process( $str ) {
switch (true) {
case (strpos($str, 'activate') === 0 ):
$this->set(substr($str, '9'));
break;
case (strpos($str, 'deactivate') === 0 ):
$this->delete(substr($str, '11'));
break;
}
}
}
function extensions() {
static $exts;
if( !$exts ) {
$exts = new Extensions;
}
return $exts;
}
class Ext implements Observer {
protected
$_name,
$_data = array();
private
$_ext,
$_hook = array();
final function __construct() {
$this->_ext = ext();
$this->_name = get_class($this);
$this->_data = value(
$this->_name,
addon_get(extensions())
);
}
final function bind( $hook, $callback, $priority = 10 ) {
if( is_callable($callback) ) {
$this->_hook[(string) $hook][(int) $priority][] = $callback;
}
}
final private function _check_requirements() {
if( isset($this->_data['plugin.dependencies']['requires']) ) {
$requirement = $this->_data['plugin.dependencies']['requires'];
foreach( $requirement as $dep ) {
if( !isset($this->_ext['ext'][$dep]) ) {
$this->_data['req_error'] = TRUE;
}
}
}
}
final private function _set_hooks() {
// Parse hook/callback in plugin.ini
if( isset($this->_data['plugin.hooks']) ) {
foreach( $this->_data['plugin.hooks'] as $hook => $calls ) {
if( is_array($calls) ) {
foreach( $calls as $k => $func ) {
$this->_ext['hook'][$hook][10][] = $func;
}
} else {
$this->_ext['hook'][$hook][10][] = $calls;
}
}
}
// Parse hook/callback set from Ext::bind($hook, $fn, $priority)
foreach ($this->_hook as $hook => $calls) {
foreach( $calls as $k => $call ) {
foreach( $call as $func ) {
$this->_ext['hook'][$hook][$k][] = $func;
}
}
}
}
final function commit() {
$this->_check_requirements();
if( method_exists($this, 'main') ) {
$this->main();
}
if( !isset($this->_data['req_error'])
|| $this->_data['req_error'] === FALSE
) {
$this->_ext['ext'][$this->_name] = $this->_data;
$this->_set_hooks();
ksort_recursive($this->_ext['hook']);
}
Registry::set('ext', $this->_ext);
}
final function update( Observable $subject ) {
switch( $subject->getEvent() ) {
case( 'activated_' . $this->_name ):
if( method_exists($this, 'install') ) {
$this->install();
}
break;
case( 'deactivated_' . $this->_name ):
if( method_exists($this, 'uninstall') ) {
$this->uninstall();
}
break;
}
}
}
function ext_commit(Ext $ext) {
$ext->commit();
// Log this transaction
$e = ext();
$e['ext'][get_class($ext)]['internal_commit'] = TRUE;
Registry::set('ext', $e);
}
function set_hook( $hook, $var = NULL ) {
$ext = ext();
if( isset($ext['hook'][$hook]) ) {
foreach( $ext['hook'][$hook] as $k => $calls ) {
foreach( $calls as $func ) {
if( is_callable($func) ) {
$var = $func($var);
# $var = call_user_func_array($func, array($var));
}
}
}
}
return $var;
}
#---------- Template Helper ----------------------------------------------------
define(
'TPL_PATH',
ini_value('path.templates')
);
final class Templates extends AddOn {
function __construct() {
$this->_active = get_option('template');
}
function load() {
$file = TPL_PATH .'/'. $this->_active . '/index.php';
$ini = TPL_PATH .'/'. $this->_active . '/template.ini';
if( file_exists($file) && file_exists($ini) ) {
require($file);
}
}
function get() {
$dir = opendir(TPL_PATH);
if( $dir ) {
while( false !== ($file = readdir($dir)) ) {
if(
$file != '.'
&& $file != '..'
&& file_exists(TPL_PATH. '/' .$file. '/index.php')
&& file_exists(TPL_PATH. '/' .$file. '/template.ini')
) {
$this->_list[$file] = $this->data(
TPL_PATH. '/' .$file. '/template.ini'
);
if( $file === $this->_active ) {
$this->_list[$file]['active'] = TRUE;
}
}
}
}
return $this->_list;
}
protected function data( $file ) {
$array = array();
$array = parse_ini_file($file, TRUE);
return strip_tags_deep($array);
}
protected function set( $addon ) {
set_option( 'template', $addon );
}
function process( $str ) {
if( strpos($str, 'activate') === 0 ) {
$this->set(substr($str, '9'));
}
}
}
function templates() {
static $tpls;
if( !$tpls ) {
$tpls = new Templates;
}
return $tpls;
}
#---------- Language Helper ----------------------------------------------------
define(
'LANG_PATH',
ini_value('path.languages')
);
Registry::set('lang', array());
function lang() {
return Registry::get('lang');
}
final class Languages extends AddOn {
function __construct() {
$this->_active = get_option('language');
}
function load() {
$array = array();
$file = LANG_PATH .'/'. $this->_active . '.php';
if( file_exists($file) ) {
$array = require $file;
}
Registry::set('lang', $array);
}
function get() {
$dir = opendir(LANG_PATH);
if ($dir) {
while (false !== ($file = readdir($dir))) {
if (
$file != '.'
&& $file != '..'
&& file_exists(LANG_PATH .'/'. $file)
) {
$filename = basename($file, '.php');
$this->_list[$filename] = array();
if( $filename === $this->_active ) {
$this->_list[$filename]['active'] = TRUE;
}
}
}
}
return $this->_list;
}
protected function set( $addon ) {
set_option( 'language', $addon );
}
function process( $str ) {
$this->set( $str );
}
}
// initialize languages
addon_init(new Languages);
// Function to translate strings
// Uses http://php.net/manual/en/function.strtr.php
// Do not translate placeholder
function __( $string, array $args = NULL ) {
$string = value($string, lang())
? value($string, lang())
: $string;
return $args === null
? $string
: strtr($string, $args);
}
#---------- Form Validator Class -----------------------------------------------
// Based from http://framework.zend.com/manual/1.12/en/zend.validate.introduction.html
abstract class Validator {
protected
$_errorMsg = 'undefined';
final function getError() {
return $this->_errorMsg;
}
abstract function validate($value);
}
final class Validate {
private
$_data = array(),
$_errors = array();
function addValidator($field, Validator $obj) {
$this->_data[$field][] = $obj;
return $this;
}
function isValid($array) {
$valid = true;
foreach( $this->_data as $field => $objects ) {
foreach($objects as $i => $obj) {
if( isset($array[$field])
&& !$obj->validate($array[$field])
) {
$valid = false;
$this->_errors[] = array(
'field' => $field,
'error' => $obj->getError()
);
break;
}
}
}
return $valid;
}
function getErrors() {
return $this->_errors;
}
}
function get_validation_error($field, array $array) {
$exists = false;
foreach( $array as $k => $v ) {
if( isset($array[$k]['field'])
&& $array[$k]['field'] === $field
) {
$exists = true;
}
}
if( $exists ) {
return $array[$k]['error'];
}
}
#---------- URI Helpers --------------------------------------------------------
// Remove index.php, and duplicate slashes from $_SERVER['REQUEST_URI']
// Based on http://brandonwamboldt.ca/my-php-router-class-825/
function _prepare_uri() {
$uri = $_SERVER['REQUEST_URI'];
$uri = str_replace( dirname($_SERVER['SCRIPT_NAME']), '', $uri );
$uri = str_replace('index.php', '', $uri);
$uri = str_replace('?/', '', $uri);
$uri = preg_replace( '/\\/+/', '/', $uri );
$uri = ltrim( $uri, '/' );
return $uri;
}
function parse_uri($component = null) {
$array = array();
$uri = _prepare_uri();
$uri = parse_url($uri);
if( isset($uri['path']) ) {
$uri['path'] = trim($uri['path'], '/');
$array['path'] = explode('/', $uri['path']);
}
if( isset($uri['query']) ) {
parse_str($uri['query'], $array['query']);
}
return isset($array[$component])
? $array[$component]
: $array;
}
function uri_part($num) {
return value($num, parse_uri('path'));
}
function uri_qstr($key) {
return value($key, parse_uri('query'));
}
function build_url() {
$str = 'http://' . $_SERVER['HTTP_HOST'];
$str .= str_replace( 'index.php', '', $_SERVER['SCRIPT_NAME'] );
$qs = ini_value('rewrite.url') === TRUE
? ''
: '?/';
$args = func_get_args();
if( !empty($args[0]) ) {
$args = implode('/', $args);
return $str . $qs . $args;
} else {
return $str;
}
}
#---------- Router Class -------------------------------------------------------
// Modified from http://brandonwamboldt.ca/my-php-router-class-825/
function route( $route, $callback ) {
$path = implode('/', parse_uri('path'));
// Custom
// Format: <:var_name|regex>
$route = preg_replace('/\\<\\:(.*?)\\|(.*?)\\>/', '(?P<\\1>\\2)', $route);
// Alphanumeric
// Format: <:var_name>
$route = preg_replace('/\\<\\:(.*?)\\>/', '(?P<\\1>[A-Za-z0-9\\-\\_]+)', $route);
// Numeric
// Format: <#var_name>
$route = preg_replace('/\\<\\#(.*?)\\>/', '(?P<\\1>[0-9]+)', $route);
// Wildcard (INCLUDING dir separators)
// Format: <*var_name>
$route = preg_replace('/\\<\\*(.*?)\\>/', '(?P<\\1>.+)', $route);
// Wildcard (EXCLUDING dir separators)
// Format: <!var_name>
$route = preg_replace('/\\<\\!(.*?)\\>/', '(?P<\\1>[^\\/]+)', $route);
// Add regex for a full match or no match
$route = '#^' . $route . '$#';
if (preg_match($route, $path, $matches)) {
$params = array();
foreach ( $matches as $key => $match ) {
if (is_string($key)) {
$params[$key] = $match;
}
}
if (is_callable($callback)) {
return call_user_func_array($callback, $params);
}
}
}
#---------- Admin Base Functions -----------------------------------------------
define(
'ADMIN_PATH',
ini_value('path.admin')
);
abstract class AdminModule extends Ext {
function register_admin_module() {
$this->_data['admin_module'][] = substr($this->_name, 5);
}
}
function registered_admin_modules() {
$modules = array();
foreach( value('ext', ext()) as $ext ) {
if( isset($ext['admin_module'][0]) ) {
$modules[] = strtolower(
$ext['admin_module'][0]
);
}
}
$modules = array_flip($modules);
return $modules;
}
#-------------------------------------------------------------------------------
Extension::init(db());
addon_init(extensions());
require('validators.php');
// View code
function center() {
echo set_hook('center');
}
$baseController = 'PageController';
switch( true ) {
case( uri_part(0) ):
$fn = ucfirst(uri_part(0)) . 'Controller';
if( is_callable($fn) ) {
$fn();
} else {
$baseController();
}
break;
default:
$baseController();
break;
}
unset($baseController);
function AdminController() {
require(ADMIN_PATH . '/index.php');
}
function PageController() {
addon_init(templates());
}
Example Admin loader, Login/Logout model, & View loader:
<?php
if( !ADMIN && uri_part(1) != 'login') {
if( !headers_sent() ) {
$url = build_url('admin', 'login');
header("Location: $url");
}
}
class AdminCenter extends Ext {
function main() {
$this->bind('center', 'AdminCenterController');
}
}
ext_commit(new AdminCenter);
function AdminCenterController() {
$modules = registered_admin_modules();
if( isset($modules[uri_part(1)]) ) {
$fn = 'Admin'. ucfirst(uri_part(1)) .'Controller';
if( function_exists($fn) ) {
$fn();
}
}
}
#---------- Login --------------------------------------------------------------
class AdminLogin extends AdminModule {
function main() {
$this->register_admin_module();
}
}
ext_commit(new AdminLogin);
function AdminLoginController() {
require(ADMIN_PATH . '/_/inc/AdminLoginView.php');
}
function AdminLogin_reqAttr() {
$array = array(
'form' => array(
'action'=> build_url('admin', 'login')
),
'email' => array(
'name' => 'login_email'
),
'password' => array(
'name' => 'login_password'
),
'submit' => array(
'name' => 'login_submit'
)
);
return set_hook('AdminLogin_reqAttr', $array);
}
# Unset any lingering errors..
if( isset($_SESSION['Login_error']) ) {
unset($_SESSION['Login_error']);
}
function process_AdminLogin($array) {
$valid = false;
$fields = AdminLogin_reqAttr();
$_email = $fields['email']['name'];
$_pword = $fields['password']['name'];
$validation = new Validate;
$validation->addValidator( $_email, new IsBlank)
->addValidator($_email, new IsEmail);
$validation->addValidator( $_pword, new IsBlank);
set_hook('process_login', $validation);
if( !$array ) return;
if( !$validation->isValid($array) ) {
$_SESSION['Login_error'] = $validation->getErrors();
} else {
$sql = 'SELECT *
FROM users
WHERE email = :email
AND password = :password';
$sth = db()->prepare($sql);
$email = _hash( $array[$_email] );
$pword = _hash( $array[$_pword] );
$sth->bindParam(':email', $email );
$sth->bindParam(':password',$pword );
$sth->execute();
if( $sth->fetchAll() ) {
$valid = true;
$_SESSION['Logged_In'] = _hash($_SERVER['HTTP_USER_AGENT']);
} else {
$_SESSION['Login_error'] = array(
__('Please enter the correct email/password combination to continue.')
);
}
}
}
#---------- Logout -------------------------------------------------------------
class AdminLogout extends AdminModule {
function main() {
$this->register_admin_module();
}
}
ext_commit(new AdminLogout);
function AdminLogoutController() {
session_destroy();
require(ADMIN_PATH . '/_/inc/AdminLogoutView.php');
}
#---------- Load View ----------------------------------------------------------
include(ADMIN_PATH . '/_theme.php');
Example (unfinished) Login View:
<?php
function post_login() {
if( isset($_SESSION['Logged_In']) ) {
echo '<meta http-equiv="refresh" content="0;url='.build_url('admin').'">';
}
}
post_login();
$array = AdminLogin_reqAttr();
// Process login
if( isset($_POST[$array['submit']['name']]) ) {
process_AdminLogin($_POST);
post_login();
}
?>
<h2><?php echo __('Login'); ?></h2>
<form method="post" action="<?php echo $array['form']['action']; ?>">
<label><?php echo __('Email'); ?>:</label>
<input type="email" name="<?php echo $array['email']['name']; ?>">
<label><?php echo __('Password'); ?>:</label>
<input type="password" name="<?php echo $array['password']['name']; ?>" />
<input type="submit" name="<?php echo $array['submit']['name']; ?>" value="<?php echo __('Login'); ?>">
</form>