Appearance
Eloquent Models: Part 1
Having established our database structure with migrations in the previous chapter, we now turn our attention to interacting with that structure using Laravel's Eloquent ORM. When we created our migrations using the make:model
command with the -m
or -ms
flags, Laravel also generated corresponding Model classes for our database tables. You can find these model files within the app/Models
directory.
The Eloquent ORM (Object-Relational Mapper) provides an elegant and straightforward ActiveRecord implementation for database interactions. Essentially, each database table (like users
, genres
, records
) has a corresponding Model class ( User
, Genre
, Record
) that acts as a powerful interface to that table.
Models allow you to perform a wide range of database operations intuitively, including:
- Querying for data in your tables.
- Defining relationships between tables (e.g., a
Record
belongs to aGenre
). - Protecting against mass assignment vulnerabilities.
- Transforming attribute values when retrieving or setting them (using accessors, mutators, and attribute casting).
- Defining computed properties (attributes that don't directly map to a database column).
- Applying reusable query constraints (scopes).
- Controlling which attributes are included when a model is converted to JSON or an array.
CHAPTER FOCUS
In this first part on Eloquent models, we will concentrate specifically on two fundamental concepts:
- Mass Assignment Protection: Securing your models against unintended data modifications.
- Defining Relationships: Establishing connections between your models that mirror the foreign key constraints in your database.
Other powerful Eloquent features like attribute casting, accessors/mutators, and scopes will be covered in Part 2.
Preventing Mass Assignment Vulnerabilities
A mass assignment vulnerability occurs when a user submits unexpected HTTP request fields, and those fields unintentionally change columns in your database that they shouldn't have access to modify. For example, a malicious user might try to submit an admin=1
field during registration to grant themselves administrator privileges.
Mass assignment itself refers to the convenient practice of creating or updating a model using an array of data, often coming directly from form input:
php
// Example of potential mass assignment
$userData = $request->all(); // Gets all data from the request
User::create($userData); // Creates a new user
1
2
3
2
3
If $userData
contained an 'admin' => true
key-value pair not present in the original form, and the admin
column wasn't protected, the user could potentially become an admin.
Laravel provides two primary ways to protect your models using special properties within the model class: protected $fillable = [];
and protected $guarded = [];
. You should generally choose one method per model for clarity.
$fillable
(Whitelist): An array listing the attribute keys that are allowed to be mass assigned. Any attribute key not in this array will be ignored during mass assignment operations. This is often considered the more secure default.$guarded
(Blacklist): An array listing the attribute keys that are not allowed to be mass assigned. All other attribute keys not listed in this array will be considered mass assignable. You can setprotected $guarded = [];
( an empty array) to make all attributes mass assignable (use with caution!).
If both $fillable
and $guarded
are defined on a model, $guarded
takes precedence. However, it's best practice to stick to using only one of them.
Let's configure mass assignment protection for our models.
The default User
model generated by Laravel already uses $fillable
. It typically allows name
, email
, and password
to be mass assigned during registration or profile updates.
- Open the
app/Models/User.php
file. - Add
'active'
and'admin'
to the existing$fillable
array. While we might not allow users to set these directly via a form, allowing them for administrative actions or seeding can be useful. We'll control access through Livewire components later.
php
class User extends Authenticatable // Note: Extends Authenticatable
{
use HasFactory, Notifiable; // Add other traits as needed (e.g., from Jetstream)
/**
* The attributes that are mass assignable.
*
* @var array<int, string>
*/
protected $fillable = [
'name',
'email',
'password',
'active',
'admin',
];
// ... other properties and methods ...
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
With mass assignment protection configured, we can now define the relationships between our models.
Adding Eloquent Relationships
While we defined foreign key constraints in our database migrations to ensure data integrity at the database level, we also need to define these relationships within our Eloquent models. This allows us to easily traverse these relationships and retrieve related data using Eloquent's expressive syntax (e.g., Record::with('genre')->get()
or Genre::with('records)->get()
).
You can find detailed information in the Laravel documentation on Eloquent Relationships. Our Vinyl Shop database primarily uses one-to-many and many-to-one (the inverse of one-to-many) relationships.
RELATIONSHIP SUMMARY FOR OUR DATABASE
This diagram summarizes the relationships we need to define:
Genre 1 <--> ∞
Record (One Genre Has Many Records)
Let's start with the relationship between Genre
and Record
. From the Genre
model's perspective, one genre can be associated with many records. This is a one-to-many relationship.
We define this in Eloquent using the hasMany()
method within the Genre
model.
- Open the
app/Models/Genre.php
file. - Add the
records()
method inside theGenre
class:
php
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany; // Import HasMany
class Genre extends Model
{
use HasFactory;
protected $guarded = ['id', 'created_at', 'updated_at'];
// Define the "one-to-many" relationship: A Genre has many Records
public function records(): HasMany // Type hint return type
{
// Eloquent assumes the foreign key on the Record model is 'genre_id'
// based on the owning model's name (Genre -> genre_id)
return $this->hasMany(Record::class);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
By convention, Eloquent assumes the foreign key on the related Record
model is the "snake_case" name of the owning model (Genre
) followed by _id
, which is genre_id
. Since our foreign key follows this convention, we only need to pass the related model class (Record::class
) to hasMany()
. The method name records
(plural) is also conventional for hasMany
relationships.
NAMING CONVENTIONS: hasMany()
- Method Name: Should typically be the plural,
camelCase
orsnake_case
name of the related model (e.g.,records
). - Foreign Key: Eloquent assumes
owning_model_name_id
(e.g.,genre_id
) on the related table. - Local Key: Eloquent assumes
id
on the owning table (genres
).
The full signature is hasMany(RelatedModel::class, 'foreign_key_on_related_table', 'local_key_on_owning_table')
. You only need to specify the second and third arguments if your keys don't follow the convention.
php
public function records(): HasMany
{
// Short version (if conventions are followed)
return $this->hasMany(Record::class);
// Long version (explicitly specifying keys)
// return $this->hasMany(Record::class, 'genre_id', 'id');
}
1
2
3
4
5
6
7
8
2
3
4
5
6
7
8
Read more: One To Many Relationships
Record ∞ <--> 1
Genre (One Record Belongs To One Genre)
Now, let's define the inverse relationship from the Record
model's perspective. A single record belongs to exactly one genre. This is the many-to-one relationship (the inverse of the hasMany
we just defined).
We define this using the belongsTo()
method within the Record
model.
- Open the
app/Models/Record.php
file. - Add the
genre()
method inside theRecord
class:
php
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo; Import BelongsTo
class Record extends Model
{
use HasFactory;
protected $guarded = ['id', 'created_at', 'updated_at'];
// Define the "many-to-one" relationship: A Record belongs to a Genre
public function genre(): BelongsTo // Type hint return type
{
// Eloquent assumes the foreign key on *this* model (Record) is 'genre_id'
// based on the relationship method name ('genre' -> 'genre_id')
// It assumes the key on the related Genre model is 'id'.
return $this->belongsTo(Genre::class)->withDefault(); // Use withDefault() as good practice
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
By convention, Eloquent determines the foreign key name by examining the name of the relationship method (genre
) and suffixing it with _id
, resulting in genre_id
. It assumes this foreign key exists on the Record
model's table. It then assumes the corresponding primary key on the Genre
model is id
. Since our schema follows these conventions, we only need to pass Genre::class
to belongsTo()
.
The ->withDefault()
method is a useful addition. If a record somehow has an invalid genre_id
that doesn't correspond to an actual genre, calling $record->genre
would normally return null
. Using ->withDefault()
ensures that an empty Genre
model instance is returned instead. This prevents potential errors if you try to access properties on a null object (often called the Null Object Pattern).
NAMING CONVENTIONS: belongsTo()
- Method Name: Should typically be the singular,
camelCase
orsnake_case
name of the related model (e.g.,genre
). - Foreign Key: Eloquent assumes
relationship_method_name_id
(e.g.,genre_id
) exists on the owning model's table (the table for the model wherebelongsTo
is defined, e.g.,records
). - Owner Key: Eloquent assumes the key on the related table (e.g.,
genres
) isid
.
The full signature is belongsTo(RelatedModel::class, 'foreign_key_on_owning_table', 'owner_key_on_related_table')
. Specify the second and third arguments only if your keys deviate from the convention.
php
public function genre(): BelongsTo
{
// Short version (if conventions are followed)
return $this->belongsTo(Genre::class)->withDefault();
// Long version (explicitly specifying keys)
// return $this->belongsTo(Genre::class, 'genre_id', 'id')->withDefault();
}
1
2
3
4
5
6
7
8
2
3
4
5
6
7
8
Read more: One To Many (Inverse) / Belongs To
Defining the Other Relationships
Now, apply the same principles to define the remaining relationships in the User
, Order
, and Orderline
models.
A User can have many Orders (one-to-many).
- Open
app/Models/User.php
. - Add the
orders()
method:
php
<?php
namespace App\Models;
// ... other use statements
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Illuminate\Database\Eloquent\Relations\HasMany;
class User extends Authenticatable
{
use HasFactory, Notifiable;
protected $guarded = ['id', 'created_at', 'updated_at'];
// Relationship: A User has many Orders
public function orders(): HasMany
{
return $this->hasMany(Order::class);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
More on Naming Conventions
As you've seen, adhering to Laravel's naming conventions makes defining Eloquent relationships much simpler, often requiring minimal configuration. Much of Laravel's "magic" relies on these conventions.
Let's recap and mention a few other important conventions:
Table Names
- Convention: Eloquent assumes the database table name corresponding to a model is the
snake_case
, plural version of the model's class name. - Override: If your table name doesn't follow this convention, you must explicitly tell Eloquent the correct table name by defining a
protected $table
property on your model.
Model Class | Assumed Table Name | $table Property Needed? | Example Override |
---|---|---|---|
Genre | genres | No | |
Record | records | No | |
Order | orders | No | |
Orderline | orderlines | No | |
User | users | No (Matches default Laravel setup) | |
Sheep | sheep | Yes (Plural is same as singular) | protected $table = 'sheep'; |
MainCountry | main_countries | No | |
LegacyData | legacyData | Yes (Doesn't match snake_case plural) | protected $table = 'legacyData'; |
Primary Keys
- Convention: Eloquent assumes each table has a primary key column named
id
. - Override: If your table's primary key column is named differently, define a
protected $primaryKey
property on your model. Eloquent also assumes the primary key is an auto-incrementing integer. If it's not incrementing or not an integer, you should set thepublic $incrementing
property tofalse
and potentially theprotected $keyType
property tostring
.
Table | Primary Key Column | $primaryKey Needed? | Example Override | Notes |
---|---|---|---|---|
genres | id | No | Assumes auto-incrementing integer | |
posts | post_id | Yes | protected $primaryKey = 'post_id'; | Still assumes auto-incrementing integer |
items | uuid | Yes | protected $primaryKey = 'uuid'; | Set $incrementing = false; , $keyType = 'string'; |
By understanding and generally following these conventions, you leverage Eloquent's power and reduce the amount of configuration code you need to write. We have now set up the basic structure and relationships for our Eloquent models. In the next part, we will explore more advanced features like attribute casting and query scopes.