2
\$\begingroup\$

I recently started to learn more about OOP in PHP and I created a website for testing. I have 4 classes: database, user, posts, comments. This is a simple database.class.php:

class DB {

    protected $pdo;
    protected $engine    = 'mysql';
    protected $host      = 'localhost';
    protected $db_name   = 'test';
    protected $db_user   = 'root';
    protected $db_passwd = '';

    public function connect() {
        try {
            $this->pdo = new PDO($this->engine.':host='.$this->host.';dbname='.$this->db_name,$this->db_user,$this->db_passwd);
            $this->pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
            return $this->pdo;
        }   catch(PDOException $e) {
            die($e->getMessage());
        }
    }
}

Then, every other class starts like this:

class user {

    public $link;

    public function __construct() {
        $pdoObj = new DB();
        $this->link = $pdoObj->connect();
        return $this->link;
    }

}

I use $link as a reference to the pdo object so I can use all the methods associated with PDO. My question is: is this wrong?

I saw a lot of source codes and most of them used 'static' and didn't instantiated the class with new Class(); but just ClassName::foo(); Is it wrong to instantiate every class and work with instances? I always do:

$obj = new user();
$obj->SelectAll();

// and so on..

If my way is wrong, how can I move my code to static?

Here is an example of a method:

public function register($username, $email, $passwd, $re_passwd) {
    # fix email and passwd regex
    $reg_error = array();
    $regex_username = '/[a-zA-Z0-9-._]{6,20}$/';
    $regex_email = '/[a-zA-Z0-9._-]{3,}@[a-zA-Z0-9]{3,}.[a-zA-Z.]{2,}$/';
    if (empty($username) || empty($email) || empty($passwd) || empty($re_passwd)) {
        $reg_error['fields'] = 'All fields are required';
    }   else {
        if (!preg_match($regex_username, $username)) {
                $reg_error['username'] = 'Username must be at least 6 characters';
        }   if (!preg_match($regex_email, $email)) {
                $reg_error['email'] = 'Invalid email';
        }   if(strlen($passwd) <= 6) {
                $reg_error['passwd'] = 'Password must be at least 6 characters long and must contain at least 1 uppercase, 1 lowercase and 1 digit value';
        }   if ($re_passwd !== $passwd) {
                $reg_error['match'] = 'Passwords do not match';
        }   if($this->usernameExists($username)) {
                $reg_error['username_exists'] = 'Username: ' .$username. ' already exists';
        }   if($this->emailExists($email)) {
                $reg_error['email_exists'] = 'Email: ' .$email. ' already exists in our database';
        }
    } 
    if (!empty($reg_error)) {
        foreach($reg_error as $error => $er) {
            echo 'Error: '.$er.'<br />';
        }
    }   else {
        try {

            $query = $this->link->prepare("INSERT INTO users(username, email, password, ip_address, activation_code)
                                            VALUES(:username, :email, :passwd, :ip_addr, :code)");
            $passwd = hash('sha512', $passwd);
            $ip_addr = $_SERVER['REMOTE_ADDR'];
            $code = substr( hash('sha512', md5(rand(999, 9999))) , 15, 40);
            $params = array(':username' => $username,
                            ':email'    => $email,
                            ':passwd'   => $passwd,
                            ':ip_addr'  => $ip_addr,
                            ':code'     => $code);
            $query->execute($params);
            if ($query->rowCount() != 0) {
                redirect('index.php?lastid='.$this->lastId());
            }   else {
                    return 'There\'s been a tehnical problem during the registration process, please try again later.';
            }
        }   catch (PDOException $e) {
                die($e->getMessage());
        }
    }
}
\$\endgroup\$
1
  • 1
    \$\begingroup\$ I'm too lazy to write a full response, especially since Yannis covered it very well, but your DB class shouldn't exist. It's basically a glorified PDO factory, but I really doubt you want to be opening a new DB connection every time you need a DB connection. (Instead, you should do as he said and pass the PDO instance to the constructor of any class that needs it.) \$\endgroup\$
    – Corbin
    Sep 23, 2012 at 6:14

1 Answer 1

10
\$\begingroup\$

What happens if you change your database credentials?

Right now your class is tied to a specific set of database credentials, and you'll need to change it if:

  • you want to use a different engine,
  • or a different host,
  • or a different user,
  • or a different password,
  • or any combination of the above.

This is why PDO requires you to pass the dsn and the credentials through its constructor, and you should do the same. Then of course your class wouldn't be much different than PDO itself, if you don't have any additional functionality, just forget about it and use PDO.

If you want to keep your class, just store your credentials in a configuration file. A simplistic solution would be an INI file, let's call it database.ini:

engine   = "mysql"
host     = "localhost"
name     = "test"
user     = "root"
password = ""

Your database class could then be:

class DB {
    private $pdo;
    private $engine;
    private $host;
    private $db_name;
    private $db_user;
    private $db_passwd;

    public function _construct($path) {
        if(!is_file($path)) {
            throw new InvalidArgumentException("Hey, that's not a file!");
        }

        $credentials = @parse_ini_file($path);

        if(empty($credentials)) { 
            throw new InvalidArgumentException("Hey, that's a file, but it's not a valid INI file!");
        }

        $this->engine    = $credentials["engine"];
        $this->host      = $credentials["host"];
        $this->db_name   = $credentials["name"];
        $this->db_user   = $credentials["user"];
        $this->db_passwd = $credentials["password"];

        try {
            $this->pdo = new PDO($this->engine.':host='.$this->host.';dbname='.$this->db_name,$this->db_user,$this->db_passwd);
            $this->pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
            return $this->pdo;
        }   catch(PDOException $e) {
            throw new Exception("Boo, wrong credentials!");
        }                        
    }

}

Looks like a lot of work? Well, it is, and it's not finished, but changing any of your credentials is now as simple as changing its value in database.ini. All you need to do to instantiate your class is:

$db = new DB("database.ini");

Let's go quickly through the main parts of the class:

  1. Don't protect what should be private

    Don't declare your class properties and methods as protected if you don't need to. Protected means:

    Members declared protected can be accessed only within the class itself and by inherited and parent classes

    If you are not going to extend the class, there's absolutely no reason to go with protected instead of private:

    Members declared as private may only be accessed by the class that defines the member.

    This might seem extremely pedantic, the world won't stop spinning because your properties are protected, but since you're a fresher, better avoid this common bad habit.

  2. Connect? Why, let's construct instead!

    The database class is useless if we don't run connect(), calling the method is a prerequisite for the class to do anything else. When that happens, it's a good sign that our method should either be called in the class constructor, or be the constructor itself. If it's absolutely necessary for the method to be called, then it makes sense to be the one function that gets called by default when we create an instance of our class, doesn't it?

  3. Check your arguments

    I'm doing two checks on our $path argument, first if it's a valid file and then if parse_ini_file returned an array (that would signify that the INI file was succesfully parsed). Throw appropriate exceptions if there are problems.

  4. The unfinished part

    $this->engine    = $credentials["engine"];
    $this->host      = $credentials["host"];
    $this->db_name   = $credentials["name"];
    $this->db_user   = $credentials["user"];
    $this->db_passwd = $credentials["password"];
    

    What I'm not doing here is check whether the indices exists in the $credentials array. That's your homework ;)

  5. The exception

    Your original code was:

    } catch(PDOException $e) {
        die($e->getMessage());
    }
    

    That doesn't make sense, you catch the exception and then die with the same message? Not catching the exception will have exactly the same effect, the script will stop, so why catch it? You should catch exceptions only when you can do something productive with them, if not, just let the script fail. Usually "something productive" means gracefully recover from the error, but that's not always possible. In my code I'm doing something that might seem minor but it's very important:

    }   catch(PDOException $e) {
        throw new Exception("Boo, wrong credentials!");
    }                        
    

    Yes, the only thing I'm doing is changing the message of the exception, the script will still fail. However PDO's constructor exception reveals your database credentials (except your password), so catching that and rethrowing it with a more generic message minimizes the risk of your database username (for example) falling in the wrong hands. Can't gracefully recover from the failure, but at least we plugged a potential security hole.

How about some setters and getters?

If you are still reading this, I'm sorry to inform you that your code doesn't work. This:

public function __construct() {
    $pdoObj = new DB();
    $this->link = $pdoObj->connect();
    return $this->link;
}

does set your database object in the $link property but it's your database object, not your PDO object. Your database object does not have a prepare() method, so this:

$query = $this->link->prepare("INSERT INTO users(username, email, password, ip_address, activation_code)
                                VALUES(:username, :email, :passwd, :ip_addr, :code)");

will simply fail. What you need to do is pass your PDO object to your User class:

class DB {

    ...

    public function getPDO() {
        return $this->pdo;
    }            
}

So this simple function will always return the database's internal PDO object. Your User constructor would now be:

public function __construct() {
    $database = new DB("database.ini");

    $this->link = $database->getPDO();
}

Notice how I removed return $this->link;? Well, don't do that, a constructor returns the object it instantiates by default, there's absolutely no sense in returning anything else, it wouldn't be a constructor if it didn't give us what it constructed, would it?

Wait! There's a dependency!

This line:

$database = new DB("database.ini");

means that we don't have the option of renaming our INI file. Ever! That's not good, that's an unwanted dependency and we really don't like those. After all we went through a lot to get our database class to not be dependent on its credentials, we can't introduce another dependency now. What we are going to do is use a method called Constructor Dependency Injection, that's the simplest way to achieve Dependency Injection. It's extremely simple, all we need to do is:

public function __construct(DB $database) {       
    $this->link = $database->getPDO();
}

That's right, pass the database as a parameter in the constructor, that's all there is to it. Now, to instantiate our user class all we need to do is:

$database = new DB("database.ini");    
$user = new User($database);

...and our classes are dependency free! To get an idea why this is good, assume you have two databases, and you need to use them both. All you need to do is:

$database1 = new DB("database1.ini");    
$user1 = new User($database1);

$database2 = new DB("database2.ini");    
$user2 = new User($database2);

Does your database only have users?

If your database has any other table, and you want to model them as classes, it might be worth looking at inheritance. All your table classes have a common dependency, the database, and you don't really have to write the same constructor every time:

class Table {
    protected $database;

    public function __construct(DB $database) {        
        $this->database = $database->getPDO();
    }
}

class User extends Table {

    // only user specific functions here

}

class Article extends Table {

    // only article specific functions here

}

class Comment extends Table {

    // only comment specific functions here

}

The User, Article, and Comment classes have inherited the same constructor from Table, no need to write the method more than once. As you probably already noticed, I've declared $database protected, it makes sense now as we need it to be available to the child classes. By the way that's your old $link property, since it holds a database object, makes sense to call it $database.

The register function

Sorry for being blunt, but this is a horrible mess:

  1. Indendation is messed up.

    Learn to format your code properly. Better yet, equip yourself with a decent tool to do it for you.

  2. Your function should either return something or echo something, not both.

    Preferably return something. Echoing HTML from your functions is a very poor practice, read up on separation of concerns. Simply put your function is where you put logic, and you shouldn't mix presentation with logic.

  3. You catch an exception and die with the same message again.

    Don't do that.

Since we've now have a handy getter in our database class, the way to get the reference to the PDO object in your function is:

class User {

    public function register($username, $email, $passwd, $re_passwd) {
        $link = $this->database->getPDO();

        ...

        $query = $link->prepare( ... );

        ...
    }

} 

And what about static?

Well, don't worry about it. Most of the time is used as a convenience, and it's not really the better way to do things. Instantiate your objects properly, and then pass them around via one of the dependency injection techniques, you're passing around the same instance, you are not instantiating it again and again.

Further reading

Addendum

Corbin raises two important issues in a comment:

(1) The exception shouldn't be masked. What if consuming code wants the details? A different part of code should be responsible for hiding potentially sensitive exceptions. (2) If Table (and other classes) need a PDO instance, they should accept a PDO instance, not a DB instance. There's no reason to accept the object just to call a single getter. (I also believe that this Database class shouldn't even exist though. It's pointless.)

My thoughts:

  1. Your database class is pointless if it doesn't have any additional functionality you didn't tell us about.

    As it is, it's just a wrapper for PDO that doesn't do anything more than PDO itself does, and in my answer I've assumed (hoped?) that you do have a reason for it and would further develop it to enhance PDO (perhaps adding a query cache, or a log?). But if you don't have any further plans for it, then it would be preferable to just scrap it and just use PDO and just feed your PDO object in Table::__construct().

  2. You shouldn't be catching exceptions if you can't do anything productive, preferably gracefully recovering from the error.

    Now, I do suggest that you should catch the PDOException just in case your database credentials are leaked, but that's more of a scare tactic than a solid suggestion. I've seen one too many new developers do similar mistakes, and perhaps my suggestion there was a bit drastic.

    What you need to keep in mind is that this particular PDOException should not reach the end user. If there's other code in between your database instantiation and the end user (a top level script, perhaps), it would make much more sense to deal with the exception there, and if you can't recover from it do something like throwing a 404 or 503 error (whichever applies).

    Take my suggestion to hide the details of the connection PDOException with a grain of salt, I'm just tired of dealing with the same crap again and again. There was a time when such an exception created a world of trouble for me, but in a typical production environment it shouldn't be easy for anyone to do anything particularly harmful just because they caught a glimpse of your database username.

\$\endgroup\$
7
  • \$\begingroup\$ Sir, thank you very much for your time and help. One more thing I would like to know is my original question: is the instantiating method bad practice, should I move to static? Like so: User::Login($variables) instead of: $user = new User(); $user->Login($variables);? \$\endgroup\$
    – Ovidiu
    Sep 22, 2012 at 8:54
  • \$\begingroup\$ @Ovidiu No, don't move to static, I've already talked about this in my answer. Only use static when you fully understand what it's about. \$\endgroup\$
    – yannis
    Sep 22, 2012 at 8:55
  • 1
    \$\begingroup\$ @YannisRizos +1, but two things bothered me. (1) The exception shouldn't be masked. What if consuming code wants the details? A different part of code should be responsible for hiding potentially sensitive exceptions. (2) If Table (and other classes) need a PDO instance, they should accept a PDO instance, not a DB instance. There's no reason to accept the object just to call a single getter. (I also believe that this Database class shouldn't even exist though. It's pointless.) \$\endgroup\$
    – Corbin
    Sep 23, 2012 at 6:17
  • \$\begingroup\$ @Corbin No disagreement for (2), I do mention somewhere in there that the database class is useless, but from that point on I'm assuming it has some additional functionality we don't know about, that justifies its existence and why Table needs it. As for (1), I've been burned before by database credentials reaching the user, and I'm a bit dogmatic about it. Obviously you are right, but 8/10 developers I've worked with let sensitive data reach end users through errors/exceptions, and now I prefer to scare people into catching them as soon as possible. \$\endgroup\$
    – yannis
    Sep 23, 2012 at 6:45
  • 1
    \$\begingroup\$ +1 Very well thought out, and explained. Nice to see new people in the neighborhood :) \$\endgroup\$
    – mseancole
    Sep 24, 2012 at 17:14

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.