tjsherrill's avatar

How to structure a relationship between one model and two others

I am stuck on this problem and need some help. I have shipments that are often transported by multiple different carriers in order to get to their destination. I.e., different legs of the trip are likely handled by different carriers. The steps could look like this:

  1. Shipment is picked up from a company location (company location 1) by a carrier (carrier 1)
  2. Shipment is dropped off at another carrier (carrier 2)
  3. Shipment is taken by carrier 2 to a third carrier (carrier 3)
  4. Shipment is taken by carrier 3 to a different company site (company site 2)

So, a shipment origin and destination can be a company location. OR shipment origination and destination could be a carrier location.

So, my thought was to set an origination_id and a destination_id on a shipment. But the problem is that the origination_id could be a company or carrier location. What are my options? TIA

0 likes
9 replies
LaryAI's avatar
Level 58

To handle the relationship between shipments and multiple carriers, you can use a polymorphic relationship. This allows a model to belong to more than one other model on a single association. In your case, both company locations and carriers can be considered as "Stoppable" entities (i.e., places where the shipment can stop).

Here's how you can structure your models and database tables:

  1. Create a stoppables table that will act as a polymorphic relation. This table will store the type of stoppable (company or carrier) and its ID.
Schema::create('stoppables', function (Blueprint $table) {
    $table->id();
    $table->morphs('stoppable'); // This will add stoppable_type and stoppable_id columns
    $table->timestamps();
});
  1. Create a shipments table that will reference the stoppables table for origin and destination.
Schema::create('shipments', function (Blueprint $table) {
    $table->id();
    $table->unsignedBigInteger('origin_id');
    $table->unsignedBigInteger('destination_id');
    $table->timestamps();

    $table->foreign('origin_id')->references('id')->on('stoppables');
    $table->foreign('destination_id')->references('id')->on('stoppables');
});
  1. Define the polymorphic relationship in your Eloquent models.
class Shipment extends Model
{
    public function origin()
    {
        return $this->belongsTo(Stoppable::class, 'origin_id');
    }

    public function destination()
    {
        return $this->belongsTo(Stoppable::class, 'destination_id');
    }
}

class Stoppable extends Model
{
    public function stoppable()
    {
        return $this->morphTo();
    }
}

class CompanyLocation extends Model
{
    public function stoppables()
    {
        return $this->morphMany(Stoppable::class, 'stoppable');
    }
}

class Carrier extends Model
{
    public function stoppables()
    {
        return $this->morphMany(Stoppable::class, 'stoppable');
    }
}
  1. When creating a shipment, you would first create a Stoppable record for both the origin and destination, setting the stoppable_type and stoppable_id to the appropriate values.
// Assuming you have a company location and a carrier instance
$companyLocation = CompanyLocation::find(1);
$carrier = Carrier::find(1);

// Create stoppables for origin and destination
$originStoppable = Stoppable::create([
    'stoppable_type' => get_class($companyLocation),
    'stoppable_id' => $companyLocation->id,
]);

$destinationStoppable = Stoppable::create([
    'stoppable_type' => get_class($carrier),
    'stoppable_id' => $carrier->id,
]);

// Create the shipment
$shipment = Shipment::create([
    'origin_id' => $originStoppable->id,
    'destination_id' => $destinationStoppable->id,
]);
  1. To retrieve the origin and destination for a shipment, you can simply use the origin and destination relationships.
$shipment = Shipment::with(['origin.stoppable', 'destination.stoppable'])->find(1);

$origin = $shipment->origin->stoppable; // This will be an instance of CompanyLocation or Carrier
$destination = $shipment->destination->stoppable; // This will be an instance of CompanyLocation or Carrier

This setup allows you to have a flexible system where a shipment's origin and destination can be either a company location or a carrier, and you can easily extend it to include other types of stoppables in the future.

1 like
martinbean's avatar

@tjsherrill I’d have some sort of overarching shipment model, which has many “legs”. Each leg could have origin and destination locations, and also the carrier that is responsible for that leg. As a shipment is taken by someone to a location; you wouldn’t have a shipment’s destination be, for example, “FedEx”. It would instead be picked up by FedEx (the carrier) at one location (origin) and delivered to another (destination).

tjsherrill's avatar

@martinbean Thanks for chiming in. In this case, Fedex (not actually) could be picking it up from a company location and dropping it at another FedEx location or a UPS Location. It sounds odd, but in this use case it is how it is.

thanks

Snapey's avatar

@tjsherrill What Martin described would cover the scenario you describe.

I would add having a Job to which one or more shipments could be allocated, and as martin suggested, the shipment is made of legs that each have a start and end point. Sometimes a job needs to be split into multiple shipments if it is only partially available. This allows you to track all movements associated with the Job.

Your 'leg' belongs to a shipment and can have start and end locations, responsible carrier, leg costings, specific notes related to the leg that should only be shared with the courier for that leg, Estimated collection and delivery times, and actual times. It will also need to have a leg sequence so that you know their order within the shipment.

So, I would start modelling as

  • Job has many shipments and shipment belongs to a job
  • Shipment has many legs and leg belongs to a shipment
  • leg has one courier and courier has many legs
  • job belongs to a client and a client has many jobs
  • all locations are instances of location model, a location might belong to a client or a courier or be adhoc
martinbean's avatar

Fedex (not actually) could be picking it up from a company location and dropping it at another FedEx location or a UPS Location

@tjsherrill I don’t see why that would change anything? They’re still locations, i.e. place on Earth with an address. Regardless of who or what business is operating at that location.

tjsherrill's avatar

@martinbean Thanks a ton for continuing this discussion. I think we are close.

To share more context, we will have Companies (customers) has_many Company Locations and Carriers (vendors) who can has_many Carrier Locations. I don't love this duplication (both companies and carriers having locations), but Carriers fall into a few different categories whereas our customers are pretty straightforward.

I get that a location is a location. But from a relationship perspective, it feels like the differences between customer locations and carrier locations may grow over time.

Reading through your note above: "all locations are instances of location model, a location might belong to a client or a courier or be ad-hoc" I get what you are saying. Maybe my concern is that while you could reach the location via the parent, reaching the parent via relationship would be hard...? So the challenge would be writing:

foreach($shipment->legs as $leg){
		$leg->origin->company->name // origin is the location. This would fail if the location were a carrier
}

Sorry for the rambling. Does this make sense?

tjsherrill's avatar

Thanks for the additional information @snapey. I hate to be dense but I am still stuck on how we get this done without morph-able. Since the origin and destination could be an instance of company location or carrier_location, we need the morph.

That said, maybe the answer is to have a location that either belongs to a company or a carrier. but then we'd have the same problem right? I hope this is making sense.

tjsherrill's avatar

I feel like it would help later. I have always thought that building relationships created more flexibility. So I could, for example, build a report for the busiest carrier locations.

I am thinking through what you are saying.

Please or to participate in this conversation.