How to decouple eloquent from the service layer?
Asked Answered
O

5

7

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.

Oppugn answered 11/5, 2015 at 12:45 Comment(1)
well you have any more questions? or the answer was what you asked?Cochran
G
2

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();
    }

    // ...
}
Grantee answered 29/5, 2015 at 8:1 Comment(0)
P
2

What I've done in the past that has brought some sanity to this situation for me was do things similar to what you are doing in your second associate method and prefix the repository with Eloquent so in the event I use something besides Eloquent, I just create a new implementation of the repository.

So in this case, I'd end up with class EloquentUserRepository implements UserInterface. I usually end up with some public methods which take and return only primitives and possibly some private methods which would be coupled to Eloquent so what I end up doing then is dropping those public methods into a AbstractUserRepository, or a trait if it makes more sense, to keep the code DRY.

Patric answered 11/5, 2015 at 14:37 Comment(0)
C
2

It really depends on the situation, I had many thoughts on those actions as well on my repositories.

What I would suggest is to simply not use the "associate" function, you can simply do:

$post->user_id = $userID;
$post->save();

** of course you need to make sure that the user with that id exists.

A) You can do it outside with a special service for "associatingUser" B) You can do it like you did with using the UserRepositoryInterface, I see no problem adding the interface as a dependency.

Option A:

class AssociateUserToPost {

private $userRepo;
private $postRepo;

public function __construct(UserRepoInterface $userRepo, PostRepoInterface $postRepo) {
    $this->userRepo = $userRepo;
    $this->postRepo = $postRepo;
}

public function associate($userId, $postId) {
    $user = $this->userRepo->getUser($userId);
    if ( ! $user )
        throw new UserNotExistException();

    $post = $this->postRepo->getPost($postId);
    if ( ! $post )
        throw new PostNotExistException();

    $this->postRepo->AttachUserToPost($postId, $userId);
}

}

option B (quite the same, code just sits in different places)

class PostRepository implements PostRepoInterface {

private $userRepo;

public function __construct(UserRepoInterface $userRepo) {
    $this->userRepo = $userRepo;
}

public function associate($userId, $postId) {
    $user = $this->userRepo->getUser($userId);
    if ( ! $user )
        throw new UserNotExistException();

    $post = $this->getPost($postId);
    if ( ! $post )
        throw new PostNotExistException();

    $this->AttachUserToPost($postId, $userId);
}

}
Cochran answered 26/5, 2015 at 19:9 Comment(0)
G
2

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();
    }

    // ...
}
Grantee answered 29/5, 2015 at 8:1 Comment(0)
L
0

Hydration!

I'm assuming that another reason calling findEloquent within the post service seems icky is because you may have already retrieved that data within the controller. Simply put, you can access the same method that Eloquent uses to transform raw query results into fully functioning models.

$userData = array(
    // simple, immutable data
);

$userCollection = User::hydrate(array($userData));

$userModel = $userCollection->first();
Lachish answered 29/5, 2015 at 19:50 Comment(0)
C
0

I think you actually need an additional layer, is what I call a Manager. This will contain all the business logic and will work only with interfaces. Under the hood it will call the services(each knowing to work with a specific resource/model)

Cleistogamy answered 2/6, 2015 at 7:27 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.