8
\$\begingroup\$

I'm working on a PHP API for my angular JS Phonegap application. At the moment I'm focusing on working with a user's facebook public profile data.

I'll explain the process a little first:

  1. The user chooses to log in with facebook on the client side (using phonegap's cordova-plugin-facebook4).
  2. On successful facebook authentication there I retrieve the user's facebook "id", "picture", "name".
  3. I make a http POST request (from the client side) to an endpoint of my PHP API called auth (sending the facebook data with the request).
  4. I check if this "id" already exists in my facebook_users table of my database and if it does I return the facebook profile details back to the client side.
  5. If it does not exist I create a new row and insert the user's details (and also return the facebook profile details back to the client side in JSON format). (Note: I may also send back a JSON web token with the profile data but i have not implemented this.)

I was wondering what you think of my PHP code so far. I've not spent much time working with OOP so i'm wondering what you think of the structure I've created?

  1. Do you think my code is efficient?
  2. Is there anything you notice I could do better?

DatabaseConnection class

<?php
/*
 * A class which contains all methods for retrieving and inserting data to our database.
 * 
 */
class DatabaseConnection{

    private $query;
    private $statement;
    private static $instance = null;    
    private $pdoConnection;


    public function __construct(){
        //construct a new PDO object.
        $this->pdoConnection = new PDO(HOSTDBNAME, USER, PASSWORD);
        $this->pdoConnection->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
        $this->pdoConnection->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
        $this->pdoConnection->exec("SET CHARACTER SET utf8mb4");
    }
    public static function getInstance(){
        if(!isset(self::$instance)){ // Check if the instance is not set.
            //create a new DatabaseConnection instance which will execute the code in the above constructor.            
            self::$instance = new DatabaseConnection(); 
        }
        return self::$instance; 
    }
    public function createNewFacebookUser($facebookUserID, $facebookName, $profilePicURL, $userPrivilegeID){
        /*
         * This method takes in a users facebook credentials as parameters and inserts them to our facebook_users table.
         * If successfully inserted then we return an array containing the userID value.
         * If unsuccessful return an exception
         * 
         */
        $data = array();
        try{
            $this->query  = "INSERT INTO facebook_users (facebookUserID, facebookName, profilePicURL, userPrivilegeID) VALUES (:facebookUserID, :facebookName, :profilePicURL, :userPrivilegeID)";
            $this->statement = $this->pdoConnection->prepare($this->query);
            $this->statement->bindValue(':facebookUserID', $facebookUserID, PDO::PARAM_STR);
            $this->statement->bindValue(':facebookName', $facebookName, PDO::PARAM_STR);

            $this->statement->bindValue(':profilePicURL', $profilePicURL, PDO::PARAM_STR);
            $this->statement->bindValue(':userPrivilegeID', $userPrivilegeID, PDO::PARAM_INT);

            $this->statement->execute();
            $data['userID'] = $this->pdoConnection->lastInsertId(); 

            return $data;
        }catch(PDOException $e){
            //Would it be a good idea to log this error in a php log file?
            $data['exceptionOccurred'] = true;
            $data['exceptionMessage'] =  $e->getMessage();
            return $data;
        }
    }

    public function checkFacebookID($facebookUserID){
        /*
         * This method checks to see if a users facebook id exists in our facebook_users table.
         */
        $data = array();
        try{

            $this->query = "SELECT userID FROM facebook_users WHERE facebookUserID = :facebookUserID LIMIT 1";
            $this->statement = $this->pdoConnection->prepare($this->query);
            $this->statement->bindValue(':facebookUserID', $facebookUserID, PDO::PARAM_STR); 
            $this->statement->execute();
            $this->statement->setFetchMode(PDO::FETCH_ASSOC);
            $rowCount = 0; //store the count into a local variable

            while($row = $this->statement->fetch()){
                //store the data from the result of the query into an associative array. 
                $data['userID'] = $row['userID'];
                $rowCount++;
            }
            if($rowCount > 0){
                //The facebookUserID exists in the database.
                $data['facebookUserIDExists'] = true;

                return $data;
            }else{
                $data['facebookUserIDExists'] = false;
                return $data;
            }

            }catch(PDOException $e){
                //Would it be a good idea to log this error in a php log file?
                $data['exceptionOccurred'] = true;
                $data['exceptionMessage'] =  $e->getMessage();
                return $data;
        }       
    }



    public function getUserProfile($userID){
        //this method takes in a userID as a parameter and gets that user's data from the profiles table in the database
        $data = array();
        try{
            $this->query ="SELECT * FROM facebook_users WHERE userID = :userID LIMIT 1";
            $this->statement = $this->pdoConnection->prepare($this->query);
            $this->statement->bindValue(':userID', $userID, PDO::PARAM_INT); 
            $this->statement->execute();
            $this->statement->setFetchMode(PDO::FETCH_ASSOC);

            while($row = $this->statement->fetch()){
                //store the data from the result of the query into an associative array. 
                $data['userDetails'] = $row;
            }
            return $data;   
        }catch(PDOException $e){
            $data['exceptionOccurred'] = true;
            $data['exceptionMessage'] =  $e->getMessage();
            return $data;
        }   
    }


}
?>

FacebookUser class

 <?php
    /*
     * A class for working with a user's facebook public profile data.
     */
    class FacebookUser {

        private $databaseConnection;
        private $facebookUserID;    
        private $facebookName; 
        private $profilePicURL;    
        private $userPrivilegeID = 1; //default value of 1 which is a regular user. 
        private $userID = null; //this will be the unique userID in our database. 
        private $exceptionOccurred; 

        public function __construct($facebookUserID, $facebookName, $profilePicURL, $userPrivilegeID = 1){
            $this->databaseConnection = DatabaseConnection::getInstance();
            $this->facebookUserID = $facebookUserID;
            $this->facebookName = $facebookName;    
            $this->profilePicURL = $profilePicURL;  
            $this->userPrivilegeID = $userPrivilegeID;
        }

        public function checkFacebookDetails(){
            /*This method checks to see if a users unique facebook ID already exist in our database.
             *If it doesnt exist then we enter their details to the database and send their profile data back to the client side.
             *If it does exist then we just send their profile data back to the client side.
             */
            $data = array();
            $checkIDResult = $this->databaseConnection->checkFacebookID($this->facebookUserID);
            if($checkIDResult['facebookUserIDExists'] === false){   
                //create a new facebook user.
                $data = $this->databaseConnection->createNewFacebookUser($this->facebookUserID, $this->facebookName, $this->profilePicURL, $this->userPrivilegeID);
                $this->userID = $data['userID'];
            }else{
                $this->userID = $checkIDResult['userID'];

            }
            //return the users facebook details (which are stored in our database).
            return $this->getFacebookUserProfile($this->userID);
        }

        public function getFacebookUserProfile($userID){
            //This method takes in a userID as a parameter and gets a users profile data from our database
            $data = array();
            $data = $this->databaseConnection->getUserProfile($userID);
            return $data;
        }

        public function getFacebookName(){
            return $this->facebookName;
        }

        public function getUserID(){
            return $this->userID;
        }

    }

    ?>

MyAPI class (for handling the endpoints of our API)

 <?php
    require_once 'config.php';
    require_once 'API.class.php';
    require_once 'DatabaseConnection.class.php';
    require_once 'FacebookUser.class.php';
    require_once 'Validator.class.php';

    class MyAPI extends API {

        protected $FacebookUser;
        protected $Validator;

        public function __construct($request, $origin) {
            //This contructor takes in the super global $_REQUEST array as a parameter which should contain a request and apiKey index
            //and also the $_SERVER['HTTP_ORIGIN']
            parent::__construct($request['request']);
            //Should also have functionality to process an API key which should be sent with any requests from the client side application.
        }

        /**
         * auth Endpoint
         * A HTTP POST request is performed on the client side to this endpoint and a users facebook data is sent with the request.
         * We firstly check if the data is valid (a valid length and valid encoding-methods defined in Validator class). 
         * We also filter the data for security.
         * We then process the data to see if it exists in our database.
         * Once this is done we return the users facebook profile details in JSON format.  
         */
        protected function auth() {
            if ($this->method == 'POST') {

                //we need to get the data the following way because we have the Content-Type header set to application/json, so $_POST will no longer be populated
                $rawPostData = file_get_contents('php://input');
                $json = json_decode($rawPostData);

                $facebookUserID = $json->facebookUserID;
                $facebookName =  $json->facebookName;
                $profilePicURL = $json->profilePicURL;
                $filteredInputs = array(); //an array to hold the filtered inputs
                //create an instance of the Validator class
                $this->Validator = new Validator();
                //Check if the inputs are a valid length and also valid utf8 encoding
                $inputsAreValid = $this->Validator->checkInputsAreValid(array(array("input" => $facebookUserID, "minLength" => 10, "maxLength" => 30), 
                                                                              array("input" => $facebookName, "minLength" => 2, "maxLength" => 40),
                                                                              array("input" => $profilePicURL, "minLength" => 10, "maxLength" => 250))); 
                if($inputsAreValid){
                    //If the inputs are a valid length and also valid utf8 then filter them.
                    $filteredInputs = $this->Validator->filterInputs(array(array("input" => $facebookUserID, "filterMethod" => FILTER_SANITIZE_NUMBER_INT),
                                                                          array("input" => $facebookName, "filterMethod" => FILTER_SANITIZE_STRING),
                                                                          array("input" => $profilePicURL, "filterMethod" => FILTER_SANITIZE_STRING)));
                }
                //create an instance of the FacebookUser class
                $this->FacebookUser = new FacebookUser($filteredInputs[0], $filteredInputs[1], $filteredInputs[2]);
                $facebookProfileData = $this->FacebookUser->checkFacebookDetails();  

                return json_encode($facebookProfileData);
            } else {
                $errorMessage = array("error" => "Only accepts POST requests");
                return json_encode($errorMessage);
            }
        }

    }
    ?>
\$\endgroup\$

1 Answer 1

7
\$\begingroup\$

A lot of times people post PHP here and it's often very ugly, this is not one of those times.

Your classes and design are beautiful, and it's very obvious what your intentions are. You've built a good structure, and you've commented clearly. In fact, I saw this comment as was immediately impressed:

//we need to get the data the following way because we have the Content-Type header set to application/json, so $_POST will no longer be populated
$rawPostData = file_get_contents('php://input');
$json = json_decode($rawPostData);

There are people who say comments are "unnecessary" and you should never have them — I disagree and this is the perfect example of why you should have them. Your comment here tells me what I need to know: why you chose this method of $_POST. That's it, that's exactly what I needed.

Now, all the good information aside, there are a couple things that could use improvement, and many of them are 'nit-picky', but if they're corrected then this code goes from very good to amazing:

  • Consistent whitespace/indentation. Each file uses mixed indentation/whitespace, if you're going to do thing { // Thing then space then open brace then you should always do that, if you're going to do thing{ // Thing then open brace without space, then you should always do that. Don't mix them, it makes for inconsistent code (and often harder to read for no reason).
  • Don't add a bunch of white-space to line parameters up. I'm looking at this array section:

    $inputsAreValid = $this->Validator->checkInputsAreValid(array(array("input" => $facebookUserID, "minLength" => 10, "maxLength" => 30), 
                                                              array("input" => $facebookName, "minLength" => 2, "maxLength" => 40),
                                                              array("input" => $profilePicURL, "minLength" => 10, "maxLength" => 250))); 
    

    Instead of writing it like that, break everything to the next line and move one-indentation level in:

    $inputsAreValid = $this->Validator->checkInputsAreValid(
        array(
            array("input" => $facebookUserID, "minLength" => 10, "maxLength" => 30), 
            array("input" => $facebookName, "minLength" => 2, "maxLength" => 40),
            array("input" => $profilePicURL, "minLength" => 10, "maxLength" => 250))); 
    

    You keep the same readability of aligning your array definitions, but you also keep things close to the left side, which is probably where everyone dealing with this code starts reading.

  • Don't use magic strings/values. This is a big one, and no one ever agrees on what's "magic", but in the following function I think it's safe to say that the $data indexers are magic:

    public function checkFacebookID($facebookUserID){
        /*
         * This method checks to see if a users facebook id exists in our facebook_users table.
         */
        $data = array();
        try{
    
            $this->query = "SELECT userID FROM facebook_users WHERE facebookUserID = :facebookUserID LIMIT 1";
            $this->statement = $this->pdoConnection->prepare($this->query);
            $this->statement->bindValue(':facebookUserID', $facebookUserID, PDO::PARAM_STR); 
            $this->statement->execute();
            $this->statement->setFetchMode(PDO::FETCH_ASSOC);
            $rowCount = 0; //store the count into a local variable
    
            while($row = $this->statement->fetch()){
                //store the data from the result of the query into an associative array. 
                $data['userID'] = $row['userID'];
                $rowCount++;
            }
            if($rowCount > 0){
                //The facebookUserID exists in the database.
                $data['facebookUserIDExists'] = true;
    
                return $data;
            }else{
                $data['facebookUserIDExists'] = false;
                return $data;
            }
    
        }catch(PDOException $e){
            //Would it be a good idea to log this error in a php log file?
            $data['exceptionOccurred'] = true;
            $data['exceptionMessage'] =  $e->getMessage();
            return $data;
        }       
    }
    

    I would also throw the SQL queries into a builder of some sort.

  • Regarding error file storage: use your own discretion - if it's important, then log it. You have a comment: Would it be a good idea to log this error in a php log file?, and there's no "one size fits all" solution here. Use your discretion, if you think that it would be good to log, then log it. If not, then don't. In this case, I would probably err on the side of logging the error itself, database errors are either very unique, or very general. There's not a lot of "in-between."

  • Build abstractions where helpful. You do the following multiple times:

    }catch(PDOException $e){
        $data['exceptionOccurred'] = true;
        $data['exceptionMessage'] =  $e->getMessage();
        return $data;
    }
    

    Instead of having to remember to set $data['exceptionOccurred'] and such, I would build a function that takes the PDOException and fills your $data, then return it:

    } catch (PDOException $e) {
        // The first parameter is whether to log or not - make this general so you can just tell the error handler
        // You can also make `$data` optional, and just *log only* if that parameter is not specified
        return ErrorHandler::buildError(true, $e, $data);
    }
    

Overall, wonderful work! This code is very clean, very easy to follow, and very maintainable. Most of the issues I pointed out are nitpicks, they're just things that help improve consistency and usability.

\$\endgroup\$
1
  • 2
    \$\begingroup\$ Thank you so much for your kind comments. I'm glad it's easy to follow and easily maintainable. That was the aim. And I appreciate all of your advice. I will certainly make the changes you suggested :) \$\endgroup\$
    – Sarah
    Oct 3, 2017 at 14:21

Your Answer

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

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