multiple "has many through" with eloquent of laravel - php

I came accros a problem with laravel's ORM, eloquent and found no solution yet.
I have some tables as follows
Team
- id
- name
User
- id
- name
- role
- team_id
Student_Info
- id
- user_id
- data1
- data2
- etc ...
Project
- id
- student_id
- name
Now, I want to query all projects a certain team, where team = 'some team'
Now the thing here is, without an ORM, it's simple, I would have done multiple joins in raw SQL.
However, because all these tables have a common column "name" I will have to alias all this stuff, which is really boring
With eloquent I can't find a way to do this query using "has many through" because it only allows on intermediate and I can't do a raw SQL as the alis thing is really a pain in the ass and as it would be very difficult to map the result to laravel's Models

There is no native relationship for this case.
I created a HasManyThrough relationship with unlimited levels: Repository on GitHub
After the installation, you can use it like this:
class Team extends Model {
use \Staudenmeir\EloquentHasManyDeep\HasRelationships;
public function projects() {
return $this->hasManyDeep(Project::class, [User::class, StudentInfo::class],
[null, null, 'student_id']);
}
}
$projects = Team::where('name', 'some team')->first()->projects;

Try with relationship existence. This assumes you have all relationships properly defined
$projects = Project::whereHas('students.user.team', function ($query) {
$query->where('name', '=', 'some team');
})->get();
That's 3 levels of nesting. Never tested. However, if you already define a Project-User relationship via hasManyThrough() you can shorten it to 2 levels only.
$projects = Project::whereHas('user.team', function ($query) {
$query->where('name', '=', 'some team');
})->get();
Those will give you the data for projects only. If you also want the the intermediate data, use eager loading instead with with(). Just replace whereHas() by with().
$projects = Project::with('user.team', function ($query) {
$query->where('name', '=', 'some team');
})->get();

Related

laravel elequent way to find single row from table

hello I am new to laravel and maybe I am a bit confused between eloquent and query builder way for writing a query but anyway can you please tell me what could be the best eloquent way to retrieve info like this in laravel 6 or 7
User > hasMany > Recipes
Recipe > belongsTo > User
I want to check if user id 2 present in users table then get only one post which id is 3
Query builder is for explicitly building SQL queries, and does not return instances of your models. Eloquent query builder, is similar but the result will contain the model(s) loaded with all their attributes, and has some handy functions for querying the relations you define in your models.
Given the limited information in your post, I am assuming when you say a post, you mean a recipe:
Query Builder:
DB::table('users')
->join('recipes', 'recipes.user_id', '=', 'users.id')
->select(['users.some_col', ... 'recipes.some_col'])
->where('users.id', 2)
->get();
If you have your models setup with the relations. You can use Eloquent like so:
User::where('id', 2)->with('recipes')->get();
If I understand you correctly it would be like this:
User::whereId($userId) //asuming it is 2
->with(['recipes' => function($q) use($recipeId) {
$q->where('id', $recipeId); //assuming it is 3
}])->first();
you can do this if i understand correctly:
$user = User::findOrFail(2); //auto 404 if user not found
$recipe = $user->Recipes()->where('id',3)->first();
You may use conditional eager loading for better performance.
$userId = 2;
$receiptId = 3;
$user = User::with(['receipts'=> function ($query) use($receiptId){
$query->where('id', $receiptId);
}
])->find($userId)

loading relationship with groupBy laravel

I have a query which selects some relationships and groups by the accommodations to remove the duplicate accommodations. But now when I want to load the discount realtion on rooms, it doesn't work because I select only the accommodation_id.
Here is my code:
$data = AccommodationRoom::with('accommodation.city', 'accommodation.accommodationFacilities', 'accommodation.gallery','discount')
->select('accommodation_id')
->whereHas('roomCapacityHistory', function ($query) use ($from_date, $to_date) {
$query->whereDate('from_date', '<=', $from_date);
$query->whereDate('to_date', '>=', $to_date);
})
->whereHas('accommodation', function ($query) use ($searchCity) {
$query->where('city_id', $searchCity);
})
->groupBy('accommodation_id')
->get();
Now if I add the id to the select it would be fine, but my groupBy doesn't work and gives me an error in this case. So I need a solution to get all my accomodations with the listed relations.
As you are looking for all your accommodations with some related models, you should actually select from your Accommodation model. This will work out of the box for the first three relations but will require some tweaking for the discount relation. The simpliest solution is to create a HasManyThrough relation on the Accommodation model:
class Accommodation extends Model
{
public function discounts()
{
return $this->hasManyThrough(Discount::class, AccommodationRoom::class);
}
}
Note: this expects your models to use foreign key columns named by convention; for different names you will need to pass the custom foreign key names as additional parameters according to the documentation.
With this relation set up, you can then use a query like the following:
$data = Accommodation::with('city', 'accommodationFacilities', 'gallery', 'discounts')
->whereHas('accommodationRooms.roomCapacityHistory', function ($query) use ($from_date, $to_date) {
$query->whereDate('from_date', '<=', $from_date);
$query->whereDate('to_date', '>=', $to_date);
})
->where('city_id', $searchCity)
->get();
Further explanation as asked for in the comments:
HasManyThrough builds a virtual relation between two models using a third model in between. Imagine you have Post, Comment and Like as models. One Post can have many Comments and one Comment can have many Likes:
has many has many
Post -----------------> Comment -----------------> Like
1 n 1 n
In this case we also know that one Post can have many Likes. And this is exactly the knowledge we utilize when using HasManyThrough. We simply tell Eloquent that a Post has many Likes, connected by the Posts in between:
has many has many
Post -----------------> Comment -----------------> Like
1 n 1 n
has many through Comment
Post ------------------------------------------------> Like
1 n

Laravel eloquent multiple table join with filter

There are theree tables in my system.
Students
Articles
categories
Student can write many articles and a article is belong to just one student. And A Article can have only one category.
Controller
public function all_articles_by_student_by_category(Request $request){
$students_id = $request->students_id;
$categories_id = $request->categories_id;
$article_list = Students::find($students_id)->articles->all();
//This return Something like, Select All Articles Written by Damith
}
Model
class Students extends Model
{
protected $fillable = ['id','first_name', 'last_name', 'age', 'created_at', 'updated_at'];
public function articles()
{
return $this->hasMany('App\Articles');
}
}
What I am try to get
Something like, Select All Articles Written by Damith for Technology Category (Category Name should be there)
What I able to do so far
Something like, Select All Articles Written by Damith using $article_list = Students::find($students_id)->articles->all(); (You can find this code from controller)
What I want from you
How do I modify $article_list = Students::find($students_id)->articles->all(); to get, something like, Select All Articles Written by Damith for Technology Category. (Category name must be there in result and it is on category table, and for where condtion you can use the category_id which is i the article table )
First off with what you have done so far the ->all() method is not needed when getting the records for a relation on a model, this would return all of the articles linked to that student:
Students::find($students_id)->articles
Go through Articles Model
You could do something like:
Article::where('student_id', $students_id)
->where('category_id', $category_id)->get();
Which would acheive the result you are after.
Go through Students Model
If you want to go through Students Model you can constrain the relation using the with method.
$student = Students::with(['articles' => function($query) use ($category_id) {
$query->where('category_id', $category_id);
}])->find($student_id);
$filteredArticles = $student->articles
Useful Links
Laravel Docs 5.5 for Eager Loading : https://laravel.com/docs/5.5/eloquent-relationships#eager-loading
When accessing Eloquent relationships as properties, the relationship data is "lazy loaded". This means the relationship data is not actually loaded until you first access the property. However, Eloquent can "eager load" relationships at the time you query the parent model.
Laravel Docs 5.5 for Constraining Eager Loads: https://laravel.com/docs/5.5/eloquent-relationships#constraining-eager-loads
Sometimes you may wish to eager load a relationship, but also specify additional query constraints for the eager loading query.
Something like this should work:
$technologyArticles = Articles::where('student_id', '=', $students_id)->where('category_id', '=', $categories_id)->get();

Laravel/Eloquent - Order By a Model's Relationship Without Using Table Names

I have a Person eloquent model that belongsTo an Address. My Laravel version is 4.2.5 and I am using PostgreSQL.
class Person extends Eloquent {
public function address() {
return $this->belongsTo('Address');
}
}
My aim is to get a collection of Person resources that are sorted by the address_1 field of their related Address model.
I can accomplish this by referencing table names as show below, but I want to do it instead with Eloquent relationships, since I do not want to deal with tables for abstraction purposes.
Person::join('addresses', 'persons.id', '=', 'addresses.person_id')
->orderBy('address_1', 'asc')->get();
I have attempted the following Eloquent method without success.
Person::with('address')->whereHas('address', function($q)
{
$q->orderBy('address_1', 'asc');
})->get();
This query fails with the error message:
Grouping error: 7 ERROR: column \"addresses.address_1\" must appear in the
GROUP BY clause or be used in an aggregate function
In response to this, I tried adding this line above the orderBy statement which causes the query to succeed, but the ordering has no effect on the resulting Person collection.
$q->groupBy('address_1');
I would much appreciate a solution where I do not have to reference table names if it is possible. I have exhausted all resources on this subject, but surely this is a common use case.
Here you go:
$person = new Person;
$relation = $person->address();
$table = $relation->getRelated()->getTable();
$results = $person->join(
$table, $relation->getQualifiedForeignKey(), '=', $relation->getQualifiedOtherKeyName()
)->orderBy($table.'.address_1', 'asc')
->get();

Laravel, How to use where conditions for relation's column?

I'm using Laravel and having a small problem with Eloquent ORM.. I can get this working simply with SQL query using a JOIN but I can't seem to get it working with Eloquent!
This is what I want, I have two tabels. one is 'Restaurants' and other is 'Restaurant_Facilities'.
The tables are simple.. and One-To-One relations. like there is a restaurant table with id, name, slug, etc and another table called restaurant_facilities with id, restaurant_id, wifi, parking, etc
Now what I want to do is.. load all restaurants which have wifi = 1 or wifi = 0..
How can i do that with Eloquent ? I have tried eager loading, pivot tables, with(), collections() and nothing seems to work!
The same problem I have for a Many-To-Many relation for cuisines!
I have the same restaurant table and a cuisine table and a restaurant_cuisine_connection table..
but how do I load all restaurants inside a specific cuisine using it's ID ?
This works.
Cuisine::find(6)->restaurants()->get();
but I wanna load this from Restaurant:: model not from cuisines.. because I have many conditions chained together.. its for a search and filtering / browse page.
Any ideas or ways ? I've been struggling with this for 3 days and still no answer.
Example Models :
class Restaurant extends Eloquent {
protected $table = 'restaurants';
public function facilities() {
return $this->hasOne('Facilities');
}
}
class Facilities extends Eloquent {
protected $table = 'restaurants_facilities';
public function restaurant() {
return $this->belongsTo('Restaurant');
}
}
PS :
This seems to be working.. but this is not Eloquent way right ?
Restaurant::leftJoin(
'cuisine_restaurant',
'cuisine_restaurant.restaurant_id',
'=', 'restaurants.id'
)
->where('cuisine_id', 16)
->get();
Also what is the best method to find a count of restaurants which have specific column value without another query ? like.. i have to find the total of restaurants which have parking = 1 and wifi = 1 ?
Please help on this.
Thank you.
I don't see anything wrong with doing the left join here, if you have to load from the Restaurant model. I might abstract it away to a method on my Restaurant model, like so:
class Restaurant extends Eloquent {
protected $table = 'restaurants'; // will be default in latest L4 beta
public function facility()
{
return $this->hasOne('Facility');
}
// Or, better, make public, and inject instance to controller.
public static function withWifi()
{
return static::leftJoin(
'restaurant_facilities',
'restaurants.id', '=', 'restaurant_facilities.restaurant_id'
)->where('wifi', '=', 1);
}
}
And then, from your routes:
Route::get('/', function()
{
return Restaurant::withWifi()->get();
});
On the go - haven't tested that code, but I think it should work. You could instead use eager loading with a constraint, but that will only specify whether the facility object is null or not. It would still return all restaurants, unless you specify a where clause.
(P.S. I'd stick with the singular form of Facility. Notice how hasOne('Facilities') doesn't read correctly?)
I stumbled across this post while trying to improve my REST API methodology when building a new sharing paradigm. You want to use Eager Loading Constraints. Let's say you have an api route where your loading a shared item and it's collection of subitems such as this:
/api/shared/{share_id}/subitem/{subitem_id}
When hitting this route with a GET request, you want to load that specific subitem. Granted you could just load that model by that id, but what if we need to validate if the user has access to that shared item in the first place? One answer recommended loading the inversed relationship, but this could lead to a confusing and muddled controller very quickly. Using constraints on the eager load is a more 'eloquent' approach. So we'd load it like this:
$shared = Shared::where('id', $share_id)
->with([ 'subitems' => function($query) use ($subitem_id) {
$query->where('subitem_id', $subitem_id)
}]);
So where only want the subitem that has that id. Now we can check if it was found or not by doing something like this:
if ($shared->subitems->isEmpty())
Since subitems is a collection (array of subitems) we return the subitem[0] with this:
return $shared->subitems[0];
Use whereHas to filter by any relationship. It won't join the relation but it will filter the current model by a related property. Also look into local scopes to help with situations like this https://laravel.com/docs/5.3/eloquent#local-scopes
Your example would be:
Restaurant::whereHas('facilities', function($query) {
return $query->where('wifi', true);
})->get();
Restaurant::whereHas('cuisines', function($query) use ($cuisineId) {
return $query->where('id', $cuisineId);
})->get();
To achieve the same thing with local scopes:
class Restaurant extends Eloquent
{
// Relations here
public function scopeHasWifi($query)
{
return $query->whereHas('facilities', function($query) {
return $query->where('wifi', true);
});
}
public function scopeHasCuisine($query, $cuisineId)
{
return $query->whereHas('cuisines', function($query) use ($cuisineId) {
return $query->where('id', $cuisineId);
});
}
}
For local scopes you DO NOT want to define them as static methods on your model as this creates a new instance of the query builder and would prevent you from chaining the methods. Using a local scope will injects and returns the current instance of the query builder so you can chain as many scopes as you want like:
Restaurant::hasWifi()->hasCuisine(6)->get();
Local Scopes are defined with the prefix scope in the method name and called without scope in the method name as in the example abover.
Another solution starring whereHas() function:
$with_wifi = function ($query) {
$query->where('wifi', 1);
};
Facilities::whereHas('restaurant', $with_wifi)
Nice and tidy.
Do you absolutely have to load it from the Restaurant model? In order to solve the problem, I usually approach it inversely.
Facilities::with('restaurant')->where('wifi' ,'=', 0)->get();
This will get all the restaurant facilities that match your conditions, and eager load the restaurant.
You can chain more conditions and count the total like this..
Facilities::with('restaurant')
->where('wifi' ,'=', 1)
->where('parking','=', 1)
->count();
This will work with cuisine as well
Cuisine::with('restaurant')->where('id','=',1)->get();
This grabs the cuisine object with the id of 1 eager loaded with all the restaurants that have this cuisine

Categories