3

I'm moving onto teaching myself OOP in PHP.

I'm creating a couple of little web apps and have followed a lot of tutorials that either create the database (using PDO) via a Singleton, or via passing the global around. I've read that these are pretty much the same thing and are both to be avoided like the plague.

So I've watched the Google Tech Talks on clean code, and read almost every SO article on dependency injection and the like. I have a couple of questions.

  1. The clean code videos suggest you shouldn't do 'work' in your constructors. Is this 'work' in reference to business logic. Ie. If my class's job is to create another object, is that an OK kind of 'work'?

For example, in trying to conform to single repsonibility classes I created three.

  1. Class DB - which actually connects to the database.
  2. Class DBFactory - which creates the DB object which connects to the database.
  3. Class DBInstance - which returns a single instance of the DBFactory created PDO object.

Please note that I'm trying to create a single instance, without creating a Singleton pattern.

So I try and pass my dependencies for each class up the chain. I find myself in a position where I have to create all of the objects (from DB down) so I can inject the dependencies. For some reason I thought it would work the other way, I'd create the first object, which would create the second for me etc. I'm clearly missing something?

Hopefully this helps others as well - there seems to be a myriad of questions relating to this stuff and databases but very little good examples.

(I should mention this does work, I do get a list of hotel names out of the database!)

TestCode.php

include './classes/DB.php';
include './classes/DBFactory.php';
include './classes/DBInstance.php';
include './classes/Location.php';

$db = new DB;
$dbfactory = new DBFactory($db);
$dbinstance = new DBInstance($dbfactory);
$dbh = $dbinstance->getDbInstance();

//Example business logic
$location_names = Location::getLocationNames($dbh);
print_r($location_names);

Class DB.php:

class DB {

    private $_dbhost = 'myhost';
    private $_dbname = 'myname';
    private $_dbuser = 'myuser';
    private $_dbpass = 'mypass';

    private $_error;

    public function connect() {
        try {
            return new PDO("mysql:host=$this->_dbhost;dbname=$this->_dbname", 
                $this->_dbuser, $this->_dbpass);
        }

        catch (PDOException $e) {
            $this->_error = 'Error! ' . $e->getMessage() . '<br />';
            die();
        }
    }

    public function getError() {
        if (isset($this->_error)) {
            return $this->_error;
        }
    }
}

Class DBFactory.php

class DBFactory {

    private $_dbh;

    public function __construct(DB $db) {
        $this->_dbh = $db;
    }

    public function Create() {
        return $this->_dbh->Connect();
    }
}

Class DBInstance.php

class DBInstance {

    private static $_dbinstance;

    public function __construct(DBFactory $dbfactory) {

        if (!isset(self::$_dbinstance)) {
            self::$_dbinstance = $dbfactory->Create();
        }
    }

    public function getDbInstance() {
        return self::$_dbinstance;
    }
}
1
  • 1
    @félix-gagnon-grenier Have I successfully created a single instance, without creating a Singleton pattern? Is there a better way to create a new database handler while still using dependency injection without having to instantiate all the objects like I did in my test class? Commented Jun 10, 2014 at 23:00

3 Answers 3

1

Your code seems to do what you want it to.. but maybe we can use less object instantiation using inheritance and maybe we can avoid static properties in instanciated classes.

Also in regard to using a pattern of dependency injection that is able to handle multiple connections, but support using a single instance of it. exemple first, classes after

$params = array
 ('host'=>'localhost',
  'db'=>'ice',
  'user'=>'kopitar',
  'pass'=>'topnet',
  'charset'=>'utf8'); // passing the charset explicitely is great

$handle = new handle($params);
$db = $handle->getInstance();

we can either pass the $db to our functions

$location_names = Location::getLocationNames($db); 

or the whole $handle. as long as $handle is not reconstructed, it will always return the same database connection.

$location_names = Location::getLocationNames($handle);

if I want to reconstruct I need the whole $handle

$handle->__construct(/* params but with another database infos */);
$db2 = $handle->getInstance();

As for the classes, I think we want the params to arrive from the instanciated class, so we can change them later.

class db {
  function __construct($params) {
    foreach ($params as $param => $value) {
      $this->{$param} = $value; // assigns the connections infos
    }
  }
  protected function connect() {
    $dsn = 'mysql:host='.$this->host.';dbname='.$this->db.';charset='.$this->charset;
    return new PDO($dsn,$this->user,$this->pass);
  }
}

the factory creates a connection from params and passes it to something else, good factory

class factory extends db {
  protected function create() {
    return $this->connect();
  }
}

now we want to have our object to keep it's connection as long as we do not rebuild it. so we give it to instance

class instance extends factory {
  function instantiate() {
    $this->instance = $this->create();
  }
}

and last but not least, our handle which returns the instance. it could be in instance class.....................

but I feel like having four and find no real reason not to.

class handle extends instance {
  function __construct($params) {
    db::__construct($params);
    $this->instantiate(); // when we construct a handle, we assign an instance to the instance property
  }
  function getInstance() {
    return $this->instance;
  }
}
Sign up to request clarification or add additional context in comments.

4 Comments

Thanks @félix-gagnon-grenier, this does look like a big improvement. I've been playing around integrating it with my code. The only thing I was wondering though is does it introduce bad dependencies? For example, isn't saying parent::__construct in the child class the equivalent to instantiating the object in the constructor if it weren't a subclass? Eg. parent::__construct($params) would be the same as $handle = new DB or similar. I'm just curious about all this, I find it an interesting challenge. Thanks for your time!
@aussie_aj I think you are right, passing it throught parent::__construct() must be quite the same, if not more costly, as calling db::__construct() directly... I''l test and update if true
done. It's even cooler that way, and it does construct less things.
Yeah @félix-gagnon-grenier this looks really good to me, and super simple. I think I'll adopt this into my application. Thanks for your guidance!
1

KISS

Don't make things more complex than they are, of course this is just my opinion, but as I see it you are building a complex solution for a problem that someone else says might exist is some cases. Php is not multi threaded so there goes one of the biggest arguments overboard. (in very rare-occasions it might be)

I'm using singletons for my database connections for about 15 years now and never ever had a problem with them, I do play around with different connections having one singleton handle several connection instances, but whatever... it works great and everyone that looks at the code.. understands it directly. I'm not using globals because they can be overwritten and are kind of hard to predict (when it holds the correct object, and when/why they don't)

Use OOP to make your code cleaner, easier to work with and more flexible. Don't use it to fix problems that aren't there and make your code more complex because others tell you to.

An very simple example of a db-connection singleton class handling several different connections.

class singleton{
  private static $_instances=array();

  public static function getInstance($connectionName){
     if(!isset(self::$_instance[$connectionName]){
        self::$_instance[$connectionName]=self::_getConnection($connectionName);
     }
     return self::$_instance[$connectionName];
  }

}

just my 2 cents

4 Comments

Thanks @ikkuh for the reply. But haven't you just created a global/global state using the singleton anyway? Anyone can access it at any time?
Yeah I did ;) Aslong as you are not multithreading this is perfectly valid. What I do is having seperate model classes that hold the actual queries, they are calling this class to access the singletons.
Just to be sure, you should never do something like while(singleton::getInstance()->doSomething()){ singleton::getInstance()->doSomethingElse() } As this DOES distort the global state. But if you are using your PDO object properly and the way it should, this should never ever happen ;)
while it is important to keep it simple, I think encapsulating tasks in specific classes is a good idea, be it only for maintainability and modularity, even if your classes only do one thing. I think it kind of is an important guideline in UNIX ideology. Systems following UNIX guidelines such as those different BSD's out there, or Solaris, tends to be very efficient.
1

Why do you have a factory if you have a singleton? This is needless.

This is a never-ending debate, but I'm advocate of do not use singletons for database connections.

As far as in most applications, you have only one data channel, you can consider your database connection unique, but this might not be always true.

In deed, the effort made to create a singleton database connection is even bigger than just create a regular one.

Also, your class DB is not configurable, therefore, you need to change it when your connection parameters change. And I think DB is a very bad name for this.

I'd rather call this Storage and do something like:

inteface Storage {
    public function insert($container, array $data);
    public function update($container, array $data, $where);
    public function delete($container, $where);
    public function getAll($container);
    public function getOne($identifier);
}

final class PdoStorage implements Storage {
    private $dbh;
    private $dsn;
    private $user;
    private $pswd;

    public function __construct($dsn, $user, $pswd) {
        $this->dsn = $dsn;
        $this->user = $user;
        $this->pswd = $pswd;
    }

    // Lazy Initialization
    private function connect() {
        if ($this->dbh === null) 
            $this->dbh = new PDO($this->dsn, $this->user, $this->pswd);
    }

    public function insert($container, array $data) {
        $this->connect();

        // ... omitted for brevity
    }
}

Now, when you need a database storage, you do:

$someObj = new SomeClass(new PdoStorage(...));

Now you might be wondering if you will need to create an PdoStorage for each single object that depends on it.

The answer is: no!

Now you can use a factory to simplify your life.

class SomeFactory {
    private $defaultStorage;

    public function __construct(Storage $storage) {
        $this->defaultStorage = $storage;
    }

    public function create($type) {
        // Somehow fetches the correct class to instantiate and put it into $class variable , for example... and then

        return new $class($this->defaultStorage); // Or you'd better do this with reflection
    }
}

$factory = new SomeFactory(new PdoStorage(...));
$factory->create('SomeClass');

This way, you can have just one database connector or more if you need.

2 Comments

Thanks for the reply @henrique-barcelos. The reason I have a separate factory with the singleton is I want to separate the responsibility of creating the database object and returning only a single instance of the database. Ie. I want a single instance of the database, without using a singleton pattern. That way later on if I did want to use multiple instances I could just change the getInstance method. butunclebob.com/ArticleS.UncleBob.SingletonVsJustCreateOne
But you can inject the singleton directly, you don't need a factory... I'm not saying this is what you SHOULD do, I completely disagree with that too...

Your Answer

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

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.