Laravel relationship with whereColumn condition - php

Appears I'm trying to do something uncommonly complex or going about this the wrong way. I can't seem to find anything online similar to what I am doing here. I am trying to query a relationship where a column in the related table is equal to a column in the table of the model.
Given the following tables
| entity
============
id
...
| foo
============
id
entity_id
type_id
...
| baz
============
id
entity_id
type_id
...
| type
============
id
...
and Models
class Entity extends Model
{
public function baz()
{
$this->hasMany( Baz::class );
}
public function foo()
{
$this->hasMany( Foo::class );
}
//...
}
class Foo extends Model
{
public function entity()
{
$this->belongsTo( Entity::class );
}
public function type()
{
$this->belongsTo( Type::class );
}
//...
}
class Baz extends Model {
public function entity()
{
$this->belongsTo( Entity::class );
}
public function type()
{
$this->belongsTo( Type::class );
}
//...
}
class Type extends Model
{
//...
}
I have the following code working, but in a different context Both Foo and Baz are filtered using $type_id
public function controllerActionWorking( Request $request, $type_id )
{
// I'm using this in another place to get rows of Foo with type_id == $type_id. Including Related Baz which also have type_id == $type_id
$results = Foo::with(
[
'entity.baz' =>
function( $query ) use ($type )
{
return $query->where( 'type_id', $type->id ))
}
])
->where( 'type_id', $type_id )
->get()
// ....
}
What I need is all Foo records, The Entity that goes with Foo, and Entities related Baz records where the Baz.type_id is equal to the type_id of the Foo record. this is non working code, but something like this is what I started trying first.
public function controllerActionHelpWanted( Request $request )
{
$results = Foo::with(
[
'entity.baz' =>
function( $query )
{
// Here I need to verify the entity.baz.type_id = foo.type_id
// the following does NOT work
// I get errros that 'foo'.'type_id' does not exist
return $query->whereColumn( 'foo.type_id', 'baz.type_id' )
})
])
->get()
// .....
}
I can use a join() something like this
$results = Foo::with( 'entity' )
->join(
'baz',
function($join)
{
return $join->on( 'foo.entity_id', 'baz.entity_id' )
->on( 'foo.type_id', 'baz.type_id' );
})
->get()
That returns an instance of Foo with combined attributes of both tables. It also looses the 'entity.baz' nested relationship. which will matter in downstream code.
I could $results->map() to filter the 'entity.baz' relationship after the query. I'd prefer to avoid the impact on performance if possible.
just seems like I'm missing something here. Any help/advice is appreciated.

Hard to read, when names are foo/baz and no logic reference is applied. Looks like many to many relation, where you can apply pivot.
public function products()
{
return $this->belongsToMany(Product::class)
->withPivot('quantity', 'price'); // joins data from intermediate table
}
Pivot tables are queried by: $obj->pivot->quantity;

As far you wrap your question into faz/baz/entity it becomes quiet abstract to me, still :)
Let's try to make your query more model-side:
class Foo extends Model
{
use HasFactory;
public function entity()
{
return $this->belongsTo(Entity::class, 'entity_id', 'id');
}
public function scopeTypes($query, $type_id)
{
return $query->where('type_id', $type_id);
}
public function scopeWithBazes($query, $type_id)
{
/* #var $query Builder */
return $query->join('bazs', function($type_id) {
return self::where('type_id', $type_id);
});
}
}
Inside controller I can do:
class TestController extends Controller
{
public function index(Request $request)
{
$foos = Foo::query()->withBazes(1)->get();
foreach($foos as $foo)
dd($foo->baz_name); // i placed baz_name in Baz table for test
}
}
Well, drawback is that the table is joined. It is joined because you don't have any intermediate tables, so you need use joins etc.
Foo::types($type) will return collection of foos with type given.
Foo::bazTypes($type) will return joined table.
Still, these kind of problems appear when you omit intermediate tables, queries become more complex but database is a bit less robust.
If you were able to make that naming abstraction into 'real' problem, maybe people would play with it more. Just make foo => invoice, baz => address, etc, so perhaps you'll find better database schema for your scenario. Sometimes you will end up with multiple queries no matter what, but it's always worth asking :)

Related

What would be the best approach to query eloquent relatioships where keys are on the same table

Considering this table
Services Table
id
company_id
category_id
1
2
4
2
4
6
And this model
CategoryModel.php
public function companies():Attribute
{
return new Attribute(
get: fn () => CompanyModel::whereHas('services', function ($q) {
$q->where('category_id', $this->id);
}),
);
}
Considering that the services table holds both company_id and category_id columns what would be the best approach to query companies relationship that have services under the current category (The companies table does not have a category_id column), my current implemetation is not optimal as it does not allow me to perform any relationship constrains.
EDIT
Each company offers multiple services and each service belongs to a single category.
I also have a reviews table (related to each service) with a rating column
The above query worked efficiently until I needed to constrain/order categories based on the reviews table.
CategoryModel.php
public function scopeHasReviews($query)
{
$query->whereHas('companies', fn ($q) => $q->whereHas('reviews'));
}
This ofcourse will not work since there is no relationship.
Using Attribute in this context is dangerous, because it can lead to N+1 problems. This is a usual many-to-many relationship, so it needs to be implemented in models:
Category.php
public function companies(): BelongsToMany
{
return $this->belongsToMany(Company::class, 'services');
}
Company.php
public function categories(): BelongsToMany
{
return $this->belongsToMany(Category::class, 'services');
}
Further, since it is not clear exactly what problem must be solved - I will write an example with sorting by number of reviews:
Review.php
public function scopeWhereRawService(Builder $query, string $service): Builder
{
return $query->whereRaw('service_id = ' . $service);
}
Company.php
public function reviews(): HasManyThrough
{
return $this->hasManyThrough(Review::class, Service::class);
}
public function categories(): BelongsToMany
{
return $this->belongsToMany(Category::class, 'services');
}
public function categoriesOrderedByReviews(): BelongsToMany
{
return $this->categories()->withCount([
'reviews as reviews_count' => fn(Builder $q) => $q->whereRawService('services.id')
])->orderByDesc('reviews_count');
}
Category.php
public function reviews(): HasManyThrough
{
return $this->hasManyThrough(Review::class, Service::class);
}
public function companies(): BelongsToMany
{
return $this->belongsToMany(Company::class, 'services');
}
public function companiesOrderedByReviews(): BelongsToMany
{
return $this->companies()->withCount([
'reviews as reviews_count' => fn(Builder $q) => $q->whereRawService('services.id')
])->orderByDesc('reviews_count');
}

Select specific column from morph relation according to type in Laravel

I am trying to write a query that selects columns from a model then selects some columns from a morph relationship table. But I have no idea to select columns, and relation tables have different columns. So some column has no slug, some have.
public function index()
{
$menus = Menu::whereActive(true)
->with([
'menuable' => function ($q) {
// This gives error if there is no relation Pages model
$q->whereActive(true)->select('pages.id', 'pages.slug');
// Below not working
// if($q->type === Page::class){
// $q->whereActive(true)->select('pages.id', 'pages.slug');
// } else if($q->type === Category::class){
// $q->whereActive(true)->select('categories.id',
'categories.slug');
// }
}
])
->get(['id', 'menuable_id', 'menuable_type', 'name']);
$response = [
'menus' => $menus,
];
return $this->sendResponse($response);
}
Models
class Menu extends Model
{
public function menuable()
{
return $this->morphTo();
}
}
class Page extends Model
{
public function menu()
{
return $this->morphOne(Menu::class, 'menuable');
}
}
class Category extends Model
{
public function menu()
{
return $this->morphOne(Menu::class, 'menuable');
}
}
How can I select specific columns from morph relation with checking morph type? I am using Laravel version 8.
The polymorphic relation is something the Eloquent aware of, and DBMS hasnot implemented this feature in it.
so there cannot be a sql query which join a table to another tables based on the morph column.
so you have to use distinct queries for every polymorphic join relation on your models:
//you can retrieve distinct menu based on their relation
Menu::whereActive(true)->hasPage()->with('pages');
//and having the ralations in the menu model:
public function posts
Menu::whereActive(true)->hasCategory();
//scope in menu class can be like:
public function scopePage($query){
return $query->where('menuable_type',Page::class);
}
public function scopeCategory($query){
return $query->where('menuable_type',Category::class);
}
//with these you can eager load the models
Menu::whereActive(true)->hasPage()->with('page');
Menu::whereActive(true)->hasCategory()->with('category');
public function page(){
return $this->belongsTo(Page::class);
}
public functioncategory(){
return $this->belongsTo(Category::class);
}
if you want a common interface to use one of these dynamically.
you can use:
$menu->menuable->id
$menu->menuable->slug
I am not sure which columns you want to response, but as i can guess from your question, I suppose you want id and slug from both models.
public function index(){
$pagedMenu = Menu::whereActive(true)->hasPage()->with('page');
$categoriedMenu = Menu::whereActive(true)->hasCategory()->with('category');
$menues = $pagedMenu->merge($categoriedMenu);
$response = [
'menus' => $menus,
];
return $this->sendResponse($response);
}
You can perfom separate filters based on the morph-class. This can be achieved with the whereHasMorph (https://laravel.com/docs/8.x/eloquent-relationships#querying-morph-to-relationships).
If you need to automatically resolve the relationship you can use with. This will preload morphables automatically into your resulting collection.
The following example uses an orWhere with 2 separate queries per morphable.
$menus = Menu::whereActive(true)
->whereHasMorph('menuable', [Page::class], function (Builder $query) {
// perform any query based on page-related entries
})
->orWhereHasMorph('menuable', [Category::class], function (Builder $query) {
// perform any query based on category-related entries
})
->with('menuable')
;
An alternative way is to pass both classes to the second argument. In that case you can check the type inside your closure.
$menus = Menu::whereActive(true)
->whereHasMorph('menuable', [Page::class, Category::class], function (Builder $query, $type) {
// perform any query independently of the morph-target
// $q->where...
if ($type === (new Page)->getMorphClass()) {
// perform any query based on category-related entries
}
if ($type === (new Category)->getMorphClass()) {
// perform any query based on category-related entries
}
})
->with('menuable')
If required you can also preload nested relationships. (https://laravel.com/docs/8.x/eloquent-relationships#nested-eager-loading-morphto-relationships)
$menus = Menu::whereActive(true)
->with(['menuable' => function (MorphTo $morphTo) {
$morphTo->morphWith([
Page::class => ['calendar'],
Category::class => ['tags'],
]);
}])

Yii2 ORM smart get related model

I have 3 Yii2 ActiveRecord models: Emplpoyee, Department and Organization.
Employee must have either one Department or one Organization, that is ensured by validation (fails if both department_id and organization_id are null or both are not null). Department must have one Organization, it's a standard yii2 ORM relation via hasOne().
Gii created this code for employee/organization relation:
class Employee
{
public function getOrganization()
{
return $this->hasOne(app\models\Organization::class, ['id' => 'organization_id']);
}
}
So when I call $employeeObject->organization, I will get null if organization_id in SQL table is null.
I want to modify this standard getter function to return $this->department->organization in be able to get $employee->organization via magic getter like this: if an employee has department - organization gets from department, otherwise - via standard relation.
Update: if I write:
/**
* #return ActiveQuery
*/
public function getOrganization()
{
if (!is_null($this->organization_id)) {
return $this->hasOne(app\models\Organization::class, ['id' => 'organization_id']);
} elseif (!is_null($this->department)) {
return $this->department->getOrganization();
} else {
// We should not be here, but what if we are?
}
}
How do I handle a situation of broken relation to Department or both organization_id and department_id is null? Which ActiveQuery do I return?
Relations definitions are used to build SQL query for related models. In some cases (eager loading, joins) query needs to be created before you get actual object, so you cannot use if ($this->organization_id) because $this is not actual model and $this->organization_id will alway be null. The only thing that you can get is to define regular relations without any conditions and getter method (getOrganizationModel()), which will return correct organization from relations:
class Employee extends ActiveRecord {
public function getDepartment() {
return $this->hasOne(Department::class, ['id' => 'department_id']);
}
public function getOrganization() {
return $this->hasOne(Organization::class, ['id' => 'organization_id']);
}
public function getOrganizationModel() {
if ($this->organization_id !== null) {
return $this->organization;
}
return $this->department->organization;
}
}
class Department extends ActiveRecord {
public function getOrganization() {
return $this->hasOne(Organization::class, ['id' => 'organization_id']);
}
}
Then you can query records with both relations and use getter to get organization:
$employees = Employe::find()->with(['department.organization', 'organization'])->all();
foreach ($employees as $employee) {
echo $employee->getOrganizationModel()->name;
}

laravel 4 Cannot Retrieve ALL results ? pivot table Many to Many

I am a bit stuck on this...
I have 3 tables: photographers, languages and languages_spoken (intermediate table).
I am trying to retrieve all the languages spoken by a photographer. I defined my models like this:
class Photographer extends Eloquent {
/**
* Defining the many to many relationship with language spoken
*
*/
public function languages() {
return $this->belongsToMany('Language', 'languages_spoken', 'language_id', 'photographer_id');
}
class Language extends Eloquent {
/**
* Defining the many to many relationship with language spoken
*
*/
public function photographers() {
return $this->belongsToMany('Photographer', 'languages_spoken', 'language_id', 'photographer_id')
->withPivot('speakslanguages');
}
This is how I was trying to retrieve all the results for the logged in photographer:
$photographer = Photographer::where('user_id', '=', $user->id);
if ($photographer->count()) {
$photographer = $photographer->first();
// TEST
$spokenlang = $photographer->languages;
die($spokenlang);
// END TEST
} else {
return App::abort(404);
}
The problem is that in my db I have 4 entries for the same photographer. but when I do this I only get the last result...
[{"id":"3","language_name":"Afrikaans","updated_at":"-0001-11-30 00:00:00","created_at":"-0001-11-30 00:00:00","native_name":"Afrikaans","ISO639_1":"af","pivot":{"language_id":"3","photographer_id":"3"}}]
Any idea on what is wrong ?
Thanks a lot for your help!!!
The third parameter to belongsToMany should be the foreign key.
In the Photographer class:
public function languages() {
return $this->belongsToMany('Language', 'languages_spoken', 'language_id', 'photographer_id');
}
...should be:
public function languages() {
return $this->belongsToMany('Language', 'languages_spoken', 'photographer_id');
}
In the Language class:
public function photographers() {
return $this->belongsToMany('Photographer', 'languages_spoken', 'language_id', 'photographer_id')
->withPivot('speakslanguages');
}
Should be:
public function photographers() {
return $this->belongsToMany('Photographer', 'languages_spoken', 'language_id')
->withPivot('column1', 'column2', 'column3'); // withPivot() takes a list of columns from the pivot table, in this case languages_spoken
}
But, since you're not even using strange keys, you don't need to pass that third parameter at all.
So this is just fine:
public function languages() {
return $this->belongsToMany('Language', 'languages_spoken');
}
And:
public function photographers() {
return $this->belongsToMany('Photographer', 'languages_spoken')
->withPivot('column1', 'column2', 'column3'); // withPivot() takes a list of columns from the pivot table, in this case languages_spoken
}

Laravel 4 eloquent - Getting the id of self based on constraints of parents

I need to get the id of a row based on the constraints of the parents. I would like to do this using eloquent and keep it elegant. Some things to note when this process starts:
I have - country_code(2 digit iso), lang_code(2 digit abbreviation for language)
i need - country_id, lang_id (primary keys)
so i can get - market_id (needed for last query)
I am able to retrieve the data I need with the following, sorry for the naming of the variables (client had weird names):
// Only receive desired inputs
$input_get = Input::only('marketCode','langCode');
// Need the country based on the "marketCode"
$countryId = Country::where('code',$input_get['marketCode'])->pluck('id');
// Get the lang_id from "langCode"
$languageId = Language::where('lang_abbr',$input_get['langCode'])->pluck('lang_id');
// Get the market_id from country_id and lang_id
$marketId = Market::where('country_id', $countryId)
->where('lang_id',$languageId)->pluck('market_id');
// Get All Market Translations for this market
$marketTranslation = MarketTranslation::where('market_id',$marketId)->lists('ml_val','ml_key');
I've tried the following, but this only eager loads the country and language based on the constraints. Eager Loading only seems to be helpful if the market_id is already known.
class Market extends Eloquent {
protected $primaryKey = 'market_id';
public function country() {
return $this->belongsTo('Country');
}
public function language(){
return $this->belongsTo('Language','lang_id');
}
}
$markets = Market::with(array(
'country' => function($query){
$query->where('code','EE');
},
'language'=> function($query){
$query->where('lang_abbr','et');
}
))->get();
You'd have to use joins in order to do that.
$market = Market::join( 'countries', 'countries.id', '=', 'markets.country_id' )
->join( 'languages', 'languages.id', '=', 'markets.language_id' )
->where( 'countries.code', '=', 'EE' )
->where( 'languages.lang_abbr', 'et' )
->first();
echo $market->id;
If this is something that happens frequently then I'd probably add a static method to the Market model.
// in class Market
public static function lookup_id( $country_code, $language_abbreviation ) { ... }
// then later
$market_id = Market::lookup_id( 'EE', 'et' );
So after looking at the relationships, I was able to get it working without the use of manual joins or queries, just the relationships defined in the ORM. It seems correct, in that it uses eager loading and filters the data needed in the collection.
// Get A country object that contains a collection of all markets that use this country code
$country = Country::getCountryByCountryCode('EE');
// Filter out the market in the collection that uses the language specified by langCode
$market = $country->markets->filter(function($market) {
if ($market->language->lang_abbr == 'et') {
return $market;
}
});
// Get the market_id from the market object
$marketId = $market->first()->market_id;
Where the models and relationships look like this:
class Country extends Eloquent {
public function markets() {
return $this->hasMany('Market')->with('language');
}
public static function getCountryByCountryCode($countryCode)
{
return Country::with('markets')->where('code',$countryCode)->first();
}
}
class Market extends Eloquent {
protected $primaryKey = 'market_id';
public function country() {
return $this->belongsTo('Country');
}
public function language(){
return $this->belongsTo('Language','lang_id');
}
}
class Language extends Eloquent {
protected $primaryKey = 'lang_id';
}

Categories