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:
Shipment
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:
Shipment
ShipmentOrganization
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?
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
I've got 3 tables:
Car: belongsToMany Owner
Owner: belongsToMany Car
CarOwner: pivot table for Car and Owner with an additional column 'active' that indicates who is the current Owner of a Car
So a Car might have multiple or no Owners and vica versa BUT a Car has only 1 current ('active') Owner.
class Car extends Model
{
use HasFactory;
public function owners()
{
return $this->belongsToMany(Owner::class, 'car_owners');
}
public function currentOwner()
{
return $this->owners()->where('active', true)->first();
}
}
My problem is if a Car has no active Owner, Laravel throws the following exception when I want to use $car->currentOwner in a Blade template:
App\Models\Car::currentOwner must return a relationship instance, but "null" was returned. Was the "return" keyword used?
How can I handle if a Car doesn't have any active Owners?
You can create a custom attribute instead of a relation function:
public function getCurrentOwnerAttribute()
{
return $this->owners()->where('active', true)->first();
}
I wan't to store updates in pivot tables inside a separate table called audits_pivot.
To do that I need to sort of hook into the attached event on the model (State), which as I found out doesn't really exist. What I can do is to listen on the custom pivot class (LicenceState) for static::saving to be called, since that is the equivalent to 'attached'. Unfortunately does the callback of static::saving not contain any information about what the pivot was attached to.
There are libraries like this one from fico7489, but that doesn't work together with Laravel Nova, which I'm using.
How can I access things like the name and Id of the Model that the pivot row was attached to?
<?php
namespace App;
use Illuminate\Database\Eloquent\Model as EloquentModel;
use Illuminate\Database\Eloquent\Relations\Pivot as EloquentPivot;
use OwenIt\Auditing\Auditable as AuditableTrait;
use OwenIt\Auditing\Contracts\Auditable;
abstract class Pivot extends EloquentPivot implements Auditable
{
use AuditableTrait;
public static function boot()
{
parent::boot();
static::saving(function ($model) {
// How can I access here things like the name and Id of the Model that the pivot row was attached to?
// What I'm looking for is basically this:
// savePivotAudit('attached', 12, 'App\Licence', 'App\State', 51, '2020-01-14 13:55:58');
});
}
private function savePivotAudit($eventName, $id, $relation, $pivotId, $date)
{
return app('db')->table('audits_pivot')->insert([
'event' => $eventName,
'auditable_id' => $id,
'auditable_type' => $this->getMorphClass(),
'relation_id' => $pivotId,
'relation_type' => $relation,
'parent_updated_at' => $date,
]);
}
}
class License extends EloquentModel {}
class State extends EloquentModel
{
use AuditableTrait;
public function licenses()
{
return $this->belongsToMany(License::class)
->using(LicenseState::class);
}
}
class LicenseState extends Pivot {}
The Accountant package does what you want.
It supports many to many relations (i.e. pivot tables), by using Eventually, which adds events for attach(), detach(), updateExistingPivot(), sync() and toggle().
There's not even a need for using custom intermediate models.
The documentation covers all aspects of installation, configuration and usage.
I have three relational table attached below.
https://drive.google.com/file/d/1q1kdURIwFXxHb2MgdRyBkE1e3DMug7r-/view?usp=sharing
I have also three separate models where defined relation among all of my table's.I can read the City Model's information from Country model using hasManyThrough() relation But cannot read the Country information from City model. I have tried to retrieve City model's using ``hasManyThrough``` but didn't get result (attached as commented country method ). Please read my model and it's relational method here..
Is there someone to help me for getting City model's information using Eloquent method hasManyThrough / hasManyThrough or using inverse of hasManyThrough / hasManyThrough ?
01.
<?php
namespace App\Hrm;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
class Country extends Model
{
//use SoftDeletes;
protected $fillable = ['name','description','status'];
public function districts(){
return $this->hasMany(District::class);
}
public function cities(){
return $this->hasManyThrough(City::class,District::class);
}
}
02.
<?php
namespace App\Hrm;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
class District extends Model
{
//use SoftDeletes;
protected $fillable = ['country_id','name','description','status'];
public function country(){
return $this->belongsTo(Country::class);
}
public function cities(){
return $this->hasMany(City::class);
}
}
3.
namespace App\Hrm;
use App\User;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
class City extends Model
{
//use SoftDeletes;
protected $fillable = ['district_id','name','description','status'];
public function district(){
return $this->belongsTo(District::class);
}
// public function country(){
// return $this->hasOneThrough(Country::class, District::class);
// }
Doesn't look like there is a native way to define the inverse of a "hasManyThrough" relationship yet in Laravel. There have been a few issues opened on github to request it, but they were closed.
You could use the staudenmeir/belongs-to-through package if you don't mind installing a third-party package for this functionality. Then you should be able to define a belongsToThrough relationship like this:
class City extends Model
{
use \Znck\Eloquent\Traits\BelongsToThrough;
public function country() {
return $this->belongsToThrough(Country::class, District::class);
}
}
Why can't use parent method?
$city = City::find(1);
$country = $city->district->country();
i just had a similar situation i was able to accomplish a belongsToThrough with hasOneThrough
public function country()
{
return $this->hasOneThrough(
Country::class, // model we are trying to get
District::class, // model we have an _id to
'id', // WHERE `district`.`id` = `city`.`district_id`
'id', // `countries`.`id`
'district_id', // local column relation to our through class
'country_id' // `district`.`country_id`
);
}
what this should generate is
SELECT * FROM `countries`
INNER JOIN `districts`
ON `districts`.`country_id` = `countries`.`id`
WHERE `districts`.`id` = ?
-- ? == city.district_id
Database structure:
City:
id: increments
district_id: integer
...
Country:
id: increments
...
District:
id: increments
country_id: integer
...
we can then do $city->country
note: i have not fully tested this but with the testing that i have done it 'works'
Edit: i originally thought that i needed to leave the localKey
parameter null otherwise the relation wont work. it turns out i didnt
fully understand what that column was doing and that was wrong. That
key is the local column that relates to our through column (unless i
still have more to learn/figure out), when left the value as null, it
would use the local id column which a. is the wrong value, b. can also
be out of range (which is how i discovered it was using the wrong
value)
in my testing i only had two rows, both with the same relations. what
i didnt realize though was that on the "through table" both row 1 and
2 and the same related (relation where are trying to reach) so i didnt
notice the issue right away. hopefully now its all working
Have three entities:
Project
Employee
Employment
Problem description: Employee can work on many projects and for each he has one employment. I want to have access to all projects and your referred employments of a certain employee.
I'm not sure but the relationship must look like a ternary:
The physical table is not defined yet. So, be free to design (most basic) them.
And my question:
How i can build using Laravel Eloquent Relationships?
Basically your four tables will be something like:
employee
id
...your fields
project
id
...your fields
employments
id
...your fields
employee_project
employee_id
project_id
employment_id
You can split the problem in 2 by 2 relations:
class Employee extends Model{
public function projects(){
return $this->belongsToMany("Project")
}
// Second relation is Optional in this case
public function employments(){
return $this->belongsToMany("Employment", 'employee_project')
}
}
A Project model
class Project extends Model{
public function employees(){
return $this->belongsToMany("Employee")
}
// Second relation is Optional in this case
public function employments(){
return $this->belongsToMany("Employment",'employee_project')
}
}
A Employment model
class Employment extends Model{
public function employees(){
return $this->belongsToMany("Employee")
}
public function projects(){
return $this->belongsToMany("Project")
}
}
At this point in your controller you can manage your relation, for example if you want to add to $employee, the project with id 1 with the employment with id 2 you can simply
$employee->projects()->attach([1 => ['employment_id' => '2']]);
I hope this answer to your question.
If you need timestamps in your pivot table, add ->withTimesetamps() to your relationships.
Employee has Employment
Employment has Project
I have a Detail (represents order details) model that I'd like to morph to either a sales order detail or a purchase order detail. So I create a table that has a 'type' column, which would have a value of 'sale' or 'purchase'.
My question is, is there a way in Laravel that I could morph the Detail model to Sale and Purchase, so that, for example, if I call App\Sale::all() it would fetch App\Detail::all()->where('type','sale') ?
Set the database tables:
You can set up your database tables in this structure :
purchases
id - integer
price - string
sales
id - integer
price - integer
details
id - integer
description - string
detailable_id - integer
detailable_type - string
Set your models:
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class Detail extends Model
{
// Get all of the owning detailable models.
public function detailable()
{
return $this->morphTo();
}
}
class Sale extends Model
{
// Get all of the sales member's order details.
public function details()
{
return $this->morphMany('App\Detail', 'detailable');
}
}
class Purchase extends Model
{
// Get all of the purchase's order details.
public function details()
{
return $this->morphMany('App\Detail', 'detailable');
}
}
Retrieve data :
And then you can retrieve your sales like this :
$sales = App\Sale::find(1);
foreach ($sales->details as $order_detail) {
//
}
Same thing with purchases :
$purchases = App\Purchase::find(1);
foreach ($purchases->details as $order_detail) {
//
}
More about polymorphic relations : http://laravel.com/docs/5.1/eloquent-relationships#polymorphic-relations
Although I haven't found an "official" way to morph a single class to another. I developed the following way that could be a solution.
First, define two models Sale and Purchase that extends Detail, and use the trait that will define later.
class Sale extends Detail {
use SaleTrait;
}
Then, use GlobalScope to add constraints to query builder. Here are the steps:
Define a trait for Sale and Purchase model,
trait SaleTrait {
public static function bootSaleTrait()
{
static::addGlobalScope(new ActiveEventsScope);
}
}
Then define the scope. NOTE: here instead of implementing ScopeInterface, I extends Sofa\GlobalScope which handles remove() method for me, so I only need to define apply() in the scope.
class SaleScope extends GlobalScope
{
public function apply(Builder $builder, Model $model)
{
$builder->where('type', 'sale');
}
}
Now I could use App\Sale::all() and App\Purchase::all() to only retrieve what I want.