'Create descendant class based on Eloquent model
Let's say I have Vehicle model (it's Eloquent model) that stores different types of vehicles (in vehicles table). Of course, there are many different types of vehicles, so I have for example:
class Car extends Vehicle {
}
class Bicycle extends Vehicle {
}
and so on.
now I need to find object based on vehicle and here's the problem. I've added the following method in Vehicle model:
public function getClass()
{
return __NAMESPACE__ . '\\' . ucfirst($this->type)
}
so I can find the class name I should use.
But the only way to get valid class is like this:
$vehicle = Vehicle::findOrFail($vehicleId);
$vehicle = ($vehicle->getClass())::find($vehicleId);
which is not the best solution because I need to run 2 exact same queries to get valid final class object.
Is there any way to achieve same without duplicating the query?
Solution 1:[1]
An alternative to @jedrzej.kurylo's method would be to just override one method in your Vehicle class:
public static function hydrate(array $items, $connection = null)
{
$models = parent::hydrate($items, $connection);
return $models->map(function ($model) {
$class = $model->getClass();
$new = (new $class())->setRawAttributes($model->getOriginal(), true);
$new->exists = true;
return $new;
});
}
Hope this helps!
Solution 2:[2]
In order for Eloquent to correctly return objects of a class determined by the type column, you'll need to override 2 methods in your Vehicle model class:
public function newInstance($attributes = array(), $exists = false)
{
if (!isset($attributes['type'])) {
return parent::newInstance($attributes, $exists);
}
$class = __NAMESPACE__ . '\\' . ucfirst($attributes['type']);
$model = new $class((array)$attributes);
$model->exists = $exists;
return $model;
}
public function newFromBuilder($attributes = array(), $connection = null)
{
if (!isset($attributes->type)) {
return parent::newFromBuilder($attributes, $connection);
}
$instance = $this->newInstance(array_only((array)$attributes, ['type']), true);
$instance->setRawAttributes((array)$attributes, true);
return $instance;
}
Solution 3:[3]
For anybody else that comes across this page, this is what worked for me. I copied the newInstance and newFromBuilder from the source code, and put them in my parent class, in this case it would be Vehicle.
I think the newInstance method is ran twice when building up a query builder instance. In the newInstance method I would check if the type is set in the attributes, and if so then get the namespace based off the type (I used PHP Enums). On the second pass $attributes gets converted to an object rather than array, not sure why but don't worry about your IDE complaining.
In the newFromBuilder method I had to pass $attributes in to the newInstance method, as before it was just passing an empty array.
$model = $this->newInstance([], true);
to:
$model = $this->newInstance($attributes, true);
Vehicle.php
/**
* Create a new instance of the given model.
*
* @param array $attributes
* @param bool $exists
* @return static
*/
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.
$model = new static;
if (isset($attributes->type)) {
$class = // Logic for getting namespace
$model = new $class;
}
$model->exists = $exists;
$model->setConnection(
$this->getConnectionName()
);
$model->setTable($this->getTable());
$model->mergeCasts($this->casts);
$model->fill((array) $attributes);
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)
{
// I had to pass $attributes in to newInstance
$model = $this->newInstance($attributes, true);
$model->setRawAttributes((array) $attributes, true);
$model->setConnection($connection ?: $this->getConnectionName());
$model->fireModelEvent('retrieved', false);
return $model;
}
By making these changes I could do Vehicle::all() and get a collection containing both Car and Bicycle classes.
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
| Solution | Source |
|---|---|
| Solution 1 | |
| Solution 2 | Marcin Nabiałek |
| Solution 3 | simplehacker |
