Have you ever wanted to create a custom calendar in your PHP code but weren’t sure how to approach it? How do you structure it? How do you consider the differences in the length of each month? How do you render a month with not just the current month’s numbers but also the leading and trailing days from the previous and next months? Thankfully, Carbon has a few often-unused tools we can leverage to make this a smoother process.
Today, we’ll be building a Calendar
PHP class and a Month
Blade component using Carbon, its CarbonPeriod feature, and Laravel’s Collections. We’ll place the Blade component in a few different contexts where it might be valuable to be able to navigate to all of the days of a current month or multiple months. This will be a good starting point for creating a custom day planner.
You’re likely familiar with Carbon, PHP’s most popular date library. If you use Laravel, you might even be using Carbon without realizing it since Laravel models return Carbon
instances by default for the created_at
and updated_at
timestamps.
However familiar you are with Carbon, you may not know that Carbon also has a CarbonPeriod
class that represents (and lets you work with) a range of dates. CarbonPeriod
has a lot of options that are worth looking into, but for our needs, we’ll only be using Carbon’s toPeriod
static method, which will give us a CarbonPeriod
instance with our range of dates. We’ll then immediately convert that instance into an array that we’ll use to build our calendar.
Let’s start by creating a Calendar
support class with a buildMonth
method that expects integers to represent a year and month. Our method will create a new CarbonImmutable
instance from those values. We’ll go ahead and return an array with year
and month
values which can be used to label our rendered month.
<?php namespace App\Support; use Carbon\CarbonImmutable; class Calendar{ public static function buildMonth($year, $month) { $startOfMonth = CarbonImmutable::create($year, $month, 1); return [ 'year' => $startOfMonth->year, 'month' => $startOfMonth->format('F'), ]; }}
Using our Calendar
class will look like this:
Calendar::buildMonth(year: 2022, month: 2);
Now that we have our buildMonth
method with a CarbonImmutable
date, let’s update it to get the ending boundary for the month. We can use Carbon’s endOfMonth
method for this.
public static function buildMonth($year, $month) { $startOfMonth = CarbonImmutable::create($year, $month, 1);+ $endOfMonth = $startOfMonth->endOfMonth(); return [ 'year' => $startOfMonth->year, 'month' => $startOfMonth->format('F'), ]; }
Note: we’re using
CarbonImmutable
instead ofCarbon
so that every time we call a method likeendOfMonth()
, we get a new instance instead of accidentally modifying the original instance. If we wanted to useCarbon
instead, we would need to call$startOfMonth->copy()
before the modifier to ensure we have a newCarbon
instance to work with instead of modifying the original instance. See the Carbon docs for more information.
Next, we can use Carbon’s toPeriod
method to get a CarbonPeriod
instance representing the period between the two dates.
public static function buildMonth($year, $month) { $startOfMonth = CarbonImmutable::create($year, $month, 1); $endOfMonth = $startOfMonth->endOfMonth(); return [ 'year' => $startOfMonth->year, 'month' => $startOfMonth->format('F'),+ 'dates' => $startOfMonth->toPeriod($endOfMonth), ]; }
Now, we want to convert our CarbonPeriod
instance into an array and map over it. For now, we want each date to return a URL path and the day number for rendering our month.
public static function buildMonth($year, $month) { $startOfMonth = CarbonImmutable::create($year, $month, 1); $endOfMonth = $startOfMonth->endOfMonth(); return [ 'year' => $startOfMonth->year, 'month' => $startOfMonth->format('F'),- 'dates' => $startOfMonth->toPeriod($endOfMonth), + 'dates' => return collect($startOfMonth->toPeriod($endOfMonth)->toArray())+ ->map(fn ($date) => [+ 'path' => $date->format('/Y/m/d'),+ 'day' => $date->day,+ ]), ]; }
That’s a good start, but since we want to render these day numbers in a month view, we need to split the dates so there are chunks of exactly seven days for each week.
We can use the chunk
method on our Collection
to split our array into chunks of seven dates. However, our total number of days is not guaranteed to be a multiple of seven, and the first day of the month is not guaranteed to be on Sunday.
To solve this, we need to include any leading and trailing dates from the boundary months in our CarbonPeriod
instance, whether we render those dates or not. We can use Carbon’s startOfWeek
and endOfWeek
methods for this, but we’ll keep our $startOfMonth
and $endOfMonth
variables since we’ll later want to determine if each day is within the month we’re rendering.
public static function buildMonth($year, $month) { $startOfMonth = CarbonImmutable::create($year, $month, 1); $endOfMonth = $startOfMonth->endOfMonth();+ $startOfWeek = $startOfMonth->startOfWeek(); + $endOfWeek = $endOfMonth->endOfWeek(); return [ 'year' => $startOfMonth->year, 'month' => $startOfMonth->format('F'),- 'dates' => return collect($startOfMonth->toPeriod($endOfMonth)->toArray())+ 'dates' => return collect($startOfWeek->toPeriod($endOfWeek)->toArray()) ->map(fn ($date) => [ 'path' => $date->format('/Y/m/d'), 'day' => $date->day, ]), ]; }
Depending on your preference, you may notice this gives you an unexpected result. Carbon’s default start of the week is Monday. So, if you’re expecting Sunday, you’ll need to pass in the desired constants from Carbon.
public static function buildMonth($year, $month) { $startOfMonth = CarbonImmutable::create($year, $month, 1); $endOfMonth = $startOfMonth->endOfMonth();- $startOfWeek = $startOfMonth->startOfWeek(); - $endOfWeek = $endOfMonth->endOfWeek();+ $startOfWeek = $startOfMonth->startOfWeek(Carbon::SUNDAY);+ $endOfWeek = $endOfMonth->endOfWeek(Carbon::SATURDAY); return [ 'year' => $startOfMonth->year, 'month' => $startOfMonth->format('F'), 'dates' => return collect($startOfWeek->toPeriod($endOfWeek)->toArray()) ->map(fn ($date) => [ 'path' => $date->format('/Y/m/d'), 'day' => $date->day, ]), ]; }
Now that we know our CarbonPeriod
instance and the resulting collection contain a number of items that’s a multiple of seven, we can use the Collection chunk
method to split the array into nested arrays with seven items each.
public static function buildMonth($year, $month) { $startOfMonth = CarbonImmutable::create($year, $month, 1); $startOfMonth = $startOfMonth->startOfMonth(); $endOfMonth = $startOfMonth->endOfMonth(); $startOfWeek = $startOfMonth->startOfWeek(Carbon::SUNDAY); $endOfWeek = $endOfMonth->endOfWeek(Carbon::SATURDAY); return [ 'year' => $startOfMonth->year, 'month' => $startOfMonth->format('F'),- 'dates' => return collect($startOfWeek->toPeriod($endOfWeek)->toArray())+ 'weeks' => return collect($startOfWeek->toPeriod($endOfWeek)->toArray()) ->map(fn ($date) => [ 'path' => $date->format('/Y/m/d'), 'day' => $date->day,- ]),+ ])+ ->chunk(7), ]; }
Now, we’re ready to create a reusable Blade component to render our month.
@props(['weeks']) <table class="m-auto text-center month"> <thead> <tr> <th>Su</th> <th>Mo</th> <th>Tu</th> <th>We</th> <th>Th</th> <th>Fr</th> <th>Sa</th> </tr> </thead> <tbody> @foreach ($weeks as $days) <tr> @foreach ($days as $day) <td> <a href="{{ $day['path'] }}" class="block py-2 hover:bg-gray-300" > {{ $day['day'] }} </a> </td> @endforeach </tr> @endforeach </tbody></table>
$calendar = Calendar::buildMonth(year: 2022, month: 2);
<div class="p-4 text-4xl text-center text-white bg-gray-900"> <a href="/" class="block"> {{ $calendar['month'] }} {{ $calendar['year'] }} </a></div> <div class="mt-8"> <x-month :weeks="$calendar['weeks']" /></div>
This component works fine at this point, but let’s tidy up our code a bit.
First, let’s style the boundary days so they aren’t as visually prominent as the days within the current month.
public static function buildMonth($year, $month) { $startOfMonth = CarbonImmutable::create($year, $month); $endOfMonth = $startOfMonth->endOfMonth(); $startOfWeek = $startOfMonth->startOfWeek(Carbon::SUNDAY); $endOfWeek = $endOfMonth->endOfWeek(Carbon::SATURDAY); return [ 'year' => $startOfMonth->year, 'month' => $startOfMonth->format('F'), 'weeks' => return collect($startOfWeek->toPeriod($endOfWeek)->toArray()) ->map(fn ($date) => [ 'path' => $date->format('/Y/m/d'), 'day' => $date->day,+ 'withinMonth' => $date->between($startOfMonth, $endOfMonth), ]) ->chunk(7), ]; }
<a href="{{ $date['path'] }}" class=" block py-2 hover:bg-gray-300+ {{ ! $date['withinMonth'] ? 'text-gray-300' : '' }} " > {{ $date['day'] }} </a>
Next, it’d be nice to show which date is currently selected. Let’s allow passing in an optional $day
parameter, and create an additional CarbonImmutable
instance $selectedDate
to represent the currently selected date. We’ll also create a selected
property on each date in our response array and set that property to true
for the item matching the specified date (if specified).
-public static function buildMonth($year, $month) -{- $startofMonth = CarbonImmutable::create($year, $month, 1);+public static function buildMonth($year, $month, $day = null) +{+ $selectedDate = CarbonImmutable::create($year, $month, $day ?? 1);+ $startOfMonth = $selectedDate->startOfMonth(); $endOfMonth = $selectedDate->endOfMonth(); $startOfWeek = $startOfMonth->startOfWeek(Carbon::SUNDAY); $endOfWeek = $endOfMonth->endOfWeek(Carbon::SATURDAY); return [ 'year' => $selectedDate->year, 'month' => $selectedDate->format('F'), 'weeks' => return collect($startOfWeek->toPeriod($endOfWeek)->toArray()) ->map(fn ($date) => [ 'path' => $date->format('/Y/m/d'), 'day' => $date->day, 'withinMonth' => $date->between($startOfMonth, $endOfMonth),+ 'selected' => $day && $date->is($selectedDate), ]) ->chunk(7), ]; }
<a href="{{ $date['path'] }}" class=" block py-2 hover:bg-gray-300 {{ ! $date['withinMonth'] ? 'text-gray-300' : '' }}+ {{ $date['selected'] ? 'font-bold bg-gray-200' : '' }} " > {{ $date['day'] }} </a>
Let’s look at another final example of how to use our month component; we’ll build a year view and a buildYear
method on the Calendar
class to support generating a full year of months.
<?php namespace App\Support; use Carbon\Carbon; class Calendar {+ public static function buildYear($year) + {+ return [+ 'year' => $year,+ 'months' => collect(range(1, 12))+ ->map(fn ($month) => static::buildMonth($year, $month)),+ ];+ } public static function buildMonth($year, $month, $day = null) { $selectedDate = CarbonImmutable::create($year, $month, $day ?? 1); $startOfMonth = $selectedDate->startOfMonth(); $endOfMonth = $selectedDate->endOfMonth(); $startOfWeek = $startOfMonth->startOfWeek(Carbon::SUNDAY); $endOfWeek = $endOfMonth->endOfWeek(Carbon::SATURDAY); return [ 'year' => $selectedDate->year, 'month' => $selectedDate->format('F'), 'weeks' => return collect($startOfWeek->toPeriod($endOfWeek)->toArray()) ->map(fn ($date) => [ 'path' => $date->format('/Y/m/d'), 'day' => $date->day, 'withinMonth' => $date->between($startOfMonth, $endOfMonth), 'selected' => $day && $date->is($selectedDate), ]) ->chunk(7), ]; } }
$calendar = Calendar::buildYear(2022);
<div class="p-4 mb-8 text-4xl text-center text-white bg-gray-900"> {{ $calendar['year'] }}</div> <div class="grid grid-cols-3 gap-8 px-8 pb-8"> @foreach ($calendar['months'] as $month) <div> <div class="p-4 mb-4 text-2xl text-center text-white bg-gray-100 dark:bg-gray-800"> {{ $month['month'] }} </div> <x-month :weeks="$month['weeks']" /> </div> @endforeach</div>
This is a good starting point. At this point, you can generate an array of dates for a month or a year’s worth of months, and each month’s array contains the boundary days from the previous and next month.
There’s plenty more we could do with our calendar beyond what we’ve covered here. Here are some ideas you could try on your own:
We appreciate your interest.
We will get right back to you.