[Code Review] Beginning CMS Framework Base

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:

  1. Static class or just plain procedural functions?
  2. Too much code or possible refactoring needed (Extensions for one)
  3. Is my interpretation of MVC/HMVC correct in the examples below?
  4. 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>