->toArray();
}
public function isNonWorkingDay(Carbon $date): bool
{
return $date->isWeekend()
|| in_array($date->toDateString(), $this->holidays);
}
public function addBusinessDays(Carbon $date, int $days): Carbon
{
$result = $date->copy();
$added = 0;
while ($added < $days) {
$result->addDay();
if (!$this->isNonWorkingDay($result)) {
$added++;
}
}
return $result;
}
}
Enter fullscreen mode Exit fullscreen mode
How It Works
The addBusinessDays method loops day by day β skipping weekends and holidays β until it has counted the required number of business days.
For example β if today is Friday and you need 3 business days:
Saturday β skip (weekend)
Sunday β skip (weekend)
Monday β count (1)
Tuesday β count (2)
Wednesday β count (3) β deadline
If Monday is a holiday:
Saturday β skip (weekend)
Sunday β skip (weekend)
Monday β skip (holiday)
Tuesday β count (1)
Wednesday β count (2)
Thursday β count (3) β deadline
Using It in the Application
php$dateHelpers = new DateHelpers();
$deadline = $dateHelpers->addBusinessDays(Carbon::now(), 3);
When a department receives a voucher β the deadline is calculated automatically and stored in the audit trail:
AuditTrail::create([
'transaction_id' => $transaction->id,
'processing_offices' => $department->name,
'process_initiate' => now(),
'deadline' => $dateHelpers->addBusinessDays(Carbon::now(), 3),
]);
Enter fullscreen mode Exit fullscreen mode
Caching for Performance
Since holidays don't change often β I cached the holidays list to avoid hitting the database on every request:
public function __construct()
{
$this->holidays = cache()->remember('holidays', now()->addDay(), function () {
return Holiday::pluck('date')
->map(fn($d) => Carbon::parse($d)->toDateString())
->toArray();
});
}
Enter fullscreen mode Exit fullscreen mode
This caches the holidays list for 24 hours β refreshing automatically every day.
Checking for Overdue Deadlines
With Carbon's diffInDays you can easily check if a deadline has passed:
$deadline = Carbon::parse($auditTrail->deadline)->startOfDay();
$daysLeft = (int) Carbon::now()->startOfDay()->diffInDays($deadline, false);
// Negative = overdue, Positive = days remaining, Zero = due today
if ($daysLeft < 0) {
// Flag as overdue
}
Enter fullscreen mode Exit fullscreen mode
The false parameter makes diffInDays return negative numbers for past dates β useful for overdue detection.
Managing Holidays
Since holidays are stored in the database β admins can manage them through a simple CRUD interface:
// Add a holiday
Holiday::create([
'name' => 'Independence Day',
'date' => '2026-06-12',
]);
// Get all holidays for the year
Holiday::whereYear('date', now()->year)->get();
Enter fullscreen mode Exit fullscreen mode
No code deployments needed when a new holiday is declared β just add it to the database.
What I Learned
Keep business logic out of the database β date calculations belong in PHP, not SQL
Cache holiday lookups β they rarely change but get queried often
Use Carbon consistently β mixing Carbon with raw date strings causes subtle bugs
Test edge cases β Friday submissions, holiday Mondays, and long weekends all need testing
Conclusion
Holiday-aware deadline calculations sound simple but have more edge cases than you'd expect. A dedicated service class with a database-driven holidays table keeps the logic clean, flexible, and easy to maintain.
This pattern works for any system that needs business day calculations β leave requests, SLA tracking, invoice due dates, and more.
Want to see this in action?
I built VMMS β a complete Voucher Management & Monitoring System using everything covered in this article.
π΄ Live demo: [https://vmms-app-production.up.railway.app/login](https://vmms-app-production.up.railway.app/login)
π° Get the full source code: [https://getvmms.gumroad.com/l/zeroqz](https://getvmms.gumroad.com/l/zeroqz)
Happy to answer any questions in the comments! π