cmixin/business-day
A PHP package for business-day calculations with customizable weekends and holidays. Add or subtract business days, check whether a date is a business day, and handle regional holiday rules—useful for scheduling, deadlines, and Laravel applications.
Install
composer require cmixin/business-day
Register the mixin — the package adds methods to Carbon via Carbon::mixin(), so it must be bootstrapped once, typically in a service provider:
use Carbon\Carbon;
use Cmixin\BusinessDay;
// AppServiceProvider::boot()
Carbon::mixin(new BusinessDay());
Check the README for your installed version: some releases document a one-liner or auto-registration, but explicitly mixing in a provider is the safest, most predictable path and makes the dependency obvious to the next developer.
Configure the calendar once
Carbon::setWeekendDays([Carbon::SATURDAY, Carbon::SUNDAY]);
Carbon::setHolidays([
'2025-01-01', // New Year
'2025-12-25', // Christmas
]);
First use case — the canonical "give me a deadline that skips weekends and holidays":
$deadline = Carbon::parse('2025-12-24')->addBusinessDays(3);
// 2025-12-30 (Christmas skipped, weekend skipped)
Once that works, the natural next lookups are isBusinessDay(), nextBusinessDay(), subBusinessDays(), and getBusinessDaysInMonth() — those four plus the add/sub pair cover the overwhelming majority of day-to-day needs.
Wrap the static calendar in your own service. The biggest day-to-day win is never touching Carbon::setHolidays() from controllers. Instead:
final class BusinessCalendar
{
public function __construct(private array $holidays, private array $weekendDays = []) {}
public function apply(): void
{
Carbon::setWeekendDays($this->weekendDays ?: [Carbon::SATURDAY, Carbon::SUNDAY]);
Carbon::setHolidays($this->holidays);
}
public function addBusinessDays(Carbon $from, int $days): Carbon
{
$this->apply();
return $from->copy()->addBusinessDays($days);
}
}
This keeps configuration near the domain concept and gives you one place to swap in a tenant/region calendar.
Per-tenant or per-locale calendars. In multi-tenant apps, resolve the holiday list from the tenant (or country) and apply it at the start of the request — middleware is a clean fit. If you can't afford global mutation, keep a stack of calendars and apply the right one immediately before each computation, ideally inside a small helper rather than scattered in business logic.
Deadline and SLA arithmetic. This is the killer use case: support tickets, shipping estimates, contract notice periods, payroll cutoffs. Encapsulate it as a domain method ($ticket->dueBy(), $contract->noticeDate()), not as inline Carbon calls, so the "is this a business day?" rule lives in one testable place.
Counting rather than incrementing. For proration, billing, or "how many working days in this period?", use the counting helpers (getBusinessDaysInMonth(), getBusinessDaysInYear()) instead of looping addDay() and testing isBusinessDay() — cheaper and far less error-prone at boundaries. For arbitrary ranges, a loop over a CarbonPeriod filtering on isBusinessDay() is readable and usually fast enough; measure before optimizing.
Compose with Carbon's existing API. The mixin is additive, so addBusinessDays() slots into code that already uses addDays(), setTestNow(), diffInDays(), and period iteration. Don't reinvent scheduling; use isBusinessDay() as a predicate inside existing Carbon flows.
Testing pattern. Freeze time and pin the calendar in a base test case:
protected function setUp(): void
{
parent::setUp();
Carbon::setTestNow('2025-12-24 09:00:00');
Carbon::setHolidays(['2025-12-25']);
}
Assert on exact date strings (->toDateString()) rather than diffs — it makes off-by-one failures immediately visible.
addBusinessDays(1) does not count today. It advances to the next business day. If you're computing "5 business days from now" and today is a business day, you generally want addBusinessDays(5) and should confirm whether your spec counts today as day one. Write a test for both the "today is a business day" and "today is a holiday" cases — this is the single most common bug with this package.
Methods mutate the instance. Like addDays(), addBusinessDays() changes the receiver. Always ->copy() first, or prefer CarbonImmutable — but verify immutable behavior against your installed version, because mixin implementations that assume mutation are a known sharp edge in Carbon add-ons.
Holidays are global static state and they leak. Set them in a provider or middleware and they persist for the whole process — including into the next test, the next queued job, and the next tenant's request. Reset in tearDown() (Carbon::setHolidays([])), and be suspicious of any test that passes alone but fails in a suite.
Order of operations around setTestNow. If you freeze time before applying the calendar, or vice versa, you can end up with holiday lists that don't match the mocked year. Apply the calendar after setTestNow() in tests, and prefer explicit year-scoped holiday fixtures.
Date-only comparison and timezones. Holiday matching is fundamentally a date comparison. A Carbon object in a non-UTC timezone can map to a different calendar date than you expect — normalize with ->toDateString() when building holiday lists, and be deliberate about the app timezone. Cross-timezone deadlines ("3 business days from now in the customer's timezone") need an explicit conversion step, not a hope.
Holiday input format. You'll get the best mileage out of 'Y-m-d' strings or Carbon instances; mixed arrays of DateTime, timestamps, and strings invite silent mismatches. Pick one representation in your calendar service and normalize on the way in.
isWeekend(), isHoliday(), and isBusinessDay() are three different questions. When a date "isn't business," debug by checking each individually — it's almost always a weekend-days configuration issue (e.g. a Middle East calendar where Fri/Sat are weekend) rather than a missing holiday.
Weekend days are configurable and easy to forget. Defaults are Sat/Sun. Any app serving regions with different weekends must set them per locale; otherwise every count is quietly wrong for those users.
Register the mixin before anything touches Carbon. Doing it lazily inside a controller means early code (middleware, model accessors, config resolution) sees a Carbon without the business-day methods, producing confusing BadMethodCallExceptions. One provider, boot() method, no exceptions.
Carbon 2 vs Carbon 3. Pin your Carbon major version in composer.json alongside this package and run the suite on upgrade — mixin method signatures and mutability semantics shift between majors, and this package's value is entirely in behaving identically across your codebase.
Extension point: subclass the mixin. If you need organization-specific methods (nextPayrollDate(), addBusinessDaysSkippingFridays()), extend Cmixin\BusinessDay and mix in your subclass rather than scattering helpers. You keep everything under the same Carbon::mixin() registration and one place to test.
Performance on long ranges. Adding thousands of business days or counting across years is loop-based under the hood. For hot paths, cache counts per month/year, or precompute a holiday index keyed by date string so lookups are isset() checks rather than array scans.
How can I help you explore Laravel packages today?