定制 tito10047/php-calendar 二次开发

按需修改功能、优化性能、对接业务系统,提供一站式技术支持

邮箱:yvsm@zunyunkeji.com | QQ:316430983 | 微信:yvsm316

tito10047/php-calendar

Composer 安装命令:

composer require tito10047/php-calendar

包简介

Php server side rendered calendar

README 文档

README

PHP Tests PHP Version 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?

  • 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

composer require tito10047/php-calendar

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 <table class="calendar"> with ghost days, today marker, and day headers.

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

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

$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

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

Skip the renderer entirely and build your own template:

$table = $calendar->getDaysTable();
// array<int weekNumber, array<int dayNumber 1–7, Day>>

In Twig (Symfony)

<table class="calendar">
    <thead>
        <tr>
            <th>Mon</th><th>Tue</th><th>Wed</th>
            <th>Thu</th><th>Fri</th><th>Sat</th><th>Sun</th>
        </tr>
    </thead>
    <tbody>
        {% for week in table %}
            <tr>
                {% for day in week %}
                    <td class="
                        {{ day.ghost    ? 'ghost'    : '' }}
                        {{ day.today    ? 'today'    : '' }}
                        {{ day.enabled  ? ''         : 'disabled' }}
                    ">
                        {% if not day.ghost %}
                            <span class="date">{{ day.date|date('j') }}</span>

                            {% if day.data %}
                                <ul class="events">
                                    {% for event in day.data %}
                                        <li>{{ event.title }}</li>
                                    {% endfor %}
                                </ul>
                            {% endif %}
                        {% endif %}
                    </td>
                {% endfor %}
            </tr>
        {% endfor %}
    </tbody>
</table>

In a Blade template (Laravel)

<table class="calendar">
    <tbody>
        @foreach ($table as $week)
            <tr>
                @foreach ($week as $day)
                    <td @class([
                        'ghost'    => $day->ghost,
                        'today'    => $day->today,
                        'disabled' => !$day->enabled,
                    ])>
                        @unless ($day->ghost)
                            <span class="date">{{ $day->date->format('j') }}</span>

                            @foreach ($day->data ?? [] as $event)
                                <div class="event">{{ $event['title'] }}</div>
                            @endforeach
                        @endunless
                    </td>
                @endforeach
            </tr>
        @endforeach
    </tbody>
</table>

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 #}
<turbo-frame id="calendar">
    <nav>
        <a href="{{ path('calendar', {year: prev|date('Y'), month: prev|date('n')}) }}">← Prev</a>
        <strong>{{ calendar.date|date('F Y') }}</strong>
        <a href="{{ path('calendar', {year: next|date('Y'), month: next|date('n')}) }}">Next →</a>
    </nav>

    {# ... render table ... #}
</turbo-frame>

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

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:

<table class="calendar">
    <thead>
        <tr>
            <td><span class="day-name">Mon</span></td>
            <!-- ... -->
        </tr>
    </thead>
    <tbody>
        <tr>
            <td class="ghost"><!-- prev month --></td>
            <td class="today"><div class="day"><span class="name">1</span></div></td>
            <td class="disabled"><div class="day"><span class="name">2</span></div></td>
            <!-- ... -->
        </tr>
    </tbody>
</table>

Available CSS classes on <td>:

Class Meaning
ghost Day belongs to an adjacent month
today Matches today's date
disabled Disabled via any disable* method

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

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

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

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

Renderer
└── MonthRendererInterface          ← wraps everything in <table>
    ├── DayNameRendererInterface    ← renders column headers
    └── WeekRowRendererInterface    ← renders each <tr>
         └── DayRendererInterface  ← renders each <td> 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

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

Calendar

Method Returns Description
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

Case Value
Monday 1
Tuesday 2
Wednesday 3
Thursday 4
Friday 5
Saturday 6
Sunday 7

Running tests

composer install
vendor/bin/phpunit

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

License

MIT

统计信息

  • 总下载量: 0
  • 月度下载量: 0
  • 日度下载量: 0
  • 收藏数: 0
  • 点击次数: 2
  • 依赖项目数: 0
  • 推荐数: 0

GitHub 信息

  • Stars: 0
  • Watchers: 1
  • Forks: 0
  • 开发语言: PHP

其他信息

  • 授权协议: Unknown
  • 更新时间: 2026-06-27

承接程序开发

PHP开发

VUE

Vue开发

前端开发

小程序开发

公众号开发

系统定制

数据库设计

云部署

网站建设

安全加固