Skip to content

Instantly share code, notes, and snippets.

@nyamsprod
Created February 18, 2023 15:19
Show Gist options
  • Save nyamsprod/c114715d59403c365964aa76eab181c4 to your computer and use it in GitHub Desktop.
Save nyamsprod/c114715d59403c365964aa76eab181c4 to your computer and use it in GitHub Desktop.
Create a Calendar in PHP using League Period
<?php
declare(strict_types=1);
require 'autoload.php';
use League\Period\DatePoint;
use League\Period\Period;
/**
* @phpstan-type CalendarDay array{year:int, month:string, day:int, path:string, withinMonth:bool, selected:bool}
* @phpstan-type CalendarWeek array(selectedDate:DateTimeImmutable|null, year:int, isoYear:int, isoWeek:int, days:iterable<CalendarDay>}
* @phpstan-type CalendarMonth array{selectedDate:DateTimeImmutable|null, year:int, month:string, weeks:iterable<CalendarWeek>}
* @phpstan-type CalendarYear array{selectedDate:DateTimeImmutable|null, year:int, months:iterable<CalendarMonth>}
*/
final class Calendar
{
/**
* @throws Exception
*
* @return CalendarYear
*/
public static function buildYear(DateTimeInterface|int $year, DateTimeInterface|null $selectedDate = null): array
{
$yearInterval = match (true) {
$year instanceof DateTimeInterface => DatePoint::fromDate($year)->year(),
default => Period::fromYear($year),
};
if (null == $selectedDate && $year instanceof DateTimeInterface) {
$selectedDate = $year;
}
$selectedDate = self::filterSelectedDate($selectedDate, $yearInterval);
/** @return iterable<CalendarMonth> */
$formatMonths = function (Period $period, DateTimeInterface|null $selectedDate): iterable {
foreach ($period->splitForward('1 MONTH') as $month) {
$firstDayOfMonth = $month->startDate;
yield self::buildMonth(
(int) $firstDayOfMonth->format('Y'),
(int) $firstDayOfMonth->format('n'),
$selectedDate
);
}
};
return [
'selectedDate' => $selectedDate,
'months' => $formatMonths($yearInterval, $selectedDate),
'year' => (int) $yearInterval->startDate->format('Y'),
];
}
/**
* @throws Exception
*
* @return CalendarMonth
*/
public static function buildMonth(int $year, int $month, DateTimeInterface|int|null $selectedDate = null): array
{
$monthInterval = Period::fromMonth($year, $month);
if (is_int($selectedDate)) {
$selectedDate = (new DateTimeImmutable())->setDate($year, $month, $selectedDate);
}
$selectedDate = self::filterSelectedDate($selectedDate, $monthInterval);
/** @return CalendarDay */
$formatDate = fn (DateTimeImmutable $date): array => [
'year' => (int) $date->format('Y'),
'month' => $date->format('F'),
'day' => (int) $date->format('j'),
'path' => $date->format('/Y/m/d'),
'withinMonth' => $monthInterval->contains($date),
'selected' => null !== $selectedDate && $date == $selectedDate,
];
/** @return CalendarWeek */
$formatWeek = fn (Period $week): iterable => [
'selectedDate' => $selectedDate,
'year' => (int) $week->startDate->format('Y'),
'isoYear' => (int) $week->startDate->format('o'),
'isoWeek' => (int) $week->startDate->format('W'),
'days' => array_map($formatDate, [...$week->rangeForward('1 day')]),
];
/** @return iterable<CalendarWeek> */
$formatWeeks = fn (Period $month): iterable => array_map($formatWeek, [...$month->splitForward('1 week')]);
$extendedMonth = $monthInterval
->startingOn(DatePoint::fromDate($monthInterval->startDate)->isoWeek()->startDate)
->endingOn(DatePoint::fromDate($monthInterval->endDate->sub(new DateInterval('P1D')))->isoWeek()->endDate);
return [
'selectedDate' => $selectedDate,
'year' => (int) $monthInterval->startDate->format('Y'),
'month' => $monthInterval->startDate->format('F'),
'weeks' => $formatWeeks($extendedMonth),
];
}
private static function filterSelectedDate(DateTimeInterface|null $date, Period $period): ?DateTimeImmutable
{
if ($date instanceof DateTimeInterface && $period->contains($date)) {
return DateTimeImmutable::createFromInterface($date)->setTime(0, 0);
}
return null;
}
}
@nyamsprod
Copy link
Author

nyamsprod commented Feb 18, 2023

This class is another implementation of the Calendar class from Building a Calendar with Carbon

It differs from the article implementation by:

  • Replacing Carbon\Carbon usage by League\Period
  • Making weeks start on Monday as per ISO-8601 week specifications.
  • Replacing Laravel\Collection by using array_map or the foreach construct on Iterator with the help of the spread operator

The public API is "augmented" by using union types to ease usage and thus can be used with the same Blade component from the article.

the usage is still the same and you can still use Carbon if you enjoy the library:

Calendar::buildMonth(2022, 2);                             // build the 2022-02 calendar without a selected date
Calendar::buildMonth(2022, 2, 23);                         // build the 2022-02 calendar with the selected date of `2022-02-23`
Calendar::buildYear(2022);                                 // build the 2022 calendar without a selected date
Calendar::buildYear(CarbonImmutable::parse('2022-02-03')); // build the 2022 calendar with the selected date of `2022-02-23`

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment