2
\$\begingroup\$

I recently wrote this PHP MVC framework. Would like your review.

index.php

<?php
//In the linked web folder we would only have index.php,js,css,images. 
//Rest of the code is in ../core
chdir('../core');
require 'common.inc.php'; //define constants, auto loading etc
run();
function run() {
 if (isset($_REQUEST['module']) && isset($_REQUEST['action'])) {
 $ctrl = getControllerFromModule();
 if($ctrl) {
 $ctrl->run();
 } else {
 HttpUtils::badRequest();
 }
 } else {
 HomeService::showHomepage();
 }
}
function getControllerFromModule() {
 $ctrl = null;
 $module = $_REQUEST['module'];
 switch($module) {
 case 'home':
 $ctrl = new HomeController();
 break;
 case 'browse':
 $ctrl = new BrowseController();
 break;
 case 'access':
 $ctrl = new AccessController();
 break;
 case 'singlePlace':
 $ctrl = new SinglePlaceController();
 break;
 case 'pg':
 $ctrl = new PaymentGatewayController();
 break;
 case 'auth':
 $ctrl = new AuthController();
 break;
 case 'static':
 $ctrl = new StaticPageController();
 break;
 case 'account':
 $ctrl = new AccountController();
 break;
 }
 return $ctrl;
}

BaseController.php

<?php
class BaseController {
 protected $GET_Map;
 protected $POST_Map;
 public function run() {
 $action = isset($_REQUEST['action'])?$_REQUEST['action']:null;
 $this->setMaps();
 $method = $_SERVER['REQUEST_METHOD'];
 if ($method == 'GET') {
 $this->runGet($action);
 } else if ($method == 'POST') {
 $this->runPost($action);
 }
 }
 protected function runGet($action) {
 $this->runMethod($action,$this->GET_Map);
 }
 protected function runPost($action) {
 $this->runMethod($action,$this->POST_Map);
 }
 protected function runMethod($action,$map) {
 if ($action && $map && isset($map[$action])) {
 $func = $map[$action];
 $func();
 } else {
 $this->defaultAction();
 }
 }
 protected function setMaps() {
 $this->map = array();
 }
 protected function defaultAction() {
//can be overridden by subclass
 $module = $_REQUEST['module'];
 $action = $_REQUEST['action'];
 $msg = "No handler found for module = $module and action = $action";
 Log::error($msg);
 HttpUtils::badRequest();
 }
}

A sample Controller:(just set GET_map,POST_map)

<?php
class HomeController extends BaseController {
 protected function setMaps() {
 $this->GET_Map['showHome'] = function() {
 HomeService::showHome();
 };
 $this->POST_Map['changeHome'] = function() {
 HomeService::changeHome();
 };
 }
}

BaseEntity.php

<?php
class BaseEntity {
/*
Requirement for it to work correctly : 
1. MySql DB Column names are same as class member names.
2. All the member variables which are to be persisted, should be protected/public. Since get_class_vars() doesn't private members.
*/
 protected $id;
 protected $createdAt;
 protected $updatedAt;
 protected $createdBy;
 protected $updatedBy;
 protected $isActive;
 public function getIsActive() {
 return $this->isActive;
 }
 public function setIsActive($isActive) {
 $this->isActive = $isActive;
 }
 public function getId() {
 return $this->id;
 }
 public function setId($id) {
 $this->id = $id;
 }
 public function getCreatedBy() {
 return $this->createdBy;
 }
 public function setCreatedBy($createdBy) {
 $this->createdBy = $createdBy;
 }
 public function getUpdatedBy() {
 return $this->updatedBy;
 }
 public function setUpdatedBy($updatedBy) {
 $this->updatedBy = $updatedBy;
 }
 public function getCreatedAt() {
 return $this->createdAt;
 }
 public function setCreatedAt($createdAt) {
 $this->createdAt = $createdAt;
 }
 public function getUpdatedAt() {
 return $this->updatedAt;
 }
 public function setUpdatedAt($updatedAt) {
 $this->updatedAt = $updatedAt;
 }
 public function __construct($id = '') {
 if ($id) {
 $this->id = $id;
 $this->buildFromDb();
 }
 }
 public function save() {
 if (!$this->id) {
 $this->createdAt = time();
 $this->updatedAt = $this->createdAt; 
 DBP::insert(static::getTableName(), $this->getFields());
 $this->id = DBP::getLastInsertId();
 } else {
 $this->updatedAt = time(); 
 DBP::update(static::getTableName(), $this->getFields(), $this->getId());
 }
 }
 public function delete() {
 DBP::delete(static::getTableName(), $this->getId());
 }
 public function buildFromDb($row = array()) {
 if (!$row) {
 $columnNames = $this->getCommaSeparatedColumnNames();
 $query = "select $columnNames from " . static::getTableName() . " where id = :id";
 $row = DBP::getSingleResult($query, array('id' => $this->id));
 }
 if ($row) {
 $this->fillThisWithDBValues($row);
 } else {
 $this->id = null;
 }
 }
 private function fillThisWithDBValues($row) {
 $fieldNames = static::getColumnNames();
 foreach ($fieldNames as $fieldName) {
 if (isset($row[$fieldName])) {
 $setterMethod = "set" . ucfirst($fieldName);
 $this->$setterMethod($row[$fieldName]);
 }
 }
 }
 public function getAssocVersion() {
 $row = array();
 $fieldNames = static::getColumnNames();
 foreach ($fieldNames as $fieldName) {
 $getterMethod = "get" . ucfirst($fieldName);
 $row[$fieldName] = $this->$getterMethod();
 }
 return $row;
 }
 protected static function getTableName() {
 //object of this class can't be persisted
 return null;
 }
 protected static function getExistingColumns() {
 $cols = array();
 $query = "SELECT `COLUMN_NAME` as col FROM `INFORMATION_SCHEMA`.`COLUMNS` WHERE `TABLE_SCHEMA`=:db_name AND `TABLE_NAME`=:table_name";
 $bindings = array('db_name' => DB_NAME, 'table_name' => static::getTableName());
 $rows = DBP::getResultSet($query,$bindings);
 foreach ($rows as $row) {
 $cols[] = $row['col'];
 }
 return $cols;
 }
 public function getFields() {
 /*
 * For get_class_vars to work correctly - 
 * all inheriting classes should have all those variables as protected which we want in getFields().
 */
 $fields = get_class_vars(get_called_class());
 foreach ($fields as $key => $value) {
 $getterMethod = "get" . ucfirst($key);
 $fields[$key] = $this->$getterMethod();
 }
 if (isset($fields['id'])) {
 unset($fields['id']);
 }
 return $fields;
 }
 private static function getColumnNames() {
 return array_keys(get_class_vars(get_called_class()));
 }
 public static function getCommaSeparatedColumnNames() {
 return implode(",", static::getColumnNames());
 }
 protected static function getColumnDefinitions() {
 //Subclass should provide its column definitions
 return array();
 }
 private static function getDefaultDefs(){
 $defs = array();
 $defs['id'] = 'bigint(20) primary key auto_increment';
 $defs['createdAt'] = 'int(11) NOT NULL';
 $defs['updatedAt'] = 'int(11) NOT NULL';
 $defs['createdBy'] = 'varchar(64) default null';
 $defs['updatedBy'] = 'varchar(64) default null';
 $defs['isActive'] = 'int(11) default 1';
 return $defs;
 }
 public static function createOrUpdateTable() {
 $existingColumnNames = static::getExistingColumns();
 $update = false;
 if($existingColumnNames) {
 $update = true;
 }
 $defs = self::getDefaultDefs();
 $tableName = static::getTableName();
 $extraDefs = static::getColumnDefinitions();
 $finalDefs = array_merge($defs,$extraDefs);
 if(!$update) {
 $query = "create table $tableName (";
 foreach ($finalDefs as $col => $def) {
 $query .= "$col $def,";
 }
 $query = rtrim($query, ",");
 $query .= ") ENGINE=InnoDB DEFAULT CHARSET=utf8";
 DBP::runQuery($query);
 } else {
 $newColumnDefs = array();
 foreach($finalDefs as $col => $def) {
 if (!in_array($col, $existingColumnNames)) {
 $newColumnDefs[$col] = $def;
 }
 }
 $query = "alter table $tableName";
 foreach($newColumnDefs as $col => $def) {
 $query .= " add column $col $def,";
 }
 $query = rtrim($query,",");
 //echo $query;die;
 DBP::runQuery($query);
 }
 }
}

A sample Entity : City.php

<?php
class City extends BaseEntity {
 protected $name;
 protected $lat;
 protected $lng;
 public function getName() {
 return $this->name;
 }
 public function setName($name) {
 $this->name = $name;
 }
 public function getLat() {
 return $this->lat;
 }
 public function setLat($lat) {
 $this->lat = $lat;
 }
 public function getLng() {
 return $this->lng;
 }
 public function setLng($lng) {
 $this->lng = $lng;
 }
 protected static function getTableName() {
 return 'city';
 }
 protected static function getColumnDefinitions() {
 //only use of this method is for creating table programmatically
 $defs = array();
 $defs['name'] = 'varchar(64) default null';
 $defs['lat'] = 'double default null';
 $defs['lng'] = 'double default null';
 return $defs;
 }
}

DBP.php (The DB class, using PDO)

<?php
class DBP {
 private static $dbh;
 private static $sth;
 private static $logQueryExceptions = true;
 private static $slowQueryErrorLog = true;
 private static $dbHost = null;
 private static $dbName = null;
 private static $dbUser = null;
 private static $dbPass = null;
 public static function enableSlowQueryErrorLog() {
 self::$slowQueryErrorLog = true;
 }
 public static function disableSlowQueryErrorLog() {
 self::$slowQueryErrorLog = false;
 }
 public static function enableQueryExceptionLog() {
 self::$logQueryExceptions = true;
 }
 public static function disableQueryExceptionLog() {
 self::$logQueryExceptions = false;
 }
 public static function configure($db_host, $db_name, $db_user, $db_pass) {
 self::$dbHost = $db_host;
 self::$dbName = $db_name;
 self::$dbUser = $db_user;
 self::$dbPass = $db_pass;
 self::$dbh = null;
 }
 private static function init() {
 if (!self::isConfigured()) {
 self::configure(DB_HOST, DB_NAME, DB_USER, DB_PASS); //these are pre-defined constants. Defined elsewhere
 }
 try {
 $dsn = 'mysql:host=' . self::$dbHost . ';dbname=' . self::$dbName . ';';
 self::$dbh = new PDO($dsn, self::$dbUser, self::$dbPass);
 self::$dbh->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
 self::$sth = null;
 } catch (Exception $e) {
 Log::error($e->getMessage());
 }
 }
 public static function getResultSet($query, $bindings = array()) {
 self::runQuery($query, $bindings);
 return self::getRows();
 }
 public static function beginTransaction() {
 self::runQuery('start transaction');
 }
 public static function commit() {
 self::runQuery('commit');
 }
 public static function rollback() {
 self::runQuery('rollback');
 }
 public static function getPlaceHolderStringAndIdBindings(Array $arr, $prefix = 'id') {
 $placeHolderStr = '';
 $idBindings = array();
 for ($i = 0; $i < count($arr); $i++) {
 $key = "$prefix$i";
 $placeHolderStr .= ":$key,";
 $idBindings[$key] = $arr[$i];
 }
 $placeHolderStr = rtrim($placeHolderStr, ",");
 return array($placeHolderStr, $idBindings);
 }
 public static function getCountFromQuery($query, Array $bindings = array()) {
 $query = "select count(1) as cnt from ($query)t";
 self::runQuery($query, $bindings);
 $rows = self::getRows();
 return $rows[0]['cnt'];
 }
 public static function getLastInsertId() {
 return self::$dbh->lastInsertId();
 }
 private static function isConfigured() {
 return (self::$dbHost && self::$dbName && self::$dbUser);
 }
 public static function runQuery($query, $bindings = array(), $attempt = 0) {
 $maxAttempts = 2;
 if ($attempt < $maxAttempts) {
 try {
 if ((stripos($query, "select") !== 0) && (isset($_SESSION['viewOnly']))) {
 return;
 }
 if (!self::$dbh) {
 self::init();
 }
 $bt = microtime(true);
 if (!self::$dbh) {
 throw new Exception('DBH is null');
 }
 self::$sth = self::$dbh->prepare($query);
 self::$sth->execute($bindings);
 $at = microtime(true);
 $diff = ($at - $bt);
 if ($diff > 2) {
 if (self::$slowQueryErrorLog) {
 Log::error("Time Taken : $diff seconds", $bindings, $query);
 }
 }
 } catch (Exception $e) {
 $error = $e->getMessage();
 if (self::$logQueryExceptions) {
 Log::error($error, $bindings, $query);
 }
 if (($e->getCode() == 'HY000') && ($attempt < $maxAttempts)) { //MySql server has gone away and other General errors denoted by HY000
 Log::error("Sleeping before attempting again to handle HY000 attempt = $attempt");
 self::$dbh = null;
 sleep(5);
 self::runQuery($query, $bindings, $attempt + 1);
 } else {
 throw $e;
 }
 }
 } else {
 Log::error("max attempts crossed. $query");
 }
 }
 public static function delete($tableName, $id) {
 $query = "delete from $tableName where id = :id";
 self::runQuery($query, array('id' => $id));
 }
 public static function insert($tableName, $fields) {
 $query = 'INSERT INTO ' . $tableName;
 $query .= '(`' . implode('`,`', array_keys($fields)) . '`) ';
 $query .= 'VALUES (' . implode(',', array_fill(0, count($fields), '?')) . ')';
 self::runQuery($query, array_values($fields));
 }
 private static function getPartialUpdateStmt() {
 return function($value) {
 return "`$value`" . '=:' . $value;
 };
 }
 public static function update($tableName, $fields, $id) {
 $query = 'UPDATE ' . $tableName . ' SET ';
 $query .= implode(',', array_map(self::getPartialUpdateStmt(), array_keys($fields)));
 $query .= " WHERE id = :id";
 self::runQuery($query, array_merge(array('id' => $id), $fields));
 }
 private static function getRows() {
 return self::$sth->fetchAll(PDO::FETCH_ASSOC);
 }
 public static function getSingleResult($query, $bindings = array()) {
 self::runQuery($query, $bindings);
 $rows = self::getRows();
 if (count($rows) > 1) {
 throw new NonUniqueResultException("$query has Multiple results, bindings = " . implode(",", $bindings));
 } else {
 return $rows ? $rows[0] : null;
 }
 }
 public static function getObject($query, Array $bindings, $className) {
 $obj = null;
 $row = self::getSingleResult($query, $bindings);
 if ($row) {
 $obj = new $className();
 $obj->buildFromDB($row);
 }
 return $obj;
 }
}
asked Jun 24, 2016 at 12:36
\$\endgroup\$
1

1 Answer 1

1
\$\begingroup\$

I can add something;

Instead of typing everything like this:

public function getName() {
 return $this->name;
}
public function setName($name) {
 $this->name = $name;
}
public function getLat() {
 return $this->lat;
}
public function setLat($lat) {
 $this->lat = $lat;
}
public function getLng() {
 return $this->lng;
}
public function setLng($lng) {
 $this->lng = $lng;
}

You can simply use two magic methods:

public function __get($key) {
 return $this->$key;
}
public function __set($key, $value) {
 $this->$key = $value;
}
answered Jun 25, 2016 at 9:31
\$\endgroup\$

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

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

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.