An Order have many ordered items
An Order's ordered items can either be a User or Product
What I am looking for is a way to retrieve all morphed objects to an Order. Instead of $order->users or $order->products I would like to do $order->items.
My progress
My progress so far involves a Many To Many Polymorphic Relationship.
My tables:
orders
id - integer
orderables (the order items)
order_id - integer
orderable_id - integer
orderable_type - string
quantity - integer
price - double
-----------
users
id - integer
name - string
products
id - integer
name - string
Example on how orderables table look
This is how I create an order and add a user and a product:
/**
* Order
* #var Order
*/
$order = new App\Order;
$order->save();
/**
* Add user to order
* #var [type]
*/
$user = \App\User::find(1);
$order->users()->sync([
$user->id => [
'quantity' => 1,
'price' => $user->price()
]
]);
/**
* Add product to order
* #var [type]
*/
$product = \App\product::find(1);
$order->products()->sync([
$product->id => [
'quantity' => 1,
'price' => $product->price()
]
]);
Order.php
/**
* Ordered users
* #return [type] [description]
*/
public function users() {
return $this->morphedByMany('Athliit\User', 'orderable');
}
/**
* Ordered products
*/
public function products() {
return $this->morphedByMany('Athliit\Product', 'orderable');
}
Currently I can do
foreach($order->users as $user) {
echo $user->id;
}
Or..
foreach($order->products as $product) {
echo $product->id;
}
But I would like to be able to do something along the lines of...
foreach($order->items as $item) {
// $item is either User or Product class
}
I have found this question, which was the closest I could find to what I am trying to do, but I can't make it work in regards to my needs, it is outdated, and also seems like a very hacky solution.
Have a different approach?
If you have a different approach than Polymorphic relationships, please let me know.
Personally, my Order models have many OrderItems, and it is the OrderItems that have the polymorphic relation. That way, I can fetch all items of an order, no matter what type of model they are:
class Order extends Model
{
public function items()
{
return $this->hasMany(OrderItem::class);
}
public function addItem(Orderable $item, $quantity)
{
if (!is_int($quantity)) {
throw new InvalidArgumentException('Quantity must be an integer');
}
$item = OrderItem::createFromOrderable($item);
$item->quantity = $quantity;
$this->items()->save($item);
}
}
class OrderItem extends Model
{
public static function createFromOrderable(Orderable $item)
{
$this->orderable()->associate($item);
}
public function order()
{
return $this->belongsTo(Order::class);
}
public function orderable()
{
return $this->morphTo('orderable');
}
}
I’ll then create an interface and trait that I can apply to Eloquent models that makes them “orderable”:
interface Orderable
{
public function getPrice();
}
trait Orderable
{
public function orderable()
{
return $this->morphMany(OrderItem::class, 'orderable');
}
}
use App\Contracts\Orderable as OrderableContract; // interface
use App\Orderable; // trait
class Product extends Model implements OrderableContract
{
use Orderable;
}
class EventTicket extends Model implements OrderableContract
{
use Orderable;
}
As you can see, my OrderItem instance could be either a Product, EventTicket, or any other model that implements the Orderable interface. You can then fetch all of your order’s items like this:
$orderItem = Order::find($orderId)->items;
And it doesn’t matter what type the OrderItem instances are morphed to.
EDIT: To add items to your orders:
// Create an order instance
$order = new Order;
// Add an item to the order
$order->addItem(User::find($userId), $quantity);
I think your solution is fine. I'd just add this helper method:
Order.php
public function items() {
return collect($this->products)->merge($this->users);
}
Then you can loop through the items with:
foreach ($order->items() as $item) {
Related
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);
}
I have these tables:
products
-- name
-- price
-- quantity
-- category_id
discounts
-- type('percentage','numeric')
-- value
-- expired_at
categories
-- name
discountables
-- discount_id
-- discountable_id
-- discountable_type
In discountables there is many to many relationship between:
discounts and categories also discounts and products
I'm done with how to discount between discounts and products
Now I'm confused How to discount the whole products that belongs to category that I add todiscountables
Relation:
Category Models
public function discounts()
{
return $this->morphToMany('App\Models\Discount', 'discountable');
}
Products Models
public function discounts()
{
return $this->morphToMany('App\Models\Discount', 'discountable');
}
Discount Model:
public function categories()
{
return $this->morphedByMany('App\Models\Category', 'discountable')->withTimestamps();
}
public function products()
{
return $this->morphedByMany('App\Models\Product', 'discountable')->withTimestamps();
}
MY code for discount the products directly discounts and products
/**
* get price of product after discount
*
* #return void
*/
public function getDiscountProductAttribute() {
foreach ($this->discounts as $discount) {
if($discount->expired_at > Carbon::now()){
if ($discount->type == 'numeric'){
return $this->price - $discount->value;
}else{
return $this->price - ($this->price * ($discount->value / 100));
}
}
}
}
So I need How to discount the whole products that belongs to category that I add todiscountables?
Your relationships are fine. I just redesign a bit to solve the problem.
class Discount extends Model
{
/**
* Check whether this discount is expired.
*
* return bool
*/
public function expired()
{
// I assume that expired_at is also a Carbon instance.
return $this->expired_at->isPast();
}
/**
* Return the discount amount for each product.
*
* #return double
*/
public function apply(Product $product)
{
if ($this->type === 'numeric') {
$this->value;
}
if ($this->type === 'percentage') {
return $product->price * ($this->value / 100);
}
return 0;
}
}
class Product extends Model
{
public function allDiscounts()
{
return $this->discounts
->merge($this->category->discounts)
->unique();
}
pubic function getTotalDiscountAttribute()
{
return $this->allDiscounts()
->reject
->expired()
->map
->apply($this)
->sum();
}
public function getTotalPriceAttribute()
{
return $this->price - $this->total_discount;
}
}
So you can get the total price for a single product after applying all sort of discounts from itself and its category (if there's any) by simply saying:
$product->total_price;
I hope that helps. Let me know if it fails at any step.
By the way. Your question reminds me of a pretty good speech a few years back. Its about solving a some what similar problem using inheritance and Null Object Pattern. It's pretty cool. You don't need to rewrite it that way but its a pretty good design.
I have next db structure - product can be in many categories, product can be in many markets. Models \App\Product, \App\Market and \App\Category are created with many to many relations - belongsToMany().
class Product extends Model
{
public function categories()
{
return $this->belongsToMany('App\Category');
}
public function markets()
{
return $this->belongsToMany('App\Market');
}
}
class Category extends Model
{
public function products()
{
return $this->belongsToMany('App\Product');
}
}
class Market extends Model
{
public function products()
{
return $this->belongsToMany('App\Product');
}
}
In route.web I get category to display products
Route::get('/catalog/{current_category?}', 'CatalogController#index')->name('catalog.index');
Current market I can get from session (user select market when open website)
$market = $request->session()->get('market'); // or Session::get('market');
// $market->id
// $market->slug
In my MarketController#index I want to get all products for category from route and for current market from session. But how can I do it? I can get category products and market products. But how can I get category and market products at the same time?
public function index(Request $request, Category $current_category = null)
{
if ($current_category) {
$market_id = $request->session()->get('market')->id;
$products = $current_category->products;
// ...
}
}
If you want product based on category , use below query:
$products = $current_category->products()->get();
If you want products based on market, first you need to get market object than you can get products based on it.
$market = Market::find($market_id);
$market_products = $market->products()->get();
If you want products based on market and category you can use below query.
$products = Product::whereHas('categories', function($q) {
$q->where('category_id', $current_category->id);
})
->whereHas('markets', function($q) {
$q->where('market_id', $market_id);
})
->get();
As pointed in comment, you can achieve it with many to many polymorphic relation
tables structure
categories
id - integer
name - string
markets
id - integer
name - string
products
id - integer
name - string
productables
product_id - integer
productable_id - integer
productable_type - string
Category model
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class Category extends Model
{
/**
* Get all of the products for the category.
*/
public function products()
{
return $this->morphToMany('App\Product', 'productable');
}
}
Market model
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class Market extends Model
{
/**
* Get all of the products for the market.
*/
public function products()
{
return $this->morphToMany('App\Product', 'productable');
}
}
Product model
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class Product extends Model
{
/**
* Get all of the categories that are assigned this product.
*/
public function categories()
{
return $this->morphedByMany('App\Category', 'productable');
}
/**
* Get all of the markets that are assigned this product.
*/
public function markets()
{
return $this->morphedByMany('App\Market', 'productable');
}
}
Than you can get products belonging to both (certain category and certain market) with
$products = \App\Product::where(['productable_id' => $category->id, 'productable_type' => get_class($category)])->orWhere(['productable_id' => $market->id, 'productable_type' => get_class($market)])->get();
assuming from your question that category and market are already known.
#YasinPatel 's solution should work too, but this way your DB architecture is more flexible. It's up to you now. Study about polymorphic relations solutions, you could find it interesting.
i have this table structure, project has one to many relation with rewards , rewards and shipping has many to many relation with pivot table reward_ship.
projects rewards shipping reward_ship
--------- -------- -------- ------------
id id id id
title amount location reward_id
amount project_id name ship_id
i am trying to extract one particular project details with all other associate tables data(rewards and shipping data using reward_ship table) in one query.
These is how i am trying
Projects Model
class Rewards extends Model {
public function projs(){
return $this->hasMany('App\Rewards');
}
public function rewds(){
return $this->belongsToMany('App\Shipping')
->withPivot('reward_ship', 'ship_id', 'reward_id');
}
public function shiplc(){
return $this->belongsToMany('App\Rewards')
->withPivot('reward_ship', 'ship_id', 'reward_id');
}
}
class Rewards extends Model {
public function proj() {
return $this->belongsTo('App\Projects');
}
}
Controller api class
Route::get('projects/{id}', function($id) {
$p = Projects::find($id);
$getd = Rewards::with('proj')
->where('rewards.project_id', '=', $p->id)
->get();
});
it doesn't work.
i search and tried many related model base query in larvel.
i know my implementation are wrong. Please suggest me to work out.
You can use Laravel 5.5 new feature API Resources.
It helps you to format the output of objects such as models or collections, to display attributes and also relationships.
So, you could do something like this in your ItemResource:
<?php
namespace App\Http\Resources;
use Illuminate\Http\Resources\Json\Resource;
class Project extends Resource
{
/**
* Transform the resource into an array.
*
* #param \Illuminate\Http\Request
* #return array
*/
public function toArray($request)
{
return [
'project_id' => $this->project_id,
'title' => $this->title,
'amount' => $this->amount,
// To access relationship attributes:
'rewards' => $this->rewards->load('shippings'),
];
}
}
Then in your controller, you just need to create a new Resource instance and pass the item object that you want to return:
use App\Http\Resources\Project as ProjectResource;
// some code
/**
* Show a single formatted resource.
*
* #param Project $project
* #return ProjectResource
*/
public function show($project)
{
return new ProjectResource($project);
}
// the rest of your code
The output should be the expected.
You have to fix the relationships that you have :
Projects Model :
public function rewards(){
return $this->hasMany('App\Rewards');
}
Rewards Model :
public function projects() {
return $this->belongsTo('App\Projects');
}
public function shippings(){
return $this->belongsToMany('App\Shipping','reward_ship', 'reward_id', 'ship_id');
}
Shipping model:
public function rewards(){
return $this->belongsToMany('App\Rewards','reward_ship', 'ship_id', 'reward_id');
}
After that you can call the relationships in the controller to eager load the wanted elements like this :
$project = Projects::with('rewards.shippings')
->where('id', $project_id)
->get();
And in the view you can loop over the rewards then get the shippings like this :
#foreach ($project->rewards as $reward)
<p>This is a reword {{ $reward->amount }}</p>
#foreach ($reward->shippings as $shipping)
<p>This is a shipping {{ $shipping->name }}</p>
#endforeach
#endforeach
class Project extends Model
{
public function rewds()
{
return $this->hasMany('App\Rewards');
}
public function shiplc()
{
return $this->hasManyThrough('App\Shipping', 'App\Rewards');
}
}
class Rewards extends Model
{
public function shiplc()
{
return $this->belongsToMany('App\Shipping');
}
public function projs()
{
return $this->belongsTo('App\Project');
}
}
class Shipping extends Model
{
public function shiplc()
{
return $this->belongsToMany('App\Shipping');
}
}
Route::get('projects/{id}', function($id) {
$p = Projects::with(['rewds', 'shiplc'])->find($id);
});
Project.php
class Project extends Model {
public function rewards() {
return this->hasMany(Reward::class, 'project_id', 'id');
}
}
Reward.php
class Reward extends Shipping {
public function shipping(){
return $this->belongsToMany(Shipping::class, 'reward_ship', 'reward_id', 'ship_id');
}
public function project(){
return $this->belongsTo(Project::class);
}
}
You can retrieve it like this:
$projectDetails = Project::where('id', $projectId)
->with(['rewards', 'rewards.shipping'])->get();
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;
}
}