I have an Animal
model, based on the animal
table.
This table contains a type
field, that can contain values such as cat or dog.
I would like to be able to create objects such as :
class Animal extends Model { } class Dog extends Animal { } class Cat extends Animal { }
Yet, being able to fetch an animal like this :
$animal = Animal::find($id);
But where $animal
would be an instance of Dog
or Cat
depending on the type
field, that I can check using instance of
or that will work with type hinted methods. The reason is that 90% of the code is shared, but one can bark, and the other can meow.
I know that I can do Dog::find($id)
, but it's not what I want : I can determine the type of the object only once it was fetched. I could also fetch the Animal, and then run find()
on the right object, but this is doing two database calls, which I obviously don't want.
I tried to look for a way to "manually" instantiate an Eloquent model like Dog from Animal, but I could not find any method corresponding. Any idea or method I missed please ?
As the OP stated inside his comments: The database design is already set and therefore Laravel's Polymorphic Relationships seems not to be an option here.
I like the answer of Chris Neal because I had to do something similar recently (writing my own Database Driver to support Eloquent for dbase/DBF files) and gained a lot experience with the internals of Laravel's Eloquent ORM.
I've added my personal flavour to it to make the code more dynamic while keeping an explicit mapping per model.
Supported features which I quickly tested:
Animal::find(1)
works as asked in your questionAnimal::all()
works as wellAnimal::where(['type' => 'dog'])->get()
will return AnimalDog
-objects as a collectionAnimal
-model in case there is no mapping configured (or a new mapping appeared in the DB)Disadvantages:
newInstance()
and newFromBuilder()
entirely (copy and paste). This means if there will be any update from the framework to this member functions you'll need to adopt the code by hand.I hope it helps and I'm up for any suggestions, questions and additional use-cases in your scenario. Here are the use-cases and examples for it:
class Animal extends Model { use MorphTrait; // You'll find the trait in the very end of this answer protected $morphKey = 'type'; // This is your column inside the database protected $morphMap = [ // This is the value-to-class mapping 'dog' => AnimalDog::class, 'cat' => AnimalCat::class, ]; } class AnimalCat extends Animal {} class AnimalDog extends Animal {}
And this is an example of how it can be used and below the respective results for it:
$cat = Animal::find(1); $dog = Animal::find(2); $new = Animal::find(3); $all = Animal::all(); echo sprintf('ID: %s - Type: %s - Class: %s - Data: %s', $cat->id, $cat->type, get_class($cat), $cat, json_encode($cat->toArray())) . PHP_EOL; echo sprintf('ID: %s - Type: %s - Class: %s - Data: %s', $dog->id, $dog->type, get_class($dog), $dog, json_encode($dog->toArray())) . PHP_EOL; echo sprintf('ID: %s - Type: %s - Class: %s - Data: %s', $new->id, $new->type, get_class($new), $new, json_encode($new->toArray())) . PHP_EOL; dd($all);
which results the following:
ID: 1 - Type: cat - Class: App\AnimalCat - Data: {"id":1,"type":"cat"} ID: 2 - Type: dog - Class: App\AnimalDog - Data: {"id":2,"type":"dog"} ID: 3 - Type: new-animal - Class: App\Animal - Data: {"id":3,"type":"new-animal"} // Illuminate\Database\Eloquent\Collection {#1418 // #items: array:2 [ // 0 => App\AnimalCat {#1419 // 1 => App\AnimalDog {#1422 // 2 => App\Animal {#1425
And in case you want you use the MorphTrait
here is of course the full code for it:
<?php namespace App; trait MorphTrait { public function newInstance($attributes = [], $exists = false) { // This method just provides a convenient way for us to generate fresh model // instances of this current model. It is particularly useful during the // hydration of new objects via the Eloquent query builder instances. if (isset($attributes['force_class_morph'])) { $class = $attributes['force_class_morph']; $model = new $class((array)$attributes); } else { $model = new static((array)$attributes); } $model->exists = $exists; $model->setConnection( $this->getConnectionName() ); $model->setTable($this->getTable()); return $model; } /** * Create a new model instance that is existing. * * @param array $attributes * @param string|null $connection * @return static */ public function newFromBuilder($attributes = [], $connection = null) { $newInstance = []; if ($this->isValidMorphConfiguration($attributes)) { $newInstance = [ 'force_class_morph' => $this->morphMap[$attributes->{$this->morphKey}], ]; } $model = $this->newInstance($newInstance, true); $model->setRawAttributes((array)$attributes, true); $model->setConnection($connection ?: $this->getConnectionName()); $model->fireModelEvent('retrieved', false); return $model; } private function isValidMorphConfiguration($attributes): bool { if (!isset($this->morphKey) || empty($this->morphMap)) { return false; } if (!array_key_exists($this->morphKey, (array)$attributes)) { return false; } return array_key_exists($attributes->{$this->morphKey}, $this->morphMap); } }
You can use the Polymorphic Relationships in Laravel as explained in Official Laravel Docs. Here is how you can do that.
Define the relationships in the model as given
class Animal extends Model{ public function animable(){ return $this->morphTo(); } } class Dog extends Model{ public function animal(){ return $this->morphOne('App\Animal', 'animable'); } } class Cat extends Model{ public function animal(){ return $this->morphOne('App\Animal', 'animable'); } }
Here you'll need two columns in the animals
table, first is animable_type
and another is animable_id
to determine the type of model attached to it at runtime.
You can fetch the Dog or Cat model as given,
$animal = Animal::find($id); $anim = $animal->animable; //this will return either Cat or Dog Model
After that, you can check the $anim
object's class by using instanceof
.
This approach will help you for future expansion if you add another animal type (i.e fox or lion) in the application. It will work without changing your codebase. This is the correct way to achieve your requirement. However, there is no alternative approach to achieve polymorphism and eager loading together without using a polymorphic relationship. If you don't use a Polymorphic relationship, you'll end up with more then one database call. However, if you have a single column that differentiates the modal type, maybe you have a wrong structured schema. I suggest you improve that if you want to simplify it for future development as well.
Rewriting the model's internal newInstance()
and newFromBuilder()
isn't a good/recommended way and you have to rework on it once you'll get the update from framework.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With