Using Domain Repositories for Services

Last year I started building out domain repositories in order to encapsulate the data retrieval for my website. They were easy to build, straightforward to implement, and simple to test. At the time all of the data retrieval was through a standard sql database, so all I had to do was pass in a connection and the interface-enforced methods would handle the query building. You can read more about this in my previous post about repositories. Lately I've started building out some other data stores that need to follow this pattern.

Yes, I'm jumping on the service train. Comments are a natural implementation that are shared between several of my websites and I'm tired of copying via inheritance across them. Plus, there is the potential for some great business logic to detect spam likelihood, something that my learnings in data science is just itching to work on. So I built a service that handles comment validation, storage, and retrieval, and needed to build out domain repositories to encapsulate the calls.

The service is similar to an API that communicates with basic HTTP requests, so the first step was creating a client. I took a shortcut on this one. My comment service is swagger-driven, so I just used a code generator to build out the client from the spec. The instantiated client is injected into the repository and the details of each request is encapsulated. If I ever wanted to communicate directly with the database the interface would hold true... The whole beauty of repositories.

  1. // first the interface

  2. interface CommentRepositoryInterface

  3. {

  4. public function getComment($commentId);

  5. ...

  6. }

  7. class ServiceCommentRepository implements CommentRepositoryInterface

  8. {

  9. protected $api;

  10. public function __construct($api)

  11. {

  12. $this->api = $api;

  13. }

  14. public function getComment($commentId)

  15. {

  16. $response = $this->api->getComment($commentId);

  17. return $this->deserializeComment($response);

  18. }

  19. protected function deserializeComment(Comment $comment)

  20. {

  21. return [

  22. 'id' => $comment->getId(),

  23. 'commenter' => [

  24. 'id' => $comment->getCommenter()->getId(),

  25. 'name' => $comment->getCommenter()->getName(),

  26. 'website' => $comment->getCommenter()->getWebsite(),

  27. ],

  28. 'body' => $comment->getBody(),

  29. 'date' => $comment->getDate(),

  30. 'url' => $comment->getUrl(),

  31. 'reply_to' => $comment->getReplyTo(),

  32. 'thread' => $comment->getThread(),

  33. ];

  34. }

  35. }

That ServiceCommentRepository::deserializeComment method though, I'm not a big fan of that. It actually points to a missing piece in my domain stack - the Entity. I've chosen not to add these piece and am (for now) content passing around associative arrays and trusting the contents. However, the client I'm using returns a fully formed object that is defined by the swagger models. Ideally I'd either use that or have a mapper to a local entity object, and maybe some factories, and... Yeah, for now I'm content sticking to associative arrays.

Testing is a breeze with this approach. All I need to do is mock out the client, define the response for each method, and done. There isn't much to test with how thin this class is, either. Much simpler than the deeper approach I took with the sql ones, when I actually spun up local sqlite instances for each query.

Oh, and caching. I'm struggling with that one. Part of me wants to build a cache repository that uses the same interface, but that's not how cache works. I need an programmatic way to decorate, and have misses automatically set based on the fallback data source. Which also means that I still need to encapsulate the repositories somehow. My controllers (or action classes) should not be concerned with the instantiation details for a sql-based or service-based domain repository. Well, more things to figure out.