Object-oriented – php class composition: how to implement “has a” relationship in the case of a DAL

object-orientedobject-oriented-designPHP

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 call create on the table object as in Table->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.

Best Answer

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:

  1. We want to be able to open and close connections to a database
  2. We want to be able to begin, commit and rollback transactions
  3. We want to be able to prepare statements
  4. 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 of fetch or fetchAll(), so that you can get additional information, such as rowCount, 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 an Accessor
  • 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 strings
  • Responses, which hold information about the execution of the Calls (e.g. number of rows/columns returned, error info, etc.)
  • An OperationRepository which is responsible for the execution of Calls and returns Responses
  • An OperationCache that stores Responses against Calls

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

Related Topic