Laravel 5 - Conditionally append variables
Asked Answered
S

2

9

A few words before

I know that you can append variables to model arrays and json representations by using the protected $appends = ["your", "vars", "here"]; array. But imagine the following situation:

The situation

Our use case would be a fictional game or similiar:

Imagine that we have a User model that holds simple information about an (human) user, like the full name, address and so on.

Now, we also have a Faction model that represents the faction/origin/guild/... of this user.

The Faction model is eager-loaded when retrieving users, because the Faction name is wanted almost every time when displaying the user information.

A User also has DailyStatistics, which holds some information about their daily scores (simple points would be enough).

The Clue

Because I want to know the points of the a faction, which is the sum of the user points, I thought about appending a new variable totalPoints.

The getTotalPointsAttribute function would look like this:

function getTotalPointsAttribute(){
    return $this->users->sum->getTotalPoints();
}

The problem

Everytime when we retrieve a user now, the eager-loaded faction would also want to calculate the totalPoints attribute. That means, that we have a lot of overhead per user.

The question

Is there a way to avoid situations like this? Can I "conditionally" append variables? Are properties calculated when they are hidden?

I tried to wrap the totalPoints variable in a simple function, instead of an accessor instead. The problem is, that Frontend-Frameworks like VueJS would need access to the totalPoints variable (or to an endpoint to retrieve that value, but this solution is the least favorable).

Selfexamination answered 24/6, 2017 at 22:17 Comment(11)
Could you use a query scope for this?Sastruga
You can use the 'with' method to specify that you want to load a relationship, and you can leave it off if you don't want the relationship.Substitute
@Substitute The problem is, that I want to eager-load the relationship all the time, but only calculating the totalPoints "sometimes/if needed"Selfexamination
@Sastruga could you give an example? I can't come up with one.Selfexamination
Assuming this is for API output, you could have the frontend define which fields they require for output. Only if they specifically require the totalPoints will you add it to the output. Adding it to appends will always attach it to your model, when having it as an accessor it's only fetched when called. Take a look at fractals and transformers here: fractal.thephpleague.com/transformersSorenson
I think you are best off writing your own query with the join methods. Then you can also use the when flag to your advantage, plus you will end up with less queries.Substitute
Though fractals/transformers look interesting, I want to achieve the result with as less "extra" dependencies as possible, which means by Eloquent only when possible. @SorensonSelfexamination
@Substitute I'm sure that there is a solution that takes less effort. I really want to avoid writing plain SQL/using the fluent query builder. Reason for that is that I already have the property "totalPoints", which will be needed in other cases too soon. That means I'd need pure SQL everytime then..Selfexamination
@Selfexamination I agree, and am hoping to find out, that there is a more "eloquent" way to do this. But I'm not sure there is. It seems that the "append"ed attributes are really meant more for attaching things to Json output rather than fetching a full relationship. The reason being that it will be an additional query which is added afterwards. You end up with a query for your user, an additional one for the relationship, and then an additional one (or more) when you tack on things in the append. To your point of needing to build every time, I'm sure you can use some clever extractions to help outSubstitute
Transformers are just a way of controlling the output you generate. From what I read, you just need the points when specified by the frontend. To remove the overhead, you should just output it when requested.Sorenson
@Sorenson Well, that's what I want to achieve :D But in a "Laravel-way" :D My current solution is creating functions, not accessors, and map the values to the models when needed. But I feel like there should be sth. better for this.Selfexamination
P
15

I met this problem as I wanted to Appends on the fly but don't want this to auto-appends on any other Controller/Models (The other way is produce 2 Models for the same Table, which is difficult to maintain).

Currently I'm maintaining a Laravel 5.4 (Since they refuse to upgrade PHP5.6 to PHP7)

For Laravel 5.4 and below

Just add a closure after completed the query builder get()

->each(function ($items) {
    $items->append('TotalPoints');
);

Source of original solutions: laravel-how-to-ignore-an-accessor

$openOrders = Order::open()->has('contents')
->get(['id','date','tableName'])
->each(function ($items) {
    $items->append('TotalPoints');
);

Your model still contains the

public function getTotalPointsAttribute()
{
    return $this->users->sum->getTotalPoints();
}

Now you can remove/comment out the the appends in your models :

protected $appends = [
    'TotalPoints',
];

Alternatively, if you're on Laravel 5.5 and above, you could use the collection magic like so:

$openOrders->each->setAppends(['TotalPoints']);

Laravel 5.5 and above now have a Laravel 5.6 #Appending At Run Time

Pentagrid answered 9/7, 2018 at 6:55 Comment(1)
With Laravel 5.8, using ->each I got: ErrorException: Undefined property: Illuminate\Pagination\LengthAwarePaginator::$eachOrangeism
P
0

You can use Laravel Appending At Run Time.

First define an accessor for the value:

function getTotalPointsAttribute(){
  return $this->users->sum->getTotalPoints();
}

Then if you want to retrieve multiple models, use it like:

$users->each->append(['total_points']);

// Or if you want to override the entire array
// of appended properties for a given models
$users->each->setAppends(['total_points']);

Or if you want just retrive single model just simply do:

$user->total_points;

// append(['total_points']) and setAppends(['total_points']) is also usable;
Prevision answered 21/1 at 16:19 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.