Apologize for writing to this thread after so much time but I've been reading about repositories a lot lately and the more I read the more confusing things are. Shawn post is pretty nice and explains some things about how to use repositories but it still left me with some confusing things.
As @cm was saying, using Repositories and tying them to Eloquent by either accepting Eloquent Models and returning an Eloquent Model or Collection seems to be the purpose of the Repository.
Even if swapping a backend is usually not something that will happen often creating a repository that can do that seems good design.
Here are two examples, one of tying a repo to Eloquent and the other one using plain models and collections.
Example one. User and UserRepository that comes with two backends: eloquent (mysql) and redis.
The problem with this approach is that in order to make the redis repository work I need to return eloquent models and collections. That means that I will have to create the model from the redis data and return it for the controller to use. This really does not make any sense to me since Eloquent is Active Record and the way I will use the model with redis would be more like a non active record model. That means that a high percentage of what the model brings will not be useful if I switch the backend to redis.
interface RepositoryInterface
{
public function findById($id): \Illuminate\Database\Eloquent\Model;
public function all(): \Illuminate\Database\Eloquent\Collection;
}
interface UserRepositoryInterface extends RepositoryInterface
{
public function getPosts($userId): \Illuminate\Database\Eloquent\Collection;
}
class UserEloquentRepository implements UserRepositoryInterface
{
public function findById($id): \Illuminate\Database\Eloquent\Model
{
return User::find($id);
}
public function all(): \Illuminate\Database\Eloquent\Collection
{
return User::all();
}
public function getPosts($userId): \Illuminate\Database\Eloquent\Collection
{
return User::find($userId)->posts;
}
}
class UserRedisRepository implements UserRepositoryInterface
{
public function findById($id): \Illuminate\Database\Eloquent\Model
{
$redisUser = json_decode(Redis::get("user-{$id}"), true);
return new User($redisUser);
}
public function all(): \Illuminate\Database\Eloquent\Collection
{
$redisUsers = json_decode(Redis::hGetAll('user'), true);
$eloquentUsers = array_map(function ($user) { return new User($user); }, $redisUsers);
return new \Illuminate\Database\Eloquent\Collection($eloquentUsers);
}
public function getPosts($userId): \Illuminate\Database\Eloquent\Collection
{
$redisPosts = json_decode(Redis::get("user-{$userId}-posts"), true);
$eloquentUserPosts = array_map(function ($user) { return new User($user); }, $redisPosts);
return new \Illuminate\Database\Eloquent\Collection($eloquentUserPosts);
}
}
class UserController extends Controller
{
protected $userRepository;
public function __construct(UserRepositoryInterface $userRepository)
{
$this->userRepository = $userRepository;
}
public function show($id): \Illuminate\Database\Eloquent\Model
{
return $this->userRepository->findById($id);
}
public function index(): \Illuminate\Database\Eloquent\Collection
{
return $this->userRepository->all();
}
public function userPosts($userId): \Illuminate\Database\Eloquent\Collection
{
return $this->userRepository->getPosts($userId);
}
}
Now for example two, I am doing a pretty similar thing but this time for an Account model. One thing to notice is that Account model does not extend Eloquent's model. It's just a simple plain model with no persistence. There's also a plain collection used to store a collection of account records. That Collection is pretty simple right now but a ton of features can be added there.
With this approach I am sure that even if I need to switch the backend from the Eloquent driver to Redis, the app will be using plain models which can be passed back to the repository to be saved, updated, etc without worrying what backend they have.
The eloquent account repository does use an Eloquent Account Model to pull all the data. This means that my app will need to have two Account Models. A plain one and an eloquent one but only when using the Eloquent driver for the backend.
Looking at this second example I really fell that one can create (not sure how that will work) an Eloquent Model directly as a Repository.
class AccountEloquentRepository extends \Illuminate\Database\Eloquent\Model implements AccountRepositoryInterface and inside there we implement all the logic we need. Not sure how this will play out and maybe we still need to have two models just of how Eloquent works but I still think this feels a cleaner approach then the first one when all our repositories are highly coupled with Eloquent.
interface AccountRepositoryInterface
{
public function findById($id): \App\PlainModel;
public function all(): \App\PlainCollection;
}
class AccountEloquentRepository implements AccountRepositoryInterface
{
public function findById($id): \App\PlainModel
{
/** @var \Illuminate\Database\Eloquent\Model $account */
$account = EloquentAccount::find($id);
return new Account($account->toArray());
}
public function all(): \App\PlainCollection
{
/** @var \Illuminate\Database\Eloquent\Collection $accounts */
$accounts = EloquentAccount::all();
return new PlainCollection($accounts->toArray());
}
}
class AccountRedisRepository implements AccountRepositoryInterface
{
public function findById($id): \App\PlainModel
{
$redisAccount = json_decode(Redis::get("account-{$id}"), true);
return new Account($redisAccount);
}
public function all(): \App\PlainCollection
{
$redisAccounts = json_decode(Redis::hGetAll('account'), true);
$accounts = array_map(function ($account) { return new Account($account); }, $redisAccounts);
return new \App\PlainCollection($accounts);
}
}
class AccountController extends Controller
{
protected $accountRepository;
public function __construct(AccountRepositoryInterface $accountRepository)
{
$this->accountRepository = $accountRepository;
}
public function show($id): \App\PlainModel
{
return $this->accountRepository->findById($id);
}
public function index(): \App\PlainCollection
{
return $this->accountRepository->all();
}
}
My question in the end is: if we write repositories but have them highly coupled to Eloquent are we gaining anything except for cleaner controllers and more testable code?