Setting Default Database Column Values in Laravel Eloquent Models
When building Laravel applications, you'll often need to set default values for database columns. Whether it's marking new users as inactive, setting default order status, or initializing counters, Laravel provides multiple approaches to handle default values effectively.
Database-Level Defaults vs Model-Level Defaults
Laravel offers two primary methods for setting default values: at the database level through migrations, or at the model level using Eloquent's $attributes
property. Understanding when to use each approach is crucial for building maintainable applications.
Database-Level Defaults
Setting defaults in your database migrations ensures data integrity at the lowest level:
// Migration file
Schema::create('posts', function (Blueprint $table) {
$table->id();
$table->string('title');
$table->text('content');
$table->boolean('is_published')->default(false);
$table->integer('view_count')->default(0);
$table->string('status')->default('draft');
$table->timestamps();
});
This approach has several advantages:
- Data integrity: Defaults are enforced even when data is inserted outside of Laravel
- Performance: No additional PHP processing required
- Database consistency: Works with any application accessing the database
However, changing database defaults requires new migrations, making them less flexible for evolving business requirements.
Model-Level Defaults
Laravel 12's Eloquent models support default attributes through the $attributes
property:
class Post extends Model
{
protected $fillable = [
'title',
'content',
'is_published',
'status'
];
protected $attributes = [
'is_published' => false,
'view_count' => 0,
'status' => 'draft',
];
protected $casts = [
'is_published' => 'boolean',
];
}
Model-level defaults offer greater flexibility:
- Easy updates: Change defaults without database migrations
- Environment-specific: Different defaults for different environments
- Dynamic values: Can use PHP expressions for complex defaults
Advanced Default Value Techniques
Dynamic Defaults with Accessors
For more complex default logic, you can use accessors to provide computed defaults:
class User extends Model
{
protected $attributes = [
'role' => 'user',
'is_active' => true,
];
protected $appends = ['full_name'];
// Dynamic default for display name
public function getDisplayNameAttribute()
{
return $this->attributes['display_name'] ?? $this->full_name;
}
public function getFullNameAttribute()
{
return trim("{$this->first_name} {$this->last_name}");
}
}
Using Model Events for Complex Defaults
Sometimes you need more sophisticated default logic that depends on other model attributes or external data:
class Order extends Model
{
protected $attributes = [
'status' => 'pending',
'currency' => 'USD',
];
protected static function boot()
{
parent::boot();
static::creating(function ($order) {
// Set order number if not provided
if (empty($order->order_number)) {
$order->order_number = 'ORD-' . strtoupper(uniqid());
}
// Set default shipping method based on customer location
if (empty($order->shipping_method)) {
$order->shipping_method = $order->customer->country === 'US'
? 'standard'
: 'international';
}
});
}
}
Real-World Examples
E-commerce Product Management
class Product extends Model
{
protected $attributes = [
'status' => 'draft',
'visibility' => 'private',
'stock' => 0,
'price' => 0,
'weight' => 0,
'tax_class' => 'standard',
'featured' => false,
];
protected $casts = [
'featured' => 'boolean',
'price' => 'decimal:2',
'weight' => 'decimal:2',
];
public function scopePublished($query)
{
return $query->where('status', 'published')
->where('visibility', 'public');
}
}
User Account Management
class User extends Model
{
protected $attributes = [
'role' => 'customer',
'is_active' => true,
'email_verified' => false,
'newsletter_subscribed' => false,
'timezone' => 'UTC',
'locale' => 'en',
];
protected $casts = [
'is_active' => 'boolean',
'email_verified' => 'boolean',
'newsletter_subscribed' => 'boolean',
'email_verified_at' => 'datetime',
];
protected static function boot()
{
parent::boot();
static::creating(function ($user) {
// Set default timezone based on IP geolocation
if (empty($user->timezone)) {
$user->timezone = request()->header('Timezone') ?? 'UTC';
}
});
}
}
Best Practices
1. Combine Both Approaches Strategically
Use database defaults for critical business logic and model defaults for UI/UX conveniences:
// Migration - critical business defaults
Schema::create('subscriptions', function (Blueprint $table) {
$table->id();
$table->string('status')->default('trial'); // Business critical
$table->timestamp('trial_ends_at')->nullable();
$table->timestamps();
});
// Model - UI/display defaults
class Subscription extends Model
{
protected $attributes = [
'notifications_enabled' => true, // User preference
'auto_renew' => true, // User preference
];
}
2. Test Your Defaults
Always test that your default values work as expected:
// tests/Unit/Models/PostTest.php
class PostTest extends TestCase
{
public function test_post_has_correct_defaults()
{
$post = new Post([
'title' => 'Test Post',
'content' => 'Test content'
]);
$this->assertFalse($post->is_published);
$this->assertEquals(0, $post->view_count);
$this->assertEquals('draft', $post->status);
}
public function test_post_defaults_persist_after_save()
{
$post = Post::create([
'title' => 'Test Post',
'content' => 'Test content'
]);
$post->refresh();
$this->assertFalse($post->is_published);
$this->assertEquals('draft', $post->status);
}
}
3. Document Your Defaults
Keep your defaults well-documented, especially when they involve business logic:
class Order extends Model
{
/**
* Default attribute values
*
* status: All orders start as 'pending' awaiting payment
* currency: Default to USD, can be overridden by user location
* tax_included: Defaults to false for B2B, true for B2C
*/
protected $attributes = [
'status' => 'pending',
'currency' => 'USD',
'tax_included' => false,
];
}
Performance Considerations
When using model-level defaults, be aware of potential performance impacts:
- Mass Creation: Model defaults only apply to individual model creation, not bulk inserts
- Database Queries: Model defaults require instantiating models, while database defaults don't
For high-volume operations, consider using database defaults or explicit value setting:
// Efficient for bulk operations
Product::insert([
['name' => 'Product 1', 'status' => 'draft', 'price' => 0],
['name' => 'Product 2', 'status' => 'draft', 'price' => 0],
// ... hundreds more
]);
// Less efficient - creates model instances
collect($productData)->each(function ($data) {
Product::create($data); // Applies model defaults
});
Conclusion
Setting appropriate default values is essential for building robust Laravel applications. Database-level defaults provide data integrity and performance, while model-level defaults offer flexibility and maintainability.
The key is choosing the right approach based on your specific requirements:
- Use database defaults for critical business logic and data integrity
- Use model defaults for user preferences and UI conveniences
- Use model events for complex, conditional defaults
- Always test your defaults thoroughly
By mastering these techniques, you'll build more reliable and maintainable Laravel applications that handle edge cases gracefully and provide better user experiences out of the box.