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();
}
}
Related
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'],
]);
}])
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 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);
}
}
I have two tables: assessments and benchmarks. benchmarks has a field called content. There is a many to many relationship between them: assessment_benchmark. I want to sort a collection of records from the assessment_benchmark table by the content attribute of the corresponding benchmark. I have tried:
$sorted = AssessmentBenchmark::all()->sortBy(function($assessmentBenchmark){
return $assessmentBenchmark->benchmark->content;
});
But this just does not work (it just returns the original order). However, when I return $assessmentBenchmark->comment for example, it does work (comment is a field in assessment_benchmark).
The models look like this:
class AssessmentBenchmark extends Model
{
public function benchmark()
{
return $this->belongsTo(Benchmark::class);
}
public function assessment()
{
return $this->belongsTo(Assessment::class);
}
}
class Benchmark extends Model
{
public function assessments()
{
return $this->belongsToMany(Assessment::class);
}
}
class Assessment extends Model
{
public function benchmarks()
{
return $this->belongsToMany(Benchmark::class);
}
}
Well, you can use below query for sorting, I'm gonna use Assessment model, because, I'm never use pivot modal before. Actually, I never had pivot model..
$assessments = Assessment::with(["benchmarks"=>function($query){
$query->orderBy("content","DESC");
}])
With method aşso provide you eagerloading, so when you put $assessments in iteration , you won't make new query for each relation
From chat discussion, it found that you have pivot field and for that you can change your belongsToMany relationship like this
class Benchmark extends Model
{
public function assessments()
{
return $this->belongsToMany(Assessment::class)->withPivot('comment','score')->withTimestamps();
}
}
class Assessment extends Model
{
public function benchmarks()
{
return $this->belongsToMany(Benchmark::class)->withPivot('comment','score')->withTimestamps();
}
}
Now fetch data
$assessment = Assessment::with(['benchmarks' => function($query){
$query->orderBy('content', 'desc');
}])->find($assessmentId);
In view you can render it like this
#foreach($assessment->benchmarks as $benchmark)
<tr>
<td>{{$benchmark->id}}</td>
<td>{{$benchmark->name}}</td>
<td>{{$benchmark->pivot->score}}</td>
<td>{{$benchmark->pivot->comment}}</td>
</tr>
#endforeach
For update you can use updateExistingPivot
For details check ManyToMany relationship https://laravel.com/docs/5.6/eloquent-relationships#many-to-many
I'm learning Laravel right now and i have following tables and resources (models, controllers, ect.):
tickets
- id
- title
- projectID
- statusID
projects
- id
- title
status
- id
- title
I have to make a list of my Tickets on the Startpage. Not nessesary to say that i need the Project- and Statustiltles and not the IDs. Currently i do:
Route::get('/', function()
{
$tickets = Ticket::all();
return View::make('layout')->with('tickets', $tickets);
});
My current output is:
tickets->id, tickets->title, tickets->projectID, tickets->statusID
The output i want is
tickets->id, tickets->title, tickets->projects->title, tickets->status->title
So i hope anyone can understand what i'm trying to ask here and maybe provide me some help. Thank you!
Resolution: I had to set the foreign_keys first in my DB. Then i used the relationships mentioned in the answers and it works fine.
My Model:
class Ticket extends \Eloquent {
protected $fillable = [];
public function project()
{
return $this->hasOne('Project', 'id', 'projectID');
}
public function status()
{
return $this->hasOne('Status', 'id', 'statusID');
}
}
My View:
#foreach($tickets as $key => $value)
...
<td>{{ $value->project->title }}</td>
<td>{{ $value->status->title }}</td>
...
#endforeach
If you configure you relationships correctly you can do that without problems using the Laravel Eager Loading feature, for example:
Eager Loading (Laravel docs)
Eager loading exists to alleviate the N + 1 query problem...
class Ticket extends Eloquent {
public function project()
{
return $this->belongsTo('Project', 'projectID', 'id');
}
public function status()
{
return $this->belongsTo('Status', 'statusID', 'id');
}
}
Now, just call the fields you want, for example:
foreach (Ticket::all() as $ticket)
{
echo $ticket->project->title;
echo $ticket->status->title;
}
Obs.: In your return object/array you can't see the relationships fields unless you do manual joins, etc. So, just configure your relationships and call the fields you want.
Sorry for my english
Define relationships specifying custom foreign keys (defaults would be status_id and project_id for your models):
// Ticket model
public function project()
{
return $this->belongsTo('Project', 'projectID');
}
public function status()
{
return $this->belongsTo('Status', 'statusID');
}
Then eager load related models:
$tickets = Ticket::with('project','status')->get();
// accessing:
foreach ($tickets as $ticket)
{
$ticket->status; // Status model with all its properties
$ticket->project; // Project model
}