Joe's
Digital Garden

Repository

The introduction of complex ORMs (Doctrine, Eloquent) into the application obscures the relationship between the query and object creation, adding additional mental overhead as usage of the ORM requires we both thuroughly understand the API of the ORM, but also the SQL generated and the structure of the database itself. At which point, the addition of mastering the ORM API, is uneccessary.

Instead, we can use PHP's PDO library and a simply Repository pattern to handle reading and writing model objects to the database. Alternatively, the same pattern can be applied to reading and writing model objects over a REST API (in which case we would swap out PDO for Guzzle).

use PDO;
class CustomerRepository
{
    const
        SELECT_CUSTOMER = "SELECT * FROM Customer WHERE id = :id";

    private $customerSelect;
    private $pdo;

    public function __construct(
        PDO $pdo
    ) {
        $this->pdo = $pdo;
    }

    public function customer(int $id): ?Customer
    {
        $customerSelect = $this->customerSelect ??
            $this->pdo->prepare(self::SELECT_CUSTOMER);

        $this->customerSelect = $customerSelect;

        $customerSelect->execute(['id' => $id]);
        $customerRaw = $customerSelect->fetch(PDO::FETCH_ASSOC);

        return $customerRaw ? new Customer(
            $customerRaw['id'],
            $customerRaw['firstName'],
            $customerRaw['lastName'],
            $this->customerAddresses($id)
        ) : null;
    }

    public function saveCustomer(Customer $customer): bool
    {
        ....
    }
}

The Repository takes some form of client as it's constructor. In this case, an instance of the PDO client since we will be reading and writing Customer models to a database. If we were reading and writing to a REST API, we would pass a Guzzle client into the constructor. It is possible that an Entity is spread out across multiple storage mediums necessitating multiple PDO, Guzzle, or caching clients to compose a complete Entity. In which case each of these clients would pass into the Repository constructor.

Passing the client into the repository constructor makes it possible to mock the return values of each client. This allows us to test the repository without needing a live database or REST server for these clients to interface.

This simple Repository contains only two methods -- one to read and hydrate Customer from the database and a second to serialize or write a Customer back to the database. I have only implement the read here, but the write would be the same. We avoid "magic numbers" by composing our SQL queries as constants. We prepare them and cache them on the object. Then use that prepared statement to fetch the row and create a new instance of Customer returning that or null depending on if the record exists.

We could add additional methods to fetch a collection of Customer objects under various conditions, e.g. customersOfState(string $state) might return an array of all Customers with addresses in a particular state or allCustomers() might return a collection of all Customers in the database.

Linked References

  • layered-web-application-architecture
  • php-application-service

    This is the common formulation I use for services as a Command. The constructor accepts all objects that are required to perform it's execution, commonly these will be [[Repository]] objects. In a complex service we might have multiple repositories for retrieving many entities from the database or serialized over REST APIs -- the logic could become very complex needing additional strategies (passed in through the constructor) or calling many methods on these entities to compute the final result. Similarly, a read operations might retrieve many objects that the controller needs to pass on to the view.

  • php-models

    Hydration of a model from the data storage medium (or mediums) is done through the [[Repository]] and Factory patterns.