John Davidson

php - Need help understanding how LazyCollection works in this scenario

0 comments
Message:


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 NotEnoughTicketsException will be thrown when accessing the contents of the LazyCollection It actually works. I don't understand why it failed before.


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 votes

Comments




© 2024 Hayatsk.info - Personal Blogs Platform. All Rights Reserved.
Create blog  |  Privacy Policy  |  Terms & Conditions  |  Contact Us