PHPackages                             tito10047/php-calendar - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Utility &amp; Helpers](/categories/utility)
4. /
5. tito10047/php-calendar

ActiveLibrary[Utility &amp; Helpers](/categories/utility)

tito10047/php-calendar
======================

Php server side rendered calendar

1.1.0(today)00PHPPHP &gt;=8.2CI passing

Since Nov 29Pushed today1 watchersCompare

[ Source](https://github.com/tito10047/php-calendar)[ Packagist](https://packagist.org/packages/tito10047/php-calendar)[ RSS](/packages/tito10047-php-calendar/feed)WikiDiscussions main Synced today

READMEChangelog (1)Dependencies (2)Versions (5)Used By (0)

php-calendar
============

[](#php-calendar)

[![PHP Tests](https://github.com/tito10047/php-calendar/actions/workflows/symfony.yml/badge.svg)](https://github.com/tito10047/php-calendar/actions/workflows/symfony.yml)[![PHP Version](https://camo.githubusercontent.com/6518db1335bf20fdff07253dc6d6d0cec955b5fb6a8ef1382ac6d73687ecc07f/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f7068702d253345253344382e312d626c7565)](https://www.php.net/)[![License](https://camo.githubusercontent.com/1ce887ab97673a4073429f1a18bd8c9557970cf8b0901e69b1911ca106833a68/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f6c6963656e73652f7469746f31303034372f7068702d63616c656e646172)](LICENSE)

**A pure PHP server-side calendar library built for Symfony UX and Laravel Livewire.**

Stop fighting JavaScript calendar widgets that break your SSR, bloat your bundle, and fight with your server state. This library renders calendars entirely on the server — immutable, composable, and framework-friendly. Feed it your events, disabled days, or any custom data. Get back clean HTML. Done.

---

Why server-side?
----------------

[](#why-server-side)

- **Works with Symfony UX Turbo / Livewire out of the box** — no hydration, no client state sync
- **Zero frontend dependencies** — just HTML + your own CSS
- **Full control over every cell** — attach any data to any day via a typed interface
- **Truly immutable** — every mutation (next month, disabled days) returns a new instance
- **Fully replaceable renderer chain** — swap any layer without touching the rest

---

Installation
------------

[](#installation)

```
composer require tito10047/php-calendar
```

---

Quick start
-----------

[](#quick-start)

```
use Tito10047\Calendar\Calendar;
use Tito10047\Calendar\Renderer;
use Tito10047\Calendar\Enum\CalendarType;
use Tito10047\Calendar\Enum\DayName;

$calendar = new Calendar(
    date: new DateTimeImmutable('2024-11-01'),
    daysGenerator: CalendarType::Monthly,
    startDay: DayName::Monday,
);

$renderer = Renderer::factory(CalendarType::Monthly, 'calendar');

echo $renderer->render($calendar);
```

That's it. You get a fully structured `` with ghost days, today marker, and day headers.

---

Calendar types
--------------

[](#calendar-types)

Three built-in views, zero configuration:

```
CalendarType::Monthly   // full month, aligned to complete weeks
CalendarType::Weekly    // one week, Mon–Sun
CalendarType::WorkWeek  // one week, Mon–Fri
```

---

Disabling days
--------------

[](#disabling-days)

All methods are immutable — they return a new `Calendar` instance.

```
// Disable specific dates
$calendar = $calendar->disableDays(
    new DateTimeImmutable('2024-11-11'),
    new DateTimeImmutable('2024-11-15'),
);

// Disable all weekends
$calendar = $calendar
    ->disableDaysByName(DayName::Saturday, DayName::Sunday);

// Disable a date range
$calendar = $calendar->disableDaysRange(
    from: new DateTimeImmutable('2024-11-25'),
    to:   new DateTimeImmutable('2024-11-30'),
);

// Disable an entire ISO week
$calendar = $calendar->disableWeek(weekNum: 47);
```

---

Navigating months
-----------------

[](#navigating-months)

```
$november = new Calendar(new DateTimeImmutable('2024-11-01'), CalendarType::Monthly);
$december = $november->nextMonth();
$october  = $november->prevMonth();
```

> **Note:** `nextMonth()` and `prevMonth()` reset disabled days. Re-apply them on the new instance if needed.

---

Attaching custom data to days
-----------------------------

[](#attaching-custom-data-to-days)

The real power: attach anything to any day — events, holidays, booking counts, whatever.

Implement `DayDataLoaderInterface`:

```
use Tito10047\Calendar\Interface\DayDataLoaderInterface;

class EventLoader implements DayDataLoaderInterface
{
    private array $byDate = [];

    public function load(DateTimeImmutable $from, DateTimeImmutable $to): void
    {
        // Called once with the full date range — bulk-load here
        $events = $this->db->query(
            'SELECT * FROM events WHERE date BETWEEN ? AND ?',
            [$from->format('Y-m-d'), $to->format('Y-m-d')]
        );

        foreach ($events as $event) {
            $this->byDate[$event['date']][] = $event;
        }
    }

    public function getData(DateTimeImmutable $date): array
    {
        // Called per day — return data for this specific date
        return $this->byDate[$date->format('Y-m-d')] ?? [];
    }
}
```

Attach it to your calendar:

```
$calendar = $calendar->setDataLoader(new EventLoader());
```

Each `Day` object will now have `$day->data` populated with whatever your loader returned.

---

Working with the days table directly
------------------------------------

[](#working-with-the-days-table-directly)

Skip the renderer entirely and build your own template:

```
$table = $calendar->getDaysTable();
// array
```

### In Twig (Symfony)

[](#in-twig-symfony)

```

            MonTueWed
            ThuFriSatSun

        {% for week in table %}

                {% for day in week %}

                        {% if not day.ghost %}
                            {{ day.date|date('j') }}

                            {% if day.data %}

                                    {% for event in day.data %}
                                        {{ event.title }}
                                    {% endfor %}

                            {% endif %}
                        {% endif %}

                {% endfor %}

        {% endfor %}

```

### In a Blade template (Laravel)

[](#in-a-blade-template-laravel)

```

        @foreach ($table as $week)

                @foreach ($week as $day)
                     $day->ghost,
                        'today'    => $day->today,
                        'disabled' => !$day->enabled,
                    ])>
                        @unless ($day->ghost)
                            {{ $day->date->format('j') }}

                            @foreach ($day->data ?? [] as $event)
                                {{ $event['title'] }}
                            @endforeach
                        @endunless

                @endforeach

        @endforeach

```

---

Symfony UX / Turbo example
--------------------------

[](#symfony-ux--turbo-example)

The calendar is immutable — perfect for Turbo Frames or Live Components where PHP re-renders on every interaction.

```
// src/Controller/CalendarController.php
#[Route('/calendar/{year}/{month}', name: 'calendar')]
public function index(int $year, int $month): Response
{
    $calendar = new Calendar(
        date: new DateTimeImmutable("$year-$month-01"),
        daysGenerator: CalendarType::Monthly,
        startDay: DayName::Monday,
    );

    $calendar = $calendar
        ->disableDaysByName(DayName::Sunday)
        ->setDataLoader(new EventLoader($this->db));

    return $this->render('calendar/index.html.twig', [
        'table'    => $calendar->getDaysTable(),
        'calendar' => $calendar,
        'prev'     => $calendar->prevMonth()->getDate(),
        'next'     => $calendar->nextMonth()->getDate(),
    ]);
}
```

```
{# templates/calendar/index.html.twig #}

        ← Prev
        {{ calendar.date|date('F Y') }}
        Next →

    {# ... render table ... #}

```

No JavaScript. No state sync. Every click is a Turbo Frame navigation that re-renders server-side.

---

Using the built-in HTML renderer
--------------------------------

[](#using-the-built-in-html-renderer)

When you just need clean HTML without writing a template:

```
$renderer = Renderer::factory(CalendarType::Monthly, translationDomain: 'calendar');
echo $renderer->render($calendar);
```

Output structure:

```

            Mon

            1
            2

```

**Available CSS classes on ``:**

ClassMeaning`ghost`Day belongs to an adjacent month`today`Matches today's date`disabled`Disabled via any `disable*` method---

The `Day` object
----------------

[](#the-day-object)

Every cell in the table is a `Day` value object:

```
final readonly class Day
{
    public DateTimeImmutable $date;
    public bool $ghost;     // belongs to adjacent month (grid filler)
    public bool $today;     // matches current system date
    public bool $enabled;   // not in the disabled list
    public ?array $data;    // populated by DayDataLoaderInterface
}
```

---

Custom days generator
---------------------

[](#custom-days-generator)

Need a custom date range — a fortnight, a quarter, a fiscal week? Implement `DaysGeneratorInterface`:

```
use Tito10047\Calendar\Interface\DaysGeneratorInterface;
use Tito10047\Calendar\Enum\DayName;

class FortnightGenerator implements DaysGeneratorInterface
{
    public function getDays(DateTimeImmutable $day, DayName $firstDay): array
    {
        $start = $day->modify('monday this week');
        $days  = [];

        for ($i = 0; $i < 14; $i++) {
            $days[] = $start->modify("+$i days");
        }

        return $days;
    }
}

$calendar = new Calendar(
    date: new DateTimeImmutable(),
    daysGenerator: new FortnightGenerator(),
);
```

---

Custom events
-------------

[](#custom-events)

Implement `EventInterface` for structured events with time ranges:

```
use Tito10047\Calendar\Interface\EventInterface;

class Meeting implements EventInterface
{
    public function __construct(
        private DateTimeImmutable $from,
        private DateTimeImmutable $to,
        private string $title,
    ) {}

    public function getFrom(): DateTimeImmutable  { return $this->from; }
    public function getTo(): DateTimeImmutable    { return $this->to; }
    public function getTitle(): string            { return $this->title; }
    public function getDescription(): string      { return ''; }
    public function getStatus(): string           { return 'confirmed'; }
}
```

Pair with a custom `EventRendererInterface` to control how events appear in each day cell.

---

Renderer chain
--------------

[](#renderer-chain)

The built-in renderer is fully composable. Swap any layer:

```
Renderer
└── MonthRendererInterface          ← wraps everything in
    ├── DayNameRendererInterface    ← renders column headers
    └── WeekRowRendererInterface    ← renders each
         └── DayRendererInterface  ← renders each  content
              └── EventRendererInterface  ← renders events within a day

```

Replace any single piece without touching the others:

```
use Tito10047\Calendar\Renderer;
use Tito10047\Calendar\Renderer\MonthRenderer;
use Tito10047\Calendar\Renderer\DayNameRenderer;
use Tito10047\Calendar\Renderer\WeekRowRenderer;
use Tito10047\Calendar\Renderer\EventRenderer;

$eventRenderer   = new EventRenderer($translator, 'calendar');
$dayRenderer     = new MyCustomDayRenderer($eventRenderer);   // ← your implementation
$weekRowRenderer = new WeekRowRenderer($dayRenderer);
$dayNameRenderer = new DayNameRenderer($translator, 'calendar');
$monthRenderer   = new MonthRenderer($dayNameRenderer, $weekRowRenderer);

$renderer = new Renderer($monthRenderer);
```

Or pass your `MonthRendererInterface` directly to bypass everything:

```
$renderer = new Renderer(new MyFullyCustomRenderer());
```

---

Internationalization
--------------------

[](#internationalization)

The library uses `symfony/contracts` `TranslatorInterface`. By default it's a no-op (returns keys as-is).

Plug in a real Symfony translator by building the renderer chain manually:

```
// $translator is your Symfony TranslatorInterface implementation
$eventRenderer   = new EventRenderer($translator, 'calendar');
$dayRenderer     = new DayRenderer($eventRenderer, CalendarType::Monthly, $translator, 'calendar');
$weekRowRenderer = new WeekRowRenderer($dayRenderer);
$dayNameRenderer = new DayNameRenderer($translator, 'calendar');
$monthRenderer   = new MonthRenderer($dayNameRenderer, $weekRowRenderer);

$renderer = new Renderer($monthRenderer);
```

Translation keys used: day short names (`Mon`, `Tue`, `Wed`, `Thu`, `Fri`, `Sat`, `Sun`) and event titles.

---

API reference
-------------

[](#api-reference)

### `Calendar`

[](#calendar)

MethodReturnsDescription`new Calendar($date, $generator, $startDay)``self`Create a calendar for the given date`disableDays(DateTimeImmutable ...$days)``self`Disable specific dates`disableDaysByName(DayName ...$names)``self`Disable all occurrences of given weekdays`disableDaysRange(?$from, ?$to)``self`Disable a date range (defaults to full calendar)`disableWeek(int $weekNum)``self`Disable all days in an ISO week number`setDataLoader($loader)``self`Attach a data loader to populate `Day->data``nextMonth()``self`Calendar for the next month`prevMonth()``self`Calendar for the previous month`getDaysTable()``Day[][]`2D array keyed `[weekNumber][dayNumber 1–7]``getDate()``DateTimeImmutable`The reference date`getStartDay()``DayName`Configured week start day`isDayDisabled($day)``bool`Check if a day is disabled`isFirstDay($day)``bool`Check if a day is the 1st of the month`isLastDay($day)``bool`Check if a day is the last of the month### `DayName`

[](#dayname)

CaseValue`Monday`1`Tuesday`2`Wednesday`3`Thursday`4`Friday`5`Saturday`6`Sunday`7---

Running tests
-------------

[](#running-tests)

```
composer install
vendor/bin/phpunit
```

CI runs the full suite across **PHP 8.1 – 8.5** on every push.

---

License
-------

[](#license)

MIT

###  Health Score

43

—

FairBetter than 90% of packages

Maintenance100

Actively maintained with recent releases

Popularity0

Limited adoption so far

Community7

Small or concentrated contributor base

Maturity55

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~574 days

Total

2

Last Release

0d ago

PHP version history (2 changes)1.0.0PHP &gt;=8.1

1.1.0PHP &gt;=8.2

### Community

Maintainers

![](https://avatars.githubusercontent.com/u/11459248?v=4)[Jozef Môstka](/maintainers/tito10047)[@tito10047](https://github.com/tito10047)

---

Top Contributors

[![tito10047](https://avatars.githubusercontent.com/u/11459248?v=4)](https://github.com/tito10047 "tito10047 (24 commits)")

###  Code Quality

TestsPHPUnit

### Embed Badge

![Health badge](/badges/tito10047-php-calendar/health.svg)

```
[![Health](https://phpackages.com/badges/tito10047-php-calendar/health.svg)](https://phpackages.com/packages/tito10047-php-calendar)
```

###  Alternatives

[rtippin/messenger-ui

Laravel messenger suite UI.

157.0k1](/packages/rtippin-messenger-ui)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
