4
\$\begingroup\$

I've been studying PHP for a while now and decided to dive into OOP. Most of my code was a mess and I've begun to refactor much of the website to OOP; however, I'm having an issue with redundancy in my class functions. Below is a my Tracking.class.php file, which is responsible for returning arrays based on the function and the instance. This is my first time using OOP so I'm unsure if I'm being overly redundant in my child classes, and not entirely sure if there is a way I could clean up my code even more. Any help would be great!

class Tracking {

    protected $type;
    protected $user_id;
    protected $response_array;
    protected $result;
    protected $typeToTable = array('weight' => 'wp_weight', 'calories' => 'wp_calories', 'move' => 'wp_move', 'sleep' => 'wp_sleep');
    protected $arrayValue = array('weight' => 'value', 'calories' => 'totalcal', 'move' => 'length', 'sleep' => 'value');

    function __construct($user_id, $type){

        $this->type = $type;
        $this->user_id = $user_id;

    }

    public static function getInstance($user_id, $type){

        switch($type) {
            case "weight" : $obj = new weightTracking($user_id, $type); break;
            case "calories" : $obj = new caloriesTracking($user_id, $type); break;
            case "move" : $obj = new moveTracking($user_id, $type); break;
            case "sleep" : $obj = new sleepTracking($user_id, $type); break;
            case "mood" : $obj = new feelTracking($user_id, $type); break;
        }

        return $obj;

    }   

    function stats( Database $pdo ) {

        $table_value = $this->arrayValue[$this->type];
        $table = $this->typeToTable[$this->type];

        $query = "SELECT AVG($table_value) as avg, MAX($table_value) as max, MIN($table_value) as min from $table WHERE user_id = :user_id";

        $pdo->query($query);

        $pdo->bind(':user_id', $this->user_id);

        $pdo->execute();

        $row = $pdo->single();

        if ( empty( $row ) ) {
            throw new Exception('No Results');
        }

        $avg = round($row['avg'], 2);
        $min = round($row['min'], 2);
        $max = round($row['max'], 2);

        $this->response_array['avg'] = $avg;
        $this->response_array['min'] = $min;
        $this->response_array['max'] = $max;

        return $this->response_array;

    }

    function returnGoalData( Database $pdo ) {

        $query = 'SELECT * FROM wp_goals WHERE goal_type = :goal_type AND user_id = :user_id AND completed = :completed';

        $pdo->query($query);

        $pdo->bind(':goal_type', $this->type);
        $pdo->bind(':user_id', $this->user_id);
        $pdo->bind(':completed', 'N');

        $pdo->execute();

        $row = $pdo->single();

        if ( empty( $row ) ) {
            throw new Exception('No Results');
        }

        if ($this->type == 'weight'){
            $this->response_array['status'] = 'success';
            $this->response_array['value'] = $row['value'];
            $this->response_array['diff'] = days_diff($row['end']);
        }
        else {
            $this->response_array['status'] = 'success';
            $this->response_array['value'] = $row['value'];
        }

        return $this->response_array;

    }

    function lastResult( Database $pdo ){

        $table_value = $this->arrayValue[$this->type];
        $table = $this->typeToTable[$this->type];

        date_default_timezone_set('America/Indiana/Indianapolis');
        $date = new DateTime('now');
        $date = date('Y-m-d', strtotime($date));

        $query = "SELECT $table_value FROM $table WHERE user_id = :user_id AND time = :time";

        $pdo->query($query);

        $pdo->bind(':user_id', $this->user_id);
        $pdo->bind(':time'   , $date);

        $pdo->execute();

        $row = $pdo->single();

        if ( empty( $row ) ) {
            $this->response_array['status'] = 'success';
            $this->response_array['last'] = '0';
        } else {
            $this->response_array['status'] = 'success';
            $this->response_array['last'] = $row['value'];
        }

            return $this->response_array;

    }

}

class caloriesTracking extends Tracking { 

    function prepareGraph( $pdo ){

        $query = "SELECT time, SUM(totalcal) as cal FROM wp_calories WHERE user_id = :user_id GROUP BY time ORDER BY time";

        $pdo->query($query);

        $pdo->bind(':user_id', $this->user_id);

        $pdo->execute();

        $row = $pdo->resultset();

        if ( empty( $row ) ) {
            throw new Exception('No Results');
        }

    // Initialize Array

        $data = array('cols' => array(array('label' => 'time', 'type' => 'date'), array('label' => 'value', 'type' => 'number')),'rows' => array());

    // Build Array

        foreach($row as $rows){
            $data['rows'][] = array('c' => array(array('v' => datecleanse($rows['time'])), array('v' => $rows['cal'])));
        }

        return $data;

    }

    function enumerateGraph( Database $pdo ){

        $query = "SELECT wp_calories.time as time, food_db.desc as `desc`, wp_calories.totalcal as totalcal, wp_calories.food_id as food_id, wp_calories.servenum as servenum, food_weight.desc as weight_desc, wp_calories.meal as meal FROM wp_calories  INNER JOIN food_db ON food_db.food_id = wp_calories.food_id INNER JOIN food_weight on food_weight.food_id = wp_calories.food_id AND food_weight.unit_id = wp_calories.unit_id WHERE user_id = :user_id ORDER BY time";

        $pdo->query($query);

        $pdo->bind(':user_id', $this->user_id);

        $pdo->execute();

        $row = $pdo->resultset();

        if ( empty( $row ) ) {
            throw new Exception('No Results');
        }

        // Build Array

        foreach($row as $rows){
            $data['rows'][] = array('time' => datecleanse($rows['time']), 'desc' => $rows['desc'], 'totalcal' => $rows['totalcal'], 'food_id' => $rows['food_id'], 'servenum' => $rows['servenum'], 'weight_desc' => $rows['weight_desc'], 'meal' => $rows['meal']);
        }

        return $data;

        }

}


class moveTracking extends Tracking { 

    function prepareGraph( Database $pdo ){

        $query = "SELECT time, length FROM wp_move WHERE user_id = :user_id GROUP BY time ORDER BY time";

        $pdo->query($query);

        $pdo->bind(':user_id', $this->user_id);

        $pdo->execute();

        $row = $pdo->resultset();

        if ( empty( $row ) ) {
            throw new Exception('No Results');
        }

        // Initialize Array

        $data = array('cols' => array(array('label' => 'time', 'type' => 'date'), array('label' => 'value', 'type' => 'number')),'rows' => array());

        // Build Array

        foreach($row as $rows){
            $data['rows'][] = array('c' => array(array('v' => datecleanse($rows['time'])), array('v' => $rows['length'])));
        }

        return $data;

    }

    function enumerateGraph( Database $pdo ){

        $query = "SELECT time, value, length, calburn FROM wp_move WHERE user_id = :user_id GROUP BY time";

        $pdo->query($query);

        $pdo->bind(':user_id', $this->user_id);

        $pdo->execute();

        $row = $pdo->resultset();

        if ( empty( $row ) ) {
            throw new Exception('No Results');
        }

        // Initialize Array

        $data = array('cols' => array(array('label' => 'time', 'type' => 'date'), array('label' => 'value', 'type' => 'string'), array('label' => 'value', 'type' => 'number'), array('label' => 'value', 'type' => 'number')), 'rows' => array());

        // Build Array

        foreach($row as $rows){
            $data['rows'][] = array('c' => array(array('v' => datecleanse($rows['time'])), array('v' => $rows['value']), array('v' => $rows['length']), array('v' => $rows['calburn'])));
        }

        return $data;

    }

}

class sleepTracking extends Tracking {

    function prepareGraph( Database $pdo ){

        $query = "SELECT time, value FROM wp_sleep WHERE user_id = :user_id ORDER BY time";

        $pdo->query($query);

        $pdo->bind(':user_id', $this->user_id);

        $pdo->execute();

        $row = $pdo->resultset();

        if ( empty( $row ) ) {
            throw new Exception('No Results');
        }

        $data = array('cols' => array(array('label' => 'time', 'type' => 'date'), array('label' => 'value', 'type' => 'number')),'rows' => array());

        foreach($row as $rows){
            $data['rows'][] = array('c' => array(array('v' => datecleanse($rows['time'])), array('v' => $rows['value'])));
        }

        return $data;

    }

}

class feelTracking extends Tracking { 

    function prepareGraph( Database $pdo ){

        $query = "SELECT time, value FROM wp_mood WHERE user_id = :user_id ORDER BY time";

        $pdo->query($query);

        $pdo->bind(':user_id', $this->user_id);

        $pdo->execute();

        $row = $pdo->resultset();

        if ( empty( $row ) ) {
            throw new Exception('No Results');
        }

        // Initialize Array

        $data = array('cols' => array(array('label' => 'time', 'type' => 'string'),array('label' => 'value', 'type' => 'number')));

        // Build Array

        foreach($row as $rows){
            $data['rows'][] = array('c' => array(array('v' => datecleanse($rows['time'])), array('v' => $rows['value'])));
        }

        return $data;

    }

    function enumerateGraph( Database $pdo ){

        $query = "SELECT value, count(*) as count FROM wp_mood WHERE user_id = :user_id GROUP BY value";

        $pdo->query($query);

        $pdo->bind(':user_id', $this->user_id);

        $pdo->execute();

        $row = $pdo->resultset();

        if ( empty( $row ) ) {
            throw new Exception('No Results');
        }

        // Initialize Array

        $data = array('cols' => array(array('label' => 'time', 'type' => 'string'),array('label' => 'Frequency', 'type' => 'number')));

        // Build Array

        foreach($row as $rows){
            $data['rows'][] = array('c' => array(array('v' => $rows['value']), array('v' => $rows['count'])));
        }

        return $data;

    }

}

class weightTracking extends Tracking {

    function lastResult( Database $pdo ){

        $query = "SELECT value from wp_weight WHERE user_id = :user_id ORDER BY time DESC Limit 1";

        $pdo->query($query);

        $pdo->bind(':user_id', $this->user_id);

        $pdo->execute();

        $row = $pdo->single();

        if ( empty( $row ) ) {
            throw new Exception('No Results');
        }

        $this->response_array['status'] = 'success';
        $this->response_array['last'] = $row['value'];

        return $this->response_array;

    }

    function prepareGraph( Database $pdo ){

        $query = "SELECT time, value FROM wp_weight WHERE user_id = :user_id ORDER BY time";

        $pdo->query($query);

        $pdo->bind(':user_id', $this->user_id);

        $pdo->execute();

        $row = $pdo->resultset();

        if ( empty( $row ) ) {
            throw new Exception('No Results');
        }

        $data = array('cols' => array(array('label' => 'time', 'type' => 'date'), array('label' => 'value', 'type' => 'number')),'rows' => array());

        foreach($row as $rows){
            $data['rows'][] = array('c' => array(array('v' => datecleanse($rows['time'])), array('v' => $rows['value'])));
        }   

        return $data;

    }

}

function days_diff($date){

    date_default_timezone_set('America/Indiana/Indianapolis');
    $today = new DateTime('now');
    $date = new DateTime($date);
    $diff = date_diff($date, $today);

    return $diff->d . " days";

}

function datecleanse($date){

    $year = substr($date, 0, 4);
    $month = intval(substr($date, 5, -3)) - 1;
    $day = substr($date, -2);
    $newd = 'Date('.$year.', '.$month.', '.$day.')';

    return $newd;

}

Edit: For reference is here my Database Class.

class Database {

    protected $host = DB_HOST;
    protected $user = DB_USER;
    protected $pass = DB_PASS;
    protected $dbname = DB_NAME;

    protected $dbh;
    protected $error;
    protected $stmt;

    public function __construct()  {
    // SET DSN
    $dsn = 'mysql:host=' . $this->host . ';dbname=' . $this->dbname;

    // Set options
    $options = array(
        PDO::ATTR_PERSISTENT    => true,
        PDO::ATTR_ERRMODE   => PDO::ERRMODE_EXCEPTION
    );

    // Create a new PDO instance
    try{
        $this->dbh = new PDO($dsn, $this->user, $this->pass, $options);
    }
    catch(PDOException $e){
        $this->error = $e->getMessage();
    }
    }

    public function query($query){
    $this->stmt = $this->dbh->prepare($query);
    }

    public function bind($param, $value, $type = null){
    if (is_null($type)) {
      switch (true) {
        case is_int($value) :
          $type = PDO::PARAM_INT;
          break;
        case is_bool($value) :
          $type = PDO::PARAM_BOOL;
          break;
        case is_null($value) :
          $type = PDO::PARAM_NULL;
          break;
        default:
          $type = PDO::PARAM_STR;
      }
    }
    $this->stmt->bindValue($param, $value, $type);
    }

    public function execute(){
    return $this->stmt->execute();
    }

    public function resultset(){
    $this->execute();
    return $this->stmt->fetchAll(PDO::FETCH_ASSOC);
    }

    public function single(){
    $this->execute();
    return $this->stmt->fetch(PDO::FETCH_ASSOC);
    }

    public function rowCount(){
    return $this->stmt->rowCount();
    }

    public function lastInsertId(){
    return $this->dbh->lastInsertId();
    }
}
\$\endgroup\$
4
  • 1
    \$\begingroup\$ Going from Precedural to OOP is an interesting journey which I took also - so my eyes are burning - but then again once upon a time I did the exact same quote you did. If none of the pros here that will give you a run through - my first two take is that you break SOLID design pattern and your code is too tightly coupled. If no one takes a crack at it - I will reply soon (its just lengthy to drill it down) \$\endgroup\$
    – azngunit81
    Apr 8, 2014 at 21:32
  • \$\begingroup\$ I'd appreciate all the help you can give me. Honestly feel like this code is a big step forward from my earlier procedural work; however, I want to make sure Im moving forward. \$\endgroup\$ Apr 8, 2014 at 22:27
  • \$\begingroup\$ When moving to OO code, you're moving in the direction of re-usable, and generic code. An important part of this is code that conforms to standards, both in style and autoloading. check the PHP coding standards and stick to them as much as possible: file-names, indentation... all of these things matter \$\endgroup\$ Apr 9, 2014 at 10:00
  • \$\begingroup\$ Oh, and extending, or wrapping PDO is not the greatest of ideas, as I have explained before. Just pass around PDO instances instead \$\endgroup\$ Apr 9, 2014 at 10:02

1 Answer 1

6
\$\begingroup\$

I will apologize for the lack of formatting and using your code segments as straight example - but as I re-read your code I see my old self a whole lot and I don't consider myself any level near some of the coders here.

However one important aspect to improvement is obviously trial and error and a lot of refactoring. So you asked about OOP going from procedural php within a single require I would assume your going into one right way which is using classes - however there is a lot of pitfalls in your code.

The first would be that to effectively do OOP you should use SOLID design pattern.

First there are plenty of ORM out there that does your PDO wrapper. One thing starters would like to do is to wrap a low level class (the PDO object) into a higher wrapper functional object - its not wrong to do so - but its been done over. In reality what you should do is not offer the user (ie you in this case) a simpler form of accessing data that you need while using the database. The database of your APP can change...what will you then? You will need to write a wrapper class again because your class is tightly coupled with PDO.

Lets take a look at the database class

protected $host = DB_HOST;
protected $user = DB_USER;
protected $pass = DB_PASS;
protected $dbname = DB_NAME;

protected $dbh;
protected $error;
protected $stmt;

Too many variables - the majority of the time the need to hold onto the DB's credential inside the object is not necessary - it should be part of your configuration of the APP and invoked as such then passed inside to the DB object to initialize the connection - once that is done you don't need this info.

If you change information then a new object should be created. Why? because image you do need to connect to two different DB (one for user, one for tracking as an example) - if you store your credential your stuck to this. Also as I can see from your constructor - your not even passing your credentials to it - its fixed from a global stand point.

The majority of your class is just a smaller typing of what is already presented ie: query, then bind, then execute. Its a rehash of the system's PDO.

Another flaw is your Single() function - it has the execute() command - what happens if your query returns more than one result and you want to iterate it one at a time - you can't re-execute the query to grab the second row. fetch()'s purpose is to go fetch and iterate through the rows one by one. You can argue that you can use resultSet() to grab all then foreach but if you are returning 10K rows you will be in a heap of trouble.

Next - lets touch a bit of the tracking class.

One flaw is the getInstance. Singleton are bad for maintainability because you can't make more than one object of it and its hard to test it.

Ideally - since all your functions are relying on the database - you should have passed it (aka injected) is part of your constructor and then call your functions which uses the DB. Its not like you will instantly use a different DB within the function because you are tightly coupled from it.

Basically: your Track class is your repository, and your PDO is redundant because its just shorthanding the coding.

thats my first pass through of your code.

\$\endgroup\$
8
  • \$\begingroup\$ "you will never need to hold onto the DB's credential inside the object" - I'd have to respectfully disagree with you on that one. Of course, you don't hold on to the password, but the user/host/selected db can be valuable in scenario's where different DB users have different rights to different dbs. adding a whoami-like method can sometimes be a useful thing \$\endgroup\$ Apr 9, 2014 at 10:06
  • \$\begingroup\$ @EliasVanOotegem I was waiting for you to comment. I'll edit it from "never" to most likely. Right there are very particular chances that you would be doing different switching but as I explained - make yourself two DB connections - pass it through the constructoor \$\endgroup\$
    – azngunit81
    Apr 9, 2014 at 11:50
  • \$\begingroup\$ Waiting for me to comment? Why? Pointing out SOLID, existing ORM's, injection (and the inherent problems of Singletons) require no comments, only up-votes. My initial comment was a bit of a nit-pick anyways. +1 anyways \$\endgroup\$ Apr 9, 2014 at 12:32
  • \$\begingroup\$ @EliasVanOotegem you are more thorough than I am. - and tks. \$\endgroup\$
    – azngunit81
    Apr 9, 2014 at 13:41
  • \$\begingroup\$ Thanks for detailed response to my question. Seems there is still quite a bit I need to learn. Completely understand your analysis of my database class, and to be honest, built it based on a tutorial as I was trying to learn PDO. As far as the tracking class, I believe I'm still trying to get a grasp on the differences between OOP and Procedural code. It feels like my class functions are tightly coupled to their respective queries, which may make my code harder to work with. Any further suggestions on how to clean up the functions in my Tracking class? \$\endgroup\$ Apr 9, 2014 at 16:45

Your Answer

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

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