2
\$\begingroup\$

I am posting the a section of the model of my application.

Any/all feedback would be very helpful as I am trying to build my PHP skills, but I will ask a couple specific questions that might help others as well.

  1. Is my escaping, as is, effective enough?

  2. I tried to separate my model into a file that will deal with functions exclusive to 'Groups' and a file that will deal with database functions used by all sections of the app. Is this a good strategy?

  3. My Group model could use some more refactoring, but what do you think of the way I take and return data? (by using $data and $input, $data being the variable that is returned the controller and will be eventually sent to the view via JSON)

  4. What are your thoughts about 'public function show_one($user_id, $group_id, $privacy)' which is meant to display one groups information, given proper privacy settings?

  5. Any other input will be greatly appreciated. I am working hard to improve my programming skills everyday.

Group Model

2) The Group Model:

<?php
require_once("database.php");

class model {
            ///data is returned to the controller
    public $data = array(
        'success'=>0,
        'msg' =>'There was a small problem, try again soon.',
        'data_type' => 'group',
        'action' => '',
        'results'=> array(),
        );
            //anything that will be put into the DB will be held in input
    public $input = array();    
    public $exclude =array();

    public function __construct($a) {
        Global $db;
        $db->escape($a);
        $this->input = $db->escape($a);
        @$this->data['action'] = $a['action'];
    }
//move insert and data return up here

}

class create_group_model extends model {

    public function insert_new_group($a) {
        Global $db;
        $b = $db->insert_array('group', $a, 'action');

        if ($b['mysql_affected_rows']===1) {
            $this->data['success'] = 1;
            $this->data['msg'] = 'Congrats, you created a new group.';
            array_push($this->data['results'], $b['mysql_insert_id']);
            return $this->data;
        } else {
            $this->data['success'] = 0;
            $this->data['msg'] = 'No group created, try again soon.';
            return $this->data;
        }           
    }

}

class search_group_model extends model {

    public function default_list_groups() {
        $this->list_groups($this->input['lat'], $this->input['lng'], 'group', 10, 30, $this->input['user_id'], 'all' );
        $this->data['msg'] = 'Updated';
        return $this->data;
    }

    public function custom_list_groups() {//add custom settings
        $this->list_groups($this->input['lat'], $this->input['lng'], 'group', 10, 10);
    }

    public function my_list_groups($id){
        Global $db;
        //fix distance 10000 here, so it doesnt take into account distance
        $b = $db->def_g_list($this->input['lat'], $this->input['lng'], 'group', 10000, 30, (int)$id, 'mine');

        if ($b !== 0) {
            $this->data['success'] = 1;
            while ($row = mysql_fetch_array($b, MYSQL_ASSOC)) {
                array_push($this->data['results'], $row);
            }
            $this->data['msg'] = 0;
            return ($this->data);       
        } else {
            $this->data['msg'] = 'There was a small problem, try again soon.';
            $this->data['success'] = 0;
            return ($this->data);
        }   
    }

    public function show_one($user_id, $group_id, $privacy) {
        Global $db;
        (bool)$confirm = FALSE;

        if($privacy === 0){
            $confirm  = TRUE;
        }else {
            $confirm  = FALSE;
            $privacy = 1;
        }

        if(!$confirm){
            $s = 'group_id';
            $f = 'user_group_rel';
            $w = sprintf("user_id =%d AND group_id=%d",$user_id, $group_id);
            $b = $db->exists($s,$f,$w);
            if(mysql_num_rows($b) ===1 && !is_num($b)) {
                $confirm=true;
            }
        }

        if($confirm){
            $s = 'group_id,group_name,location_name,description,
            user_id,lat,lng,created_by_name,state';
            $f = 'group';
            $w = sprintf("group_id=%d AND privacy=%d",(int)$group_id, (int)$privacy);
            $b = $db->exists($s,$f,$w);
            if(mysql_num_rows($b) ===1 && !is_int($b)) {
                $this->data['success'] = 1;
                $this->data['msg'] = 0;
                while ($row = mysql_fetch_array($b, MYSQL_ASSOC)) {
                    array_push($this->data['results'], $row);
                }
                $this->data['results'][0]['people']= $this->group_mems($user_id, $group_id);
                $this->data['results'][0]['total_people']= $this->group_mems_count($group_id);
                return ($this->data);
            }
        }

        $this->data['msg'] = 'There was a small problem, try again soon.';
        $this->data['success'] = 0;
        echo 'still going';
        return ($this->data);
    }

    public function group_mems($me, $group_id) {
        Global $db;
        $result = array();
        $q = sprintf("
            SELECT 
                t1.group_id, t2.user_id, t2.username, t2.first_name, t2.last_name, t2.user_id = %d as me
            FROM user_group_rel t1
            LEFT JOIN users t2 ON t1.user_id = t2.user_id
            WHERE  group_id = %d and status = 1
            ORDER BY me desc",
        (int)$me, (int)$group_id);
        $b = $db->query($q);
        if ($b !== 0 and is_resource($b)) {
            while ($row = mysql_fetch_array($b, MYSQL_ASSOC)) {
                    array_push($result, $row);
            }
            return $result;     
        } else {
            return 'err';
        }

    }

    public function group_mems_count($group_id) {
        Global $db;
        $result = array();
        $q = sprintf("
            SELECT 
            count(t1.user_id) as people
            FROM user_group_rel t1
            WHERE  group_id = %d and t1.status = 1",
        (int)$group_id);

        $b = $db->query($q);
        if ($b !== 0 and is_resource($b)) {
            while ($row = mysql_fetch_array($b, MYSQL_ASSOC)) {
                    array_push($result, $row);
            }
            return $result[0]['people'];        
        } else {
            return 'err';
        }

    }


    private function list_groups($lat, $lng, $table, $distance, $limit, $id, $whos) {
        Global $db;

        $b = $db->def_g_list($lat, $lng, $table, $distance, $limit, (int)$id, $whos);
        if ($b !== 0) {
            $this->data['success'] = 1;
            while ($row = mysql_fetch_array($b, MYSQL_ASSOC)) {
                array_push($this->data['results'], $row);
            }
            return ($this->data);       
        } else {
            $this->data['msg'] = 'There was a small problem, try again soon.';
            $this->data['success'] = 0;
            return ($this->data);
        }

    }
}

class join_group_model extends model {

    public function join_group() {
        Global $db;
        $pass = 0;
        if(array_key_exists('password', $this->input) && strlen($this->input['password'])>20) {
            $pass = $db->pass_check('group', $this->input['group_id'],$this->input['password'] );
        }
        else {
            $pass = $db->pass_check('group', $this->input['group_id'],'NULL' );
        }


        if($pass !==0) {    
            array_push($this->exclude, 'password', 'action');
            $b = $db->insert_array('user_group_rel',$this->input, $this->exclude);
            //echo print_r($b);
            if ($b !== 0) {
                $this->data['success'] = 1;
                $this->data['results'] = $this->input['group_id'];
                $this->data['msg'] = ' you joined a new group. ';
                return ($this->data);       
            } else {
                $this->data['msg'] = 'There was a small problem, try again soon.';
                $this->data['success'] = 0;
                return ($this->data);
            }
        }   
    }

    public function unjoin_group() {
        Global $db;

        $b = $db->delete('user_group_rel', (int)$this->input['user_id'], (int)$this->input['group_id']);
        if ($b !== 0) {
            $this->data['success'] = 1;
            $this->data['results'] = $this->input['group_id'];
            $this->data['msg'] = ' you left that group. ';
            return ($this->data);       
        } else {
            $this->data['msg'] = 'There was a small problem, try again soon.';
            $this->data['success'] = 0;
            return ($this->data);
        }
    }


}

Database

3) Application wide database methods. A couple notes:

a) Probably the most glaring mistake here is not using PDO statements.

<?php
require_once('../config/config.php');

class MySQLDatabase {

    private $connection;
    public $last_query;
    private $magic_quotes_active;
    private $real_escape_string_exists;

  function __construct() {
    $this->open_connection();
        $this->magic_quotes_active = get_magic_quotes_gpc();
        $this->real_escape_string_exists = function_exists( "mysql_real_escape_string" );
  }

    public function open_connection() {
        $this->connection = mysql_connect(DB_SERVER, DB_USER, DB_PASS);
        if (!$this->connection) {
            die("Database connection failed: " . mysql_error());
        } else {
            $db_select = mysql_select_db(DB_NAME, $this->connection);
            if (!$db_select) {
                die("Database selection failed: " . mysql_error());
            }
        }
    }

    public function close_connection() {
        if(isset($this->connection)) {
            mysql_close($this->connection);
            unset($this->connection);
        }
    }

    public function query($sql) {
        $this->last_query = $sql;
        $result = mysql_query($sql, $this->connection);
        $this->confirm_query($result);
        return $result;
    }

    public function escape($q) {

        if(is_array($q)) 
            foreach($q as $k => $v) 
                $q[$k] = $this->escape($v); //recursive
        else if(is_string($q)) {
            $q = mysql_real_escape_string($q);
        }    
        return $q;
    }

    private function confirm_query($result) {
        if (!$result) {
        $output = "Database query failed: " . mysql_error() . "<br /><br />";
        die( $output );
        }
    }

    public function exists($s,$f, $w) { 
        //rewrite bottom 2 functions rid of this function
        $q = sprintf("SELECT %s FROM %s WHERE %s LIMIT 1",$s,$f, $w);
        //echo $q;
        $result = $this->query($q);
        if (mysql_num_rows($result)===0) {
            return 0;
        } else if (mysql_num_rows($result)===1) {
            return $result;
        } else{
            return 0;
        }

    }

    public function pass_check($t, $id, $p) {
        if ($p==='NULL'){
            $q = sprintf("SELECT * FROM %s WHERE %s_id = %s AND password is NULL", $t,$t,$id); 
        }
        else{
            $q = sprintf("SELECT * FROM %s WHERE %s_id = %s AND password = '%s'", $t,$t,$id,$p);
        }
        $result = $this->query($q);
        if (mysql_num_rows($result)===0) {
            return (int)0;
        } else if (mysql_num_rows($result)>0) {
            return $result;
        }
    }


    public function insert_array($table, $data, $exclude = array()) {

        $fields = $values = array();
        if( !is_array($exclude) ) $exclude = array($exclude);

        foreach( array_keys($data) as $key ) {
            if( !in_array($key, $exclude) ) {
                $fields[] = "`$key`";
                $values[] = "'" .$data[$key] . "'";
            }
        }

        $fields = implode(",", $fields);
        $values = implode(",", $values);

        if( mysql_query("INSERT INTO `$table` ($fields) VALUES ($values)") ) {
            return array( "mysql_error" => false,
                          "mysql_insert_id" => mysql_insert_id(),
                          "mysql_affected_rows" => mysql_affected_rows(),
                          "mysql_info" => mysql_info()
                        );
        } else {
            echo print_r(array( "mysql_error" => mysql_error() ));
            return 0;
        }
    }

    public function def_g_list($lat, $lng, $table, $dist, $limit, $id, $whos='all') {
        //either refactor to make this function more flexible or get rid of table variable
        $where = '';
        if(is_int($id) && $id>0 && $id < 100000 && $whos == 'all'){
            //subquery used to display only groups the user is NOT IN- probably a better way to do this
            $where = sprintf("
                t2.group_id NOT IN (
                SELECT user_group_rel.group_id
                FROM user_group_rel
                WHERE user_group_rel.user_id =%d)",
            (int)$id);
        } else if (is_int($id) && $id>0 && $id < 100000 && $whos == 'mine') {
            $where = 't3.user_id = '  . (int)$id;
        } else {
            return 0;
        }
        $d_formula = $this->distance_formula($lat, $lng, 't1');
        //sorry for this query
        $q = sprintf("
            SELECT
                t1.group_id, t1.group_name, t1.location_name, t1.description, t1.lat, t1.lng,                         t1.privacy,t2.people, %s
            FROM %s AS t1
            JOIN (SELECT group_id, count(group_id) as people
                  FROM user_group_rel
                  GROUP BY group_id) t2
                  ON t1.group_id = t2.group_id          
            JOIN (SELECT user_group_rel.group_id, user_group_rel.user_id
                  FROM user_group_rel ) t3
                  ON t1.group_id = t3.group_id
            WHERE %s 
            GROUP BY t1.group_id
            HAVING distance < %s
            ORDER BY distance
            LIMIT %s",
        $d_formula, $table, $where,$dist, $limit );

        $result = $this->query($q);
        if (mysql_num_rows($result)===0) {
            return 0;
        } else if (mysql_num_rows($result)>0) {
            return $result;
        }

    }   

    function delete($table,$uid,$cid) {
        $q = sprintf('
            DELETE FROM `disruptly`.`%s`
            WHERE `%s`.`user_id` = %d
            AND `%s`.`group_id` = %d
            ',$table, $table, $uid,$table, $cid
        ); 
        //echo $q;
        $result = $this->query($q);
        if ($result===0) {
            return 0;
        } else if ($result===1) {
            return $result;
        }   
    }

    public function distance_formula($lat, $lng, $table) {
        //get rid of the round after'SELECT *,' for more accurate results
        $q = sprintf("round(3956 * 2 * ASIN(SQRT( POWER(SIN((%s -abs(%s.lat)) * pi()/180 / 2),2) + COS(%s * pi()/180 ) * COS(abs(%s.lat) *  pi()/180) * POWER(SIN((%s - %s.lng) *  pi()/180 / 2), 2) )),2) as distance ", $lat, $table, $lat, $table, $lng, $table);
        return $q;
    }   

}


$database = new MySQLDatabase();
$db =& $database;

Edit - If anyone disagrees or has something to add, don't be shy.

I'm going to ask some follow-up questions:

  1. You said, "...these are models whose functionality is designed for its controller rather than a controller whose functionality is designed for its model." This was spot on. I started coding my "controller" in mind first, or rather what data will the view be requesting from the model. In the future, I should construct my model first?

  2. Globals are evil, lesson (re)learned. Never again. No question here, just a fact.

  3. You say, "Let me just say that this is not the implementation I am suggesting..." I imagine in reference to my overall structure. I'm not asking you to outline a new implementation, but how you would approach/think about it differently. (2-3 sentences is fine)

  4. "Why are all of your methods returning the $data property?" It is a phonegap application, so the whole 'view' is already on the client. The controller sends $data(JSON) for each ajax request for the info to be filled in. This might be the root of the botched MVC pattern, this was my thinking:

View: Client-side app, which has all the templates already loaded and fills them with requests to the controller via ajax

Controller: Skinny gatekeeper between client and model - not much going on here besides accepting get/post requests, validation(does username have 5 letters?) and forwarding data to the appropriate functions in the model. Then passing the model's data relatively untouched to the 'view'(will add an html sanitizing function).

Model (Group): All methods/classes pertaining to group functionality (SQL Statement prep, parse and return).

Model (Database): Where all the application wide sql queries actually get escaped, executed and returned 'untouched'

Q) Am I correct in my understanding that you're saying the Model(Group) looks more like a controller than a model because of the parsing?

  1. "There is no reason to typecast something that is already of the type you are trying to cast it to." My reasoning for using typcasting and sprintf(with %d) was perhaps a failed attempt at data integrity/security. For example, I expect user_id to be a number but what if someone sends a string or an sql injection technique? I thought those were 2 ways of potentially saving yourself from a 1=1. You say no not worth it? (probably the most correct answer is just use PDO).
\$\endgroup\$
  • \$\begingroup\$ Curious why you limit error output instead of properly catching and handling exceptions - @$this->data['action'] = $a['action'];. If this buggers up, you'll never know. \$\endgroup\$ – ggdx Dec 5 '18 at 13:49
2
\$\begingroup\$

This is better, still a bit long, but unavoidably so. I might reiterate a few things from my last answer, but it is only because I'm too lazy to read it again to make sure I'm not. Sounds kind of ironic, but I do find it easier to just write it, rather than read it :)

General Overview

First let's have a quick look at your before diving in. I count four model classes, and what appears to be a fifth, which I will ignore for now. Hopefully each class is in its own file and not saved all into one. If not, you might want to strongly consider it. None of those four classes resembles a "true" model. Instead what you have here appear to be three model-controllers. Esentially, that means these are models whose functionality is designed for its controller rather than a controller whose functionality is designed for its model. The reason this is bad is because it leaves room for repetition. A final model should not be broken up into multiple parts, it should be one functional whole. That's not to say that you can't have a base class and extend subclasses to get to that whole, just that you shouldn't need to call three separate models to accomplish the task of what one model can do.

Now, A Little More In Depth

The very first thing I see is a global. Globals are evil. Pretend you never saw them. You will NEVER need them. I repeat NEVER. Especially not in a class. That's what properties are for.

class model {
    public $db;//should actually be private or protected

    public function __construct() {
        $this->db = new MySQLDatabase();
    }
}

Now every class that extends the model can use the same $db instance, and since it is a public property, you can also use that instance outside of the class once the model has been initialized. If you have an instance created before you initialize the model, and you want to use that one instead, then you should pass that instance to the constructor and set it as your property. The same holds true for regular functions as well: Pass it in, then return the modified version. However, this is not always the best solution. Sometimes, you can extend a class to accomplish the same thing.

//Using an existing instance
public function __construct( $db ) {
    $this->db = $db;
}
//Extending
class model extends MySQLDatabase() {
    //keep your current properties, they are fine
    public function __construct( $query ) {
        $this->input = $this->escape( $query );
        $this->data[ 'action' ] = isset( $a[ 'action' ] ) ? $a[ 'action' ] : NULL;
    }
}

Let's look at what we did here. The first thing I did was rename your parameter. Don't use vague names like $a. What is $a? I had to go all the way to the escape() method just to find out. Don't make your readers, or yourself, have to figure out what something is. Be descriptive with your variables/properties/functions/methods. Not too descriptive, but descriptive enough that we can work with what is given. This is called self-documenting code and makes your code much easier to read and debug. Next, I removed that redundant escape(). That method only ever returns a value, therefore declaring it without setting it to some variable or property is pointless. Lastly, I removed that error suppression. Don't use error suppression in your code. It is a sign of bad coding and should be avoided. The only exception might be while debugging, but then it should promptly be removed. What I've demonstrated above is a ternary statement and should fix those warnings you were occasionally getting about "action" being undefined in $a. Its the same as an if/else statement (if == ?, else == :), only shorter. Maybe you've seen it before, maybe not. Either way, be careful using them as they are not always well liked because they can cause issues with legibility if used incorrectly. My rule of thumb is, no nesting, complex, or long ternary; as long as it's short and sweet it's just fine.

Let me just say that this is not the implementation I am suggesting, I am just using this for demonstration. If just scanning the structure of these classes, as I did in the general overview, then the class and method names might look convincingly like a model's, but upon closer examination we find that it is actually a controller class. This would explain why I thought your models resembled model-controllers. And of course, a controller should not extend a model, of which your MySQLDatabase class is. As such, that very first example I showed you is the better choice here. Create a $db property and instantiate it in the constructor. It shouldn't be passed as an already existing instance because one shouldn't exist. It's the controller's job to instantiate the model.

Now that we know that your first four classes are actually controllers, we can better understand how they should work together. I'm going to leave the structure of these classes alone for now, because that will make this answer quite a bit longer, but you should take another look at that as well. Just remember: Models read and manipulate the data; Controllers request those actions to be performed and parse it for the view; Views are templates that hold placeholders that the Controller fills in.

Ignoring MVC

From here on out, I'm going to ignore the MVC pattern and just focus on the actual code, I believe I got most of that anyways. That being said, there is already much here for you to look over, so I'm only going to scan over the rest of this and point out those things that do jump out at me.

In your default_list_groups() method you dissect the $input property before passing it to the list_groups() method. Instead, it would make more sense to pass the entire $input array as a parameter and dissect it in the list_groups() method. This decreases your parameter list and makes your code easier to read. I noticed you use this same method in other methods without all the other $input parameters you used here. In order to compensate you will have to use checks or default values in the list_groups() method to determine if these parameters have been set.

$this->list_groups( $this->input, 'group', 10, 30, 'all' );

Why are all of your methods returning the $data property? Wait until you are ready to render your view and use the render() method to extract that for you. This reduces repetition and ensures there are no "confusing" return values. By the way, I see no render()-like method, so here is an example of what I am talking about, just in case.

public function render( $view ) {
    extract( $this->data );
    include $view;
}

It appears that you only use the $data[ 'msg' ] property to report errors or track status. A better way would be to use an array that way you could track more than one at a time. Right now you overwrite this node with the last value that is processed, which can be pretty confusing if there were an error earlier but you can't see it because the last operation proved successful.

$this->data[ 'msg' ] = array();
//etc...
$this->data[ 'msg' ] [] = 'There was a small problem, try again soon.';//though this message should be more specific about what happened.

Don't recreate a boolean. That's what you are doing with your $data[ 'success' ] property. If you have a variable that switches between two states (on/off, 0/1, etc...), then use a TRUE/FALSE boolean. The TRUE state is commonly replaced with an actual return product. For example:

return isset( $val ) ? $val : FALSE;

array_push() isn't really used much anymore. The more common way to append data onto an array is to use the following syntax:

$this->data[ 'results' ] [] = $row;

There is no reason to typecast something that is already of the type you are trying to cast it to. Typecasting is for converting a variable's type to something it's not. For example, using that switch example from above, if we typecast 0 to a bool, then we get FALSE. If we typecast 1 to a bool, we get TRUE, which is why its better to explicitly use a boolean rather than have PHP need to convert it for you. Typecasting is useful, but don't use it if you don't have to. Most of the typecasting I see here appears to be unnecessary, but I'm not going to examine each case. Just ask yourself this about each case, "Is this necessary? Is this variable ever set to anything that is not of this type? Why am I typecasting it?" The answer to these questions will help you determine how to proceed.

Try not to use the "not" operator, unless you have to. If you have an if/else pair, then you can switch those statements around. Why not not? Sorry couldn't help myself :) Anyways, maybe this is just a stylistic choice, but it appears to be a commonly accepted one. Also, $b is a little easier to read than !$b, especially if you don't add spaces around the operator.

if( $b !== 0 ) {
    //if contents
} else {
    //else contents
}
//Is the same as
if( $b === 0 ) {
    //previous else contents
} else {
    //previous if contents
}

Of course, there is an even better way to do this. Assuming that your if/else statement has default values, you can just set those default values and then if the right condition is passed, the values are updated. For example:

$this->data['msg'] = 'There was a small problem, try again soon.';
$this->data['success'] = 0;

if ($b !== 0) {
    $this->data['success'] = 1;
    while ($row = mysql_fetch_array($b, MYSQL_ASSOC)) {
        array_push($this->data['results'], $row);
    }
    $this->data['msg'] = 0;
}

return $this->data;

Something else that may not be as obvious about the above example is that the return statement is no longer associated with either the if nor the else statements. It has been removed from both. We can do this because the return was the same in both instances, it was just its value that changed. So, if we follow the "Don't Repeat Yourself" (DRY) Principle, then we only need to do this once, at the very end of the method. As the name implies, the DRY principle means that your code shouldn't repeat. So, perhaps even those default values are unnecessary. I am unsure, it depends on how this is implemented. If those values already exist from when the class was first instantiated, or are somehow reset, then there is no reason to restate them. This principle is very important in OOP and can be applied to many things. Of equal importance is the Single Responsibility Principle, which you should also take a look at. Here's another violation of DRY. You have the below loop, in several different locations throughout the code, or one mostly identical to it. If this were a method, it would not need to be retyped each time. And since we are in a class, if the results are the same, you can just set this to a class property and use that property thereafter instead of needing to keep recreating it.

while ($row = mysql_fetch_array($b, MYSQL_ASSOC)) {
    array_push($this->data['results'], $row);
}

sprintf() is a special case function. If you can accomplish the same thing with just a normal string, you should use the normal string. sprintf() is just too slow to be worth it, not to mention it abstracts a concept that need not be abstracted. For example:

$w = sprintf("user_id =%d AND group_id=%d",$user_id, $group_id);
//is the same as
$w = "user_id=$user_id AND group_id=$group_id";//easier to read
//and the same as
$w = 'user_id=' . $user_id . ' AND group_id=' . $group_id;//only slightly easier to read

There is a difference between the && and AND operators and the || and OR operators. The && and || pair have a higher precedence than the AND and OR pair. As such, using the lower precedence pairs could cause unforeseen issues and should be avoided. After five years I have never needed that second set, it is almost always preferable to use the first. When are these preferable? I honestly don't know, you'd have to ask someone a little bit wiser in their inner workings than me. I just know you will get many weird looks and will always be advised to use the others. Additionally, the AND and OR operators should always be capitalized when being using. Please note: This does not include the AND in MySQL, that is always AND and never &&.

if( $b !== 0 AND is_resource( $b ) ) {
//should be
if( $b !== 0 && is_resource( $b ) ) {

Avoid using die(). It is a very inelegant way of stopping script's execution. A much better way would be to identify the problem, log it, then create a nice error page that lets the user know something went wrong.

die( "Database connection failed: " . mysql_error() );
//vs
log( mysql_error() );//for you
include '404.php';//for the user

What is the point of confirm_query()? It would be one thing if this method were reused, but it is only being used by query(). Drop the unnecessary abstraction and just perform the checking inside the original method. Unless you plan on creating a common method, reused by many other methods, that logs errors and reports errors to the end user, which isn't a half bad idea. How it is currently I would not have expected the method to just die if something went wrong. Another good reason not to use die(), but that's not the main concern here.

$result = mysql_query($sql, $this->connection);
if( ! $result ) {
    //log and report
}

Please, always use braces on your statements, even one-liners. It makes your code much easier to read and update. It also ensures that there are no mistakes, because braceless statements can only be one line, it is very easy to forget to add those braces when you go to extend that statement to two lines. If the braces are always there it ensures that no such errors ever occur. Additionally, be consistent in your style. Below you have two statements that use no braces and then an else statement that does.

if(is_array($q)) 
    foreach($q as $k => $v) 
        $q[$k] = $this->escape($v); //recursive
else if(is_string($q)) {
    $q = mysql_real_escape_string($q);
}

When you have a return statement that is essentially a FALSE value, use the actual FALSE value, unless the non-FALSE value is explicitly needed. For instance 1-1 should return a 0, but ! $results should return a FALSE. Or NULL. Personally I prefer FALSE, but that's up to you. A zero would only make sense here if I explicitly wanted the number of results so that I could use it somewhere.

Don't assign two variables at the same time. This makes it rather difficult to visually track down where the second variable came from.

$fields = $values = array();
//should be
$fields = array();
$values = array();

There are reasons why you would sometimes need to explicitly use array_keys() in a foreach loop, but if you need both the key and value, then you should just do the following:

foreach( $data as $key => $value ) {
    if( ! in_array($key, $exclude) ) {
        $fields[] = "`$key`";
        $values[] = "'$value'";
    }
}

Here are a couple of general comments about some things I found common throughout most of the code. Interior comments only add noise. If you wish to document your code, use PHPDoc comments. A bunch of interior comments can quickly lead to illegibility. Parenthesis are not always necessary. For example, include, require, echo, return and a few others don't require parenthesis. While this is a stylistic choice, many find it preferable because it makes legibility so much better, especially when using parameters that do require parenthesis.

Sorry, I kind of flew over the last half of this review. I'm pretty sure there is more I could help you with, but this is already very large. Hopefully this helps you get started though. I believe I answered each of your questions in my review. If you have anything you'd like clarity on, just let me know.

Edit Per OP Edit

Sorry for the delay. Had a busy weekend.

1) That quote was from the general overview. In it I was explaining how your code looked like a Model if we did not look too closely at it, later I explained these are actually Controllers. I was attempting to show you how easy it is to make that kind of mistake. Please see the step-by-step below for the second part of this question.

3) I explained this in the same paragraph that quote came from. This is also where I explained that your Models were actually Controllers. In that quote I was saying that extending the base MySQLDatabase class was the wrong approach and that you should use the one above that, now also below for reference. The reasoning for this was that the MySQLDatabase class is a Model, and those classes you were calling Models were actually Controllers. Controllers should not extend Models, they should instantiate them when they are instantiated, such as in the constructor.

class model {
    public $db;//should actually be private or protected

    public function __construct() {
        $this->db = new MySQLDatabase();
    }
}

4) Lets look at the typical Controller real quick: One instance is equal to one View, as such one Controller should correspond to one View. When you have multiple methods whose task is to "render" information to different Views, then you are creating unnecessary overhead. Phones aren't as quick or resourcefull as a regular computer. They need all the help they can get. So if you have methods that each of these controllers are going to need, you can create a parent class and extend it for each of these specific Controllers. So this quote is saying that only ONE method should return the render information. BTW: Your view of MVC appears right, but I don't know if that is just because you are using the right terms for the wrong things, or if you actually understand it. Read over my answer and the step-by-step below to make sure your idea corresponds. I believe the only problem you had was mixing up your Controllers and Models.

5) PDO probably would be the best bet, but isn't the only thing here. Typecasting isn't helpful if the original value wasn't already of a similar type. For instance, typecasting the string "25" to an int is acceptable because it is easily translateable, but typecasting the string "twenty-five" to an int is not because its just a string. We, as people might understand the conversion, but PHP will not. Besides, a better method would be typehinting. Its where you declare a variable's type in the parameter list and if not passed the right type it will throw an error and cancel execution. The only problem with this is that you cannot use scalar types, such as int and string. I wouldn't be too worried about that though, pretty much anything can be converted to a string or int OTF. For those instances where it can't, strval() and intval(), or the typecasting equivalent, are acceptable. However, the variable's type should be confirmed and maintained during sanitization and validation, so typecasting should be unnecessary.

public function test( Array $array ) {//$array must now contain an array, else execution quits

My Step-By-Step To MVC

The following is my step-by-step approach to creating an MVC framework. It serves me well, but it is not the ONLY way. Any way that works for you is the right way. There's no ONE right way to do anything. Take suggestions and test them and expirement, but don't get stuck trying to force yourself to do something one way because that is the only way you were shown. That's like being forced to use your non-dominant hand because your parents don't want a {insert-dominant-hand-here}y. That's what happened to me for a while (not the hand thing, though that did too and now my handwriting is attrocious), and I started despising MVC because of it. Everyone was saying, "start by creating the Model, its the ONLY logical first step". Then one day I sat down and said, "Damnit, there's got to be a better, easier way!" This is the one I found:

  1. Start with the View. Lay it out, and style it however you want. Statically add all of the information and make sure everything looks right.
  2. Extract the dynamic parts from the view and replace them with variables, and maybe some simple if and for/foreach statements.
  3. Create a Controller with a render method containing the variables and an include for the view. Make sure to declare the variables before the include so the View can use them.
  4. Create a Model with the appropriate methods to return each of those values and replace the Controller's variables with calls to those methods. If necessary create methods in the Controller for recursive calls to Model methods and repeat tasks.
  5. Flesh out and tidy up the rest of your Model and Controller. As you add more Controllers that use that Model, the Model may grow, but make sure those methods aren't repeating or similar and can be used in any controller.

There, you are done. There's a little more to it than this, but this scaffolding should get you started. And the best part is that you can test it at any point and it should always work. In fact, you should not notice a difference at all.

\$\endgroup\$
  • \$\begingroup\$ Awesome, rather than putting my response here I put it up top. \$\endgroup\$ – Emin Israfil Sep 8 '12 at 1:02
  • \$\begingroup\$ @Tobias: Answer updated per edit \$\endgroup\$ – mseancole Sep 10 '12 at 14:39

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.