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;
    }

}
\$\endgroup\$
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;
}
\$\endgroup\$

Your Answer

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

Not the answer you're looking for? Browse other questions tagged or ask your own question.