belongsToMany withPivot that belongs to another item with pivot - laravel

I have a problem I haven't been able to solve in days. Maybe there is someone here who can help me.
Basic idea
I have a model named Product. Each Product can have multiple ProductAttributes (belongsToMany with pivot).
Example:
Product: Car
ProductAttributes:
Color
HORSEPOWER
The pivot table for ProductAttribute holds the values for the respective attributes (color = blue, PS = 180).
This already works very well.
Problem
Now I want to implement product packages. A product package (ProductBundle) has many products. But these products should have their own pivot tables for their attributes. So in a product bundle I want to be able to specify that the car I created has more PS than defined in the actual product.
For this I need 2 pivot tables for the attributes.
What I've already tried
ProductBundle belongsToMany Product using a different pivot-table
ProductBundle belongsToMany ProductBundleProduct (ProductBundleProduct has a field called product_id which refers to the actual "basic product")
In both scenarios I have the problem that the pivot table for the attributes of the product belonging to a product bundle is not saved correctly:
Product
/**
* #return \Illuminate\Database\Eloquent\Relations\BelongsToMany
*/
public function productBundleAttributes()
{
return $this->belongsToMany(ProductAttribute::class,
product_bundle_product_attribute')
->withPivot($this->attributefields)-> withTimestamps();
}
controller
$prod = Product::findOrFail($product['id']);
$added = $productbundle->products()->save($prod, [
'custom' => $product['custom'],
'title' => $product['title'],
# 'factor' => $product['factor']
]);
/*Save attributes*/
$added->syncProductBundleAttributes($product['attributes']],
$productbundle->id);
sync method
public function syncProductBundleAttributes(
array $attributes,
int $id
) {
$this->checkProductAttributesRecursively(collect($attributes)->transform(function (
$attributes
) use (
$id
) {
$attribute['product_bundle_id'] = $id;
$attribute['product_attribute_id'] = $attribute['id'];
return $attribute;
})->toArray());
$this->productBundleAttributes()->attach($this->result);
return $this->result;
}
Unfortunately, this means that only one attribute is stored at a time.

You can have an additional column in the pivot table between Product and ProductAttributes, say product_bundle_id. So this way you can get attributes for specific bundle, or basic attributes with product_bundle_id as 0
Look at Saving Additional Data On A Pivot Table in Laravel Eloquent, Many to Many

Related

One-to-Many relationship between multiple tables

I'm trying to make a relationship between Product and Brand models with an intermediate model BrandProduct, intermediate model also contains some additional info like product_model etc.
My aim is to access all the products that belong to a specific brand with additional manufacturer info and also I want to access the brand of a specific product with additional info.
I've a Model named Product with these attributes
id
name
sku
quantity
etc.....
Another Model named Brand with following attributes
id
name
logo
And an intermediate Model named BrandProduct with attributes
id
brand_id
product_id
model
etc.....
I'm registering Brands and Products separately and making a relationship between them by BrandProduct Model with additional attributes like product_model.
Brand.php model contains:
public function products(){
return $this->hasMany('App\Models\BrandProduct');
}
Product.php model contains:
public function manufacturer(){
return $this->hasOne('App\Models\BrandProduct');
}
And BrandProduct.php model contains:
public function data(){
return $this->belongsTo('App\Models\Product', 'product_id', 'id');
}
public function brand(){
return $this->belongsTo('App\Models\Brand', 'brand_id', 'id');
}
Now I can successfully retrieve Product > Manufacturer by
$p = Product::find(id)->manufacturer
But I can't get inverse relation BrandProduct > Data by trying
$p = BrandProduct::find(id)->data
Similarly I can retrieve all the Brand > Products by
$p = Brand::find(id)->products
But can't get inverse relation
$b = BrandProduct::find(id)->brand
In the end I'd like to achieve something like this:
//For Brand > Products
$p = Brand::find(id)->products;
$product_model = $p[0]->model;
$product_name = $p[0]->data->name;
//For Product > Manufacturer
$p = Product::find(id)->manufacturer;
$product_model = $p->model;
$brand_name = $p->brand->name;
Please tell me what's wrong with my approach all other relationships are working fine except this one.

Find Records Based on Distant belongsToMany > belongsTo Relationship

I'm attempting to display a product page with a list of assets that match a specific asset type. For example, for product "Acme Cream", there are two assets: nutrition-facts.pdf (of type Document) and marketing-video.mp4 (of type Video). On the product page, I'd like to display the first asset that match the 'Video' asset type (if any exist).
I have the following relationships:
The Product model includes a DB column asset_id.
class Product extends Model
{
/**
* The assets that belong to the product.
*/
public function assets()
{
return $this->belongsToMany('App\Asset', 'product_asset');
}
}
The Asset model includes DB columns id and asset_type_id.
class Asset extends Model
{
/**
* Get the asset type that owns the asset.
*/
public function asset_type()
{
return $this->belongsTo('App\AssetType');
}
/**
* The products that belong to the asset.
*/
public function products()
{
return $this->belongsToMany('App\Product', 'product_asset');
}
}
The AssetType model has two DB columns id and name.
class AssetType extends Model
{
/**
* A asset type can have many assets
*/
public function assets()
{
return $this->hasMany('App\Asset');
}
}
How can I efficiently fetch the one product asset by filtering on asset_type? Keep in mind, I've already queried the DB using Product::find(id) and passed that data into the view. Will this require another query (eager loading might help with that). I know I could use a foreach loop, but it seems to me there's gotta be a nicer, more 'eloquent' way.
I'm trying to use it in this situation (pseudo code) on the product detail page (show.blade.php):
if assets matching type 'Video', then display first in this div. Else, don't display the div.
It seems like it should be a simple line:
$product->assets()->asset_type()->where('name', '=', 'Video')->first()
The closest I've come so far to this is this ugly looking thing:
>>> $product = App\Product::with(['assets'])->find(1)
>>> $product->assets()->with(['asset_type' => function ($query) { $query->where('name', '=', 'Video'); }])->get()
However, it still returns all assets, except the "asset_type" attribute is null for those that don't match. Adding ->whereNotNull('asset_type')->get() only results in an error that asset_type column cannot be found.
Also, this sounds like a chance to use the "Has Many Through" relationship, but I'm unclear how to set this up.
Any help is greatly appreciated! Thanks.
You need to eager-load your relationship with filtering:
Assuming you fetch the relationship with your product info
$typeName = 'Video';
$product = App\Product::with([
'asset' => function($query) use($typeName) {
//Filter asset by type
//See: https://laravel.com/docs/5.6/eloquent-relationships#constraining-eager-loads
return $query->whereHas('asset_type',function($query) use($typeName) {
//Filter where the type's name equals..
//Each query is relative to its scope, in this case the 'type' relationship which refers to your 'type' Model
return $query->where('name','=',$typeName);
});
},
//Nested relationship loading: https://laravel.com/docs/5.6/eloquent-relationships#querying-relations
'assets.asset_type'
])
->find(1);
$assets = $product->assets;
Assuming you fetch only the assets
$productId = 1;
$typeName = 'Video';
//Returns a collection of eloquent models
$assets = Asset::whereHas('product',function($query) use ($productId) {
//Filter product by its id
return $query->where('id','=',$productId);
})
->whereHas('asset_type',function($query) use ($typeName) {
//Filter type by its name
return $query->where('name','=',$typeName);
})
->get();

Laravel 5 multi level category

i currently i have 2 tables category & subcategory and everything works just fine, but i need another level of sub category which will be like category->sub->sub for that matter i tried to find a reasonable solution and i end up with bunch of packages to handle that for me.
now the question is
do i have to delete my category tables and their relations to my
another modes before i try any package or i should just add package top of my current tables?
what is the best package for my purpose base on your experience?
thanks in advance.
You don't need to depend on packages to implement this.
You can design your categories table like following:
|----------|------------|---------------|
| id | name | category_id |
|----------|------------|---------------|
Here category_id is nullable field and foreign key referenced to id of categories table.
For category category_id field will be NULL and for sub-category category_id will be it's parent category id. For sub sub category, category_id will be parent sub category id.
In model, you can write relation like following:
Category.php
/**
* Get the sub categories for the category.
*/
public function categories()
{
return $this->hasMany(Category::class);
}
Now you can get your sub categories like $category->categories.
N.B: You don't need the subcategory table, Only one table will do the work.
Update- Show product categories
Update Category.php:
/**
* Get the parent category that owns the category.
*/
public function parent()
{
return $this->belongsTo(Category::class);
}
In Product.php:
/**
* Get the category that owns the product.
*/
public function category()
{
return $this->belongsTo(Category::class);
}
Now, you need to get product category and all of its parents. It'll be an array of categories from parents to child. Then you can show as you wish.
$category = $product->category;
$categories = [$category];
while (!is_null($category) && !is_null($category = $category->parent)) {
$categories.unshift($category);
}
// $categories = ['parent category', 'sub category', 'sub sub category' ..]
Show category title sequentially
foreach ($categories as $category) {
echo $category->title . '<br>';
}

Laravel: return multiple relationships

I have the following table:
The table is called user_eggs and it stores the user eggs.
eggs are items with additional data (hatch_time)
As you can see, user 2 has 2 eggs, which are items 46 and 47.
My items table stores the item general information such as name, image, description, etc...
How I can return the user eggs using $user->eggs() including the item data in my items table of the egg item_id?
I tried:
User Model:
/**
* Get the eggs
*/
public function eggs()
{
return $this->belongsToMany(Egg::Class, 'user_eggs','user_id','item_id')
->withPivot('id','hatch_time');
}
but $user->eggs() returns an empty array.
Any ideas?
A simple approach will be:
in your UserEgg model define:
/**
* Get the user associated with egg.
*/
public function _user()
{
return $this->belongsTo('App\User','user_id');
}
/**
* Get the item associated with egg.
*/
public function item()
{
return $this->belongsTo('App\Item','item_id');
}
then in your controller:
use the model to extract everything like this:
$userEggs = UserEgg::where('user_id',2)->get();
foreach($userEggs as $userEgg){
$associateduser = $userEgg->_user;
$associatedItem = $userEgg->item;
}
Short answer
If you loop through the user's eggs:
foreach($user->eggs as $egg){
$item = Item::find($egg->pivot->item_id);
}
If you want to query:
$user->eggs()->wherePivot('item_id', 1)->get();
Long answer
From the Laravel Documentation
Retrieving Intermediate Table Columns
As you have already learned, working with many-to-many relations requires the presence of an intermediate table. Eloquent provides some very helpful ways of interacting with this table. For example, let's assume our User object has many Role objects that it is related to. After accessing this relationship, we may access the intermediate table using the pivot attribute on the models:
$user = App\User::find(1);
foreach ($user->roles as $role) {
echo $role->pivot->created_at;
}
Notice that each Role model we retrieve is automatically assigned a pivot attribute. This attribute contains a model representing the intermediate table, and may be used like any other Eloquent model.
By default, only the model keys will be present on the pivot object. If your pivot table contains extra attributes, you must specify them when defining the relationship:
return $this->belongsToMany('App\Role')->withPivot('column1', 'column2');
If you want your pivot table to have automatically maintained created_at and updated_at timestamps, use the withTimestamps method on the relationship definition:
return $this->belongsToMany('App\Role')->withTimestamps();
Filtering Relationships Via Intermediate Table Columns
You can also filter the results returned by belongsToMany using the wherePivot and wherePivotIn methods when defining the relationship:
return $this->belongsToMany('App\Role')->wherePivot('approved', 1);
return $this->belongsToMany('App\Role')->wherePivotIn('priority', [1, 2]);

Laravel - Relationship between a table with two other tables

I have 3 tables: users, pools, permissions. Users and Pools have a pivot table since they have a many-to-many relationship.
Permission entries are created by me only manually. Then, each pool can assign permissions to their own users as they see fit. Therefore, the pivot table for linking users to permissions needs to have both pool_id and user_id and permission_id
So how does this pivot table work? How do I make a three-way pivot table?
EDIT: My question is basically asked here, with no satisfactory answer!
For pivot table linking 3 models you need additional data send when attaching models like in this answer: Laravel - Pivot table for three models - how to insert related models?:
$model->relation()->attach($otherModel, ['thirdModelKeyName' => 'thirdModelKeyValue']);
You can create custom relation class, say ThreeBelongsToMany, that will extend belongsToMany and override attach() method.
Then override belongsToMany() method on your models, that are involved in such relation (or use a Trait there) to return mentioned custom relation class instead of generic belongsToMany when applicable.
The attach() method could look like this:
public function attach($id, array $attributes = array(), $touch = true, $otherId = null)
{
if ( ! is_null($otherId))
{
if ($otherId instanceof Model) $otherId = $otherId->getKey();
$attributes[$this->getThirdKey()] = $otherId;
}
return parent::attach($id, $attributes, $touch);
}
Also you can use some helpers to fetch those related models, like for example (key names should be obtained with methods for flexibility, they are hard coded to make it short):
/**
* Accessor for far related model (thru pivot)
*
* #return mixed
*/
public function getTrackAttribute()
{
if (is_null($this->pivot)) return null;
if (is_null($this->pivot->track_id)) return null;
return ($this->pivot->track) ?: $this->pivot->track = Track::find($this->pivot->track_id);
}

Resources