Open philo23 opened 4 years ago
Typically, if an Address is serving a specific role within the context of the Order, I would just add the id of the address to the order:
Schema::create('orders', function ($table) {
$table->bigIncrements('id');
...
$table->unsignedBigInteger('delivery_address_id')->nullable();
$table->unsignedBigInteger('collection_address_id')->nullable();
...
$table->foreign('delivery_address_id')->references('id')->on('addresses');
$table->foreign('collection_address_id')->references('id')->on('addresses');
});
// On the Order model
public function deliveryAddress() : BelongsTo
{
return $this->belongsTo(Address::class, 'delivery_address_id');
}
public function collectionAddress() : BelongsTo
{
return $this->belongsTo(Address::class, 'collection_address_id');
}
If you need dynamic address roles (attaching more addresses to the order than just the delivery and collection address), you would need a third table with the role there:
Schema::create('order_address', function($table) {
$table->bigIncrements('id');
$table->unsignedBigInteger('order_id');
$table->unsignedBigInteger('address_id');
// I'm showing an enum here just to clarify the purpose of this column
// that may or may not work depending on your business rules
$table->enum('role', ['delivery_address', 'collection_address', 'shipping_address']);
// how you index (and even the primary key) depends on your business rules
}
Note: the code above was typed in directly without any testing :)
morphMap
and getMorphClass
like:https://laravel.com/docs/master/eloquent-relationships#custom-polymorphic-types
// Illuminate\Database\Eloquent\Relations\Relation;
Relation::morphMap([
'DeliveryOrder' => Order::class,
'CollectionOrder' => Order::class,
]);
class Address extends Model
{
public function addressable () {
return $this->morphTo('addressable');
}
}
class Order extends Model
{
protected $morphClass = null;
public function getMorphClass () {
return $this->morphClass ?: static::class;
}
public function deliveryAddress () {
$this->morphClass = 'DeliveryOrder';
return $this->morphOne(Address::class, 'addressable');
}
public function collectionAddress () {
$this->morphClass = 'CollectionOrder';
return $this->morphOne(Address::class, 'addressable');
}
}
This would be nice, because there (that I can find) doesn't seem to be a way to link models together without defining the names of the relationships explicitly.
For example, if you have several models such as:
If a user is looking at an article and you wanted to show them related content (could be any other model the admin has linked), such as a newsletter or event. You can't currently do that without a middle model as @philo23 suggests.
I was hoping to just create a trait called Relatable
and then apply it to each of my models. Then I could just access $model->related
and fetch a list of other models related to that one. Another method on the model class could be used to find the inverse.
It would work just like many-many polymorphic relationships do now, but it would just have one more column to store the model of the first ID column.
The Problem
Currently you can't have more than one polymorphic relationship against a single "type" of model at a time. I propose some how the option to include an optional "group" to the
morphable_type
value.For example: An
Order
model that has a delivery address and an invoice address, each represented by a polymorphicAddress
model. Without manually adding an extra column to the Address model to track the 'delivery' or 'invoice' subtype the two relations end up getting merged together.Example
The problem is probably clearest with an actual example so heres one using an Order with two Addresses for delivery and invoice from earlier:
Current Solutions
The two solutions that can work around this right now are:
Creating a new custom column to store the "group" and then including a
->where()
on the end of themorphOne()
relation. While this will solve fetching the related models, it "breaks" saving/creating them through the relation because Laravel doesn't know it needs to automatically fill out the extra column.The other solution would be including an intermediate one-to-one Model, something like:
This second way seems like the "correct" Laravel way to do it right now, but also seems like overkill to create a whole new intermediate model/table just to get a unique identifier for the relation.
Proposed Solution
When defining a polymorphic relationship include the option to set a group/subtype or whatever you decide to call it, eg:
Which would then give you the option to store the addressable_type MorphClass value as something like
App\Address::delivery
instead of justApp\Address
. Doing it this would mean theres no backward compatibility break by adding a new parameter to the internals ofmorphOne()
andmorphMany()
and would also still let you customise the morph class withRelation::morphMap
.Not including a
->group()
or setting it to null would mean morphs work exactly as they do now, but setting one would effectively group all of those morphs into the same bucket. I also see no reason whymorphMany()
couldn't benefit from this too.Proof of Concept
I've included two traits below that you can apply to a model to get a very crude version this functionality working, but as I mentioned the implementation is more of a hack than something I'd want to actually use in production and depend on always working.
As far as I can tell though these are the only two affected methods,
getMorphClass
on the parent model andnewRelatedInstance
of the morphing model, even if its not actually the right place to be injecting these new checks.To be clear though, I don't think this should be the exact solution implemented, its more-so proof that its possible and the current code isn't a mile away from being able to do this.
https://gist.github.com/philo23/9d023f2e15705a5202a2c80e2ac3342a
If there's any interest in this concept, I'd be happy to have a go at actually implementing it, but I'll probably have a few questions. Thanks for reading through this wall of text!