Laravel scope query with nested models in 3-way pivot table - php

I have a Product Model that needs to be connected with Processings, that describe how the Product can be manipulated, and Positions, that describe where the Product can be manipulated.
In other words, a Product can be processed into multiple positions.
The "Product Data" should be returned with nested processings into positions, so that, for every Position into the database, a Product can be associated with related Processings, in a scheme similar to the one below:
Products [
product_attributes,
Positions [
position_attributes,
Processings: [
processing_attributes
]
]
]
So, I have created a 3-way Pivot Table with the following code:
Schema::create('product_position_processing', function (Blueprint $table) {
// 3-way pivot
$table->integer('product_id')->unsigned()->index();
$table->foreign('product_id', 'ppp_aid_foreign')->references('id')->on('products')->onDelete('cascade');
$table->integer('position_id')->unsigned()->index();
$table->foreign('position_id', 'ppp_bid_foreign')->references('id')->on('positions')->onDelete('cascade');
$table->integer('processing_id')->unsigned()->index();
$table->foreign('processing_id', 'ppp_cid_foreign')->references('id')->on('processings')->onDelete('cascade');
$table->primary(['product_id', 'position_id', 'processing_id'], 'index-base-combined');
// these attributes define the minimum and maximum order amount based on the product + position + processing combination
$table->integer('minimum_order_amount')->nullable();
$table->integer('maximum_order_amount')->nullable();
$table->timestamps();
});
Then, I have defined the relations between the 3 different models in this way:
Processing Model
class Processing extends Model {
use SoftDeletes;
protected $dates = ['deleted_at'];
protected $guarded = ['id', 'created_at', 'updated_at'];
protected $casts = [
'is_publicly_hidden' => 'boolean'
];
protected $hidden = ['pivot'];
public function products() {
return $this -> belongsToMany ( Product::class, 'product_position_processing', 'processing_id', 'product_id' );
}
public function positions () {
return $this -> belongsToMany ( Position::class, 'product_position_processing', 'processing_id', 'position_id' );
}
...
...
}
Position Model
class Position extends Model {
use SoftDeletes;
protected $dates = ['deleted_at'];
protected $guarded = ['id', 'created_at', 'updated_at'];
protected $hidden = ['pivot'];
public function products() {
return $this->belongsToMany ( Product::class, 'product_position_processing', 'position_id', 'product_id' );
}
public function processings ( ) {
return $this -> belongsToMany ( Processing::class, 'product_position_processing', 'position_id', 'processing_id' );
}
...
...
}
Product Model
class Product extends Model implements SluggableInterface {
use SoftDeletes;
use SluggableTrait;
protected $dates = ['deleted_at'];
protected $guarded = ['id', 'created_at', 'updated_at'];
protected $sluggable = [
'build_from' => 'name',
'save_to' => 'slug',
'include_trashed' => true
];
...
...
public function processings() {
return $this->belongsToMany(Processing::class, 'product_position_processing', 'product_id', 'processing_id');
}
public function positions() {
return $this->belongsToMany(Position::class, 'product_position_processing', 'product_id', 'position_id');
}
...
...
public function scopeWithCompleteData ($query) {
return $query->with([
...
...
'positions' => function($query) {
return $query->with(['processings' => function ( $query ) {
return $query->select('id', 'name')->groupBy('processing_id');
}])->groupBy('position_id');
},
...
...
]);
}
}
Now, the Products are returned in json via the ProductsController that, with the following code, returns a JSON in this format.
Product Getter:
...
...
$products = Product::withCompleteData() -> get();
return $this -> ok ( $products );
Example "ALL Products" #get JSON:
{
"status": "OK",
"data": [
{
"id": 1,
"category_id": 1,
"subcategory_id": 0,
"brand_id": 1,
"sku": "BD615149D6",
...
...
"positions": [
{
"id": 1,
"name": "POS1",
"processings": [
{
"id": 1,
"name": "PROC1",
"pivot": {
"position_id": 1,
"processing_id": 1
}
},
{
"id": 2,
"name": "PROC2",
"pivot": {
"position_id": 1,
"processing_id": 2
}
},
{
"id": 3,
"name": "PROC3",
"pivot": {
"position_id": 1,
"processing_id": 3
}
}
]
},
{
"id": 2,
"name": "POS2",
"created_at": "2016-08-01 07:39:11",
"updated_at": null,
"deleted_at": null,
"processings": []
},
{
"id": 3,
"name": "POS3",
"created_at": "2016-08-01 07:39:11",
"updated_at": null,
"deleted_at": null,
"processings": []
}
]
},
{
"id": 2,
"category_id": 2,
"subcategory_id": 0,
"brand_id": 2,
"sku": "BD615149D6",
...
...
"positions": [
{
"id": 5,
"name": "POS5",
"created_at": "2016-08-01 07:39:11",
"updated_at": null,
"deleted_at": null,
"processings": []
}
]
}
]
}
The problem is, the data returned is not correct. The Query Builder seems to connect the various relationships in a strange way. Unfortunately I'm still not enough proactive in understanding what lies below the magic of the query builder, and don't know how to debug and where to make modifications to the code.
Can anyone help out with this 3-way pivot table with nested objects into the Laravel query builder?

Related

Laravel Eloquent how to get relationship self object?

I have tables with below relationship
And my HousingAdvertisement model has
public function nearPlaces()
{
return $this->hasMany(HousingAdNearPlace::class);
}
and HousingAdNearPlace
public function nearPlace()
{
return $this->hasOne(NearPlace::class, 'id');
}
when I make query like this:
HousingAdvertisement::with('nearPlaces.nearPlace')->where('user_id', '=', auth()->user()->id)->get();
I got HousingAdNearPlace object in HousingAdvertisement model:
[...
{
...,
"near_places": [
{
"id": 27,
"housing_advertisement_id": 48,
"near_place_id": 3,
"created_at": "2021-06-29T12:23:35.000000Z",
"updated_at": "2021-06-29T12:23:35.000000Z",
"near_place": null
},
{
"id": 28,
"housing_advertisement_id": 48,
"near_place_id": 4,
"created_at": "2021-06-29T12:23:35.000000Z",
"updated_at": "2021-06-29T12:23:35.000000Z",
"near_place": null
}
]
...]
How can I got self NearPlace model like this:
[...
{
...,
"near_places": [
{
"id": 3,
"name": "Park",
"slug": "park",
"created_at": "2021-06-29T06:25:57.000000Z",
"updated_at": "2021-06-29T06:25:57.000000Z"
},
{
"id": 4,
"name": "Beach",
"slug": "beach",
"created_at": "2021-06-29T06:25:57.000000Z",
"updated_at": "2021-06-29T06:25:57.000000Z"
}
]
...]
You need "Has Many Through" relationship on HousingAdvertisement
public function nearPlaces()
{
return $this->hasManyThrough(NearPlace::class, HousingAdNearPlace::class);
}
And also define id keys as in ducumentation: https://laravel.com/docs/8.x/eloquent-relationships#has-many-through-key-conventions
You need to use laravel Model Accessor for generating slug from HousingAdvertisement's name row.
For that use -
public function slug()
{
return Str::lower($this->name);
}
This will make a slug from name attribute. Add it on your HousingAdvertisement model. Now you need to cast it with query builder.
For that use -
protected $casts = [
'slug' => 'string',
];
Add this on your HousingAdvertisement model. Finally you can query your database like -
HousingAdvertisement::find(auth()->user()->id)->get(['id', 'name', 'slug', 'created_at', 'updated_at']);

Laravel : How return nested JSON from models

I have an app contain many tables :
products table , order table , order_items table.
I made models for all of them as below :
class Product extends Model
{
use HasFactory;
protected $fillable = [
'product_name',
'category_id',
'price',
'sale',
'counter',
'isfavore'
];
public function Order_item()
{
return $this->hasMany('App\Models\Order_item');
}
public function category()
{
return $this->belongsTo('App\Models\Category' ,'category_id');
}
}
class Order extends Model
{
use HasFactory;
protected $fillable = [
'user_id',
'status',
];
public function order_item()
{
return $this->hasMany('App\Models\Order_item');
}
public function user()
{
return $this->belongsTo('App\Models\User' ,'user_id');
}
}
class Order_item extends Model
{
use HasFactory;
protected $fillable = [
'order_id',
'product_id',
'quantity',
];
public function order()
{
return $this->belongsTo('App\Models\Order' ,'order_id');
}
public function product()
{
return $this->belongsTo('App\Models\Product' ,'product_id');
}
}
I want to display details like below depend on order_id :
{
"id": 7,
"user_id": 23,
"status": "ordered",
"order_item": [
{
"id": 30,
"order_id": 7,
"product_id": 5,
"quantity": 1,
"product_name" : "banana", //-----------------------want to add from product model
"price" : "2000" //-----------------------want to add from product model
},
{
"id": 31,
"order_id": 7,
"product_id": 1,
"quantity": 1,
"product_name" : "apple",
"price" : "1500"
}
]
}
I create this controller which show same result above without product name and price:
public function get_order($id){
$order=Order::find($id);
$order->order_item;
return $order;
}
Now just I need to add product name and price from product model ,How can I do that?
You have to load the relationships.
Try this:
public function get_order($id){
$order=Order::find($id);
$order_items = Order_item::with('product')->where('order_id',$id)->get();
return $order;
}
I get best answer from #Develope and #danish-khan-I ,just change controller to :
public function get_order($id){
$order=Order::with('order_item.product')->find($id);
return $order;
}
class Product extends Model
{
use HasFactory;
protected $fillable = [
'product_name',
'category_id',
'price',
'sale',
'counter',
'isfavore'
];
public function Order_item()
{
return $this->hasMany('App\Models\Order_item');
}
public function category()
{
return $this->belongsTo('App\Models\Category' ,'category_id');
}
}
class Order extends Model
{
use HasFactory;
protected $fillable = [
'user_id',
'status',
];
public function order_item()
{
return $this->hasMany('App\Models\Order_item');
}
public function user()
{
return $this->belongsTo('App\Models\User' ,'user_id');
}
}
class Order_item extends Model
{
use HasFactory;
protected $fillable = [
'order_id',
'product_id',
'quantity',
];
public function order()
{
return $this->belongsTo('App\Models\Order' ,'order_id');
}
public function product()
{
return $this->belongsTo('App\Models\Product' ,'product_id');
}
}
--- CONTROLLER ---
public function get_order($id){
$order=Order::with('order_item.product')->where('id',$id)->first();
}
---- RESPONSE ----
{
"id": 7,
"user_id": 23,
"status": "ordered",
"order_item": [
{
"id": 30,
"order_id": 7,
"product_id": 5,
"quantity": 1,
"product" : {
"product_name" : "banana",
"price" : "2000"
}
},
{
"id": 31,
"order_id": 7,
"product_id": 1,
"quantity": 1,
"product" : {
"product_name" : "banana",
"price" : "2000"
}
}
]
}

Laravel API Resource: How to return infinite nested array of categories with their children?

I use Laravel 6.x and below is my response JSON.
{
"data": [
{
"id": 1,
"name": "quam",
"parent_id": 0
},
{
"id": 2,
"name": "quia",
"parent_id": 1
},
{
"id": 3,
"name": "beatae",
"parent_id": 1
},
{
"id": 4,
"name": "aut",
"parent_id": 2
},
{
"id": 5,
"name": "provident",
"parent_id": 0
},
{
"id": 6,
"name": "voluptate",
"parent_id": 0
},
{
"id": 7,
"name": "vel",
"parent_id": 2
},
{
"id": 8,
"name": "sed",
"parent_id": 3
},
{
"id": 9,
"name": "voluptates",
"parent_id": 0
},
{
"id": 10,
"name": "adipisci",
"parent_id": 6
},
...
]
}
But it want to be like this:
{
"data": [
{
"id": 1,
"name": "quam",
"children": [
{
"id": 2,
"name": "quam"
"children":[
{
"id": 4,
"name": "aut"
},
{
"id": 7,
"name": "vel",
"children": [
...
]
}
]
},
{
"id": 3,
"name": "quam",
"children":[
{
"id": 8,
"name": "sed"
}
]
},
]
},
{
"id": 5,
"name": "provident"
},
{
"id": 6,
"name": "voluptate",
"children": [
{
"id": 10,
"name": "adipisci"
}
]
},
{
"id": 9,
"name": "voluptates"
},
...
}
In fact, I want to remove the parent_id attribute and add children array to each object that consists of other objects have this parent_id.
CategoryResource.php
class CategoryResource extends JsonResource
{
public function toArray($request)
{
return [
'id' => $this->id,
'name' => $this->name,
'parent_id' => $this->parent_id,
];
}
}
CategoryController.php
class CategoryController extends Controller
{
public function index()
{
return CategoryResource::collection(Category::all());
}
}
How can I implement this structure?
From what I see your problem is just the relations. To create a "tree resource" you have to load a lot of relations.
IMHO it's not a good choice, expecially if you have to load a lot of elements but generally, structures like these may be a dangerous bottleneck.
Anyway... The easy way it's the eager loading, so you have to add your base model with this attribute (have a look at the official documentation)
class Parent extends Model {
// [...]
/**
* The relationships that should always be loaded.
*
* #var array
*/
protected $with = ['children'];
// [...]
public function children() {
return $this->hasMany('whatever');
}
}
next you have to update your JSON Resource as follows (also for this, have a look at the official documentation about resource relationships).
class CategoryResource extends JsonResource
{
public function toArray($request)
{
return [
'id' => $this->id,
'name' => $this->name,
'parent_it' => $this->parent_id,
'childrend' => ChildrenResource::collection($this->whenLoaded('children')),
];
}
}
In this way, since everytime you request a Parent it will eager load its children, the resource will recursively map into a Child resource each relation down to the leaf.
Assuming Category is an eloquent model, Model's can reference themselves in relationships and those relationships can be recursive.
class Category extends Model
{
protected $hidden = ['parent_id'];
public function children()
{
return $this->hasMany('App\Category', 'parent_id')->with('children');
}
}
So now getting the structure you want is as simple as
Category::with('children:id,name,parent_id')->get('id', 'name', 'parent_id');
You have to include the parent_id in the select statement in order for the relationships to work but the $hidden variable I added to the model keeps parent_id from showing up in serialized results. The only caveat here is that all categories will have a children property, which will be empty for Categories that don't have children. So in your toArray method you will have to check for empty children[] and exclude them
First you need to define a relation for retrieving children of the main category which has no parents with this method
/**
* get sub product categories of this category
*
* #return mixed
*/
public function childCategories()
{
return $this->hasMany(Category::class,'parent_id');
}
Then you need to load the children of children categories with this method :
/**
* get recursive all sub categories of this category.
*
* #return mixed
*/
public function childrenCategories()
{
return $this->hasMany(Category::class,'parent_id')->with('childCategories');
}
Now you can retrieve them with this static function
/**
* get all Main categories with children.
*
* #return mixed
*/
public static function allMainCategoriesWithChildren()
{
return self::whereNull('parent_id')->with('childrenCategories')->get();
}
now you can use it in your resource or return it in controller directly
use App\Category;
return Category::allMainCategoriesWithChildren();

Laravel keeps pulling back all relations despite using `with()`

For some bizarre reason my Laravel 5.6 app continues to return a User object with all of its relations.
My query in the Api/UserController:
public function show($user_id)
{
return User::with('meta', 'roles')->find($user_id);
}
The response:
{
"id": 1,
"name": "Admin",
"email": "admin#example.com",
"company_id": 1,
"meta": {
"id": 1,
"user_id": 1,
"laptop": 0,
"mobile": 0,
"created_at": "2018-03-07 14:58:41",
"updated_at": "2018-04-06 16:13:10"
},
"roles": [
{
"id": 2,
"name": "admin",
"label": "Admin",
"permissions": null,
"pivot": {
"user_id": 1,
"role_id": 2
}
}
],
"company": {
"id": 1,
"name": "Company",
"active": 1,
"created_at": "2018-04-12 15:06:01",
"updated_at": "2018-05-15 11:20:15",
"is_max_user_limit_reached": true
}
}
The route (inside routes/api.php):
Route::group(['middleware' => 'auth:api'], function () {
Route::resource('/users', 'Api\UserController', ['as' => 'api']);
});
User model:
namespace App\Models;
use App\Models\Role;
class User extends Authenticatable implements HasMedia
{
use HasApiTokens, Notifiable, Billable, HasMediaTrait;
protected $table = 'users';
protected $fillable = ['name', 'email', 'password', 'is_active', 'company_id', 'stripe_id', 'card_brand', 'card_last_four', 'trial_ends_at'];
protected $hidden = ['password', 'remember_token','card_brand', 'card_last_four'];
protected $appends = ['extra', 'is_staff_only', 'first_four_training_sections', 'is_free_tier', 'is_agency_tier', 'is_team_tier', 'is_enterprise_tier'];
public static $rules = [
// create rules
'name' => 'required',
'email' => 'required|email|unique:users'
];
public function meta()
{
return $this->hasOne(UserMeta::class);
}
public function company()
{
return $this->belongsTo(Company::class, 'company_id')->where('active', 1);
}
public function roles()
{
return $this->belongsToMany(Role::class);
}
public function getExtraAttribute()
{
return [
'roles' => [
'cpo' => (int)$this->hasRole('cpo'),
'ap' => (int)$this->hasRole('ap'),
'cao' => (int)$this->hasRole('cao'),
]
];
}
public function getIsStaffOnlyAttribute()
{
if($this->roles->count() == 1 && $this->hasRole('staff')) {
return true;
}
return false;
}
public function getIsFreeTierAttribute()
{
return $this->company->subscription_tier == 0;
}
public function getIsAgencyTierAttribute()
{
return $this->company->subscription_tier == 1;
}
public function getIsTeamTierAttribute()
{
return $this->company->subscription_tier == 2;
}
public function getIsEnterpriseTierAttribute()
{
return $this->company->subscription_tier == 3;
}
public function getFirstFourTrainingSectionsAttribute() {
return UserTrainingSection::where('user_id', $this->id)->orderBy('id')->take(4)->get();
}
}
This is very strange behavior. I am asking for only the roles and meta related data but it's always returning every single relation on the User model.
Even if I try User::find($user_id); it will still return all the relations.
Anyone know what's going on here?
I'm using Laravel 5.6 and PHP 7.2
You can use the $hidden property to remove relationships from the response:
protected $hidden = ['company', ...];
You can also temporarily remove relationships:
$user->addHidden('company');

Laravel fetch only pivot columns in many to many relationship

I have a User model that relates to a Section model through a pivot model UserTrainingSection. I have a pivot table that stores the foreign keys for both tables called section_user.
I have 2 additional pivot table columns called completed and completed_date.
The problem I am having is that when I fetch my data it returns all the columns from the User model along with the additional pivot columns.
class Section extends Model
{
public $table = "sections";
public $fillable = [
'id',
'name',
'description',
'parent',
'position',
'completion_percentage'
];
public function users()
{
return $this->belongsToMany(User::class, 'section_user')->using('App\Models\UserTrainingSection')->withPivot('completed', 'completed_date');
}
}
In my API service I fetch my data like this:
Section::with(['users' => function ($q) {
$q->where('users.id', Auth::user()->id);
}])->first();
How do I only return the pivot table columns and exclude the columns from the user table?
At the moment it returns something like this:
"sections": [
{
"id": 2,
"name": "Subsection 1 training",
"description": null,
"parent": 1,
"position": 2,
"completion_percentage": null,
"created_at": "2018-05-04 09:54:09",
"updated_at": "2018-05-11 09:14:59",
"users": [
{
"id": 1,
"name": "Test",
"email": "test#test.com",
"created_at": "12-04-2018 14:51:42",
"updated_at": "2018-04-19 14:14:36",
"pivot": {
"section_id": 2,
"user_id": 1,
"completed": 1,
"completed_date": "31/05/2018",
"expires": "31/05/2019"
},
}
]
}
]
What I would like to return is something like this:
"sections": [
{
"id": 2,
"name": "Subsection 1 training",
"description": null,
"parent": 1,
"position": 2,
"completion_percentage": null,
"created_at": "2018-05-04 09:54:09",
"updated_at": "2018-05-11 09:14:59",
"users": [
{
"pivot": {
"section_id": 2,
"user_id": 1,
"completed": 1,
"completed_date": "31/05/2018",
"expires": "31/05/2019"
}
}
]
}
]
So I basically get rid of the user data I don't want and only return the pivot data.
As per your expected output i guess you need data from junction model only, If that is the case i suggest you to define direct mapping of Section and UserTrainingSection
class Section extends Model
{
public function training_users()
{
return $this->hasMany('App\Models\UserTrainingSection', 'section_id');
}
public function users()
{
return $this->belongsToMany(User::class, 'section_user')->using('App\Models\UserTrainingSection')->withPivot('completed', 'completed_date');
}
}
In query you can simply do
Section::with('training_users')->first();
In your with statement you are fetching all User data by definition.
You can add a select statement to only get the results you want. You can add that either in the relationship in your model, or where you access the model in your API.
Example 1:
Section::with(['users' => function ($q) {
$q->where('users.id', Auth::user()->id)->select('completed', 'completed_date');
}])->first();
Example 2:
public function users()
{
return $this->belongsToMany(User::class, 'section_user')->using('App\Models\UserTrainingSection')->withPivot('completed', 'completed_date')->select('completed', 'completed_date');
}
(Not tested)
See the documentation

Categories