Expressive Eloquent Collections
Expressive Eloquent Collections
What is eloquent?
The goal
Readability
The result
- Readability.
- Extracting repeated logic.
- Default and improved sorting methods.
- Thinning models.
- Testability.
- Reducing database queries.
- Sharing a filtering API with eloquent scopes.
- Encapsulation.
Hey! I'm Tim
Developer; Musician; ๐ถ lover;
Meet Taz
You: "awwwww"
Why I wanted to improve readability
The language of a model
Problem space specific.
if ($invoice->payment !== null) {
//
}
if ($invoice->isPaid()) {
//
}
if ($invoice->due_at->lt(now())) {
//
}
if ($invoice->due_at->isPast()) {
//
}
if ($invoice->isOverdue()) {
//
}
The language of a collection
Set of generic items.
if ($collection->contains($item)) {
//
}
if ($collection->isEmpty()) {
//
}
Zero matches found
$items = collect([
new GenericItem,
new GenericItem,
]);
๐ค
Eloquent models speak the domain language.
Eloquent collections only contain eloquent models.
Why don't eloquent collections speak the domain language as well?
Eloquent collections
$allInvoicesArePaid = $invoices->every(function ($invoice) {
return $invoice->isPaid();
});
if ($allInvoicesArePaid) {
//
}
Higher order collection proxy
if ($invoices->every->isPaid()) {
//
}
Extended collections
class InvoiceCollection extends Eloquent\Collection
{
//
}
Wire it up
class Invoice extends Eloquent
{
public function newCollection($models = [])
{
//
}
// ...
}
๐
$allInvoicesArePaidOrProcessing = $invoices->every(function ($invoice) {
return $invoice->isPaid() || $invoice->paymentIsProcessing();
});
if ($allInvoicesArePaidOrProcessing) {
//
}
class InvoiceCollection extends Eloquent\Collection
{
public function areAllPaidOrProcessing()
{
return $this->every(function ($invoice) {
return $invoice->isPaid() || $invoice->paymentIsProcessing();
});
}
}
๐
if ($invoices->areAllPaidOrProcessing()) {
//
}
Readability โ
Just the beginning
Extracting repeated logic
Keepin' it DRY
Once, twice
$total = $invoices->reduce(function ($total, $invoice)
return $total->add($invoice->cost);
}, new Money);
...thrice!
$total = $invoices->totalCost();
Default and improved sorting methods
๐ฅบ
$users = User::query()->search($term, ['name'])->get();
$users = $users->sortByDesc(function ($user) use ($term) {
// known set of users, don't @ me
return similar_text(
Str::lower($term),
Str::lower($user->name)
);
});
๐ค
$users = User::query()->search($term, ['name'])->get();
$users = $users->sortByRelevance($term, ['name']);
Thinning models
$users->each(function ($user) {
$user->sendOverdueNotice();
});
$users->sendOverdueNotices();
// collection
$users->loadMissing('posts');
// model
$user->loadMissing('posts');
public function loadMissing($relations)
{
$this->newCollection([$this])->loadMissing($relations);
return $this;
}
Testability
public function test_it_calculates_totals()
{
$invoices = factory(Invoice::class)->times(2)->create([
'cost_in_cents' => 3333,
]);
$this->assertSame(6666, $invoices->totalCost()->inCents());
}
Reducing database queries
$notifications->each->update(['read_at' => now()]);
Notification::query()
->whereKey($notifications->modelKeys())
->update(['read_at' => now()]);
NotificationCollection extends Eloquent\Collection
{
public function query()
{
if ($this->isEmpty()) {
return new NullQuery(static::class);
}
return Notification::query()->whereKey($this->modelKeys());
}
// ...
}
NotificationCollection extends Eloquent\Collection
{
public function markAsRead()
{
$now = now();
$this->query()->update(['read_at' => $now]);
$this->each->setAttribute('read_at', $now);
return $this;
}
// ...
}
$notifications->markAsRead();
// $notifications->each->update(['read_at' => now()]);
// Notification::query()
// ->whereKey($notifications->modelKeys())
// ->update(['read_at' => now()]);
๐คฅ
~~Reducing database queries~~
Cleaning up database queries
Sharing a filtering API with eloquent scopes
Slight tangent...
$users->where('name', '=', 'Jasmine');
User::query()->where('name', '=', 'Jasmine');
User::query()->whereName('Jasmine');
User::query()->whereNotNull('name');
User::query()
->whereName('Jasmine')
->whereNotNull('confirmed_email_at')
->active()
// ...
class Notification extends Eloquent
{
public function scopeWhereUnread($builder)
{
$builder->whereNull('read_at');
}
}
$notifications = Notification::query()->whereUnread()->etc(...);
class NotificationCollection extends Eloquent\Collection
{
public function whereUnread()
{
return $this->where('read_at', '=', null);
}
}
$notifications->whereUnread()->etc(...);
๐ค
#symmetry
Notification::whereUnread()->etc(...);
$notifications->whereUnread()->etc(...);
Encapsulation
๐คจ
~~Chicken or the egg~~
Model or the Collection
So, like, what even is Eloquent?
Model
Query Builder
+
Collection
Factories