Laravel create a 1-1 relationship from a many-many relationship - php

Hi I'm having a struggle trying to return a single record from a many to many relationship.
So in my app I've Clubs and Addresses entities.
Clubs have 0, 1 or n Addresses and one of them may be the main address.
An Address can also be used by some other entities (like Events, Members etc..)
My tables are the following :
clubs: id, name
addresses: id, street, city, zip club
club_address: id, club_id, address_id, is_main
I can currently request all the addresses of my club like so :
class Club {
public function addresses()
{
return $this->belongsToMany('Address', 'club_address', 'club_id', 'address_id')->withPivot('is_main'); // club_address
}
}
Now what I'd like is to get the main address or null when I request a club.
I can't be satisfied with simply adding ->wherePivot('is_main', '=', 1) because it's still returning an array of 1 or 0 element when I want an array or null.
I'd like something like this
class Club {
// Get all the addresses in an array
public function addresses()
{
return $this->belongsToMany('Address', 'club_address', 'club_id', 'address_id')->withPivot('is_main'); // club_address
}
// Get the main address or null
public function address()
{
return $this->addresses()->wherePivot('is_main', '=', 1)->first();
}
}
But the problem is that I can't eager load address because it's not returning a Relation Model ...

first() on an eloquent model returns a collection - even if that collection is zero (which is what you have basically said).
However first() on a collection returns null is there is no first...
So..
class Club {
public function addresses()
{
return $this->belongsToMany('Address', 'club_address', 'club_id', 'address_id')->withPivot('is_main');
}
public function address()
{
// First first is on the model, second first
// is on the collection
return $this->addresses()
->wherePivot('is_main', '=', 1)
->first()
->first();
}
}
Note This is essentially just a shorthand trick. Your attribute could just as easily, and possibly more readable, use something like:
class Club {
public function addresses()
{
return $this->belongsToMany('Address', 'club_address', 'club_id', 'address_id')->withPivot('is_main');
}
public function address()
{
$record = $this->addresses()
->wherePivot('is_main', '=', 1)
->first();
return count($record) === 0 ? null : $record;
}
}

I found a way to do the trick by extending the BelongsToMany Relation class and overriding two methods with their BelongsTo Relation equivalent.
But I wouldn't say it's prudent to use this, but it seems ok for my uses.
namespace App\Relations;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Query\Expression;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
class BelongsToOneFromMany extends BelongsToMany {
/**
* Initialize the relation on a set of models.
*
* #param array $models
* #param string $relation
* #return array
*/
public function initRelation(array $models, $relation)
{
foreach ($models as $model)
{
$model->setRelation($relation, null);
}
return $models;
}
/**
* Match the eagerly loaded results to their parents.
*
* #param array $models
* #param \Illuminate\Database\Eloquent\Collection $results
* #param string $relation
* #return array
*/
public function match(array $models, Collection $results, $relation)
{
$foreign = $this->foreignKey;
$other = $this->otherKey;
// First we will get to build a dictionary of the child models by their primary
// key of the relationship, then we can easily match the children back onto
// the parents using that dictionary and the primary key of the children.
$dictionary = array();
foreach ($results as $result)
{
$dictionary[$result->getAttribute($other)] = $result;
}
// Once we have the dictionary constructed, we can loop through all the parents
// and match back onto their children using these keys of the dictionary and
// the primary key of the children to map them onto the correct instances.
foreach ($models as $model)
{
if (isset($dictionary[$model->$foreign]))
{
$model->setRelation($relation, $dictionary[$model->$foreign]);
}
}
return $models;
}
}

Related

How to return the count of duplicate relationships with Laravel Eloquent

I have the following tables Orders, Lamps and Lamp_Order which is a pivot table. The Lamp_Order table stores the id of an Order and a Lamp. An Order can contain multiple of the same Lamps. So it could be that there are for example 5 Lamps with an id of 1 connected to the same Order. I want to get the count of the same Lamps within an Order. So this method or function that I want to make should return 5 in this case.
I currently have this function in my OrderController to return the Order with the related Lamps:
public function index()
{
$orders = Order::all();
// Get the Lamps for each Order.
foreach ($orders as $order) {
$order->lamps;
}
return response()->json([
'orders' => $orders
], 200);
}
The response in my vue front-end looks like this:
As you can see there are some lamps with the same ID being returned. Instead I would like to return this Lamp with the count of how many times it is related to that Order.
My Order model looks like this:
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class Order extends Model
{
/**
* The attributes that are mass assignable.
*
* #var array
*/
protected $fillable = [
'name', 'email'
];
/**
* Get related Image.
*
* #return void
*/
public function image()
{
return $this->hasOne(Image::class);
}
/**
* Get related Lamps.
*
* #return void
*/
public function lamps()
{
return $this->belongsToMany(Lamp::class)->withPivot('room');
}
public function countSameRelationships()
{
}
/**
* Detach related lamps when deleting Orders.
*
* #return void
*/
public static function boot()
{
parent::boot();
static::deleting(function ($order) {
$order->lamps()->detach();
});
}
}
I was thinking about creating a function in the Order model which I call in the index function in the OrderController. Can someone tell me if there is some sort of already existing function to count these "duplicate" relationships? Or what would be a good approach to tackle this problem? I prefer the solution to return the right data directly from the Laravel backend. But if it is also possible to apply some sort of filter function to remove and count duplicate relationships that would be fine as well.
Okay, at this moment I made the following solution in my Vue front-end as I didn't manage to solve the problem in my back-end:
removeAndCountDuplicates(order) {
// map to keep track of element
// key : the properties of lamp (e.g name, fitting)
// value : obj
var map = new Map();
// loop through each object in Order.
order.forEach(data => {
// loop through each properties in data.
let currKey = JSON.stringify(data.name);
let currValue = map.get(currKey);
// if key exists, increment counter.
if (currValue) {
currValue.count += 1;
map.set(currKey, currValue);
} else {
// otherwise, set new key with in new object.
let newObj = {
id: data.id,
name: data.name,
fitting: data.fitting,
light_color_code: data.light_color_code,
dimmability: data.dimmability,
shape: data.shape,
price: data.price,
watt: data.watt,
lumen: data.lumen,
type: data.type,
article_number: data.article_number,
count: 1
};
map.set(currKey, newObj);
}
});
// Make an array from map.
const res = Array.from(map).map(e => e[1]);
return res;
},
This function increments a counter and adds it to the object. If someone has a solution that works in the back-end I would like to see that answer as well.
You can use Eager Loading in laravel:
public function index()
{
$orders= User::with(['lamps' => function($query) {
$query->select('lamps.id', DB::raw("COUNT('lamps.id') AS lamp_count"));
$query->groupBy('lamps.user_id');
}])->get();
return response()->json([
'orders' => $orders
], 200);
}

Laravel ORM dynamic model attributes

I have a database set up that I am not sure how to code in laravel. I am trying to basically get dynamic attribute names from custom input.
Here's the DB setup:
Category:
-ID
-Name
Product:
-ID
-Category_id
Product_Attribute:
-ID
-Category_id
-Attribute_Name
Attribute_value:
-ID
-Product_id
-Product_attribute_id
-Value
There can be multiple values for each attribute and I don't have a set list of attributes as it can change depending on the category/product. Some products have some attributes and some don't. Some will have the same key/name as in other categories but will rarely overlap for my purposes but i can work around that if need be.
I there a way to setup laravel so i can look through the keys / values as well as call them by name
echo $product->$Attribute_Name;
or
echo $product->attributes[$Attribute_Name];
or something similar
but i also need to pull all products where attribute name = y and attribute Value = X
select * from Products join Attribute_value on products.ID = Attribute_value.Product_id join Product_Attribute on Category_id = Products.Category_id and Product_Attribute.ID = Attribute_value.Product_attribute_id where Product_Attribute = '{attribute_name}' and Attribute_value = '{Attribute_value}'
This is only return the products but not with the associated data or the other attributes. I can't find and easy way of loading that data without having to build a class to populate it. Ideally I would like to be able to change the values and save them using the ORM similar to how a one to many relationship works.
I have seen this type of structure before in databases. I was wondering if there was a way to do this easily in laravel without having to create a bunch of custom functions to load the attributes for each product.
Thanks
I'm a little confused by your question, but what you first want to do is create all the models and relationships. You don't necessarily need a model for each of the four tables, but I'd strongly recommend it.
class Category
{
public function products(): HasMany
{
return $this->hasMany(Product::class);
}
public function productAttributes(): HasMany
{
return $this->hasMany(ProductAttribute::class);
}
}
class Product
{
public function category(): BelongsTo
{
return $this->belongsTo(Category::class);
}
public function attributeValues(): HasMany
{
return $this->hasMany(AttributeValue::class);
}
}
class ProductAttribute
{
public function category(): BelongsTo
{
return $this->belongsTo(Category::class);
}
public function attributeValues(): HasMany
{
return $this->hasMany(AttributeValue::class);
}
}
class AttributeValue
{
public function product(): BelongsTo
{
return $this->belongsTo(Product::class);
}
public function productAttribute(): BelongsTo
{
return $this->belongsTo(ProductAttribute::class);
}
}
This code expects you to consider the Laravel naming standards of tables and properties.
After defining the classes and relationships, you may load products with their attributes like this:
$products = Product::query()
->with('attributeValues.productAttribute')
->where('category_id', $categoryId)
->get();
Because this makes accessing an attribute by it's name a pain...
$product = $products->first();
$color = optional($product->attributeValues
->where('productAttribute.name', 'color')
->first())->value ?? 'white';
... you can also override the __get($name) method to add a nice accessor for your attributes:
class Product
{
public function __get(string $name)
{
if ($name === 'attrs') {
return (object) $this->attributeValues->mapWithKeys(function ($attributeValue) {
return [$attributeValue->productAttribute->name => $attributeValue->value];
});
}
return parent::__get($name);
}
}
After doing so, you should be able to access your attributes like this:
$product = $products->first();
$color = $product->attrs->color;
// or if you need to retrieve an attribute by name stored in a variable
$name = 'color';
$attr = $product->attrs->$name;
Of course you can also omit the (object) cast in the __get($name) accessor to return an array instead. You then receive this syntax: $product->attrs['color']. Either way, this will return an error if a property is not set / not in the array. Make sure to catch this. You may also want to add some caching to avoid building the attrs object/array over and over again.
Please note: The $attributes property is used by the Eloquent base model internally to store all the properties of a model. So this name is reserved and you should use something else like attrs instead.
Edit: Two more options for getters would be the following ones:
class Product
{
public function getColorAttribute(string $default = 'white'): string
{
return optional($this->attributeValues
->where('productAttribute.name', 'color')
->first())->value ?? $default;
}
public function getAttr(string $name, $default = null)
{
return optional($this->attributeValues
->where('productAttribute.name', $name)
->first())->value ?? $default;
}
}
Similarly, you could design a setter:
class Product
{
public function setAttr(string $name, $value): void
{
if ($this->hasAttr($name)) {
$attr = $this->attributeValues
->where('productAttribute.name', $name)
->first();
$attr->value = $value;
$attr->save();
} else {
throw new \Exception(sprintf('This product may not have the attribute [%s].', $name));
}
}
public function hasAttr(string $name): bool
{
return $this->attributeValues
->contains('productAttribute.name', $name);
}
}

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