Problem
I’ve been reading a lot about repository patterns these days. At first, the pattern seemed easy.
Most of the examples I read over the web use an ORM like below.
interface MemberRepositoryInterface {
public function find($id);
}
class MemberRepository implements MemberRepositoryInterface {
// here we're passing framework sepific ORM implementation
public function __construct(Model_Member $model)
{
$this->model = $model;
}
public function find($id)
{
return $this->model->find($id);
}
}
// In a controller
class Test_Controller {
public function __construct(MemberRepositoryInterface $memberRepository)
{
$this->repository = $memberRepository;
}
public function show()
{
$member = $this->repository->find(1);
$this->template->content = View::factory("testView")->set('member', $member)->render();
}
}
It looks fine up to this point. Now..when you pass $member variable to a view then you can access all the framework specific methods of the ORM such as…delete(), save(), and so on.
what’s the best way to prevent people from using framework specific ORM methods?
I thought about it..then the answer is to use a plain old PHP object.
// Modified Repository
class MemberModel {
public $id;
public $firstName;
public $lastName;
public function __construct(array $data)
{
foreach($data as $key=>$value)
{
if(isset($this->$key))
{
$this->$key = $value;
}
}
}
}
class ContactModel {
public $id;
public $firstName;
public $lastName;
public function __construct(array $data)
{
foreach($data as $key=>$value)
{
if(isset($this->$key))
{
$this->$key = $value;
}
}
}
}
class MemberRepository implements MemberRepositoryInterface {
// here we're passing framework sepific ORM implementation
public function __construct(Model_Member $model)
{
$this->model = $model;
}
public function find($id)
{
$record = $this->model->find($id)->as_array();
return new MemberModel($record);
}
public function find_with_contacts($id)
{
//find a member and contacts then use MemberModel and ContactModel
// then return the data
}
}
Is this good enough? or are there other best practices?
Solution
You are already using the repository of Kohana. They hide their repository behind static factory methods though (copied from the documentation):
// Find user with ID 20
$user = ORM::factory('User')
->where('id', '=', 20)
->find();
// Or
$user = ORM::factory('User', 20);
Yet they fail to achieve the important thing about ORM and repositories: decoupling the application from the database. This is actually two-fold: the SQL-syntax leaks into the application space (the querybuilder) and the database-structure is mapped 1:1 on the model. Changing the model or database requires to change the other respectively. Comparing this to the great explanation at MSDN: the mapper is missing and the client business logic has access to the query object (and therefore to the datasource). A major (not related) issue is the all-presence of static method calls.
What you try is to fix this (thumbs up ;)). You have your business entities (MemberModel
and ContactModel
) and a repository MemberRepository
. But instead of delegating the the ORMs repository you should use the query builder directly:
class MemberRepository implements MemberRepositoryInterface {
public function find($id)
{
$record = DB::select(...);
}
}
The second important task is the mapping of database fields to entity fields (and vice versa). This should not be done in your business entity! The business entity does not know anything about how it is saved:
class MemberRepository implements MemberRepositoryInterface {
public function find($id)
{
$record = DB::select(...);
// map $record to $entity;
return $entity;
}
}
Other means to manage your model in the repository as well (e.g. delete, update, create), not in your business entity!
Last your model. For now it is fine. But soon it should contain your business logic (no, not in your services or controllers ;)). You’ll probably start with some data validation when setting properties. I’d suggest to use setters/getters right away, saves you refactoring later on.
Two final remarks about ORM at the end:
- Repositories tend to be very repetitive. Especially when they only provide only the basic CRUD methods. Pretty much every ORM Framework provides a general repository for these methods (as Kohana does and what you used).
- Some explanations on ORM pass query objects to the repository. Those are not to be confused with the query builder from your dbal. The query passed to the repository is written in the business language (e.g. the fields of your model) and mapped to a query of the querybuilder by the repository. This avoids leaking of database details into the business logic.