It seems to me that you are trying to build your own ORM and to practice is good. For large projects, and for greater comfort think about adopting some ORMs like Doctrine, Eloquent, etc (depending on the framework).
An approach that does not use dependency injection may need to instantiate the database object itself in a constructor. Let's give an example that utilizes a singleton to provide the DB object.
class Product {
private $pdo = null;
// other product properties here
public function __construct() {
// get db
try {
// set PDO object reference on this object
$this->pdo = PDOSingleton::getInstance();
} catch (Exception $e) {
error_log('Unable to get PDO instance. Error was: ' .
$e->getMessage();
// perhaps rethrow the exception to caller
throw $e;
}
// query DB to get user record
}
// other class methods
}
// example usage
$product = new Product(1);
When using dependency injection, that might instead look like this:
class Product {
private $pdo = null;
// other product properties here
// pass PDO object to the constructor. Enforce parameter typing
public function __construct(PDO $pdo) {
$this->pdo = $pdo;
// query DB to get product record
}
// other class methods
}
// example usage
// instantiate PDO object. This probably happens near beginning
// of code execution and might be the single instance you pass around
// the application
try {
$pdo = new PDO(...);
} catch (PDOException $e) {
// perhaps log error and stop program execution
// if this dependency is required
}
// somewhere later in code
$product = new Product($pdo);
This may seem like only a subtle difference, but developers who use this approach like it because it:
decouples the consuming class from the details of how to instantiate the dependency. Why should the user class have to know which singleton to use in order to get it's PDO dependency? All the class should care about is knowing how to work with the dependency (i.e what properties and methods it has), not how to create it. This more closely follows the single responsibility principle which is typically desired in OOP, as the user class only has to deal with instantiating a user representation, not also with instantiating its dependencies.
eliminates duplicate code across classes that need the dependency as you don't need all the handling in each class around instantiating/creating the dependency. In the example code, I eliminate the need to potentially handle failed DB instantiation within the constructor, as I know I already have a valid PDO object passed as parameter (if I did not get one passed, I would get invalid argument exception).