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);
}
}
Related
I want to get a relation, here is my code:
OrderController.php
public function orders()
{
$orders = Order::with('packages')->OrderBy('created_at', 'DESC')->paginate();
return view('admin/orders/orders')->with(compact('orders'));
}
Order.php
function packages(){
return $this->hasMany('App\Models\Package', 'id','cart_items');
}
but the problem is cart_items contains a value like this (5,6) comma separated, how can I get relation hasMany from array?
Its's not possible by ORM need to work with mutator and accessor. Please remove packages method and try this in your Order model;
protected $appends = ['packages'];
public function getPackagesAttribute ()
{
if ($this->cart_items) {
$cart_items = explode(',', $this->cart_items);
if(count($cart_items)>0) {
return Package::whereIn('id', $cart_items)->get();
}
} return null;
}
I'm trying to create offers and assign them to parent categories, to be more specific i have an Offer model and inside the offer model i have this many to many relationship
public function category() {
return $this->belongsToMany(Category::class);
}
I want the above function to return ONLY the categories which have NULL parent_category which mean they are the parent categories. Is it possible with the above code?
Without knowing the entire scope of your project, I'd suggest one of the following: either change the name of the relation (A) or keep the relation as is and query it when you need it (B).
Option A -
public function childCategory() {
return $this->belongsToMany(Category::class)->whereNull('parent_category');
}
Option B -
public function category() {
return $this->belongsToMany(Category::class);
}
$offer = Offer::with('category')
->whereHas('category' function ($query) {
$query->whereNull('parent_category');
});
public function category() {
return $this->belongsToMany(Category::class)->where('parent_category', null);
}
I am trying to retrieve the inverse side of the one to many relationship where the method is camelCase. i.e
Owning Class: OneToMany
class Brand extends Model
{
public function products()
{
return $this->hasMany(Product::class);
}
}
Owned Class
class Product extends Model
{
public function MyBrand()
{
return $this->belongsTo(Brand::class);
}
}
retrieve the inverse related model like this:
$product = Product::find(1);
$brand = $product->my_brand;
dd($brand->name);
Error, Trying to get property 'name' of non-object
I also tried this:
$brand = $product->myBrand;
it did not work.
however, if i make my method like below it works:
public function brand()
{
return $this->belongsTo(Brand::class);
}
question is : how to make it work when the method is in CameCase ?
Try this,
public function my_brand()
{
return $this->belongsTo(Brand::class);
}
And call this relation as,
$product = Product::find(1);
$brand = $product->my_brand;
dd($brand->name);
You need to change, method name in brand(), because Eloquent will automatically determine the proper foreign key column in the Brands table.
https://laravel.com/docs/7.x/eloquent-relationships#one-to-many
If you want to keep it MyBrand() you need to specify foreign key:
public function MyBrand()
{
return $this->belongsTo(Brand::class,'product_id');
}
Specify it's foreign key in relationship
class Product extends Model
{
public function MyBrand()
{
return $this->belongsTo(Brand::class,'brand_id');
//brand_id is foreign key in your product table
}
}
$product = Product::find(1)->with('MyBrand'); //But it will be good to eager load it
$product->MyBrand->name; //It will definitely return name now
I have 2 models that are joined by a relationship which has a composite key - these are Product and Category. I need to use soft deletes on all tables, so that the models and relationships can be restored if required.
In my Product model I have:
function categories()
{
return $this->belongsToMany('App\Category', 'product_categories')->whereNull('product_categories.deleted_at')->withTimestamps();
}
In my Category model I have:
function products()
{
return $this->belongsToMany('App\Product', 'product_categories')->whereNull('product_categories.deleted_at')->withTimestamps();
}
I read elsewhere about chaining the whereNull method, as queries like $category->products->contains($product->id) were otherwise returning the soft deleted relationships.
My question is what is the best way to handle deleting and restoring these soft deleted relationships? For restoring, for example, I tried:
$product->categories()->restore($category_id);
The above produced an SQL error saying the deleted_at field was ambiguous (because it joined the categories table to product_categories).
Update - It appears the root issue is that the BelongsToMany class does not support soft deletes - so attach, detach and sync all perform hard deletes. What would be the best approach to overriding this class?
Basically, there will be only one deleted_at field and instead of using the $product->categories() use two custom (common) methods in both (Product and Category) models for example, you may create a trait like this:
// SoftDeletePC.php
trait SoftDeletePC {
// SoftDelete
public function softDeleteProductCategory($productId, $categoryId)
{
\DB::table('product_categories')
->where('product_id', $productId)
->where('category_id', $categoryId)
->update(['deleted_at' => \DB::raw('NOW()')]);
}
// Restore
public function restoreProductCategory($productId, $categoryId)
{
\DB::table('product_categories')
->where('product_id', $productId)
->where('category_id', $categoryId)
->update(['deleted_at' => null]);
}
}
Then use this trait in both models using use TraitProductCategory and call the method from both models for example:
// App/Product.php
class product extends Model {
use SoftDeletePC;
}
// App/Category.php
class Category extends Model {
use SoftDeletePC;
}
So, instead of using this:
Product->find(1)->categories()->restore(2);
You may use something like this:
$product = Product->find(1);
$product->softDeleteProductCategory(1, 2); // Set timestamp to deleted_at
$product->restoreProductCategory(1, 2); // Set null to deleted_at
Hope this may work for you.
I have ended up creating some custom methods in my Product model to accomplish what I require - not my ideal solution, but it works nevertheless. My custom sync looks like this:
class Product extends Model
{
// update relationship to categories
function categories_sync($category_ids)
{
// categories
$existing_category_ids = $this->categories()->lists('category_id')->all();
$trashed_category_ids = $this->categories('onlyTrashed')->lists('category_id')->all();
if(is_array($category_ids)) {
foreach($category_ids as $category_id) {
if(in_array($category_id, $trashed_category_ids)) {
$this->categories()->updateExistingPivot($category_id, ['deleted_at' => null]);
}
elseif(!in_array($category_id, $existing_category_ids)) {
$this->categories()->attach($category_id);
}
}
foreach($existing_category_ids as $category_id) {
if(!in_array($category_id, $category_ids)) {
$this->categories()->updateExistingPivot($category_id, ['deleted_at' => date('YmdHis')]);
}
}
}
else {
foreach($existing_category_ids as $category_id) {
$this->categories()->updateExistingPivot($category_id, ['deleted_at' => date('YmdHis')]);
}
}
}
}
The above relies upon an extended categories() method:
// relationship to categories
function categories($trashed=false)
{
if($trashed=='withTrashed') {
return $this->belongsToMany('App\Category', 'product_categories')->withTimestamps();
}
elseif($trashed=='onlyTrashed') {
return $this->belongsToMany('App\Category', 'product_categories')->whereNotNull('product_categories.deleted_at')->withTimestamps();
}
else {
return $this->belongsToMany('App\Category', 'product_categories')->whereNull('product_categories.deleted_at')->withTimestamps();
}
}
I have 3 models: Shop, Products and Tags. Shop and Products are in one to many relation, and Products to Tags many to many.
I want to grab for each Shop all unique Tags (since many products can have same tags).
class Shop extends Eloquent {
public function products() {
return $this->hasMany('Product');
}
}
class Product extends Eloquent {
public function shop() {
return $this->belongsTo('Shop');
}
public function tags() {
return $this->belongsToMany('Tag');
}
}
class Tag extends Eloquent {
public function products() {
return $this->belongsToMany('Product');
}
}
One of the solutions that I came up with is following. Problem is that I don't get unique tags. There is a solution to put another foreach loop to go thru tags array and compare id in tag object. I would like to optimize a little bit, what do you think is better/cleaner solution?
class Shop extends Eloquent {
...
public function getTagsAttribute() {
$tags = array();
foreach($this->products as $product)
{
foreach ($product->tags as $tag)
{
$tags[] = $tag;
}
}
return $tags;
}
}
#WereWolf's method will work for you, however here's a trick that will work for all the relations:
$shop = Shop::with(['products.tags' => function ($q) use (&$tags) {
$tags = $q->get()->unique();
}])->find($someId);
// then:
$tags; // collection of unique tags related to your shop through the products
Mind that each of the $tags will have pivot property, since it's a belongsToMany relation, but obviously you don't rely on that.
Probably you may try this:
$tags = Tag::has('products')->get();
This will return all the Tags that's bound to any Product. If necessary, you may also use distinct, like this, but I think it's not necessary in this case:
$tags = Tag::has('products')->distinct()->get();
Update: Then you may try something like this:
public function getTagsAttribute()
{
$shopId = $this->id;
$tags = Tag::whereHas('products', function($query) use($shopId) {
$query->where('products.shop_id', $shopId);
})->get();
return $tags;
}