Laravel: how to get count() of all relations NOT collection - laravel

I have an accessor like so
public function getRegisteredCountAttribute()
{
return $this->attendees->count();
}
However, I have noticed that this counts the attendees in my collection after the query. So if my query removes some of the attendees I don't get the proper count.
Here is my query
$programs = ScheduledProgram::where('registration_start_date', '<=', $today)
->where('end_date', '>=', $today)
/* ->with(['attendees'=>function($q) use ($user_id)
{
$q->where('user_id', $user_id);
}])
->with(['scheduledProgramSegments.attendees'=>function($q) use ($user_id)
{
$q->where('user_id', $user_id);
}])
*/
->get();
I get a different number from my accessor $program->registered_count when I uncomment the comment in query section above. I guess that the accessor is giving me the count from the collection and not doing a new query to get the count I really need.
How do I get the count of registered attendees in the program?
I should note that the models attendeesand programs have a many-to-many (belongsToMany) relation with a pivot table that also has fields for registered, waitlisted.
I saw this article but I couldn't find the next belongsToMany.
Models
class ScheduledProgram extends Eloquent {
public function scheduledProgramSegments()
{
return $this->hasMany('ScheduledProgramSegment');
}
public function attendees()
{
return $this->belongsToMany('Attendee', 'prog_bookings')->withPivot('registered','paid','waitlisted');
}
public function getRegisteredCountAttribute()
{
return $this->attendees()->count();
}
}
class ScheduledProgramSegments extends Eloquent {
public function attendees()
{
return $this->belongsToMany('Attendee', 'bookings')->withPivot('paid');
}
public function scheduledProgram()
{
return $this->belongsTo('ScheduledProgram');
}
}
class ProgBooking extends Eloquent {
public function scheduled_program()
{
return $this->belongsTo('ScheduledProgram');
}
public function attendee()
{
return $this->belongsTo('Attendee');
}
}

When you fetch the $programs and eagerly load attendees with some additional constraint, those fetched filtered attendees are saved in $program->attendees attribute. When you call count() on that collection you'll get a number of attendees in that filtered collection.
If you need to count all attendees in given program you'll need to do:
public function getRegisteredCountAttribute()
{
return $this->attendees()->count();
}
Notice the additional () - as a result you'll call count() not on the eagerly loaded collection of attendees with additional constraints applied - you'll call that on the relation itself.

Related

Restricting hasMany relationship where child.field != parent.field without joins

I'm looking for a way to qualify a hasMany relationship to exclude/include children where the value a specific child field does/not match that of a specific parent field without using joins.
The issue with joins is that the ->select() filters out many descendant relationships (unless they are each added to the join, which is too much to manage). The descendant relationships for example would be order_item.options which is a belongsToMany of OrderItem.
Case in point:
class Order extends Model
{
public function items()
{
return $this->hasMany(OrderItem::class, 'order_id', 'id');
}
public function items_removed_by_store()
{
return $this->hasMany(OrderItem::class, 'order_id', 'id')
//->join('order', 'order_item.order_id', 'order.id')
//->where('order_item.deleted_by', '!=', 'order.customer_id')
//->select('order_item.*', 'order.customer_id')
->onlyTrashed();
}
public function items_removed_by_customer()
{
return $this->hasMany(OrderItem::class, 'order_id', 'id')
//->join('order', 'order_item.order_id', 'order.id')
//->where('order_item.deleted_by', '=', 'order.customer_id')
//->select('order_item.*', 'order.customer_id')
->onlyTrashed();
}
}
and I'm looking to query it like:
Location::has('orders.items_removed_by_customer')->get()
Get me locations where customers have removed items from their orders.
It seems like you are currently joining the relationship on its parent. Note you already have the parent data
Maybe try something like this
class Order extends Model
{
public function items()
{
// Note: it is not required to pass 'order_id', 'id' to this method as thats the default value
return $this->hasMany(OrderItem::class);
}
public function items_removed_by_store()
{
return $this->items()
->where('order_items.deleted_by', '!=', $this->customer_id)
->onlyTrashed();
}
public function items_removed_by_customer()
{
return $this->items()
->where('order_item.deleted_by', '=', $this->customer_id)
->onlyTrashed();
}
}

Laravel check hasMany of hasMany relationship if has IDs?

Hello I have the following relationship setup:
Product class:
public function attributes()
{
return $this->hasMany(ProductAttribute::class);
}
ProductAttribute class:
public function attribute()
{
return $this->belongsTo(Attribute::class);
}
public function values()
{
return $this->hasMany(ProductAttributeValue::class, 'product_attribute_id');
}
ProductAttributeValue class:
public function attributeValue()
{
return $this->belongsTo(AttributeValue::class, 'attribute_value_id');
}
How to check if Product has values with ids 5 and 15?
I am trying to make a query like this:
Product::whereHas('values', function($q) use ($product_values_ids) {
$q->whereIn('attribute_value_id', $product_values_ids);
})->get();
however it is not working. I cannot access directly $product->values.
Any suggestions on how to access directly the values of attributes from the Product?
Update:
I have just managed to make it work with a many to many trough relationship:
Product class:
public function values()
{
return $this->hasManyThrough(ProductAttributeValue::class, ProductAttribute::class);
}
is there a way to get only the results that have all the ids listed in the $product_values_ids array?
You have to add new relation to Product model:
public function values(): HasManyThrough
{
return $this->hasManyThrough(ProductAttributeValue::class, ProductAttribute::class);
}
and then:
$builder = Product::query();
foreach($product_values_ids as $id) {
$builder->whereHas('values', function($q) use ($id) {
$q->where('id', $id);
});
}
$product = $builder->get();

Laravel how to get only relation data

this is my User.php relation code
public function activities()
{
return $this->hasMany(Activities::class, 'builder');
}
and this is my query to get only relation data
return User::whereHas('activities', fn($query) => $query->where('user_id', 1))
->paginate();
but it returns only user data without appling any relation, and its pagination not get to use pluck
i have also tried this
User::where('username', request()->username)->has('activities')->paginate();
but i need only get relation data not user with relation, and i prefer do it with whereHas
You need to create reverse relation for Activities model:
class Activities extends Model
{
// ...
public function user(): BelongsTo
{
return $this->belongsTo(User::class, 'user_id');
}
}
And get all the activities using Activities builder:
$paginated = Acitivities::query()
->whereHas('user', static function(Builder $userQuery) {
$userQuery->where('name', request()->username); // Think about security!
})
->paginate();
Note: All the models must be named in singular (e.g. Activity, User, Product and etc.).

Order on second degree relationship in Eloquent

I have two models in a many-to-many relationship: Fixture and Event.
Fixture:
public function events()
{
return $this->belongsToMany(Event::class, 'fixture_events')->withPivot('player_id');
}
Event:
public function fixtures()
{
return $this->belongsToMany(Fixture::class, 'fixture_events')->withPivot('player_id');
}
You will notice that the pivot table has an additional field player_id. This is because FixtureEvent also had a relationship to a model called Player.
FixtureEvent:
public function fixture()
{
return $this->hasOne(Fixture::class, 'id', 'fixture_id');
}
public function event()
{
return $this->hasOne(Event::class, 'id', 'event_id');
}
public function player()
{
return $this->belongsTo(Player::class, 'id', 'player_id');
}
And Player has:
public function events()
{
return $this->hasMany(FixtureEvent::class);
}
My problem arises when I want to get all the fixture_events for a player and sort them by a field in the events table. This field is named sequence.
However, whatever I do, the events always come out ordered by ID.
This is the query that I would like to order by events.sequence, whether by using some type of join or whatever works (this is inside the Player model so $this is a player object):
$events = $this->events()->whereHas('fixture', function ($query) use ($round, $competition_id) {
$query->where('fixtures.round', '=', $round)->where('competition_id', $competition_id);
})->get();
I've tried adding a join query here on fixture_events.event_id = events.id and then ordering by events.sequence but this doesn't work.
I've also tried adding orderBy directly in the model relationship, i.e. in the Fixture model:
public function events()
{
return $this->belongsToMany(Event::class, 'fixture_events')->orderBy('sequence')->withPivot('player_id');
}
But this does nothing for my problem.
How do I make this happen?
Update
At first I misread the relations, can you try with the below query?
$events = $this->events()->whereHas('fixture', function ($query) use ($round, $competition_id) {
$query->where('fixtures.round', '=', $round)->where('competition_id', $competition_id);
})->with(['events.event' => function ($query) {
$query->orderBy('sequence');
}])->get();
You have a couple of alternatives, but first I suggest you to edit your relationship to include the sequence field you are trying to load.
Then proceed with one of the following:
Order by on the relationship definition, but I think you have to load that field from the pivot table, otherwise you won't have its value, and prefix the relations table on the orderby field.
public function events() {
return $this->belongsToMany(Event::class, 'fixture_events')
->withPivot(['player_id', 'sequence'])
->orderBy('fixture_events.sequence');
}
or with:
public function events() {
return $this->belongsToMany(Event::class, 'fixture_events')
->withPivot(['player_id', 'sequence'])
->orderBy('pivot_sequence');
}
Order by a pivot field outside the relation can be done like this:
$events = $this->events()->whereHas('fixture', function ($query) use ($round, $competition_id) {
$query->where('fixtures.round', '=', $round)->where('competition_id', $competition_id);
})->with(['fixture' => function ($query) {
$query->orderBy('sequence');
}])->get();
or with:
$events = $this->events()->whereHas('fixture', function ($query) use ($round, $competition_id) {
$query->where('fixtures.round', '=', $round)->where('competition_id', $competition_id);
})
->orderBy('pivot_sequence')
->get();
Let me know if any of these methods works!

Laravel relationships avoid query where foreign key is null

When eager loading, is it possible to avoid doing an extra query when the foreign key in a relationship is null and therefore does not match any related record? In my example I have a Product and User.
A Product is owned by a User but can also be optionally edited by a User. So my model looks like this:
class Product extends Model
{
public function user()
{
return $this->belongsTo(User::class);
}
public function editor()
{
return $this->belongsTo(User::class, 'editor_id');
}
}
When a product has not been edited, it's editor_id attribute is NULL.
If I hadn't been eager loading I know I could do something like the following:
$product = Product::find(1);
if (!is_null($product->editor_id)) {
// Load the relation
}
However, this isn't an option for me and I would ideally like to avoid an extra, unnecessary query being run when eager loading:
Query: select * from `users` where `users`.`id` in (?)
Bindings: [0]
I was wondering if something similar to the following would be possible?
public function editor()
{
if (!is_null($this->editor_id)) {
return $this->belongsTo(User::class, 'editor_id');
}
}
When doing the above I get this error:
Call to a member function addEagerConstraints() on a non-object
I'm guessing that this is because it's not guaranteed that this method returns a Relation object.
Thanks
I solved this by creating a new Relation subclass that implements the required methods but simply returns null when actually obtaining results:
namespace My\App;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Relations\Relation;
class NullRelation extends Relation {
public function __construct() {}
public function addConstraints() {}
public function addEagerConstraints(array $models) {}
public function initRelation(array $models, $relation) {}
public function match(array $models, Collection $results, $relation) {
return [];
}
public function getResults() {
return null;
}
}
Then inside your relation method(s) you can check for null and return an instance of NullRelation instead:
public function editor() {
if ($this->editor_id === null) {
return new NullRelation();
} else {
return $this->belongsTo(User::class, 'editor_id');
}
}
It's a bit ugly and you'd be repeating youself a lot, so if I were using this in more than one place I'd probably create a subclass of the Model, then create versions of the belongsTo, hasOne methods and perform the check there:
public function belongsToIfNotNull($related, $foreignKey = null, $otherKey = null, $relation = null) {
if ($this->$foreignKey === null) {
return new NullRelation();
} else {
return parent::belongsTo($related, $foreignKey, $otherKey, $relation);
}
}
Finally in your modal that inherits the new subclass, your relation method simply becomes
public function editor() {
return $this->belongsToIfNotNull(User::class, 'editor_id');
}
Laravel Docs
Querying Relations When Selecting
When accessing the records for a model, you may wish to limit your results based on the existence of a relationship.
$posts= Post::has('editor')->get();
You may also specify an operator and a count:
$posts = Post::has('editor', '>=', 3)->get();

Resources