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
- Application Service uses [[Repositories]] to retrieve [[Models]], and if required, mutates those models
- 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.