Laravel 5 - Conditionally append variables - php

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).

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

Related

Get Laravel collection's item using collection value

Suppose we got a collection from the database like this:
$projects = Projects::all();
Now, for example, I want to get the specific project using the specified column's value. For example, suppose any project has a unique pr_code, Now I want to get an item of collection that pr_code is 1234.
Note = I Know using Projects::where('pr_code', 1234)->first() But I didn't want this. I want use inside collection
How I could do that?
Collections also have where and first (and whereFirst ) functions. So you just need to change the order of functions in your chain: Projects::all()->firstWhere('pr_code', 1234)
Projects::all() queries all records in projects and returns them as a Collection of Projects models, this could be quite a performance hit.
Projects::where('pr_code', 1234)->get() will query for the specific projects and return a Collection of Projects models that have pr_code of 1234.
Projects::where('pr_code', 1234)->first() will do the same, but return the first as a Projects model.
I recommend naming the model Project rather than the plural Projects. The Laravel model is smart enough to know to use the plural name of the database table.

Use a model (resource) attribute (accessor) with metrics in Laravel Nova

I have a MySQL table called purchases and a model called Purchase in Laravel that is related to it. My table has two columns, quantity and unit_cost. The total cost of a purchase is not a column in my table. Instead, it is defined by an accessor in Laravel that multiplies unit_cost by quantity and returns it as total_cost.
Now I am trying to define a trend metric in Laravel nova that is supposed to output the value of my daily sales in the selected range. When I try to use return $this->sumByDays($request, Purchase::class, 'total_cost') it gives me an error that says total_cost is not a column.
Fair enough! So, I tried to use Purchase::selectRaw(DB::RAW('unit_cost * quantity AS total_cost')) in the second argument, but it didn't work. I saw multiple posts, none of them could solve the problem of accessing a model accessor in Laravel Nova metrics. Any help is appreciated.
Unfortunately you can't use the built-in sumByDays helper function.
But good news! You can still implement it yourself within the calculate function. Just make sure your function returns a Laravel\Nova\Metrics\TrendResult.
Something like this:
$purchases = DB::table('purchases')
->select(DB::raw('quantity * unit_cost as total_cost'))
->groupBy(DB::raw('DATE(created_at) DESC'))
->get();
return new TrendResult()
->trend($purchases);
I don't have time to test this at the moment but it should get you started.
While trying to fix this problem, I checked through the sum by day or hours code and I realised that the column is an instance of Query Expression.
So that means you can still use the built in nova sum by days.
Make sure you import use Illuminate\Database\Query\Expression;
Just do this
$xpression = new Expression('quantity * unit_cost');
return $this->sumByDays($request, Purchase::class, $xpression);
It worked for me easily, I hope it does for others too.
The expression could be used in various ways in my case it was to perform a different calculation.

Laravel sort and save position in database

I have a table in my database called 'users', it has these columns:
id, points, rank.
Everytime I update points, I want to sort all users by points desc and save rank position in that column.
I was wondering about getting all rows from database, sort them by using collection method and using update to update rank column. But i was wondering about how intense it would be to update 100k users one by one.
Maybe some experienced Laravel developers can suggest me a best possible solution in this situation?
If you want data orderred by points you can use
$user = User::orderBy('point', 'desc')
->get();
Alright the things you need here is an Eloquent Observer so each time you update the points on update it will sort all the users by points desc and save the rank position.
However with that amout of data I would never use collections or anything simillar as it will be slow, I suggest you using raw methods to do that.
Now there are two possible options if latency matters on this case then you can place all the logic in the observer itself like below:
First of seperate the observer logic to a different class like:
namespace App\Observers;
class UserRankObserver
{
public function updated($model)
{
//check if points column its updated
//if yes then run the query to sort all user by points and save rank
}
}
Then on your User model you need to register the observer like :
public static function boot()
{
parent::boot();
User::observe(new \App\Observers\UserRankObserver);
}
Case two when the latency does not matter then I suggest on the method updated I would just pass the data to a background job to complete the action like :
namespace App\Observers;
class UserRankObserver
{
public function updated($model)
{
//check if points column its updated
//if yes then pass the data to a background job to complete the task.
(new UpdateUserRankJob($data))->dispatch();
}
}

Concept of table relations in Laravel

I have two tables:
Cards
Notes
Each Card has multiple Notes. So there is a relation between them like this:
class Card extends Model {
public function notes ()
{
return $this->hasMany(Note::class);
}
}
Ok well, all fine.
Now I need to understand the concept of these two lines:
$card()->$notes()->first();
and
$card()->$notes->first();
What's the difference between them? As you see in the first one $note() is a function and in the second one $note isn't a function. How will they be translated in PHP?
The first one points out to the card table and the second one points out to the notes table, right? or what? Anyway I've stuck to understand the concept of tham.
I don't know about $ before the $notes in your code but if you trying to say something like this.
1- $card->notes()->first();
2- $card->notes->first();
In the code in line 1, first you have a $card model and then you wanted to access all notes() related to that $card, and because of adding () after notes you simply call query builder on notes, show you can perform any other database query function after that, something like where, orderBy, groupBy, ... and any other complicated query on database.
But in the second one you actually get access to a collection of notes related to that $card, we can say that you get all related notes from database and set it into laravel collections and you are no more able to perform database query on notes.
Note: because laravel collections have some methods like where(), groupBy(), whereIn(), sort(), ... you can use them on the second one, but in that case you perform those methods on collections and not database, you already get all results from database

laravel Eloquent join and Object-relationship mapping

Ok so i'm kind of newish to eloquent and laravel (not frameworks tho) but i hit a wall here.
I need to perform some queries with conditions on different tables, so the eager load (::with()) is useless as it creates multiples queries.
Fine, let use the join. But in that case, it seems that Laravel/Eloquent just drops the concept of Object-relationship and just return a flat row.
By exemple:
if i set something like
$allInvoicesQuery = Invoice::join('contacts', 'contacts.id', '=', 'invoices.contact_id')->get();
and then looping such as
foreach ($allInvoicesQuery as $oneInvoice) {
... working with fields
}
There is no more concept of $oneInvoice->invoiceFieldName and $oneInvoice->contact->contactFieldName
I have to get the contacts fields directly by $oneInvoice->contactFieldName
On top of that the same named columns will be overwrited (such as id or created_at).
So my questions are:
Am i right assuming there is no solution to this and i must define manually the field in a select to avoid the same name overwritting like
Invoice::select('invoices.created_at as invoice.create, contacts.created_at as contact_create)
In case of multiple joins, it makes the all query building process long and complex. But mainly, it just ruins all the Model relationship work that a framework should brings no?
Is there any more Model relationship oriented solution to work with laravel or within the Eloquent ORM?
Instead of performing this join, you can use Eloquent's relationships in order to achieve this.
In your Invoice model it would be:
public function contact(){
return $this->belongsTo('\App\Contact');
}
And then of course inside of your Contact model:
public function invoices(){
return $this->hasMany('\App\Invoice');
}
If you want to make sure all queries always have these active, then you'd want the following in your models:
protected $with = ['Invoice']
protected $with = ['Contact'];
Finally, with our relationships well defined, we can do the following:
$invoices = Invoice::all();
And then you can do:
foreach($invoices as $invoice)[
$invoice->contact->name;
$invoice->contact->phone;
//etc
}
Which is what I believe you are looking for.
Furthermore, you can find all this and much more in The Eloquent ORM Guide on Laravel's site.
Maybe a bit old, but I've been in the same situation before.
At least in Laravel 5.2 (and up, presumably), the Eloquent relationships that you have defined should still exist. The objects that are returned should be Invoice objects in your case, you could check by dd($allInvoiceQuery); and see what the objects are in the collection. If they are Invoice objects (and you haven't done ->toArray() or something), you can treat them as such.
To force only having the properties in those objects that are related to the Invoice object you can select them with a wildcard: $allInvoicesQuery = Invoice::select('invoices.*')->join('contacts', 'contacts.id', '=', 'invoices.contact_id')->get();, assuming your corresponding table is called invoices.
Hope this helps.

Categories