Laravel: Querying and accessing child objects in nested relationship with where clauses - laravel

I am trying to access the child objects of nested relationships that return many results from the parents object.
Let's say I have 4 models : Country - Provinces - Cities - Municipalities
Their relationships are as follows :
Country Model
class Country extends Eloquent
{
protected $table = 'countries';
public function provinces()
{
return $this->hasMany('Province');
}
}
Province Model
class Province extends Eloquent
{
protected $table = 'provinces';
public function cities()
{
return $this->hasMany('City');
}
public function country()
{
return $this->belongsTo('Country');
}
}
City Model
class City extends Eloquent
{
protected $table = 'cities';
public function municipalities()
{
return $this->hasMany('Municipality');
}
public function province()
{
return $this->belongsTo('Province');
}
}
Municipality Model
class Municipality extends Eloquent
{
protected $table = 'municipalities';
public function cities()
{
return $this->belongsTo('City');
}
}
Now what I am trying to do is get all municipalities in a given country that have a population over 9000 and are located in provinces that are considered West.
So far I have something like this :
$country_id = 1;
$country = Country::whereHas('provinces', function($query){
$query->where('location', 'West');
$query->whereHas('cities', function($query){
$query->whereHas('municipalities', function($query){
$query->where('population', '>', 9000);
});
});
})->find($country_id);
Now I can easily get the provinces with $country->provinces but I can't go any deeper than that.
EDIT1 : Fixing the belongsTo relationship as noticed by Jarek.
EDIT2: In addition to Jarek's answer, I wanted to share what I also found however Jarek's is probably the more proper method.
Instead of trying to go from top to bottom (Country -> Municipality) I decided to try the other way (Municipality -> Country) Here's how it works (and I tested it, also works)
$municipalities = Municipality::where('population', '>', 9000)
->whereHas('city', function($q) use ($country_id){
$q->whereHas('province', function($q) use ($country_id){
$q->where('location', 'West');
$q->whereHas('country', function($q) use ($country_id){
$q->where('id', $country_id);
});
});
})->get();
I have no idea if this is an actual proper way or if performance would be accepted but it seemed to do the trick for me however Jarek's answer looks more elegant.

Your Municipality-City is probably belongsTo, not hasMany like in the paste.
Anyway you can use hasManyThrough relation to access far related collection:
Country - City
Province - Municipality
Unfortunately there is no relation for 3 level nesting, so you can't do this just like that.
Next, your code with whereHas does not limit provinces to west and municipalities to 9000+, but only limits countries to those, that are related to them. In your case this means that result will be either Country (if its relations match these requirements) or null otherwise.
So if you really want to limit related collections, then you need this piece:
$country = Country::with(['provinces' => function($query){
$query->where('location', 'West');
}, 'provinces.cities.municipalities' => function ($query){
$query->where('population', '>', 9000);
}])->find($country_id);
This is applying eager loading constraints, and what it does is:
1. loads only West provinces for country with id 1
2. loads all the cities in these provinces
3. loads only 9k+ municipalities in these cities
Since you're not interested in cities, you could use hasManyThrough on the Province:
// Province model
public function municipalities()
{
return $this->hasManyThrough('Municipality', 'City');
}
then:
$country = Country::with(['provinces' => function($query){
$query->where('location', 'West');
}, 'provinces.municipalities' => function ($query){
$query->where('population', '>', 9000);
}])->find($country_id);
However in both cases you can't access the municipalities directly, but only like this:
// 1 classic
$country->provinces->first()->cities->first()->municipalities;
// 2 hasManyThrough
$country->provinces->first()->municipalities;
That being said, if you'd like to work with all those municipalities, you need this trick:
$country = Country::with(['provinces' => function($query){
$query->where('location', 'West');
}, 'provinces.municipalities' => function ($query) use (&$municipalities) {
// notice $municipalities is passed by reference to the closure
// and the $query is executed using ->get()
$municipalities = $query->where('population', '>', 9000)->get();
}])->find($country_id);
This will run additional query, but now all the municipalities are in single, flat collection, so it is very easy to work with. Otherwise you likely end up with a bunch of foreach loops.

Related

retrieving related field in controller index function gives error but ok in show function

I define the relation in Company table (where I added the plural):
protected $table = 'companies';
public function country() {
return $this->belongsTo(Country::class, "country_id")->withDefault(['country' => 'unknown']);
}
I also did the same in the Country model.
When I use the following code in the controller show function it works:
public function show (Company $company) {
$company->country = $company->country()->pluck('country');
But if I use the same code in the index function in a loop, I get an error "Call to undefined method stdClass::country()":
public function index (Company $company) {
if (request('tag')) {
$companies = Tag::where('name',request('tag'))->firstOrFail()->companies;
$companies->page_title = "Businesses matching tag '".request('tag')."'";
} else {
$companies = DB::table('companies')
->where([['is_active', '=', '1']])
->orderBy('company')
->get();
}
foreach($companies as $key => $thisCompany) {
...
$thisCompany->country = $company->country()->pluck('country');
}
I guess it is due to the fact that $company is created in the loop and not passed through the function like in show(Company $company)... but I could not find how to solve this issue... so help will be appreciated.
I have added the model in the argument of the function and change the name of the $company variable in the loop by $thisCompany to avoid confusion with the $company model.
No error but the field $country->country does not contain the name of the country but "Illuminate\Support\Collection {#443 …1}"
Why is it so complicated? Please help...
Paul, sorry, I think I didn't explain myself well in the comments.
What I meant by "What about if you change DB::table('companies') by Company?", is to stop using DB Query Builder to use the Eloquent Company model.
Specifically in this segment of code:
$companies = DB::table('companies')
->where([['is_active', '=', '1']])
->orderBy('company')
->get();
So, it could be:
$companies = Company::where([['is_active', '=', '1']])
->orderBy('company')
->get();
The explanation is that in the first way (with DB Query Builder), the query will return a collection of generic objects (the PHP stdClass object) that do not know anything about the Company and its relationships.
On the other hand, if you use the Eloquent model Company, it will return a collection of Company objects, which do know about relationships, and specifically the relationship that you have defined as country.
Then, when you loop over the collection, you will be able to access the country relation of each Company object:
foreach($companies as $key => $company) {
//...
$company->country = $company->country()->pluck('country');
}
Finally, your code could looks like:
public function index () {
if (request('tag')) {
$companies = Tag::where('name',request('tag'))->firstOrFail()->companies;
$companies->page_title = "Businesses matching tag '".request('tag')."'";
} else {
$companies = Company::where([['is_active', '=', '1']])
->orderBy('company')
->get();
}
foreach($companies as $key => $company) {
//...
$company->country = $company->country()->pluck('country');
}
//...
}

mb_strpos() expects parameter 1 to be string, object given when querying 2 tables in Laravel

Ok, so I don't know if this can be done, but I need to combine the results of 2 Where clauses from 2 tables into one variable.
So far I have this working to query one table:
$allCompanies = Products::where('category_id', $id->id)->groupBy('company_id')->get();
And this for the other:
$companies = Company::where('visible', 0)->get();
But is there any way to get them into the same query string? Something like so I can get where the ID matches the ID column in one table AND where visible is 0 in the other?
I tried this:
$allCompanies = Products::with('company', function($q){
$q->where('visible', 0);
})->where('category_id', $id->id)
->groupBy('company_id')->get();
but got this error:
mb_strpos() expects parameter 1 to be string, object given
Company Model:
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class Company extends Model
{
public function products()
{
return $this->hasMany('App\Products');
}
}
Products Model
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class Products extends Model
{
protected $fillable = [
'name',
'description',
'image',
'size',
'price',
'stock',
'company_id',
'category_id'
];
public function category()
{
return $this->belongsTo('App\Categories', 'category_id');
}
public function company()
{
return $this->belongsTo('App\Company', 'company_id');
}
}
When using callback in with, you should pass callback as value of associative-array.
Your query should be:
$allCompanies = Products::with(['company' => function($q) {
$q->where('visible', 0);
}])
->where('category_id', $id->id)
->groupBy('company_id')
->get();
Ref: https://laravel.com/docs/8.x/eloquent-relationships#nested-eager-loading-morphto-relationships
The most Laravel way of achieving this is through Relationships and whereHas. If you have a relationship defined you could do the following:
$products = Products::where('category_id', $id->id)
->whereHas('company', function($q) {
$q->where('visible', 0);
});
Which will query for all the products with a certain category_id that also have a relationship with a company that has a column visible with value 0
Depends on what you want:
If you want all the products from visible companies:
$products = Products::whereHas('company', function($q) {
$q->where('visible',0);
})->get();
If you want all the companies with their products (which I would advise):
$Companies = Company::with('products')->where('visible',0)->get();

Get all championships that are teams in Eloquent

I have a tournament, a tournament can have many >
public function championships()
{
return $this->hasMany(Championship::class);
}
and a Championship hasOne Category. In Category, I have the isTeam attribute.
Now I need a function that get me all the championships that have the isTeam = 1 in Category table.
public function teamChampionships()
{
}
Of course, I have defined : $tournament->championships, $championship->category
In my controller, I get all of them:
$tournament = Tournament::with('championship.category')->find($tournament->id);
Any idea???
Try
$tournament = Tournament::with(['championships' => function ($query) {
$query->whereHas('category', function($subquery) {
$subquery->where('isTeam', '=', 1);
});
}])->get();
If the above doesn't work, try a different approach. Define isTeam() scope in Category model
public function scopeIsTeam($query) {
return $query->where('isTeam', 1);
}
Then you can use it like this
$tournament = Tournament::with('championships.categoryIsTeam')
->find($tournament->id);
Even better, create another scope in Championship that loads only teams
public function categoryTeam() {
return $this->hasOne(Category::class)->isTeam();
}
Sorry for too much information. One of those should do the job.

Laravel: getting an array inside an objet bei joinig two tables

I have two tables: films and actors. Thy have a n-m relation.
I have a connection table actor_film with two columns film_id and actor_id.
In order to get the list of films added in the current month with the actors that play in each film I did this:
$current_month_film = DB::table('films')
->join('actor_film', 'actor_film.film_id', '=', 'films.id')
->join('actors', 'actors.id', '=', 'actor_film.actor_id')
->select('films.*', 'actors.name as actors_name')
->whereMonth('films.created_at', '=', Carbon::now()->month)
->orderBy('films.created_at', 'desc')
->groupBy('films.name')
->get();
return view('statistics.month')->withCurrent_month_film($current_month_film);
I get 40 films each of it showing just ONE actor, even if I know there are several actors in one film.
If I delete the "->groupBy('films.name')" I get 132 record: one film several times and each time one actor.
How could I get a list of actors inside each film, joinig the two tables? something like:
[
{
film.id: 1,
film.name,
film.actors
{
actor[1].name: Morgan Freeman,
actor.[2].name: Meryl Streep
actor [n].name: ....
}
},
{
film.id: 2,
film.name,
film.actors
{
actor[1].name: Morgan Freeman,
actor.[2].name: Meryl Streep
actor [n].name: ....
}
}
]
If you want to use Eloquent and you have your relation in place then your can do it as:
Film::whereMonth('created_at', Carbon::now()->month)
->orderBy('created_at', 'desc')
->with(['actors' => function($q) {
$q->select('name');
}])
->get()
Docs
As you're using Laravel, you should make the use of Laravel Relationships. You should have a models like this:
class Actor extends Model
{
public function films() {
return $this->belongsToMany(Film::class, 'actor_film');
}
}
class Film extends Model
{
public function actors() {
return $this->belongsToMany(Actor::class, 'actor_film');
}
}
and to get the film with their respective actors, you can use with method like this:
$films = Film::with(['actors' => function($q) {
$q->select('name');
}])
->whereMonth('created_at', Carbon::now()->month)
->orderBy('created_at', 'desc')
->get();
foreach($films as $film) {
$film_name = $film->name;
$actors_arr = $film->actors->toArray();
}
See more about Laravel's Eloquent Relationships & Eager Loading
Hope this helps!
In Controller
$current_month_film = Film::select('*')->with(['actor_film','actors'])
->whereMonth('films.created_at', '=', Carbon::now()->month)
->orderBy('films.created_at', 'desc')
->groupBy('films.name')
->get();
In App\Model\Film Model
namespace App;
use Illuminate\Database\Eloquent\Model;
class Film extends Model
{
//
protected $primaryKey = "id"; //primary key
protected $table = 'film';
public function actor_film(){
return $this->belongsTo('App\Model\ActorFilm','film_id','id');
}
public function actors(){
return $this->hasMany('App\Model\Actors','id','actor');
}
}

Eloquent where condition based on a "belongs to" relationship

Let's say I have the following model:
class Movie extends Eloquent
{
public function director()
{
return $this->belongsTo('Director');
}
}
Now I'd like fetch movies using a where condition that's based on a column from the directors table.
Is there a way to achieve this? Couldn't find any documentation on conditions based on a belongs to relationship.
You may try this (Check Querying Relations on Laravel website):
$movies = Movie::whereHas('director', function($q) {
$q->where('name', 'great');
})->get();
Also if you reverse the query like:
$directorsWithMovies = Director::with('movies')->where('name', 'great')->get();
// Access the movies collection
$movies = $directorsWithMovies->movies;
For this you need to declare a hasmany relationship in your Director model:
public function movies()
{
return $this->hasMany('Movie');
}
If you want to pass a variable into function($q) { //$variable } then
function($q) use ($variable) { //$variable }
whereBelongsTo()
For new versions of Laravel you can use whereBelongsTo().
It will look something like this:
$director = Director::find(1);
$movies = Movie::whereBelongsTo($director);
More in the docs.
is()
For one-to-one relations is() can be used.
$director = Director::find(1);
$movie = Movie::find(1);
$movie->director()->is($director);

Resources