What To Return From Repositories
Repositories are swell. It's a great idea to have a central place to retrieve entities(models) from. They're even better if you're interfacing them and have different implementations, like an EloquentPersonRepository
. It's awesome to hide your ORM behind an interface. Your calling code likely doesn't need to know the ORM exists...but the repositories will still return <insert-your-ORM-here> models to the client code. Isn't the client code suppose to be unaware of the ORM? It doesn't make sense for your client code to do something like, $person->siblings()->attach($brother)
when attach()
is an Eloquent method. What do you do then? have your repositories cast everything to arrays? convert them to instances of stdClass
?
Eloquent is a very nice, clean ORM. It's super easy to work with. I think for many developers it may be their very first ORM, which is great! I think that because more than once I have seen questions come up like the following:
Eloquent was my introduction into ORMs and I had the exact same question. Let me illustrate a scenario with some code. I'll use the above example.
Note: I've left out some classes and interfaces in the code samples for brevity.
Our Order model
class Order extends Eloquent
{
protected $table = 'orders';
public function user()
{
return $this->belongsTo('User');
}
}
Our Order repository interface
interface OrderRepository
{
public function findById($id);
public function findByOrderNumber($orderNumber);
}
A repository implementation
class EloquentOrderRepository implements OrderRepository
{
protected $orders;
public function __construct(Order $orders)
{
$this->orders = $orders;
}
public function findById($id)
{
return $this->orders->find($id);
}
public function findByOrderNumber($orderNumber)
{
return $this->orders->whereOrderNumber($orderNumber)->first();
}
}
That's lookin good. Let's create a controller that has an action to transfer an Order to a User.
Our Controller
class OrderController extends BaseController
{
protected $users;
protected $orders;
protected $mailer;
public function __construct
(
UserRepository $users,
OrderRepository $orders,
OrderMailer $mailer
)
{
$this->users = $users;
$this->orders = $orders;
$this->mailer = $mailer;
}
public function transferOrder($userId, $orderId)
{
$user = $this->users->findById($userId);
$order = $this->orders->findById($orderId);
$order->user()->associate($user);
$order->save();
$this->mailer->sendTransferNotification($user->email, $order->orderNumber);
}
}
Do you see the problem? Part of the reason we created repositories was to hide Eloquent, yet we're using Eloquent's associate()
method on an Eloquent BelongsTo
object. We're also using Eloquent's magic __get()
and __set()
methods for the mailer. Our controller obviously knows about our ORM. Hmm...
Solution
Everyone seems to be telling you you should interface all the things, yet, somehow models have been flying under the radar. It's been causing confusion about what in the world your repositories should return.
Eloquent has made it super easy to work with models through the use of the magic methods, __get()
and __set()
. Interfacing your Eloquent models doesn't even enter your mind! Damn you Taylor for making it so easy! (kidding!)
If you really want to abstract the ORM, you must interface your models! You'll also want stop using the save()
method on your models outside of your repositories. This is only seen in ActiveRecord implementations.
Let's refactor.
Interface all the things!
interface Order
{
public function setUser(User $user);
public function getUser();
public function setOrderNumber($orderNumber);
public function getOrderNumber();
}
interface OrderRepository
{
public function findById($id);
public function findByOrderNumber($orderNumber);
public function save(Order $order);
}
class EloquentOrder extends Eloquent implements Order
{
protected $table = 'orders';
public function setUser(User $user)
{
$this->user()->associate($user);
}
public function getUser()
{
return $this->user;
}
public function setOrderNumber($orderNumber)
{
$this->orderNumber = $orderNumber;
}
public function getOrderNumber()
{
return $this->orderNumber;
}
private function user()
{
return $this->belongsTo('User');
}
}
Our new controller
class OrderController extends BaseController
{
protected $users;
protected $orders;
protected $dispatcher
public function __construct (
UserRepository $users,
OrderRepository $orders,
Dispatcher $dispatcher
) {
$this->users = $users;
$this->orders = $orders;
$this->dispatcher = $dispatcher;
}
public function transferOrder($userId, $orderId)
{
$user = $this->users->findById($userId);
$order = $this->orders->findById($orderId);
$order->setUser($user);
$this->orders->save($order);
$this->dispatcher->fire(OrderEvents::ORDER_TRANSFERED, [$user, $order]);
}
}
Now our controller really has no idea about Eloquent or any ORM. Interfacing models gives us a couple of nice benefits apart from abstraction. Mocking models in tests becomes trivial and if you're using an IDE, like PHPStorm, you get some super helpful code completion! You won't always have to remember if you need to attach()
or associate()
a model to another model. Hide that stuff behind your interfaced method.
You might also have noticed that we made the EloquentOrder::user()
method private. This is Eloquent related. Your client code can't use it anymore! That's good.
You probably also noticed I added a dispatcher and am firing an event in the controller. You should probably use the mailer in an event listener, which can make use of your new User
and Order
interfaces.
Considerations
Things aren't all rosy though. Sometimes things get a bit awkward by accommodating both ORM implementations. For example, if you went the other way and did $user->addOrder($order)
. In Eloquent that would update and save the model right away, not so in Doctrine. You would still need to use the UserRepository::save()
method for that change to actually get sent to the database. This is a price you pay by accommodating both ORM implementations sometimes. I suggest treating relationships like normal model attribute and always using the repositories to persist the changes. Eloquent is smart enough to know if the model is actually dirty and whether or not to perform any database calls. For example:
class OrderController extends BaseController
{
// other code
public function transferOrder($userId, $orderId)
{
$user = $this->users->findById($userId);
$order = $this->orders->findById($orderId);
$user->addOrder($order);
$this->users->save($user); // In EloquentUserRepository, only save if $user->isDirty is true
$this->dispatcher->fire(OrderEvents::ORDER_TRANSFERED, [$user, $order]);
}
}
This brings up my next point...getting to a point where you can swap out your ORM and not alter your client code is a challenge. You have to ditch the ActiveRecord mentality.
Is all this necessary to write "good" code? Nope. It all depends on your app and priorities. Do you think you may want to swap in Doctrine2, a data mapper implementation? If so, I'd encourage you to interface your models. If you're fairly confident you won't need to swap out Eloquent, don't bother unless you like the code completion(I do!) and/or testing benefits.
It's tough and unless you're familiar with multiple ORMs you'll probably have some work to do if/when you swap ORMs.
The best display of this I've found is the FOSUserBundle for Symfony. It can be used with both Propel(an ActiveRecord implementation) and Doctrine2(a data mapper implementation). Check out the Models, Propel, and Doctrine directories to see what I mean.
All that said, you must decide for yourself if this abstraction is worth it for your project. It not always is!