'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