Home > Net >  hasOne/belongsToMany or Many to Many
hasOne/belongsToMany or Many to Many

Time:01-13

I am struggling to figure out what relationship I need to define for some functionality in my web app. I am creating a logistics application, and have a few key models:

  1. Shipment
  2. Organization

A shipment can have different organizations associated with it (e.g., a shipper, consignee, notify organization, vendor organizations). These organizations can belong to many different shipments (and other models for that matter).

My first instict was to have a many to many relationship:

  1. Shipment
  2. ShipmentOrganization
  3. Organization

And then just have an extra column on the ShipmentOrganization model called "type" that defines the organizations relation on that specific shipment (e.g., "shipper", "consignee").

However, I was thinking if I can use a hasOne / belongsToMany relationship instead?

So a shipment have shipper and consignee column:

id | shipment_number | consignee_id | shipper_id
-------------------------------------------
1  | 9841928048091 | 2 | 3

The 2 and 3 then refer to an organization's id on the organization table.

Example of the Organization table:

id | name 
-----------------
1  | Acme Inc
2  | Exxon Inc
3  | Shell Inc
4  | EvilCorp Inc

The organizations doesn't "know" what type they are - because they can be all types depending on the shipment. So as far as the organization model know, they are just that - an organization.

Now, a shipment can only have one shipper, one consignee associated with it.

As described above, Acme Inc. can be a shipper on "shipment one" and a consignee on "shipment two".

What is the correct relationship for this scenario?

CodePudding user response:

Ok, was waiting for someone to answer and see what the big idea would be, but, yea well, disappointing .. anyway, here we go.

If I understood correctly, you have a shipment and an organisation model. The shipment model has two relational attributes, consignee_id and shipper_id where both relates to the organisation model.

In this setup, it is predistined to use the hasOne relationship builder.

// app/models/shipment.php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Shipment extends Model {
  
  public function consignee() {
    return $this->belongsTo(Organisation::class, 'consignee_id');
  }
  
  public function shipper() {
    return $this->belongsTo(Organisation::class, 'shipper_id');
  }

}
// app/models/organisation.php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Organisation extends Model {

  public function shipment() {
    return $this->hasOne(Shipment::class);
  }

}

And querying the shipment would result in

$Shipment = Shipment::find(1);
$Shipment->shipper; // returns Organisation with id: 2
$Shipment->consignee; // returns Organisation with id: 3

Now this just returns an Organisation model, so the variable, in this case the name of the relation, determines the role the organisation played in any given shipment. This might get in the way when we transform the model into an array, then every notion of the organisations role in any shipment will get lost.

[
    "id" => 3,
    "name" => "Shell Inc",
    "created_at" => null,
    "updated_at" => null,
]

Another important thing to note is, that when dispatching the retrieved Organisation model with a role as shipper as in $Shipment->shipper will lose its role in any event dispatched or job created when doing something simple like SomeEvent::dispatch($Shipment->shipper). You will never know in the event what role this organisation played.

// event usage example
$Shipment = Shipment::find(1);
SomeEvent::dispatch($Shipment->shipper);

// some-event example
use App\Models\Organisation;

class SomeEvent {

  public function handle(Organisation $organisation) {
    // lost any track of the role this organisation played
  }

}

There is not any satisfiable way to append an attribute on handled models by relationship builders or add data to the models retrieved without an immense overhead.

So let's solve this another way. Instead of returning the Organisation model, let both consignee and shipper return their own models and let the Organisation model be their parent.

// app/models/consignee.php

namespace App\Models;

class Consignee extends Organisation {}
// app/models/shipper.php

namespace App\Models;

class Shipper extends Organisation {}

And make some changes to the Organisation model so both the Shipper and Consignee models know which table they're attached with. While we're at it, we can also add an attribute to the Organisation model which can determine the role the organisation played in this shipment. I called it role.

// app/models/organisation.php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Str;

class Organisation extends Model {

    protected $table = 'organisations';

    protected $appends = ['role'];

    public function getRoleAttribute() {
        return Str::snake(Str::singular(class_basename($this)));
    }

    public function shipment() {
        return $this->hasOne(Shipment::class);
    }

}

Now we need to change the relationships in the Shipment model for each relation to have its own model.

// app/models/shipment.php

class Shipment extends Model {

    public function consignee() {
        return $this->belongsTo(Consignee::class, 'consignee_id');
    }

    public function shipper() {
        return $this->belongsTo(Shipper::class, 'shipper_id');
    }

}

And we're done. Querying the models would look like:

$Shipment = Shipment::find(1);
$Shipment->shipper; // returns a Shipper model
$Shipment->consignee; // returns a Consignee model

Transforming a Shipper or Consignee model will keep its role:

[
  "id" => 3,
  "name" => "Shell Inc",
  "created_at" => null,
  "updated_at" => null,
  "role" => "shipper",
]

This approach allows for a foundation to be expanded and tuned, e.g. dispatching events or creating jobs.

Another example would be: The Organisation model could own a function which would return all shipments where it played a certain role. We can do this with both Shipper and the Consignee model.

class Organisation extends Models {

    ...
   
    public function shipments() {
        return $this->hasMany(Shipment::class);
    }

    ...

}

$Shipper = Shipper::find(1); // organisation with id one,
$Shipper->shipments; // its role as shipper in all shipments.

$Consignee = Consignee::find(1); // the same organisation, but shipper before,
$Consignee->shipments; // will return all shipments as its role as consignee.

In the case of an event, now the SomeEvent example from above can know exactly what role it played by either using the its role or class instance.

// event usage example
$Shipment = Shipment::find(1);
SomeEvent::dispatch($Shipment->shipper);

// some-event example
use App\Models\Organisation;
use App\Models\Shipper;
use App\Models\Consignee;
use Illuminate\Support\Str;

class SomeEvent {

  public function handle(Organisation $org) {
    // simple if statement
    if($org instanceof Shipper::class) {
      // it's a shipper
    }

    // or more robust
    $this->handleByRole($org);
  }

  public function handleByRole(Organisation $org) {
    $method = "handle" . Str::ucfirst($org->role);
    return $this->{$method}($org);
  }

  public function handleShipper(Shipper $shipper) {}
  public function handleConsignee(Consignee $consignee) {}

}

Hope you get the idea.

  • Related