The Model::create() method seems innocent—pass an array, get a new database record. But this convenience hides a critical security feature: mass assignment protection. Without it, malicious users could modify any database column. Let's explore how Laravel protects your data while keeping your code clean.
What Is Model::create()?
The create() method creates and saves a new model in a single step:
// Create a new user
$user = User::create([
'name' => 'John Doe',
'email' => 'john@example.com',
'password' => Hash::make('secret'),
]);
// Combines these two operations:
$user = new User();
$user->name = 'John Doe';
$user->email = 'john@example.com';
$user->password = Hash::make('secret');
$user->save();
Convenient syntax, but powerful security underneath.
The Mass Assignment Vulnerability
Without protection, create() would be dangerous:
// Controller receiving user input
public function store(Request $request)
{
// Dangerous - no protection
User::create($request->all());
}
A malicious user could send:
{
"name": "Hacker",
"email": "hacker@evil.com",
"password": "secret",
"is_admin": true,
"account_balance": 1000000
}
Without mass assignment protection, they'd create an admin account with a million-dollar balance. Laravel prevents this.
The create() Flow
When you call Model::create(), Laravel executes a security-conscious process:
Step 1: Static Method Entry
// When you call
$user = User::create($attributes);
// Behind the scenes
public static function create(array $attributes = [])
{
$model = new static($attributes);
$model->save();
return $model;
}
The static method creates a new instance and saves it.
Step 2: Constructor Mass Assignment
// Behind the scenes in __construct
public function __construct(array $attributes = [])
{
$this->syncOriginal();
$this->fill($attributes);
}
The constructor calls fill() to set attributes.
Step 3: The fill() Method
// Behind the scenes
public function fill(array $attributes)
{
$totallyGuarded = $this->totallyGuarded();
foreach ($this->fillableFromArray($attributes) as $key => $value) {
if ($this->isFillable($key)) {
$this->setAttribute($key, $value);
} elseif ($totallyGuarded) {
throw new MassAssignmentException($key);
}
}
return $this;
}
This is where mass assignment protection happens.
Step 4: Fillable Check
// Behind the scenes
public function isFillable($key)
{
// If fillable is defined, check if key is in it
if (in_array($key, $this->getFillable())) {
return true;
}
// If guarded contains '*', nothing is fillable
if ($this->isGuarded($key)) {
return false;
}
return empty($this->getFillable()) && !str_starts_with($key, '_');
}
Laravel checks if the attribute can be mass-assigned.
Step 5: Attribute Setting
// Behind the scenes
protected function setAttribute($key, $value)
{
// Check for mutator
if ($this->hasSetMutator($key)) {
return $this->setMutatedAttributeValue($key, $value);
}
// Cast the value
$value = $this->castAttributeToSave($key, $value);
// Store in attributes array
$this->attributes[$key] = $value;
}
Allowed attributes are set on the model.
Step 6: Database Insert
// Behind the scenes in save()
if (!$this->exists) {
$saved = $this->performInsert($query);
}
// Generates SQL
INSERT INTO users (name, email, password, created_at, updated_at)
VALUES (?, ?, ?, ?, ?)
The model is inserted into the database.
The $fillable Property
Define which attributes can be mass-assigned:
class User extends Model
{
protected $fillable = [
'name',
'email',
'password',
];
}
// These work
User::create([
'name' => 'John',
'email' => 'john@example.com',
'password' => Hash::make('secret'),
]);
// This is silently ignored (is_admin not fillable)
User::create([
'name' => 'John',
'email' => 'john@example.com',
'is_admin' => true, // Ignored!
]);
Whitelist approach: Only explicitly listed attributes can be mass-assigned.
The $guarded Property
Define which attributes CANNOT be mass-assigned:
class User extends Model
{
protected $guarded = [
'is_admin',
'account_balance',
];
}
// These work
User::create([
'name' => 'John',
'email' => 'john@example.com',
]);
// These are silently ignored
User::create([
'is_admin' => true, // Guarded!
'account_balance' => 1000, // Guarded!
]);
Blacklist approach: Everything except listed attributes can be mass-assigned.
$fillable vs $guarded
Use $fillable (Recommended)
protected $fillable = [
'name',
'email',
'password',
];
Pros:
- Explicit about what's allowed
- Safer default (deny everything except listed)
- New columns are protected by default
Cons:
- Must update when adding safe columns
Use $guarded
protected $guarded = [
'id',
'is_admin',
];
// Or guard everything
protected $guarded = ['*'];
Pros:
- Less maintenance for models with many columns
- New columns are allowed by default
Cons:
- Less secure (new columns allowed by default)
- Easy to forget to guard sensitive columns
The Golden Rule
Never use both simultaneously. Choose one approach:
// Good - uses fillable
protected $fillable = ['name', 'email'];
// Good - uses guarded
protected $guarded = ['is_admin'];
// Bad - confusing and error-prone
protected $fillable = ['name', 'email'];
protected $guarded = ['is_admin'];
Bypassing Mass Assignment Protection
Sometimes you need to bypass protection:
forceFill() Method
// Ignores fillable/guarded
User::create([
'name' => 'Admin',
'email' => 'admin@example.com',
])->forceFill([
'is_admin' => true,
])->save();
Use forceFill() for internal operations, never with user input.
Guarding Nothing
protected $guarded = [];
// Everything is mass-assignable
User::create($request->all()); // Dangerous!
Warning: Only use $guarded = [] if you validate ALL input thoroughly.
Unguard Temporarily
// Disable mass assignment protection
Model::unguard();
User::create(['is_admin' => true]); // Works
// Re-enable protection
Model::reguard();
Useful for seeding, never for production code with user input.
Best Practices for Mass Assignment
1. Always Validate Before create()
// Good - validated input
public function store(Request $request)
{
$validated = $request->validate([
'name' => 'required|string|max:255',
'email' => 'required|email|unique:users',
'password' => 'required|min:8',
]);
User::create($validated);
}
// Bad - unvalidated input
public function store(Request $request)
{
User::create($request->all()); // Dangerous!
}
2. Use $fillable for Security-Critical Models
class User extends Model
{
// Explicit whitelist
protected $fillable = [
'name',
'email',
'password',
];
// is_admin, account_balance, etc. are protected
}
3. Hash Passwords in Mutators, Not Controllers
class User extends Model
{
protected $fillable = ['name', 'email', 'password'];
// Automatic password hashing
public function setPasswordAttribute($value)
{
$this->attributes['password'] = Hash::make($value);
}
}
// Controller stays clean
User::create([
'name' => 'John',
'email' => 'john@example.com',
'password' => 'plain-text', // Automatically hashed
]);
4. Never Use request()->all() Directly
// Bad - passes ALL input including hidden fields
User::create($request->all());
// Good - only specific fields
User::create($request->only(['name', 'email', 'password']));
// Better - validated input
User::create($request->validated());
Common Patterns
Create with Relationships
$user = User::create([
'name' => 'John Doe',
'email' => 'john@example.com',
]);
// Create related records
$user->posts()->create([
'title' => 'First Post',
'content' => 'Hello World',
]);
Create or Update (Upsert)
// Update if exists, create if not
User::updateOrCreate(
['email' => 'john@example.com'], // Search criteria
['name' => 'John Doe'] // Values to set
);
Batch Creation
// Create multiple records
User::insert([
['name' => 'User 1', 'email' => 'user1@example.com'],
['name' => 'User 2', 'email' => 'user2@example.com'],
]);
// Note: insert() bypasses mass assignment protection
// And doesn't fire model events
firstOrCreate
// Find or create
$user = User::firstOrCreate(
['email' => 'john@example.com'],
['name' => 'John Doe']
);
Model Events with create()
The create() method fires several events:
class User extends Model
{
protected static function booted()
{
static::creating(function ($user) {
// Before INSERT query
Log::info('Creating user', ['email' => $user->email]);
});
static::created(function ($user) {
// After INSERT query
Mail::to($user)->send(new WelcomeEmail());
});
}
}
Events fire in this order:
creating- Before database insertcreated- After database insertsaved- After any save operation
Error Handling
try {
$user = User::create([
'name' => 'John',
'email' => 'duplicate@example.com', // Duplicate email
]);
} catch (\Illuminate\Database\QueryException $e) {
// Handle database errors (unique constraint, etc.)
if ($e->getCode() === '23000') {
return back()->withErrors(['email' => 'Email already exists']);
}
}
Testing create() with Mass Assignment
public function test_cannot_mass_assign_admin_role()
{
$user = User::create([
'name' => 'Test User',
'email' => 'test@example.com',
'is_admin' => true, // Attempt to exploit
]);
$this->assertFalse($user->is_admin);
$this->assertDatabaseHas('users', [
'email' => 'test@example.com',
'is_admin' => false,
]);
}
Performance Considerations
// create() - Fires events, runs through full model lifecycle
User::create(['name' => 'John']); // ~10-20ms
// insert() - Raw query, no events, no mass assignment protection
User::insert(['name' => 'John']); // ~5-10ms
// Use create() for normal operations
// Use insert() only for bulk operations where you control the data
Debugging Mass Assignment Issues
// Enable mass assignment exceptions
// In config/app.php
'debug' => true,
// Or in code
protected $guarded = ['*']; // Guard everything
User::create(['name' => 'John']);
// Throws: MassAssignmentException
Common Mistakes
Mistake 1: Forgetting to Define $fillable
class Post extends Model
{
// No $fillable or $guarded defined
}
Post::create(['title' => 'Test']); // Silently fails!
// Fix: Define $fillable
protected $fillable = ['title', 'content'];
Mistake 2: Using Both $fillable and $guarded
// Confusing - don't do this
protected $fillable = ['name', 'email'];
protected $guarded = ['is_admin'];
// Pick one approach
protected $fillable = ['name', 'email'];
Mistake 3: Allowing All with Empty Guarded
// Dangerous in production
protected $guarded = [];
User::create($request->all()); // All input allowed!
Conclusion
Laravel's Model::create() method brilliantly balances convenience and security through mass assignment protection. By understanding how $fillable and $guarded work, the flow from user input to database insert, and best practices for validation, you can write code that's both clean and secure.
Remember: mass assignment protection is your first line of defense against malicious input. Always define $fillable, validate user input, and never blindly trust request()->all().
Building secure Laravel applications with complex data models? At NeedLaravelSite, we specialize in Laravel security and application migrations from version 7 to 12. From mass assignment protection to input validation strategies, we build applications that are secure by default.