This project has 3 models:
Concert
: has many Tickets, belongs to many Orders.Ticket
: id, concert_id (not nullable), order_id (nullable)Order
: not really relevant, but added for completion
I've been playing around with LazyCollections, but I don't fully understand how they work. I ran into some trouble implementing a reserveTickets
method for my Concert
class. I can easily fix it by using a normal Collection
instead of a LazyCollection
but I want to understand why this is happening.
// Create 1 Concert
$concert = Concert::factory()->create();
// Create 3 tickets associated to the concert.
Ticket::factory()->for($concert)->count(3)->create();
// Reserve 2 tickets (set their reserved_at timestamp) and return them
$reservedTickets = $concert->reserveTickets(quantity: 2);
// Does not contain the 2 reserved tickets I expect it should have.
$reservedTickets->all();
The reserveTickets
method depends on the findTickets
method. Both are defined in the Concert
model.
public function reserveTickets(int $quantity): LazyCollection
{
return $this->findTickets($quantity)->each->reserve();
}
public function findTickets(int $quantity): LazyCollection
{
$tickets = $this->tickets()->available()->limit($quantity)->cursor();
throw_if(exception: NotEnoughTicketsException::class, condition: $quantity > $tickets->count());
return $tickets;
}
If I change findTickets
's implementation to
$tickets = $this->tickets()->available()->limit($quantity)->cursor()->remember();
The custom It actually works. I don't understand why it failed before.NotEnoughTicketsException
will be thrown when accessing the contents of the LazyCollection
Not using remember()
makes the SELECT query execute 3 times, making the throw_if
line useless most of the time.
Here is the relevant code for the models.
Concert
model
<?php
namespace App\Models;
use App\Exceptions\NotEnoughTicketsException;
use Illuminate\Database\Eloquent;
use Illuminate\Support\{Collection, LazyCollection};
class Concert extends Eloquent\Model
{
use Eloquent\Factories\HasFactory;
protected $guarded = [];
/* RELATIONSHIPS */
public function tickets(): Eloquent\Relations\HasMany
{
return $this->hasMany(Ticket::class);
}
public function orders(): Eloquent\Relations\BelongsToMany
{
return $this->belongsToMany(Order::class, 'tickets');
}
/* METHODS */
public function orderTickets(string $email, int $ticket_quantity): Order
{
$tickets = $this->findTickets($ticket_quantity);
return $this->createOrder($email, $tickets);
}
public function reserveTickets(int $quantity): LazyCollection
{
return $this->findTickets($quantity)->each->reserve();
}
public function findTickets(int $quantity): LazyCollection
{
$tickets = $this->tickets()->available()->limit($quantity)->cursor();
throw_if(exception: NotEnoughTicketsException::class, condition: $quantity > $tickets->count());
return $tickets;
}
}
Ticket
model
<?php
namespace App\Models;
use Illuminate\Database\Eloquent;
class Ticket extends Eloquent\Model
{
use Eloquent\Factories\HasFactory;
protected $guarded = [];
/* SCOPES */
public function scopeAvailable(Eloquent\Builder $query): void
{
$query->whereNull('order_id')->whereNull('reserved_at');
}
/* METHODS */
public function reserve(): void
{
$this->fill(['reserved_at' => now()])->save();
}
}
My Answer:
>create();
// Create 3 Tickets for the Concert
$tickets = Ticket::factory()->count(3)->create([
'concert_id' => $concert->id,
]);
// Get the Concert with its Tickets using LazyCollection
$concertWithTickets = Concert::where('id', $concert->id)->with('tickets')->get();
// Reserve the Tickets
$concertWithTickets->tickets->each(function ($ticket) {
$ticket->order_id = 1;
$ticket->save();
});
In this scenario, when you use LazyCollection to retrieve the Concert with its Tickets, the Tickets are not actually loaded into memory until you try to access them. This means that when you try to iterate over the Tickets using the `each` method, the Tickets are loaded one by one from the database, resulting in multiple queries being executed.
To avoid this issue, you can force the Tickets to be loaded into memory by converting the LazyCollection to a normal Collection using the `all` method before iterating over them:
php
$concertWithTickets->tickets->all()->each(function ($ticket) {
$ticket->order_id = 1;
$ticket->save();
});
By doing this, all the Tickets will be loaded into memory at once, and you will only need to execute one query to update all the Tickets.
Alternatively, you can also use the `load` method to eagerly load the Tickets when retrieving the Concert:
php
$concertWithTickets = Concert::where('id', $concert->id)->with('tickets')->first();
This will load the Concert with its Tickets in a single query, making it more efficient to iterate over the Tickets.
Rate this post
4 of 5 based on 8076 votesComments