4
\$\begingroup\$

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?

\$\endgroup\$
  • \$\begingroup\$ What's the purpose of Model_Member (and the difference to MemberModel)? \$\endgroup\$ – Fge Feb 22 '14 at 10:00
  • \$\begingroup\$ @Fge // Model_Member is an object that extends ORM (framework object), which provides database access. \$\endgroup\$ – Moon Feb 22 '14 at 10:30
  • \$\begingroup\$ Are you you trying to build a repository on top of an existing (3rd party) ORM? What Framework do you use as ORM? What class of this framework is the Model_Member? \$\endgroup\$ – Fge Feb 22 '14 at 13:02
  • \$\begingroup\$ @Fge // not necessarily. I'm just using this ORM because I need a database connection. I think it doesn't matter what framework I'm using. Does it? \$\endgroup\$ – Moon Feb 22 '14 at 23:20
  • \$\begingroup\$ The framework itself does not matter, but how you use it. If its an ORM framework it most likely already has repositories to use. I'd guess you are already using the framework's repository then and your are building just another layer of the same abstraction around it (which is duplication in abstraction -> bad :)). If its a dbal framework the approach is mostly fine. Either way, the naming of this class is confusing. Names should indicate the responsibility just by reading it, without having to look at the implementation. \$\endgroup\$ – Fge Feb 23 '14 at 0:17
4
\$\begingroup\$

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.
\$\endgroup\$
  • \$\begingroup\$ That's such a great answer that I've been looking for the last couple of weeks. \$\endgroup\$ – Moon Feb 23 '14 at 1:51

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.