Laravel 5 Eloquent - adding calculated value as column to collection - laravel

I am calculating the distance of a teacher from a school.
I am querying the user table and then the userable_type teacher using whereHas(). The teacher table has the longitude and latitude values.
The following worked for me elsewhere when I query the teacher model directly but not when querying through the user model first!
$query = User::where('userable_type', 'App\Teacher');
$query->with('userable');
if($school->latitude == '' || $school->longitude == '') {
\Session::flash('warning', 'Please check your school profile postcode; it appears that the system was unable to geocode your postcode! Results shown do not reflect any distances of staff from your school!');
} else {
$query->whereHas('teacher', function ($query) use ($school) {
$haversine = "(3961 * acos(cos(radians($school->latitude))
* cos(radians(latitude))
* cos(radians(longitude)
- radians($school->longitude))
+ sin(radians($school->latitude))
* sin(radians(latitude))))";
$query->select() //pick the columns you want here.
->selectRaw("{$haversine} AS distance")
->whereRaw("{$haversine} < ?", '20');
// ->havingRaw('{$haversine}', '<', '20');
});
}
$query->whereDoesntHave('thisSchool');
$teachers = $query->get();
The 'distance' field is not created but no errors are thrown! So my question is, in this scenario, how do I add the 'distance' value to each user as I would like to order by the distance?
This is the query where the Teacher model is queried directly, and it works!
$query = Teacher::with('user', 'user.schools', 'criterias', 'criterias.stafftype', 'criterias.teachingstage', 'criterias.teachingsubject')
->where('public', 1)
->where('verified', 1);
if($school->latitude == '' || $school->longitude == '') {
\Session::flash('warning', 'Please check your school profile postcode; it appears that the system was unable to geocode your postcode! Results shown do not reflect any distances of staff from your school!');
} else {
if( $filters['distance'] != '' ) {
$haversine = "(3961 * acos(cos(radians($school->latitude))
* cos(radians(latitude))
* cos(radians(longitude)
- radians($school->longitude))
+ sin(radians($school->latitude))
* sin(radians(latitude))))";
$query->select() //pick the columns you want here.
->selectRaw("{$haversine} AS distance")
->whereRaw("{$haversine} < ?", [$filters['distance']]);
}
}
$query->orderBy( 'active', 'DESC' );
$query->orderBy( 'distance', 'ASC' );
$teachers = $query->paginate( $filters['perpage'] );
Thanks,
K...

OK, I found a work-around for this problem and it can be found here:
Laravel 5 eloquent - add child field to parent model

Related

Laravel order by eagerly loaded column

I am using laravel eager loading to load data on the jquery datatables. My code looks like:
$columns = array(
0 => 'company_name',
1 => 'property_name',
2 => 'amenity_review',
3 => 'pricing_review',
4 => 'sqft_offset_review',
5 => 'created_at',
6 => 'last_uploaded_at'
);
$totalData = Property::count();
$limit = $request->input('length');
$start = $request->input('start');
$order = $columns[$request->input('order.0.column')];
$dir = $request->input('order.0.dir');
$query = Property::with(['company','notices']);
$company_search = $request->columns[0]['search']['value'];
if(!empty($company_search)){
$query->whereHas('company', function ($query) use($company_search) {
$query->where('name','like',$company_search.'%');
});
}
$property_search = $request->columns[1]['search']['value'];
if(!empty($property_search)){
$query->where('properties.property_name','like',$property_search.'%');
}
if(!Auth::user()->hasRole('superAdmin')) {
$query->where('company_id',Auth::user()->company_id);
}
$query->orderBy($order,$dir);
if($limit != '-1'){
$records = $query->offset($start)->limit($limit);
}
$records = $query->get();
With this method I received error: Column not found: 1054 Unknown column 'company_name' in 'order clause' .
Next, I tried with following order condition:
if($order == 'company_name'){
$query->orderBy('company.name',$dir);
}else{
$query->orderBy($order,$dir);
}
However, it also returns similar error: Column not found: 1054 Unknown column 'company.name' in 'order clause'
Next, I tried with whereHas condition:
if($order == 'company_name'){
$order = 'name';
$query->whereHas('company', function ($query) use($order,$dir) {
$query->orderBy($order,$dir);
});
}else{
$query->orderBy($order,$dir);
}
But, in this case also, same issue.
For other table, I have handled this type of situation using DB query, however, in this particular case I need the notices as the nested results because I have looped it on the frontend. So, I need to go through eloquent.
Also, I have seen other's answer where people have suggested to order directly in model like:
public function company()
{
return $this->belongsTo('App\Models\Company')->orderBy('name');
}
But, I don't want to order direclty on model because I don't want it to be ordered by name everytime. I want to leave it to default.
Also, on some other scenario, I saw people using join combining with, but I am not really impressed with using both join and with to load the same model.
What is the best way to solve my problem?
I have table like: companies: id, name, properties: id, property_name, company_id, notices: title, slug, body, property_id
The issue here is that the Property::with(['company','notices']); will not join the companies or notices tables, but only fetch the data and attach it to the resulting Collection. Therefore, neither of the tables are part of the SQL query issued and so you cannot order it by any field in those tables.
What Property::with(['company', 'notices'])->get() does is basically issue three queries (depending on your relation setup and scopes, it might be different queries):
SELECT * FROM properties ...
SELECT * FROM companies WHERE properties.id in (...)
SELECT * FROM notices WHERE properties.id in (...)
What you tried in the sample code above is to add an ORDER BY company_name or later an ORDER BY companies.name to the first query. The query scope knows no company_name column within the properties table of course and no companies table to look for the name column. company.name will not work either because there is no company table, and even if there was one, it would not have been joined in the first query either.
The best solution for you from my point of view would be to sort the result Collection instead of ordering via SQL by replacing $records = $query->get(); with $records = $query->get()->sortBy($order, $dir);, which is the most flexible way for your task.
For that to work, you would have to replace 'company_name' with 'company.name' in your $columns array.
The only other option I see is to ->join('companies', 'companies.id', 'properties.company_id'), which will join the companies table to the first query.
Putting it all together
So, given that the rest of your code works as it should, this should do it:
$columns = [
'company.name',
'property_name',
'amenity_review',
'pricing_review',
'sqft_offset_review',
'created_at',
'last_uploaded_at',
];
$totalData = Property::count();
$limit = $request->input('length');
$start = $request->input('start');
$order = $columns[$request->input('order.0.column')];
$dir = $request->input('order.0.dir');
$query = Property::with(['company', 'notices']);
$company_search = $request->columns[0]['search']['value'];
$property_search = $request->columns[1]['search']['value'];
if (!empty($company_search)) {
$query->whereHas(
'company', function ($query) use ($company_search) {
$query->where('name', 'like', $company_search . '%');
});
}
if (!empty($property_search)) {
$query->where('properties.property_name', 'like', $property_search . '%');
}
if (!Auth::user()->hasRole('superAdmin')) {
$query->where('company_id', Auth::user()->company_id);
}
if ($limit != '-1') {
$records = $query->offset($start)->limit($limit);
}
$records = $query->get()->sortBy($order, $dir);

How to put the condition on User table with condition on subject table

Need to find the record with either first_name of the User table equal the keyword or subject of the TeacherSubject matches the keyword but 'step_completed' should equal '5' and 'is_approved' should equal '1' from the User table even if the orwhereHas condition is true.
$condition_array['radius'] = 25;
$condition_array['lat'] = $request->input('lat');
$condition_array['lng'] = $request->input('lng');
$condition_array['keyword'] = $request->input('keyword');
$sorting_type = $request->input('sorting_type');
$condition_array['filter'] = $request->input('filter');
if($sorting_type != ""){
if($sorting_type == 0){
$order_column = 'first_name';
$order_order = 'DESC';
}
if($sorting_type == 1){
$order_column = 'first_name';
$order_order = 'ASC';
}
}else{
$order_column = 'id';
$order_order = 'ASC';
}
$teachers = User::select('id','email','first_name','last_name','profile_picture','country_code','mobile')->with('teacherSubject')
->where(function($query) use($condition_array){
$query->where(['step_completed'=>5,'is_approved'=>1]);
$query->where(['first_name'=>$condition_array['keyword']]);
$query->orWhereHas('teacherSubject', function ($query1) use($condition_array){
$query1->where('subject',$condition_array['keyword']);
});
})
->orderBy($order_column,$order_order)
->get();
Need to find the record with either first_name of the User table equal the keyword or subject of the TeacherSubject matches the keyword but 'step_completed' should equal '5' and 'is_approved' should equal '1' from the User table even if the orwhereHas condition is true.
So if I would translate your statement into a query, I think it should look like this:
$teachers = User
::select('id','email','first_name','last_name','profile_picture','country_code','mobile')
->with('teacherSubject')
->where(['first_name'=>$condition_array['keyword']])
->orWhere(function($query) use($condition_array) {
$query->whereHas('teacherSubject', function($teacherQuery) use($condition_array) {
$teacherQuery->where('subject',$condition_array['keyword']);
})
->where(['step_completed'=>5,'is_approved'=>1]);
})
->orderBy($order_column,$order_order)
->get();
This way you have 2 conditions:
users.first_name == keyword OR
teacher_subject_user_id == users.id AND teacher_subject.subject = keyword AND users.step_completed == 5 AND is_approved == 1
If any of both is true, it will show up as a result.
Please let me know if it works :)

Laravel Pivot Table, Get Room belonging to users

The structure of my pivot table is
room_id - user_id
I have 2 users that exist in the same room.
How can I get the rooms they both have in common?
It would be nice to create a static class to have something like this.
Room::commonToUsers([1, 5]);
Potentially I could check more users so the logic must not restrict to a certain number of users.
Room::commonToUsers([1, 5, 6, 33, ...]);
I created a Laravel project and make users, 'rooms', 'room_users' tables and their models
and defined a static function in RoomUser Model as below :
public static function commonToUsers($ids)
{
$sql = 'SELECT room_id FROM room_users WHERE user_id IN (' . implode(',', $ids) . ') GROUP BY room_id HAVING COUNT(*) = ' . count($ids);
$roomsIds = DB::select($sql);
$roomsIds = array_map(function ($item){
return $item->room_id;
}, $roomsIds);
return Room::whereIn('id', $roomsIds)->get();
}
in this method, I use self join that the table is joined with itself, A and B are different table aliases for the same table, then I applied the where condition between these two tables (A and B) and work for me.
I hope be useful.
I don't know the names of your relations, but I guess you can do like this :
$usersIds = [1, 5];
$rooms = Room::whereHas('users', function($query) use ($usersIds) {
foreach ($usersIds as $userId) {
$query->where('users.id', $userId);
}
})->get();
It should work. whereHas allows you to query your relation. If you need to have a static method, you can add a method in your model.
There might be a more efficient way but laravel collection does have an intersect method. You could create a static function that retrieves and loop through each object and only retain all intersecting rooms. something like this
public static function commonToUsers($userArr){
$users = User::whereIn('id',$userArr)->get();
$rooms = null;
foreach($users as $user){
if($rooms === null){
$rooms = $user->rooms;
}else{
$rooms = $rooms->intersect($user->rooms);
}
}
return $rooms;
}
This code is untested but it should work.
Room has many users, user has many rooms, so you can find the room which have those two users.
If your pivot table's name is room_users, then you can easily get the common room like this:
public static function commonToUsers($user_ids) {
$room = new Room();
foreach($user_ids as $user_id) {
$room->whereHas('users', function($query) use ($user_id) {
$query->where('room_users.user_id', $user_id);
});
}
return $room->get();
}
This code will convert to raw sql:
select *
from `rooms`
where exists (
select * from `rooms` inner join `room_users` on `rooms`.`id` = `room_users`.`room_id` where `rooms`.`id` = `room_users`.`room_id` and `room_users`.`user_id` = 1
)
and exists
(
select * from `rooms` inner join `room_users` on `rooms`.`id` = `room_users`.`room_id` where `rooms`.`id` = `room_users`.`room_id` and `room_users`.`user_id` = 5
)

how to return a model that have specific tags

I have a model that can have more then one tag, relationship is defined like this:
public function tags()
{
return $this->belongsToMany(ListTag::class, 'listing_listtag', 'listing_id', 'tag_id');
}
and inside tag model I have:
public function listings()
{
return $this->belongsToMany(Listing::class, 'listing_listtag');
}
I am making a filter where user can select some tags and I should return only models that have all of these tags assigned.
For example if I select "tag_1" this will return all models with that tag assigned to them. Problem I have is if user selects "tag_1" and "tag_2" I need to return only models that have both of those assigned and not models that have only one of them assigned.
Currently I have this:
$listings = Listing::closest($lat, $lng, $radius)
->orderBy('distance');
if($request->tags){
$listings->whereHas('tags', function($query) use($filter_tags) {
foreach ($filter_tags as $filter_tag) {
$query->where('slug', $filter_tag);
}
});
$append += array('tags' => $request->tags);
}
But that returns 0, even though I selected two tags that one of the models has assigned to it so atleast that models should have been returned.
Does the code look ok?
This query is produced:
"query" => """
select count(*) as aggregate from `listings` where (6371 * acos(cos(radians(55.6181338)) \n
* cos(radians(lat)) \n
* cos(radians(lng) \n
- radians(13.0274283)) \n
+ sin(radians(55.6181338)) \n
* sin(radians(lat)))) < ? and exists (select * from `listtags` inner join `listing_listtag` on `listtags`.`id` = `listing_listtag`.`tag_id` where `listing_listtag`.`listing_id` = `listings`.`id` and `slug` = ? and `slug` = ?)
"""
"bindings" => array:3 [▼
0 => "5"
1 => "wireless-internet"
2 => "delivery"
]
Why don't you query the inverse:
a tag with a where clause (e.g. $tags = Tag::query()->where('name, 'bla');
define the hasOne relation on tag to Listing? (or other model, you aren't really clear if this is the model we're talking about)
get it by looping the tags and calling the relation

Eloquent eager load Order by

I have problem with eloquent query. I am using eager loading (one to one Relationship) to get 'student' With the 'exam', Using the code below.
Student::with('exam')->orderBy('exam.result', 'DESC')->get()
And i want to order received rows by the 'result' column in 'exam'. I am using
->orderBy('exam.result', 'DESC')
But it is not working. Any ideas how to do it ?
Try this:
Student::with(array('exam' => function($query) {
$query->orderBy('result', 'DESC');
}))
->get();
If you need to order your students collection by the result column, you will need to join the tables.
Student::with('exam')
->join('exam', 'students.id', '=', 'exam.student_id')
->orderBy('exam.result', 'DESC')
->get()
In this case, assuming you have a column student_id and your exams table are named exam.
If you ALWAYS want it sorted by exam result, you can add the sortBy call directly in the relationship function on the model.
public function exam() {
return this->hasMany(Exam::class)->orderBy('result');
}
(credit for this answer goes to pfriendly - he answered it here: How to sort an Eloquent subquery)
tl;dr
Student::with('exam')->get()->sortByDesc('exam.result');
This will sort the results of the query after eager loading using collection methods and not by a MySQL ORDER BY.
Explanation
When you eager load you can't use an ORDER BY on the loaded relations because those will be requested and assembled as a result of a second query. As you can see it in the Laravel documentation eager loading happens in 2 query.
If you want to use MySQL's ORDER BY you have to join the related tables.
As a workaround, you can run your query and sort the resulting collection with sortBy, sortByDesc or even sort. This solution has advantages and disadvantages over the join solution:
Advantages:
You keep Eloquent functionality.
Shorter and more intuitive code.
Disadvantages:
Sorting will be done by PHP instead of the database engine.
You can sort only by a single column, unless you provide a custom closure for the sorter functions.
If you need only a part of the ordered results of a query (e.g. ORDER BY with LIMIT), you have to fetch everything, order it, then filter the ordered result, otherwise you will end up with only the filtered part being ordered (ordering will not consider the filtered out elements). So this solution is only acceptable when you would work on the whole data set anyway or the overhead is not a problem.
This worked for me:
$query = Student::select(['id','name']);
$query->has('exam')->with(['exam' => function ($query) {
return $query->orderBy('result','ASC');
}]);
return $query->get();
You could use \Illuminate\Database\Eloquent\Relations\Relation and query scopes to add far column through relationship, I wrote a traits for this, it misses HasOne o HasMany but having BelongsTo and BelongsToMany could easily adapted
Also the method could be enhanced to support more than depth 1 for multiple chained relationship, I made room for that
<?php
/**
* User: matteo.orefice
* Date: 16/05/2017
* Time: 10:54
*/
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Support\Facades\DB;
use Illuminate\Database\Eloquent\Builder;
trait WithFarColumnsTrait
{
public function scopeWithFarColumns(Builder $query , $relationPath , $columns , $tableAliasPrefix = null)
{
$relationPath = array_wrap($relationPath);
$tableAliasPrefix = $tableAliasPrefix ?: WithFarColumnsTrait::randomStringAlpha(3);
$currentModel = $this;
$subQueries = [];
$relationIndex = 0;
foreach ($relationPath as $relationName) {
if (method_exists($currentModel , $relationName)) {
$relation = $currentModel->$relationName();
} else {
throw new BadMethodCallException("Relationship $relationName does not exist, cannot join.");
}
$currentTable = $currentModel->getTable();
if ($relationIndex == 0) {
$query->addSelect($currentTable . '.*');
}
$relatedModel = $relation->getRelated();
/**
* #var string
*/
$relatedTable = $relatedModel->getTable();
if ($relation instanceof BelongsTo) {
foreach ($columns as $alias => $column) {
$tableAlias = $tableAliasPrefix . $relationIndex;
$tableAndAlias = $relatedTable . ' AS ' . $tableAlias;
/**
* Al momento gestisce soltanto la prima relazione
* todo: navigare le far relationships e creare delle join composte
*/
if (!isset($subQueries[$alias])) {
$subQueries[$alias] = $currentQuery = DB::query()
->from($tableAndAlias)
->whereColumn(
$relation->getQualifiedForeignKey() , // 'child-table.fk-column'
'=' ,
$tableAlias . '.' . $relation->getOwnerKey() // 'parent-table.id-column'
)
->select($tableAlias . '.' . $column);
// se la colonna ha una chiave stringa e' un alias
/**
* todo: in caso di relazioni multiple aggiungere solo per la piu lontana
*/
if (is_string($alias)) {
$query->selectSub($currentQuery , $alias);
} else {
throw new \InvalidArgumentException('Columns must be an associative array');
}
}
else {
throw new \Exception('Multiple relation chain not implemented yet');
}
} // end foreach <COLUMNs>
} // endif
else if ($relation instanceof BelongsToMany) {
foreach ($columns as $alias => $column) {
$tableAlias = $tableAliasPrefix . $relationIndex;
$tableAndAlias = $relatedTable . ' AS ' . $tableAlias;
if (!isset($subQueries[$alias])) {
$pivotTable = $relation->getTable();
$subQueries[$alias] = $currentQuery = DB::query()
->from($tableAndAlias)
->select($tableAlias . '.' . $column)
// final table vs pivot table
->join(
$pivotTable , // tabelle pivot
$relation->getQualifiedRelatedKeyName() , // pivot.fk_related_id
'=' ,
$tableAlias . '.' . $relatedModel->getKeyName() // related_with_alias.id
)
->whereColumn(
$relation->getQualifiedForeignKeyName() ,
'=' ,
$relation->getParent()->getQualifiedKeyName()
);
if (is_string($alias)) {
$query->selectSub($currentQuery , $alias);
} else {
throw new \InvalidArgumentException('Columns must be an associative array');
}
}
else {
throw new \Exception('Multiple relation chain not implemented yet');
}
} // end foreach <COLUMNs>
} else {
throw new \InvalidArgumentException(
sprintf("Relation $relationName of type %s is not supported" , get_class($relation))
);
}
$currentModel = $relatedModel;
$relationIndex++;
} // end foreach <RELATIONs>
}
/**
* #param $length
* #return string
*/
public static function randomStringAlpha($length) {
$pool = array_merge(range('a', 'z'),range('A', 'Z'));
$key = '';
for($i=0; $i < $length; $i++) {
$key .= $pool[mt_rand(0, count($pool) - 1)];
}
return $key;
}
}
There is an alternative way of achieving the result you want to have without using joins. You can do the following to sort the students based on their exam's result. (Laravel 5.1):
$students = Student::with('exam')->get();
$students = $students->sortByDesc(function ($student, $key)
{
return $student->exam->result;
});

Resources