Search code examples
laraveleloquentrepository-patternservice-layer

How to decouple eloquent from the service layer?


I am attempting to create a clean cut service layer, whereby the service layer acts upon one or more repositories, and each repositories acts on its own eloquent model.

For example, I may have:

ForumService
  |
  +-- PostRepo extends PostInterface
  |     |
  |     +-- Post (Eloquent)
  |
  +-- UserRepo extends UserInterface
        |
        +-- User (Eloquent)

Each service defines it's required dependencies via ioc. So, something like:

// MessageService
// ..
public function __construct(UserInterface $userRepository, 
                            MessageInterface $messageRepository) {
    // ..
}

My repositories are resolved via their bindings in their respective service providers, such as:

class UserRepositoryServiceProvider extends ServiceProvider 
{
    public function register()
    {
        $this->app>bind(
            'App\Models\Repositories\User\UserInterface',
            'App\Models\Repositories\User\UserRepository');
    }
}

This all works just fine. Each service gets the repositories it requires.

To keep the service layer clear of any specific dependency on eloquent, anything that leaves a repo is a simple, immutable, data object.

Key points in everyday language:

  • Only the repo's talk to their own models directly
  • Repo's return simple, immutable, data objects
  • Services act to tie multiple repo's together and present simplified objects back to the controllers, and ultimately the views.

However I can't come up with a clean pattern to associate eloquent models to each other at the service or repo layer.

Given the Post model has a belongsTo(User::class) relationship, how do I cleanly create that relationship at the Post repository layer.

I have tried:

public function associate($authorId) 
{
    $post->author()->associate($authorId);
}

But associate expects a user eloquent object, not just an id. I could do:

public function associate($authorId) 
{
    $post->from()->associate($userRepo->findEloquent($authorId));
}

But I feel like I am surfacing a eloquent model up into a repo that shouldn't be acting on it.


Solution

  • The easy way:

    public function assignToAuthor($postId, $authorId) 
    {
        $post = $this->find($postId); // or whatever method you use to find by id
    
        $post->author_id = $authorId;
    }
    

    Now, the above implies that you know the foreign key author_id of the relation. In order to abstract it just a bit, use this:

    public function assignToAuthor($postId, $authorId) 
    {
        $post = $this->find($postId);
    
        $foreignKey = $post->author()->getForeignKey();
    
        $post->{$foreignKey} = $authorId;
    }
    

    Mind, that you still need to save the $post model, but I suppose you already know that.


    Depending on your implementation of the simple, immutable, data object that you use, you could also allow passing the objects instead of raw ids. Something between the lines:

    public function assignToAuthor($postId, $authorId) 
    {
        if ($postId instanceof YourDataOject) {
           $postId = $postId->getId();
        }
    
        if ($authorId instanceof YourDataOject) {
           $authorId = $authorId->getId();
        }
    
        // ...
    }