I want to avoid inheritance as much as possible, but I have a case in existing code that I need to work with.
Consider a model CartItem
which is being inherited by different implementation models, such as CartItemTypeX
and CartItemTypeY
The three models share the same database table cart_items, and have the exact same structure. One of the columns is named payload
and its content can be different for each of the types, which is why we have the different models.
The table also has a column type
which is being saved automatically by the following simple logic:
static::creating(function (Model $model) {
$model->type = $model->getMorphClass();
});
So far so good. Each CartItem is properly being saved into the database with its proper type.
Now, we have some routes that accept a CartItem
as a parameter.
For instance, let's consider the following:
class SomeController {
public function update(CartItemRequest $request, CartItem $cartItem) {
// Our logic goes here...
// We would like $cartItem to automatically be of the implementation type (CartItemTypeX for instance)
}
}
I have some additional hurdles to take when tackling this issue. For instance, our optimistic locking mechanism saves the version if of the model in a related model. It does that using a morph relationship, so it has the morph class CartItemTypeX
and not CartItem
because we never save a CartItem
as such.
This is what I have been looking into.
Overriding the newFromBuilder
method on CartItem
I tried overriding this method on the class, like this:
public function newFromBuilder($attributes = [], $connection = null)
{
$classname = Relation::getMorphedModel($attributes->type);
$instance = new $classname();
$instance->exists = true;
$instance->setRawAttributes((array) $attributes, true);
$instance->setConnection($connection ?: $this->getConnectionName());
$instance->fireModelEvent('retrieved', false);
return $instance;
}
This seems to be working fine when I'm retrieving the models through the builder (for instance CartItem::whereIsNull('order_id')->get()
will return CartItemTypeX
models (I just remember I haven't tried testing with different cart types, but I suspect that this would work...)
however, when the CartItem
is being injected from the route into my controller, it's still a model of type CartItem
and not CartItemTypeX
So that didn't do the trick to me.
Just adding a method to get the implementation
So I forgot about delegating this responsibility to Eloquent (which had my preference, as I want to keep my controller and business code as clean and simple as possible)
I then tried applying the same logic as above in a new method adding to the CartItem
class:
class CartItem {
public function getImplementation() {
$classname = Relation::getMorphedModel($this->type);
$instance = new $classname();
$instance->exists = $this->exists;
$instance->setRawAttributes((array) $this->attributes, true);
$instance->setConnection($this->connection ?: $this->getConnectionName());
return $instance;
}
}
and in my controller, I can then easily do this:
class MyController {
public function view(Request $request, CartItem $cartItem) {
$cartItem = $cartItem->getImplementation();
/* ... */
}
}
That works as well, but my versioning trait I discussed above is failing on me. On retrieving the Model, this trait fetches the related through the morph relation. As it was of type CartItem
when being retrieved from the databases, it uses the morph type for CartItem
and not CartItemTypeX
to retrieve it from the database.
So by just in code instantiating the correct implementation class and setting attributes rawly (is that a word?), we do not have the correct value for version
which raises conflicts when the model is being saved.
So back to the first solution?
The first solution I inherited in the code, is something like this:
class CartItem {
public function getImplementation() {
$classname = Relation::getMorphedModel($this->type);
return $classname::find($this->id);
}
}
and yes, this works. I get in a CartItem
and by doing $cartItem->getImplementation()
, Eloquent is retrieving the CartItemTypeX
from the database. The trait for versioning (optimistic locking) also works as it is supposed to do, as we are just fetching it freshly from the database...
Oh - and there's that issue again... a fresh retrieve. This implementation is killing performance obviously, as to retrieve a CartItemTypeX, we will now always have two retrieves: One for CartItem
because of route parameter injection into the controller method, and a second time when we fetch the implementation.
Surely there must be a better way. Any thoughts or insights would be much appreciated!