Laravel: Historical ranking system with ManyToMany relationship - php

I would like to display a page where Users are listed by their Rank. A Rank can have multiple Users (for example, 2 Users can be at the 2nd place) and a User can have multiple Ranks too, because I would like to keep an historic of Users and Ranks through time.
Here's User.php
namespace App;
use Illuminate\Foundation\Auth\User as Authenticatable;
class User extends Authenticatable
{
// ...
public function Ranks() {
return $this->belongsToMany('App\Rank');
}
}
And Rank.php
namespace App;
use Illuminate\Database\Eloquent\Model;
class Rank extends Model
{
public $timestamps = false;
public function Users() {
return $this->belongsToMany('App\User')->withPivot('created_at');
}
}
I also have a rank_user table in my database, with id, rank_id, user_id and created_at fields.
This is how I record it in database:
$user->ranks()->attach($rank_id, ['created_at' => Carbon::now()->addMinutes(105)]);
In order to list users by rank, and I mean at the current (most recent) rank, this is what I have:
$ranks = Rank::with('users')->get();
For example, for the 1st rank, it gives me all users who have been at the 1st place since the beginning instead of giving me all users who currently are at the first place.
I don't know how to limit the query to the most recent date.

I asked one of my friends and here is the solution we found:
I first have to get the most recent date contained into the rank_user pivot table.
$date = DB::table('rank_user')->select('created_at')
->orderBy('created_at', 'desc')
->first();
Then, I can use withPivot() to specify that I want results only for the most recent date we found earlier:
$ranks = Rank::with(['users' => function($query) use ($date) {
$query->wherePivot('created_at', $date->created_at);
}])->get();
And voilĂ  ! In that way, I only get the most recent user(s) inside each rank.

Related

Searching collection records by relationship attributes

I have a game where people can get some items and equip them.
The items data is placed in two tables that are in relationship.
Items table contains all the possible items and user_items table contains the items that are owned by a player.
user_items table: id | user_id | item_id | is_equipped
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Item extends Model
{
use HasFactory;
public function userItems()
{
return $this->belongsTo(UserItem::class);
}
}
items table: id | item_name | body_part
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class UserItem extends Model
{
use HasFactory;
public function items()
{
return $this->hasOne(Item::class, 'id', 'item_id');
}
}
Now I am getting a collection of the user's items
$userItems = UserItem::where('user_id', Auth::id())->get(),
How do I search this collection by related table's columns? For example I want to get user $userItems where is_equipped == 1 and body_part == "head".
What you need is filter by the relation like this:
$userItems = UserItem::where('user_id', Auth::id())->whereHas('items', function($q)
{
$q->where('is_equipped', '=', 1);
})->get();
You can use the Eloquent's relationships to search the collection by related table's columns.
To get the user's items where is_equipped == 1 and body_part == "head", you can use the following code:
$userItems = UserItem::where('user_id', Auth::id())
->whereHas('items', function ($query) {
$query->where('is_equipped', 1)->where('body_part', 'head');
})->get();
This code first queries the user_items table for all items that belong to the user. Then, it uses the whereHas method to filter the results based on the related items table's columns. The closure passed to whereHas receives a $query variable that is a instance of Query Builder that you can use to filter the items table.
You could also use the join method to join the items table to the user_items table and then filter by the columns in the items table:
$userItems = UserItem::join('items', 'items.id', '=', 'user_items.item_id')
->where('user_items.user_id', Auth::id())
->where('items.is_equipped', 1)
->where('items.body_part', 'head')
->get();
This will give you a collection of user_items that are owned by the user and have is_equipped = 1 and body_part = 'head' in the items table.

how to get sub items using parent item's condition in laravel eloquent?

There are two tables.
tbl_orders
id, user_id, order_name, order_date
tbl_order_items
id, order_id, item_name, item_price
From these two tables, I need to get order items using a Laravel eloquent query, and I need to pull out the condition with a user_id of 1.
I want data using affiliation rather than DB queries or joins.
please help.
Thank you.
You need to have one to many relationship between orders and order_items as below in your Model classes as mentioned in the Laravel docs:
Order.php:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Order extends Model{
protected $table = 'orders';
public function order_items(){
return $this->hasMany(OrderItems::class);
}
}
OrderItems.php
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class OrderItems extends Model{
protected $table = 'order_items';
public function order(){
return $this->belongsTo(Order::class);
}
}
Controller code:
In your method, you can just do as:
$orders = Order::where('user_id',Auth::user()->id)->get(); // or any user ID you like
foreach($orders as $order){
foreach($order->order_items as $o_item){
// rest of the code goes here
}
}

Laravel 5.4 need to get distinct records through eloquent relation

I have a table "transactions". in that table I have multiple columns which are id, user_id, customer_name, restaurant_name and time-stamps also.
What I need is if I have two or three same records in the table means restaurant_name is repeating with the same user_id. I need to get only unique records. If user ordered from same restaurant 3 times I need to get only 1 from those.
Example:
If I order form Pizza Hut 3 time and ordered from Subway 5 times. The result should contain 1 pizza hut and 1 subway.
Note: 1 user may have many transactions
Transaction Model:
<?php
namespace App;
use App\restaurant;
use App\User;
use Illuminate\Database\Eloquent\Model;
class Transaction extends Model
{
public function user(){
return $this->belongsTo(User::class);
}
public function restaurant(){
return $this->belongsTo(restaurant::class);
}
protected $fillable = [
'user_id','customer_name', 'restaurant_name' , 'ordered_items' ,
];
}
User Model:
<?php
namespace App;
use App\restaurant;
use App\User;
use Illuminate\Database\Eloquent\Model;
class Transaction extends Model
{
public function user(){
return $this->belongsTo(User::class);
}
public function restaurant(){
return $this->belongsTo(restaurant::class);
}
protected $fillable = [
'user_id','customer_name', 'restaurant_name' , 'ordered_items' ,
];
}
I am trying to getting desired results like this but It is showing me an error:
BadMethodCallException in Macroable.php line 74:
Method distinct does not exist.
$user->transactions->distinct("restaurant_name");
distinct is not an existing function for Laravel collections, but unique is.
$user->transactions->unique("restaurant_name");
However that will query all transactions and filter in code. To get the distinct rows using a query, you could do the following:
$user->transactions()->groupBy('restaurant_name')->get();

Laravel distinct result

I'm building a PM system, and I have a problem.
This is my PM table:
id, user_id, to, content
Now, in my inbox page I'm fetching all the users that sent me a message.
$pms = DB::table('pm')->select('user_id')->distinct()->where('to', Auth::id())->get();
The problem is if I add more columns to the select method, it won`t be distinct anymore..
You can easily do that using Eloquent and its whereHas() method.
First, define models and the relation in your model:
class Message extends Model {
protected $table = 'pm';
}
class User extends Model {
public function sent_messages() {
return $this->hasMany(Message::class);
}
}
Now, fetch all users that have a related Message models where to column matches your ID:
$usersThatSentMeMessages = User::whereHas('sent_messages', function($query) {
$query->where('to', Auth::id());
});

Laravel eager loading with limit

I have two tables, say "users" and "users_actions", where "users_actions" has an hasMany relation with users:
users
id | name | surname | email...
actions
id | id_action | id_user | log | created_at
Model Users.php
class Users {
public function action()
{
return $this->hasMany('Action', 'user_id')->orderBy('created_at', 'desc');
}
}
Now, I want to retrieve a list of all users with their LAST action.
I saw that doing Users::with('action')->get();
can easily give me the last action by simply fetching only the first result of the relation:
foreach ($users as $user) {
echo $user->action[0]->description;
}
but I wanted to avoid this of course, and just pick ONLY THE LAST action for EACH user.
I tried using a constraint, like
Users::with(['action' => function ($query) {
$query->orderBy('created_at', 'desc')
->limit(1);
}])
->get();
but that gives me an incorrect result since Laravel executes this query:
SELECT * FROM users_actions WHERE user_id IN (1,2,3,4,5)
ORDER BY created_at
LIMIT 1
which is of course wrong. Is there any possibility to get this without executing a query for each record using Eloquent?
Am I making some obvious mistake I'm not seeing? I'm quite new to using Eloquent and sometimes relationship troubles me.
Edit:
A part from the representational purpose, I also need this feature for searching inside a relation, say for example I want to search users where LAST ACTION = 'something'
I tried using
$actions->whereHas('action', function($query) {
$query->where('id_action', 1);
});
but this gives me ALL the users which had had an action = 1, and since it's a log everyone passed that step.
Edit 2:
Thanks to #berkayk looks like I solved the first part of my problem, but still I can't search within the relation.
Actions::whereHas('latestAction', function($query) {
$query->where('id_action', 1);
});
still doesn't perform the right query, it generates something like:
select * from `users` where
(select count(*)
from `users_action`
where `users_action`.`user_id` = `users`.`id`
and `id_action` in ('1')
) >= 1
order by `created_at` desc
I need to get the record where the latest action is 1
I think the solution you are asking for is explained here http://softonsofa.com/tweaking-eloquent-relations-how-to-get-latest-related-model/
Define this relation in User model,
public function latestAction()
{
return $this->hasOne('Action')->latest();
}
And get the results with
User::with('latestAction')->get();
I created a package for this: https://github.com/staudenmeir/eloquent-eager-limit
Use the HasEagerLimit trait in both the parent and the related model.
class User extends Model {
use \Staudenmeir\EloquentEagerLimit\HasEagerLimit;
}
class Action extends Model {
use \Staudenmeir\EloquentEagerLimit\HasEagerLimit;
}
Then simply chain ->limit(1) call in your eager-load query (which seems you already do), and you will get the latest action per user.
My solution linked by #berbayk is cool if you want to easily get latest hasMany related model.
However, it couldn't solve the other part of what you're asking for, since querying this relation with where clause would result in pretty much the same what you already experienced - all rows would be returned, only latest wouldn't be latest in fact (but latest matching the where constraint).
So here you go:
the easy way - get all and filter collection:
User::has('actions')->with('latestAction')->get()->filter(function ($user) {
return $user->latestAction->id_action == 1;
});
or the hard way - do it in sql (assuming MySQL):
User::whereHas('actions', function ($q) {
// where id = (..subquery..)
$q->where('id', function ($q) {
$q->from('actions as sub')
->selectRaw('max(id)')
->whereRaw('actions.user_id = sub.user_id');
})->where('id_action', 1);
})->with('latestAction')->get();
Choose one of these solutions by comparing performance - the first will return all rows and filter possibly big collection.
The latter will run subquery (whereHas) with nested subquery (where('id', function () {..}), so both ways might be potentially slow on big table.
Let change a bit the #berkayk's code.
Define this relation in Users model,
public function latestAction()
{
return $this->hasOne('Action')->latest();
}
And
Users::with(['latestAction' => function ($query) {
$query->where('id_action', 1);
}])->get();
To load latest related data for each user you could get it using self join approach on actions table something like
select u.*, a.*
from users u
join actions a on u.id = a.user_id
left join actions a1 on a.user_id = a1.user_id
and a.created_at < a1.created_at
where a1.user_id is null
a.id_action = 1 // id_action filter on related latest record
To do it via query builder way you can write it as
DB::table('users as u')
->select('u.*', 'a.*')
->join('actions as a', 'u.id', '=', 'a.user_id')
->leftJoin('actions as a1', function ($join) {
$join->on('a.user_id', '=', 'a1.user_id')
->whereRaw(DB::raw('a.created_at < a1.created_at'));
})
->whereNull('a1.user_id')
->where('aid_action', 1) // id_action filter on related latest record
->get();
To eager to the latest relation for a user you can define it as a hasOne relation on your model like
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\DB;
class User extends Model
{
public function latest_action()
{
return $this->hasOne(\App\Models\Action::class, 'user_id')
->leftJoin('actions as a1', function ($join) {
$join->on('actions.user_id', '=', 'a1.user_id')
->whereRaw(DB::raw('actions.created_at < a1.created_at'));
})->whereNull('a1.user_id')
->select('actions.*');
}
}
There is no need for dependent sub query just apply regular filter inside whereHas
User::with('latest_action')
->whereHas('latest_action', function ($query) {
$query->where('id_action', 1);
})
->get();
Migrating Raw SQL to Eloquent
Laravel Eloquent select all rows with max created_at
Laravel - Get the last entry of each UID type
Laravel Eloquent group by most recent record
Laravel Uses take() function not Limit
Try the below Code i hope it's working fine for u
Users::with(['action' => function ($query) {
$query->orderBy('created_at', 'desc')->take(1);
}])->get();
or simply add a take method to your relationship like below
return $this->hasMany('Action', 'user_id')->orderBy('created_at', 'desc')->take(1);

Categories