Is there an elegant way of structuring a series of three Eloquent models that are components of each other?

Viewed 30

This is a bit of an interesting question, but I'm hoping to get some advice around how best to structure the a few Eloquent models and their relationships.

Summary

I'm attempting to create a structure that most elegantly represents a group of Products, Components, and Parts, and allows me to calculate the cost of both a Product and a Component.

  • An App\Part is purchased from a third-party, so they will inserted into the database with unit cost.

  • An App\Component is made from Parts (ie: a Component belongsToMany() Parts, and vice versa). The cost of the Component is the sum of the App\Part prices, but the part can also be sold directly to a customer.

  • An App\Product is made up from both Parts and Components, with the cost being the sum of the App\Part and App\Component costs; therefore, both the Part and the Component belongsToMany() Products.

A Product and a Component share many of the same data fields, such as a method of procedure (instructions for building the product/component), a sale price, etc.

Think of a part as something like a bolt or a piece of lumber, which can be used to make a table leg (a Component) which could then be used to create any number of different types of tables (a Product). The part can be used directly by the Product or by Components which are used to make the Product.

model relationship diagram

What I've Considered

I've come up with two different approaches so far, but neither of which really seem very clean, so my hope is that someone might steer me in better direction:

  1. Leverage a Polymorphic relationship on the App\Part, allowing it to be related to both the App\Component and the App\Product, then a Many-to-Many relationship between the App\Product and the App\Component. The component will have to keep a record of it's calculated unit cost, then when calculating the cost of the Product, I can total the costs of both Parts and Components, and add them.

  2. When creating an App\Component, add a reference to it back into the parts table as a Part, ie: a table leg is made from a two different pieces of lumber, and some screws (I dunno, I'm not a carpenter). And then once an entry for table leg is created in the parts table, the App\Product just needs a Many-to-Many relationship with the Parts.

The Ask

Neither one of these approaches sits with me particularly well, so I'd appreciate any advice from anyone with any suggestions, or anyone who might've tackled a similar problem in the past.

1 Answers

It's not clear what the difference between a component and a part is other than a component is made of parts. For example if you said a part can be a base part or a composite part which relates to itself would that get rid of the need for components? If that's the case this is something that you could realistically do:

Say your tables are

parts
-----
id | name | cost 

part_parts
-----
base_part_id | composite_part_id

You can define your part model as:

class Part extends Model {
   protected $with [ 'consistsOfParts' ]; 
   protected $appends = [ 'total_cost' ];

   public function consistsOfParts() {
       return $this->belongsToMany(Part::class, 'part_parts', 'composite_part_id', 'base_part_id');
   }

   public function constintuentOfParts() {
       return $this->belongsToMany(Part::class, 'part_parts', 'base_part_id', 'composite_part_id');
   }

   public function getTotalCostAttribute() {        
        return $this->consistsOfParts->sum('cost') + $this->cost;
   }
 
}

This should simplify things by allowing you to calculate the total cost of a product as e.g. $product->parts->sum('total_cost');

Note that this will forcibly eager load the related parts so might end up in an additional query per product retrieved.

You can then use polymorphism (if you need to) to distinguish between Part and Component simply by checking (or storing as a column) whether the part is basic or composite.

Related