skip to content

Table Conventions & Mass Assignment

How a model maps to its table by convention, the properties or #[Table] attribute that override it, and what $fillable and $guarded let fill() write. Mass assignment is a classic security probe.

on this pageshow

explore

questions

5

In Laravel's Eloquent, which table, primary key and timestamp columns does a model assume by convention, and how do you override them?

level: juniorimportance: must knowfreq 70%

answer

  1. snake_case plural of the class name
  2. id, int, auto-incrementing
  3. created_at and updated_at managed
  4. #[Table] with key, keyType, incrementing
  5. const UPDATED_AT = null

basics

~20 s

Eloquent assumes the snake_case plural of the class name as the table, an auto-incrementing integer id key, and managed created_at and updated_at columns. Override them with the $table, $primaryKey, $keyType, $incrementing and $timestamps properties, or Laravel 13's #[Table] attribute.

solid answer

~40 s

By convention a model named `RoomBooking` maps to `room_bookings`, uses `id` as an auto-incrementing integer key, and expects `created_at` and `updated_at`, which Eloquent fills on insert and update. When a table breaks the convention you override each piece: `protected $table`, `protected $primaryKey`, `protected $keyType = 'string'` together with `public $incrementing = false` for a non-numeric key, `public $timestamps = false`, or the `CREATED_AT` / `UPDATED_AT` constants to rename a column (`null` skips it). Laravel 13 adds class attributes for the same settings, such as `#[Table('tblBookings', key: 'booking_ref', keyType: 'string', incrementing: false)]`, `#[WithoutIncrementing]` and `#[WithoutTimestamps]`, and the properties still work. `php artisan make:model Booking -mfsc` scaffolds the model with a migration, factory, seeder and controller.

code

php · 14 lines
php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Attributes\Table;
use Illuminate\Database\Eloquent\Model;

#[Table('tblBookings', key: 'booking_ref', keyType: 'string', incrementing: false)]
class Booking extends Model
{
    public const CREATED_AT = 'created_on';

    public const UPDATED_AT = null;
}

go deeper

for a junior

Recall the three defaults: plural snake_case table, integer id key, created_at and updated_at. Then name the property that overrides each one.

for a middle

Explain why a string key needs both keyType and incrementing changed, and how the UPDATED_AT constant differs from turning timestamps off entirely.

for a senior

Show how you adopt a legacy table safely: explicit table, key and timestamp mapping, no composite keys, and one consistent style of properties or Laravel 13 attributes across the codebase.

for a principal

Weigh leaning on conventions against explicit mapping for tables the application does not own, where a rename in the schema silently breaks a guessed name.

## What an Eloquent model assumes An **Eloquent model** is a PHP class extending `Illuminate\Database\Eloquent\Model` that represents one database table, one row per instance. Eloquent follows the **Active Record** style: the object knows its own table and saves itself. To keep model classes nearly empty, Eloquent derives everything it needs about the table from **conventions** and lets you override only what differs. | Concern | Convention | How it is derived | |---|---|---| | Table name | `room_bookings` for `RoomBooking` | `Str::snake(Str::pluralStudly(class_basename(...)))` in `getTable()` | | Primary key column | `id` | `protected $primaryKey = 'id'` | | Key type | integer | `protected $keyType = 'int'` | | Auto-increment | yes | `public $incrementing = true` | | Timestamps | `created_at`, `updated_at` managed | `public $timestamps = true`, constants `CREATED_AT` / `UPDATED_AT` | | Connection | the default connection | `protected $connection` is null | So `Flight` reads `flights`, and `AirTrafficController` reads `air_traffic_controllers`: only the last word of the class name is pluralised. ## Adopting a legacy table Picture a hotel-reservations app that must read an old table named `tblBookings`. Its key is a string column `booking_ref` such as `HTL-00042`, it records `created_on` and it has no "updated" column at all. None of that matches the conventions, so the model must say so: ```php class Booking extends Model { protected $table = 'tblBookings'; protected $primaryKey = 'booking_ref'; protected $keyType = 'string'; public $incrementing = false; public const CREATED_AT = 'created_on'; public const UPDATED_AT = null; } ``` - `$table` stops Eloquent from guessing `bookings`. - `$primaryKey` names the key used by `find()`, `save()` and `delete()`. - `$keyType = 'string'` and `$incrementing = false` must travel **together** for a non-numeric key. - Setting `UPDATED_AT` to `null` keeps `created_on` managed while skipping the missing column; `$timestamps = false` would turn off both. ## The string-key trap Declaring only `$primaryKey = 'booking_ref'` looks enough and is not. Trace what happens: 1. `$incrementing` is still `true`, so `getCasts()` adds a cast of the key to `$keyType`, which is still `'int'`. 2. Reading `$booking->getKey()` casts `'HTL-00042'` to an integer, which is `0`. 3. On insert, an incrementing model calls `insertGetId()` and overwrites the key attribute with whatever the driver reports as the last inserted id. The fix is the pair above: string key type, incrementing off. ## Properties or attributes in Laravel 13 Laravel 13 added **PHP attributes** on model classes for the same settings. They are an alternative, not a replacement: the properties above still work. - `#[Table('tblBookings', key: 'booking_ref', keyType: 'string', incrementing: false, timestamps: false, dateFormat: 'U')]` covers table name, key, key type, incrementing, timestamps and the stored date format in one place (every argument is optional). - `#[WithoutIncrementing]` and `#[WithoutTimestamps]` are one-flag shortcuts. - `#[Connection('legacy')]` pins the model to another connection. Mixing the two styles on one model makes precedence rules matter: a non-null `$table` declared on the class keeps priority over `#[Table]`'s name, the attribute's `key` only applies while `$primaryKey` is still `'id'`, while `#[WithoutIncrementing]` or `#[Table(incrementing: ...)]` override the property. Pick one style per model. ## Generating the class `php artisan make:model Booking` writes `app/Models/Booking.php`. Flags add the companions that usually come with a model: - `-m` a `create_bookings_table` migration, `-f` a `BookingFactory`, `-s` a `BookingSeeder`, `-c` a `BookingController`; `-mfsc` combines all four. - `-a` / `--all` adds a policy, a resource controller and form requests on top. - `-p` / `--pivot` generates a pivot model instead of a plain one. ## Limits worth knowing - Eloquent does **not** support composite primary keys; each model needs one uniquely identifying key column, and extra uniqueness belongs in a multi-column unique index. - The table-name guess is English pluralisation of the last word, so irregular or non-English names often need an explicit table. - Conventions only describe the table; they never create it. The migration that builds the table is a separate file. ## Checking what Eloquent resolved When a mapping misbehaves, inspect it rather than guess: - `php artisan model:show Booking` prints the model's table, connection, attributes and relations as Eloquent resolved them, which exposes a wrong guessed table name at once. - `(new Booking)->getTable()`, `getKeyName()`, `getKeyType()` and `getIncrementing()` return the effective values at runtime, including anything a trait or attribute changed. - `usesTimestamps()` tells you whether Eloquent will write the timestamp columns for that model. Common mistakes in review are a `$table` that duplicates the convention (noise that goes stale on rename), a string key without `$keyType`, and `$timestamps = false` used where only one of the two columns is missing.

  • If an Eloquent model sets both a `$table` property and a `#[Table]` attribute, which one wins?
    For the table name, a non-null `$table` declared on the class itself wins; the attribute only fills the name when the property is null or inherited. The attribute's `key` applies only while `$primaryKey` is still `'id'`. For auto-increment it is the reverse: `#[WithoutIncrementing]` or `#[Table(incrementing: false)]` overrides the property. Mixing both styles makes these rules matter, so keep one style per model.
  • Can an Eloquent model use a composite primary key?
    No. Eloquent needs one uniquely identifying key column per model; composite primary keys are not supported. For a legacy table keyed on two columns, the usual answers are adding a surrogate key column when you can change the schema, or keeping the pair as a unique index while the model uses a single key column.
  • What does `php artisan make:model Booking -a` create beyond `-mfsc`?
    `--all` switches on the factory, seeder, migration, controller, policy and resource options, and it also asks for form requests. So on top of the model, migration, factory, seeder and controller you get a `BookingPolicy`, a resource-style controller with the seven CRUD actions, and store and update form request classes.

saying these in an interview costs you the question

  • Eloquent reads the table name from the database schema at runtime
  • Setting only $primaryKey is enough for a string key
  • The table for RoomBooking is room_booking, the singular class name
  • Laravel 13 removed the $table and $primaryKey properties in favour of attributes
  • $timestamps = false is the only way to drop the updated_at column
  • Eloquent supports composite primary keys through an array $primaryKey
open as a page

In an Eloquent model, what is the difference between $fillable and $guarded, and what happens to a key neither allows?

level: middleimportance: must knowfreq 82%

basics

~20 s

$fillable is an allow-list of keys fill() and create() may set; $guarded is a deny-list. A default model is totally guarded and throws MassAssignmentException, while a model with a list silently drops disallowed keys by default.

open as a page

In Laravel 13, how do Eloquent's HasUuids and HasUlids traits change a model's primary key, and when is the key value assigned?

level: middleimportance: should knowfreq 45%

basics

~20 s

HasUuids and HasUlids make the primary key a PHP-generated string, a UUIDv7 or a lowercase ULID, and report the key as a non-incrementing string. The value is assigned when the model is inserted, so a new unsaved instance has no key.

open as a page

An Eloquent model sets $guarded = [] and a controller calls Reservation::create($request->all()); why is that a mass-assignment hole, and how do you harden it?

level: seniorimportance: should knowfreq 55%

basics

~20 s

With $guarded = [] every posted key that names a column is written, so a user can add is_vip or user_id to the request. Harden it with a $fillable allow-list, explicit input arrays, server-set privileged fields and strict discarding in development.

open as a page

In an Eloquent model, what does the $attributes property do, and why must its default values be written in raw database form?

level: middleimportance: nice to knowfreq 30%

basics

~20 s

The $attributes property gives new model instances default column values. They sit in the raw attributes array without passing through casts or mutators, so a JSON-cast column needs the string '[]', not a PHP array.

open as a page