I'm learning about OOD and good practices in OOP and find myself struggling with some key concepts. As a practice I'm rewriting my custom PDO database abstraction layer which used to be a single file class with >2000 lines of code.
I learned one should use inheritance if classes are in a "is an" relationship and composition if they have a "has a" relationship. Composition can be implemented as this, given that I would avoid php's traits
(example from here):
<?php
class Head {
}
class Human {
private $head;
public function __construct(Head $head) {
$this->head = $head;
}
}
$bob = new Human(new Head);
Good. However, in my case I want to composite a class B to A, while there can be multiple instances of B. Precisely, the main database
class (A) has one or multiple table
classes (B). Injecting a table
object similar to the head
object in the above example might be not what I want. Later, there might be also maybe a select class or a insert class. I do this just for practice and learn how I can keep my classes small in file size. Should I all inject all dependencies during construction and recylcle them? Or should I instantiate them within the main database
class and inject the connection to the subclasses. The main database class holds the PDO object in '$_connection'.
Q1: what is the best way to compose the classes database
and table
.
I can think of these strategies.
Strategy #1
<?php
class db extends PDO{
private $_connection;
public function __construct($dsn){
$this->_connection = new parent::__construct($dsn);
}
public function createTable($def){
$table = new Table(this->_connection, $def);
}
}
Cons:
- I have the
new
operator in a method which I assume is generally not ideal. Better, I should inject all instances. - I have to declare a
createTable
method in the base class. This spams my base class. If functionality increases the base class will be bigger and bigger, which is what I wanted to circumvent in the first place. I would rather like to be able to callcreate
on the table object as inTable->create()
. - I'm not sure about the the injection of the connection to the table class. Is that good practice?
Strategy #2
<?php
class db extends PDO{
private $_connection;
public $table;
public function __construct($dsn, $table){
$this->_connection = new parent::__construct($dsn);
$this->table = $table;
}
}
$db = new db($dsn, new $Table)
$db->table->create($def);
Cons:
- I don't have the
connection
available in the Table class as it is neither a child nor is the connection manually injected.
I don't think the db and Table classes are in a "is a" relationship and thus should not be inherited from each other. But currently I'm lacking a good composition implementation.
Disclaimer
I tried to work for a solution but need help on what could be the best practice for this. Composition, as posted with the example (human, head), just doesn't feel right here in the case of database and table. I hope I'll receive helpful answers, also links or buzz words are welcome as I'm just learning and I seem to have a hard time to enter the next level.
-
1What is your specific question?Robert Harvey– Robert Harvey2018年01月02日 19:54:57 +00:00Commented Jan 2, 2018 at 19:54
-
See Q1. Not clear? I need to know how an expert would arrange class table and the base class.bln_dev– bln_dev2018年01月02日 21:48:35 +00:00Commented Jan 2, 2018 at 21:48
-
To be more precise. I find a lot of examples that show how to compose classes. Basically inject all dependencies to the constructor. But I have a hard time to use this pattern in that case of a table class. And I wanted to know if my other strategy with new Table is a valid one.bln_dev– bln_dev2018年01月02日 21:55:05 +00:00Commented Jan 2, 2018 at 21:55
-
2Here is what I suggest: focus on writing each of your classes so that it only does one job, and does that job well. If your problem is that your classes are too big, your solution is to write smaller classes. Stop worrying about inheritance for the time being, and focus on composition. Composition is merely the way you get your classes to communicate with each other and to work together to accomplish a goal, and that's all that it is.Robert Harvey– Robert Harvey2018年01月03日 01:06:27 +00:00Commented Jan 3, 2018 at 1:06
-
2Here is an example of a class pattern that does one job and does it well: martinfowler.com/eaaCatalog/repository.htmlRobert Harvey– Robert Harvey2018年01月03日 01:07:43 +00:00Commented Jan 3, 2018 at 1:07
1 Answer 1
I'm not sure why you have a Table
object, or why your db
class extends PDO
, but I'll try to explain a decent approach to database access in a PHP context, as this is an area I've spent a lot of time on and have a great deal of interest in.
Basic DI approach with PDOs and prepared statements
When considering database access for PHP in a PDO
/PDOStatement
context, we can really boil it down to a bare few things:
- We want to be able to open and close connections to a database
- We want to be able to begin, commit and rollback transactions
- We want to be able to prepare statements
- We want to be able to execute SQL as prepared statements by providing parameters
Point 1 is the responsibility of a database accessor, and involves the creation of a PDO. This is a "has a" relationship.
Points 2 and 3, I would argue, are also the responsibility of a database accessor, by way of the database accessor's PDO. The database accessor would act as a Mediator or Facade in this case.
Point 4 is the responsibility of a data access object (DAO).
Your database accessor would implement an interface something like this:
interface DatabaseAccessorInterface
{
public static function beginTransaction(): void;
public static function commitTransaction(): void;
public static function dropConnection(): void;
public static function getConnection(): PDO;
public static function isActiveTransaction(): bool;
public static function prepare($sql): PDOStatement;
public static function rollbackTransaction(): void;
}
...and your data access object abstract an implementation would look something like this:
abstract class AbstractDAO
{
private $db;
public function __construct(DatabaseAccessorInterface $db)
{
$this->db = $db;
}
protected function db(): DatabaseAccessorInterface
{
return $this->db;
}
}
class UserDAO extends AbstractDAO
{
public function getAllUsers(): array
{
$sql = "SELECT * FROM user";
$stmt = $this->db()->prepare($sql);
$stmt->execute();
return $stmt->fetchAll();
}
public function getUserByUsername($username): array
{
$sql = "SELECT * FROM user WHERE username = :username";
$stmt = $this->db()->prepare($sql);
$parameters = [
':username' => $username
];
$stmt->execute($parameters);
return $stmt->fetch();
}
}
Note that this is a traditional "easy" approach to the problem, and is not the best approach IMO but is a great starting point if you're getting into designing around PDOs and prepared statements.
Ways to augment this approach include:
- Adding a configurator, such that the database accessor "has a" configuration
- Implementing a cache of prepared statements
- Returning a custom
Result
object instead offetch
orfetchAll()
, so that you can get additional information, such asrowCount
,errorCode
, etc.
Author's thoughts
Personally, I believe thinking about data access in a database context is heavily restricting in this day and age. Nowadays, databases are simply one form of persistence amongst a myriad of choices. We could be connecting to an SQL database, a NoSQL database, a CSV file, a remote API, etc. I think it's better to broaden the idea of data access from simply connecting to and querying against a database, to that of accessing a remote data store that could take on many forms.
When we look at it this way, we find that there are various parts in play:
- An
Accessor
which is responsible for a connection to a particular type of data source - An
AccessorConfiguration
which holds configuration information for anAccessor
- A
PersistenceStrategy
which hold information about which artefact in the data source will be the target for queries and commands Calls
, which are equivalent to SQL query stringsResponses
, which hold information about the execution of theCalls
(e.g. number of rows/columns returned, error info, etc.)- An
OperationRepository
which is responsible for the execution ofCalls
and returnsResponses
- An
OperationCache
that storesResponses
againstCalls
Once we start down this road, we find the interfaces become a lot simpler in their language, and you avoid polluting your code with database-specific lingo:
interface PersistenceStrategyInterface
{
/**
* Deletes a DataEntity from the persistence mechanism.
*
* @param DataEntityInterface $dataEntity
* @return ResponseInterface
*/
public function delete(DataEntityInterface $dataEntity);
/**
* Gets a DataEntity from the persistence mechanism.
*
* @param DataEntityInterface $dataEntity
* @return ResponseInterface|null
*/
public function get(DataEntityInterface $dataEntity);
/**
* Saves the DataEntity to the persistence mechanism.
*
* @param DataEntityInterface $dataEntity
* @return ResponseInterface
*/
public function save(DataEntityInterface $dataEntity);
}
interface OperationRepositoryInterface
{
/**
* Gets the Response from the OperationRepository.
*
* @param CallInterface $call The Call that generates the Response.
* @param AccessorInterface $accessor The Accessor that the Call is made against.
* @return ResponseInterface
*/
public function response(CallInterface $call, AccessorInterface $accessor);
}
Disclaimer: this code is from Circle314 framework, which I am currently developing
-
Wow. Your answer is mind opening! Thanks many times. This is a good starting point. I think moving the prepare once and for all to the main class is elegant. May I ask, how would you instantiate UserDAO? Either outside the DatabaseAccessor (e.g.
$db = new DatabaseAccessor(); $user_dao = new UserDAO($db);
) or within ($user_dao = new UserDAO($this->db);
)? Again, thanks for sharing your knowledge!bln_dev– bln_dev2018年01月03日 07:51:03 +00:00Commented Jan 3, 2018 at 7:51 -
1I generally use a dependency injection container, to ensure that only one instance of the
UserDAO
(andAccessor
for that matter) are created. These classes are essentially services, so should be single instance. The best DI container I've found in PHP is one called PHP-DI (I have no affiliation to it, it's the work of Matthieu Napoli), though there are others of quality such as Pimple from Symfony and Container by PHP League.e_i_pi– e_i_pi2018年01月03日 08:10:56 +00:00Commented Jan 3, 2018 at 8:10
Explore related questions
See similar questions with these tags.