Be part of JetBrains PHPverse 2026 on June 9 – a free online event bringing PHP devs worldwide together.

joruro's avatar

Multiple models to same table

Hi,

I am doing some experiments with my project's database and I came across with a tricky problem. Think that I have a table called "vehicles" where I can different types of vehicles like cars, motorcycles or trucks. Eloquent expects a single model capable of managing this table. However, in this case, I would like to have a Model for each type of vehicle which would extend the VehicleModel. For singles queries to DB where I want a specific type of vehicle I don't see any problem. For example, I can load a CarModel from DB with this call:

CarModel::find($carModelId)

Unfortunately, when I need to fetch all the items, I have to use the parent class, i.e., the VehicleModel.

VehicleModel::all();

This returns a collection of VehicleModels. I would like to have a collection of CarModels, MotorcycleModels and TruckModels. How can I do this? Does the Eloquent reserve any solution like some kind of factory pattern to this?

0 likes
13 replies
RachidLaasri's avatar

How about you use one single Model for all of your vehicles and then use QueryScope for every single type of vehicles.

Examples :

  • Fetching all vehicles
Vehicle::all();
  • Cars :
Vehicle::cars()->get();

and so on...

joruro's avatar

Hum... @RachidLaasri you gave me a possible solution but I think that you didn't get the point. To deal with different types of vehicles, I want to have a Model for each type. I want to avoid a hundred of conditions to get the right behavior of each vehicle's type.

Thank you!

jekinney's avatar

Either way you'll end up at the same result. If you don't want a single model with 20 query scopes, use a trait. If you want to models, then extend the base model which in essence will just perform a query scope.

Or use repositories that wrap your queries in the same way.

JarekTkaczyk's avatar

@joruro Eloquent doesn't handle things like this, but such override will work for you:

(assuming type is the column that determines which model to use, you could obviously store real class name instead, so the code would be simpler)

// Vehicle model
public function newFromBuilder($attributes = [], $connection = null)
{
    switch (array_get((array) $attributes, 'type')) {
        case 'car':
            $model = new Car;
            break;
        case 'motorcycle':
            $model = new Motorcycle;
            break;
        case 'truck':
            $model = new Truck;
            break;
        default:
            $model = $this->newInstance();
    }

    $model->exists = true;

    $model->setRawAttributes((array) $attributes, true);

    $model->setConnection($connection ?: $this->connection);

    return $model;
}

With this you can call Vehicle::all() and get beatiful collection just like you wanted with single query:

[
    Truck {..},
    Car {..},
    Car {..},
    Motorcycle {..},
    Vehicle {..}, // if type was neither of motorcycle, car or truck
    ...
]

Mind that I never tested this, so play with it, call the relations and so on, for it may lead to unexpected results somewhere else.

12 likes
joruro's avatar

Hi, thank you for your help. I am very grateful.

@JarekTkaczyk your solution is until now the best that I found but the best of the best would be a solution provided natively by Laravel. May be, next versions of Laravel will have some kind of support for these situations.

@martinbean A polymorphic relation works very well with models that have their own table. Not my case where all the models represent the same table and inherit the same model.

martinbean's avatar

@joruro You could create individual model classes that extend the base Vehicle, but each apply a global query scope limiting results to the relevant “type”. For example:

class Truck extends Vehicle
{
    protected static function boot()
    {
        static::addGlobalScope(new VehicleTypeScope('truck'));

        parent::boot();
    }
}

Now when you perform find() or similar operations, you’ll only get models of that type:

$trucks = Truck::all();
26 likes
lindowx's avatar

@JarekTkaczyk I changed the code

    public function newFromBuilder($attributes = [], $connection = null)
    {
        $entryClassName = array_get((array) $attributes, 'type');
        if(strpos($entryClassName, '\\') !== 0) {
            $entryClassName = __NAMESPACE__ . '\\' . $entryClassName;
        }

        if (class_exists($entryClassName)
            && is_subclass_of($entryClassName, self::class)
        ) {
            $model = new $entryClassName;
        } else {
            $model = $this->newInstance();
        }

        $model->exists = true;
        $model->setRawAttributes((array) $attributes, true);
        $model->setConnection($connection ?: $this->connection);

        return $model;
    }
2 likes
forrestedw's avatar

@ANDREWCLARK - Hey, a bit late to this conversation but I like the look of @martinbean's approach too. However, I've been trying it but can't get Factories to work. Taking something like your example above I want to be able to do:

factory(App\Truck::class)->create();

But that gives me this error in Tinker:

PHP Notice:  Undefined index: App/Truck in .../vendor/laravel/framework/src/Illuminate/Database/Eloquent/Model.php on line 241

How can I create Factories for different types of Vehicle?

Edit: I could do factory(App\Vehicle::class)->state('truck')->create() and define a different state in the VehicleFactory for each type of Vehicle, but I'd rather a more intuitive approach (and shorter Factory files) if there is one.

1 like
DavidPetrov's avatar

@FORRESTEDW - Currently facing the same issue. But I guess that's a compromise that you'll have to accept, after all your custom class only filters retrieved results from the parent class... I'm also on the search for a more intuitive approach!

danielandrino's avatar

For the factories to work, just extend the base factory and merge the definition with the parent class:

class CarFactory extends VehicleFactory
{
		public function definition()
    	{
        		return array_merge(parent::definition(), [
            			'attribute1' => 'fake value',
            			'attribute2' => 'fake value',
        		]);
		}

		// You may also use configure() to perform afterMaking and afterCreating actions
}

Note: in your models, you will need to manually specify the table for Eloquent:

class Car extends Vehicle
{
 		protected $table = 'vehicles';

		// Optionally add your global scope to boot() as explained by martinbean
}

Please or to participate in this conversation.