5

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 orderables table (a orders items)

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.

Community
  • 1
  • 1
FooBar
  • 5,752
  • 10
  • 44
  • 93

2 Answers2

5

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);
Martin Bean
  • 38,379
  • 25
  • 128
  • 201
  • Very nice approach. Could you also show how you create an order and attach order items? – FooBar Jul 27 '15 at 12:07
  • Have tried something like: `$order->items()->save(User::find(1));` – FooBar Jul 27 '15 at 12:28
  • This works, though, not very pretty: `$order->items()->create(['orderable_id' => '1', 'orderable_type' => 'App\User']);` – FooBar Jul 27 '15 at 12:42
  • @Mattias For that reason, I have a method that wraps that logic which takes an `Orderable` instance and quantity: `$order->addItem(Orderable $item, int $quantity);`. You could also create a named constructor: `OrderItem::createFromOrderable(Orderable $item);` – Martin Bean Jul 27 '15 at 13:24
  • @Mattias Updated answer with new model methods and an example of usage. – Martin Bean Jul 27 '15 at 13:28
0

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) {
Ben Claar
  • 3,285
  • 18
  • 33
  • Nice workaround, but it does not support stuff like: `$order->items()->whereNull('some-field');`, `$order->items()->count()`, `$order->with('items')->first();`, etc. – FooBar Jul 27 '15 at 10:26
  • Actually it does -- Laravel collections can do `->where('blah', '=', null)`, `->count()`, `->first()`, and you can do `->with('projects', 'users')` in order to eager load items. I agree it's not a "first order" solution, but it offers nearly the same functionality. I do like Martin' solution better, though. – Ben Claar Jul 27 '15 at 15:38