This question already has answers here:
Laravel sorting last record
(5 answers)
Closed 2 years ago.
I'm trying to minimize the amount of queries I'm using by "eager loading" a threads' replies pagination. But I'm a little unsure how to do it. Below is the code:
$unpinnedThreads = Thread::all()->sortByDesc(function($thread) {
$replies = $thread->replies->sortByDesc('created_at');
$lastTouchedPost = Carbon::minValue();
if (!empty($replies->toArray())) {
$lastTouchedPost = $lastTouchedPost->max($replies->first()->created_at);
}
return $lastTouchedPost->max($thread->created_at);
});
View:
#foreach($thread->replies->paginate(15)->setPath($thread->path())->getUrlRange(ceil($thread->replies->count()
/ 15 - 2), ceil($thread->replies->count() / 15)) as $key => $pagination)
#if($key > 1)
{{ $key }}
#endif
#endforeach
So far I have alot of queries in the debug bar that looks like this:
select * from `replies` where `replies`.`thread_id` = ? and `replies`.`thread_id` is not null
I was wondering how I could reduce the number of queries.
When I code: Thread::with('replies')->sortByDesc->, it throws calls to undefined method. However when I code ->with('replies'), it throws an error that says:
Method Illuminate\Database\Eloquent\Collection::with does not exist.
Can anyone please help me?
Thank you.
Edit::
I've deduced the issue is in my method:
$unpinnedThreads = Thread::all()->sortByDesc(function($thread) {
$replies = $thread->replies->sortByDesc('created_at');
$lastTouchedPost = Carbon::minValue();
if (!empty($replies->toArray())) {
$lastTouchedPost = $lastTouchedPost->max($replies->first()->created_at);
}
return $lastTouchedPost->max($thread->created_at);
});
Does anyone know how I can optimize the method?
Answer found here: Laravel sorting last record
"One solution I would suggest is to have the replied touch the topics. https://laravel.com/docs/5.3/eloquent-relationships#touching-parent-timestamps
This way you can always order by the Topic's updated_at because whenever a reply is created/edited it will update the Topic as well.
To achieve this you would just need to add:
protected $touches = ['topic'];
The above is assuming that the method name for the topics relationship in the replies model is topic().
Hope this helps!"
That is really strange. It appears that you are calling with() in the model Thread::with('replies'), but the error is saying that you are calling it on a collection. The collection object does not have the with() method.
The bellow code should work:
$collection = Thread::with(['replies'])->get()->sortByDesc(function($thread)
{
if (count($thread->replies))
{
return $thread->replies->sortBy('created_at')->last()->created_at;
}
else
{
return $thread->created_at;
}
});
If you are not using the defaults, created_at must be configured:
protected $casts = [
'created_at' => 'date',
]
Don't forget to create the relationship inside your Thread model:
public function replies()
{
return $this->hasMany(Reply::class, 'thread_id', 'id');
}
Related
By the way, this is not really a question. I just want really to share how to validate using a specific database in multiple database connections. Since I got this error and it took almost 2 days and thank you to the members of the Codeigniter 4 community who help me how to solve it.
I would like to share it also here for future references if someone encounters this error. Shout out to kenjis for the help!
At first, I thought it was just a bug in the codeigniter4 that if I have a multiple database connections the validation rules only use the default DB group. But unfortunately, No. I just don't specify what DB group I should verify.
In case you encounter this error, just add this on your controller:
protected function validate($rules, array $messages = []): bool
{
$this->validator = \Config\Services::validation();
// If you replace the $rules array with the name of the group
if (is_string($rules)) {
$validation = config('Validation');
// If the rule wasn't found in the \Config\Validation, we
// should throw an exception so the developer can find it.
if (! isset($validation->{$rules})) {
throw ValidationException::forRuleNotFound($rules);
}
// If no error message is defined, use the error message in the Config\Validation file
if (! $messages) {
$errorName = $rules . '_errors';
$messages = $validation->{$errorName} ?? [];
}
$rules = $validation->{$rules};
}
return $this->validator->withRequest($this->request)->setRules($rules, $messages)->run(null, null, 'other-db-group-name');
}
reference: https://github.com/codeigniter4/CodeIgniter4/issues/5654
Is is possible to modify a request before it gets inserted into the database?
public function store(StoreRequest $request)
{
$request->date_posted = strtotime($request->date_posted);
//insert data here.
}
Yes it can be, what you have works perfectly fine
public function store(StoreRequest $request)
{
$request->date_posted = strtotime($request->date_posted);
or
$datePosted = $request->date_posted + 2;
$datePosted = $request->date_posted . 'some other addons';
//insert data here.
}
these are just examples but i hope you get what im saying.
Fyi if you input into the db, the created_at and updated_at will update and so you don't need to insert the time the post was made manually.
Try this solution maybe can resolve your issue . Add setter in model so it will be look like this
Class ModelX {
public function setDateDatePosted Attribute($date_posted)
{
$this->attributes['date_posted'] = strtotime($date_posted);
}
}
hope it useful for you .
I want to count the number of upvotes and downvotes from ScripRating for a certain Script.
Script.php:
public function ratings()
{
return $this->hasMany('ScriptRating');
}
ScriptRating.php:
public function script()
{
return $this->belongsTo('Script');
}
The script_rating database table:
id (primary, increments)
script_id(integer)
rating(integer) <-- Can be either 1 (upvote) or -1 (downvote)
To retrieve a script and display the ratings:
$script = Script::where('title', '=', $title)->get();
{{ $script->ratings }}
This works fine, it returns an array: [{"id":1,"script_id":1,"rating":1}]. But at this point I'm stuck. How could I count the total upvotes and downvotes for a certain script?
I also have one more small question what I'm finding confusing. This does the same as the code above:
$script = Script::where('title', '=', $title)->with('ratings')->get();
{{ $script->ratings }}
What is the difference between these two methods and which one should I use?
Thanks in advance!
Edit
I made three scopes:
public function scopeTotalRating($query, $scriptId) {
return $query->where('script_id', $scriptId)->get()->sum('rating');
}
public function scopeThumbsUp($query, $scriptId) {
return $query->where('script_id', $scriptId)->having('rating', '=', 1)->get()->sum('rating');
}
public function scopeThumbsDown($query, $scriptId) {
return $query->where('script_id', $scriptId)->having('rating', '=', -1)->get()->sum('rating');
}
And display them as following:
{{ ScriptRating::thumbsUp($script->id) }}
You can use
{{ $script->ratings->count() }}
This will display the number total ratings a script has.
However what you're interested in doing is grouping the ratings into upvotes and downvotes, so you'll need to query your relationship by a group by clause.
Script::where('title', '=', $title)->with([
'ratings' => function($query) {
$query->groupBy('rating');
})
])->get();
I believe the collection returned should be now grouped by 1 and -1. Let me know of the results!
EDIT: You can also take a look here at the documentation on querying relationships:
http://laravel.com/docs/4.2/eloquent#querying-relations
EDIT for response:
The simplest way to do this without using group by would be separate queries:
$script = Script::where('title', $title)->first();
if ($script) {
$upvotes = ScriptRating::where('script_id', $script->id)->having('rating', '>', 0)->get()->count();
$downvotes = ScriptRating::where('script_id', $script->id)->having('rating', '<', 0)->get()->count();
}
Also the difference between your scripts mentioned is called eager loading or lazy loading. When you specify ->with() in your query, this is called eager loading. If you don't do this, the query will be ran when you specify $script->ratings
More about eager/lazy loading here:
http://laravel.com/docs/4.2/eloquent#eager-loading
Edit for another response:
You would use the ->whereHas('ratings') function if you want to gather scripts that only have ratings. You can also check the existence of the script having ratings by doing an if statement:
if ($script->ratings->count() > 0) {
// The script has ratings
} else {
// The script does not have ratings
}
If you don't want to keep repeating this code you could always put a function inside your Script.php model by using the following:
public function hasRatings()
{
return $this->ratings->count() > 0;
}
Then you can do:
if ($script->hasRatings())
You can add to the Script model class those 2 functions:
public function ratingsSumRelation()
{
return $this->hasOne('ScriptRating')->selectRaw('script_id, sum(rating) as sum_all')
->groupBy('script_id');
}
public function getRatingSumAttribute()
{
return $this->ratingsSumRelation ?
$this->ratingsSumRelation->sum_all: 0;
}
and now display sum using:
{{ $script->rating_sum }}
I am not sure how to increment the value in a column using Eloquent Model in Laravel 4?
This is what I currently have and I am not sure how correct is this.
$visitor = Visitor::where('token','=','sometoken')->first();
if(isset($visitor)){
$visitor->increment('totalvisits');
}else{
Visitor::create(array(
'token'=>'sometoken',
'totalvisits'=>0
));
}
With Query Builder we could do it using
DB::table('visitors')->increment('totalvisits');
Looks like the code that I posted worked after all
$visitor = Visitor::where('token','=','sometoken')->first();
if(isset($visitor)){
$visitor->increment('totalvisits');
}else{
Visitor::create(array(
'token'=>'sometoken',
'totalvisits'=>0
));
}
Prior to a fix a few weeks ago the increment method actually fell through to the query builder and would be called on the entire table, which was undesirable.
Now calling increment or decrement on a model instance will perform the operation only on that model instance.
Laravel 5 now has atomic increment:
public function increment($column, $amount = 1, array $extra = [])
{
if (! is_numeric($amount)) {
throw new InvalidArgumentException('Non-numeric value passed to increment method.');
}
$wrapped = $this->grammar->wrap($column);
$columns = array_merge([$column => $this->raw("$wrapped + $amount")], $extra);
return $this->update($columns);
}
which essentially works like:
Customer::query()
->where('id', $customer_id)
->update([
'loyalty_points' => DB::raw('loyalty_points + 1')
]);
Below is old answer for Laravel 4 where the built-in increment was a seperate select and then update which of course leads to bugs with multiple users:
If you'd like to accurately count your visitors by ensuring the update is atomic then try putting this in your Visitor model:
public function incrementTotalVisits(){
// increment regardless of the current value in this model.
$this->where('id', $this->id)->update(['totalVisits' => DB::raw('last_insert_id(totalVisits + 1)')]);
//update this model incase we would like to use it.
$this->totalVisits = DB::getPdo()->lastInsertId();
//remove from dirty list to prevent any saves overwriting the newer database value.
$this->syncOriginalAttribute('totalVisits');
//return it because why not
return $this->totalVisits;
}
I'm using it for a change tag system but might work for your needs too.
Does anyone know what to replace the "$this->where('id',$this->id)" with because since dealing with $this Visitor it should be redundant.
i am new with codeigniter.
i have used the following code to execute query recursively.
Suppose $q query select 4 id (10,11,20,24)
then for each id showreply function (in foreach) call recursively then how can return the combine result.
$resultq3 = $this->showreply($reply_id);
<?php
public function showreply($reply_id)
{
$q1 =$this->db->select('*')
->from('forum_reply AS fr')
->where('fr.parent_id',$reply_id1)
->order_by('fr.id ')->get();;
foreach($q1->result_array() as $row4)
{
$id = $row4['id'];
$parent_id = $row4['parent_id'];
if($parent_id!=0)
{
$this->showreply($id);
}
}
return $result;
}
?>
I'm not really understanding what it is you're asking here. Maybe showing the showReply function would help, but you already have the combined result and are splitting that out in your foreach so what's the problem? Also why are you assigning reply_id to reply_id1? What is the point of that? Just use $reply_id in your query.
You're also executing an if statement that makes little sense since you can filter out the id's you don't want in the query itself (and are you seriously ever going to have an id that = 0?)
In fact the more I look at this code the more confused I become. Where is $id getting populated for $this->showreply($id)?
<?php
public function showreply($reply_id)
{
$q1 =$this->db->select('*')
->from('forum_reply AS fr')
->where('fr.parent_id',$reply_id)
->where('fr.parent_id !=',0)
->order_by('fr.id ')->get();;
//$i=0;
foreach($q1->result_array() as $row4)
{
$parent_id = $row4['parent_id'];
$this->showreply($id);
}
//below is the actual answer to your question on how to return the combined results.
return $q1->result_array();
}
?>
Okay after rereading your question I think I have a better understanding. If you pass the id's as an array like this:
$reply_id = array(10,11,20,24);
You can then modify your query to use:
$this->db->where_in('fr.parent_id', $reply_id);
That will return the results as one combined result with all 4 ids included.