Skip to content
Carl Victor Fontanos
Carl Victor Fontanos

Carl Victor Fontanos

Software Engineer

I build web applications and share what I learn along the way.

© 2026

DatePeriod: The Loop You Were Hand-Rolling for Calendars

C
Carlo Fontanos
· 2 min read

The hand-written version of "loop over each day" is a while loop that clones a DateTime, adds a day, and - in the versions I keep finding in real code - either mutates a shared object, skips the last day, or double-counts across a DST change. PHP has had the purpose-built tool since 5.3:

$period = new DatePeriod(
    new DateTimeImmutable('2026-07-01'),
    new DateInterval('P1D'),                 // step: 1 day
    new DateTimeImmutable('2026-08-01'),     // end (exclusive by default)
);

foreach ($period as $day) {
    echo $day->format('D Y-m-d') . PHP_EOL;  // each $day is a fresh immutable object
}

Three arguments - start, step, end - and iteration just works. Because the start was a DateTimeImmutable, every yielded date is immutable too: no aliasing bugs, nothing to clone defensively.

The end-exclusivity decision

By default the end date is not included - half-open ranges, like array slices, which compose without overlap (July's range ends where August's begins). When you want it included, say so explicitly (PHP 8.2+):

new DatePeriod($start, $interval, $end, DatePeriod::INCLUDE_END_DATE);

Before 8.2, the idiom was extending $end by one step. Either way, the decision is now visible in the code instead of buried in a <= vs < comparison.

Recurrences: the other constructor

// "Next 12 billing dates, monthly from signup"
$cycles = new DatePeriod($signupDate, new DateInterval('P1M'), 12);

// "Every Monday for 8 weeks" - anchor with a date phrase first
$start = new DateTimeImmutable('next monday');
$standups = new DatePeriod($start, new DateInterval('P1W'), 8);

Count instead of end date. Note the monthly-billing classic hiding here: P1M from Jan 31 overflows into March (the same month-arithmetic trap as modify('+1 month')) - for billing anchored to month-ends, generate from 'first day of' anchors and clamp the day.

Interval spellings worth memorizing

P1D, P1W, P1M, P1Y for calendar steps; PT1H, PT30M for time; composites like P1DT12H. The P/T split (Period/Time) is the only syntax hurdle. For business-day iteration, DatePeriod has no skip rule - iterate days and filter (if ($day->format('N') >= 6) continue;), which stays honest about holidays needing a real calendar anyway.

// Calendar-month grid, padded to full weeks - DatePeriod does the heavy lifting
$first = new DateTimeImmutable('first day of this month midnight');
$gridStart = $first->modify('monday this week');
$gridEnd = $first->modify('last day of this month')->modify('sunday this week');
$weeks = iterator_to_array(new DatePeriod($gridStart, new DateInterval('P1D'),
    $gridEnd, DatePeriod::INCLUDE_END_DATE));

That's a month-view calendar's date grid in four lines - the kind of code that used to be a 40-line function with three off-by-one fixes in its git history. Ranges are data; iterate them like data.

C
Written by Carlo Fontanos

Full-stack web developer sharing practical tutorials and building tools that ship.

Got something on your mind?

My inbox is open - no forms disappearing into the void here.

  • Just say hello Found a tutorial useful? Spotted a mistake? Tell me.
  • Hire me for a project Have something custom in mind? Let's talk scope and timelines.
  • Product support Bought something here? I'll help you get it running.

I usually reply within 1-2 business days.

Message sent!

Your details are only used to reply to you.

Keep reading