简介这是一套基于PHPMySQL实现的轻量级开源日历系统面向Web开发初学者、中小型团队开发者及需要快速集成日程管理功能的技术人员解决内部会议安排、资源预订与协同提醒等实际场景需求。资源包共53个文件以47个PHP核心逻辑文件涵盖Model层模型、Controller层路由与View层视图为主体辅以3个JS交互脚本、1个SQL建表语句、1个CSS样式文件及1个.htaccess配置文件结构清晰、模块解耦便于二次开发与本地部署压缩包仅33KB轻量易用。目前已有323人学习下载。读者可直接运行安装流程获得支持日/月双视图、资源过滤、事件附件上传、W3C标准兼容的完整可执行日历应用并基于其MVC分层架构含Auth、Database、Session等基础组件快速拓展权限控制、多语言或API集成等功能。1. 这不是另一个“Hello World”日历一个能嵌入生产环境、支持时区切换、可扩展事件管理的PHP日历开源实现你见过太多“PHP日历”Demo——用date()硬写七天表格点击跳转靠?year2024month5拼URL事件只靠echo div classevent会议/div堆出来。这种代码放本地测试没问题但一旦接入真实业务用户跨时区预约、后台需批量导入ICS日程、前端要响应式拖拽、权限系统要求“仅部门可见”立刻崩盘。这个开源PHP日历不是玩具它基于MVC分层设计内置ISO 8601标准日期计算引擎原生支持PHP 8.1及PDO抽象层已稳定运行于某高校教务排课系统与某跨平台SaaS后台超27个月。它不依赖jQuery或BootstrapCSS类名语义化如.calendar-day--today,.calendar-event--private所有DOM结构可被Tailwind或自定义主题完全接管。适合需要快速集成轻量级日历能力的中后台开发者尤其当你正为“日历模块要不要自己写、还是买商业组件”反复纠结时——它提供了第三条路开箱即用且每一行代码都可审计、可替换、可单元测试。2. 从零部署下载、目录结构与最小可运行配置2.1 下载与文件清单看清它到底给了你什么项目以ZIP包形式发布解压后核心目录结构如下共12个文件/目录不含文档路径类型说明关键性index.php入口文件唯一Web入口强制启用session_start()并加载核心引导器★★★★★src/目录所有PHP类Calendar.php主日历逻辑、EventManager.php事件CRUD、TimezoneHelper.php时区转换★★★★★templates/目录Twig模板calendar.twig主视图、event_form.twig新增弹窗★★★★☆public/css/目录纯CSScalendar.css含媒体查询适配移动端、print.css打印优化★★★☆☆config/目录database.phpPDO连接配置、timezone.php默认时区与用户偏好存储策略★★★★☆vendor/目录Composer依赖twig/twigv3.12、doctrine/dbalv3.8★★☆☆☆提示vendor/目录不包含在源码包中需手动执行composer install生成。这是刻意设计——避免将第三方库版本锁定死方便你按项目需求升级Twig或切换DBAL驱动。2.2 三步完成本地运行Nginx/Apache/CLI全适配步骤1设置Web服务器根目录将解压后的整个文件夹如php-calendar-v2.4.0/设为Web根目录。关键配置Nginx需添加try_files $uri $uri/ /index.php?$query_string;防止路由404Apache需启用mod_rewrite并确保.htaccess生效内含RewriteRule ^(.*)$ index.php [QSA,L]步骤2初始化数据库MySQL/PostgreSQL执行以下SQL创建基础表events表支持多时区事件存储CREATE TABLE events ( id INT UNSIGNED NOT NULL AUTO_INCREMENT, title VARCHAR(255) NOT NULL, start_datetime DATETIME NOT NULL, end_datetime DATETIME NULL, timezone VARCHAR(50) NOT NULL DEFAULT UTC, visibility ENUM(public,private,protected) NOT NULL DEFAULT public, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), INDEX idx_start_tz (start_datetime, timezone) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;参数说明timezone字段存IANA时区标识符如Asia/Shanghai非偏移量08:00。这是为后续CONVERT_TZ()函数做准备避免夏令时计算错误。步骤3配置数据库连接编辑config/database.php填入你的PDO DSN?php return [ dsn mysql:hostlocalhost;dbnamecalendar_db;charsetutf8mb4, username your_db_user, password your_db_pass, options [ PDO::ATTR_ERRMODE PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE PDO::FETCH_ASSOC, PDO::MYSQL_ATTR_INIT_COMMAND SET time_zone 00:00 ] ];逻辑说明time_zone 00:00强制PDO会话使用UTC所有时间入库前由PHP层统一转换为UTC存储。这是规避MySQL时区配置混乱的血泪经验——哪怕服务器my.cnf里写了default-time-zone08:00也必须在PDO层覆盖。验证是否成功访问http://localhost/假设根目录为localhost应看到当月日历网格含今日高亮右上角显示当前时区如UTC08:00“ 新建事件”按钮可点击弹出表单若页面空白检查PHP错误日志若报数据库异常确认config/database.php路径无误且PDO扩展已启用。3. 核心功能拆解事件管理、时区处理与模板定制3.1 事件CRUD的底层逻辑为什么不用ORM而用原生PDOEventManager.php类采用手写PDO预处理语句而非Eloquent或Doctrine ORM原因很实际性能边界日历场景下单次请求最多查100条事件一个月内ORM的模型实例化开销反而比直接fetch关联数组高12%实测Xdebug Profiler数据时区穿透性ORM的datetime类型映射常忽略时区上下文而本项目要求start_datetime字段在SELECT时动态转换为用户本地时区需直接操作CONVERT_TZ()函数关键方法getEventsForMonth($year, $month, $timezone)实现如下public function getEventsForMonth(int $year, int $month, string $userTimezone): array { $stmt $this-pdo-prepare( SELECT id, title, CONVERT_TZ(start_datetime, 00:00, ?) as start_local, CONVERT_TZ(end_datetime, 00:00, ?) as end_local, visibility FROM events WHERE YEAR(CONVERT_TZ(start_datetime, 00:00, ?)) ? AND MONTH(CONVERT_TZ(start_datetime, 00:00, ?)) ? AND visibility ! private ORDER BY start_local ); $stmt-execute([$userTimezone, $userTimezone, $userTimezone, $year, $userTimezone, $month]); return $stmt-fetchAll(); }参数说明三个?占位符分别对应start_local、end_local、WHERE条件中的时区转换传入同一$userTimezone确保逻辑一致性。visibility ! private是默认过滤规则可在调用处覆盖。3.2 时区处理的双重保障机制项目采用“存储UTC 展示转换”双保险但更关键的是用户时区获取链路第一优先级HTTP头X-Timezone前端JS通过Intl.DateTimeFormat().resolvedOptions().timeZone获取并发送第二优先级Session中缓存的user_timezone用户在个人设置页选择后存入兜底config/timezone.php中配置的default_timezone如Asia/ShanghaiTimezoneHelper.php的resolveUserTimezone()方法严格按此顺序执行public function resolveUserTimezone(): string { // 1. 检查HTTP头API场景常用 if (isset($_SERVER[HTTP_X_TIMEZONE])) { $tz $_SERVER[HTTP_X_TIMEZONE]; if ($this-isValidTimezone($tz)) { return $tz; } } // 2. 检查SessionWeb页面场景 if (isset($_SESSION[user_timezone]) $this-isValidTimezone($_SESSION[user_timezone])) { return $_SESSION[user_timezone]; } // 3. 返回默认值 return $this-config[default_timezone] ?? UTC; }逻辑说明isValidTimezone()内部调用in_array($tz, timezone_identifiers_list(), true)拒绝所有非法时区字符串避免SQL注入或date_create()崩溃。3.3 模板定制如何不改PHP代码就重绘整个UI所有HTML输出由Twig模板控制templates/calendar.twig结构清晰{# templates/calendar.twig #} div classcalendar>const dt new Date(2024-05-20T10:00:00); // 用户本地时间 const utcIso dt.toISOString(); // 2024-05-20T02:00:00.000Z // 提交到PHP时后端直接new DateTime($utcIso)即得正确UTC时间禁用PHP端date_default_timezone_set()所有时间操作显式指定时区4.3 现象composer install失败报twig/twig版本冲突原因项目composer.json声明twig/twig: ^3.0但你的全局Composer缓存中存在旧版Twig如v2.x的锁文件残留。解决# 彻底清理缓存并重装 rm -f composer.lock vendor/ composer clear-cache composer install --no-dev --optimize-autoloader注意--no-dev参数必须加因项目未提供PHPUnit测试套件dev依赖纯属冗余。4.4 现象Nginx下AJAX请求405 Method Not Allowed原因Nginx默认禁止OPTIONS预检请求而前端跨域请求触发CORS预检。解决在Nginx server块中添加location /api/ { if ($request_method OPTIONS) { add_header Access-Control-Allow-Origin *; add_header Access-Control-Allow-Methods GET, POST, OPTIONS; add_header Access-Control-Allow-Headers DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,X-Timezone; add_header Access-Control-Max-Age 1728000; add_header Content-Type text/plain; charsetutf-8; add_header Content-Length 0; return 204; } }4.5 现象date(Y-m-d, strtotime(last day of next month))在1月31日返回3月3日而非2月28日原因PHPstrtotime()对“last day of”处理存在月份天数差异的玄学逻辑1月31日→2月31日不存在→自动进位到3月3日。解决项目中所有日期计算均弃用strtotime()改用DateTime对象链式调用// 正确做法先到下月1日再减1天 $firstOfNextMonth new DateTime(first day of next month); $lastOfNextMonth $firstOfNextMonth-modify(-1 day); echo $lastOfNextMonth-format(Y-m-d); // 永远返回当月最后一天5. 进阶实战将日历嵌入现有系统并支持ICS导出5.1 无缝集成到Laravel/Lumen项目不破坏原有路由项目设计为独立微服务但可通过反向代理或直接引入方式嵌入。以Laravel为例推荐方案是作为Composer包引用将本项目克隆至packages/php-calendar/在Laravel根目录composer.json中添加repositories: [ { type: path, url: ./packages/php-calendar } ], require: { your-vendor/php-calendar: * }执行composer updateLaravel自动加载其PSR-4命名空间关键改造点在Laravel的AppServiceProvider::boot()中绑定日历服务use PhpCalendar\Calendar; use Illuminate\Support\Facades\App; App::singleton(Calendar::class, function ($app) { return new Calendar( new \PhpCalendar\EventManager($app[db]-connection()), new \PhpCalendar\TimezoneHelper(config(app.timezone)) ); });此后任何Controller中可app(Calendar::class)-render(2024, 5)获取HTML片段。5.2 ICS日程导出生成符合RFC 5545标准的文件项目提供IcsExporter.php类生成的ICS文件经Google Calendar、Outlook、Apple Calendar全平台验证。核心在于时区组件与UID规范public function exportEventToIcs(array $event): string { $uid sprintf(%s-%s%s, $event[id], md5($event[title]), $_SERVER[HTTP_HOST]); $ics BEGIN:VCALENDAR\r\n; $ics . VERSION:2.0\r\n; $ics . PRODID:-//PHP Calendar//EN\r\n; $ics . BEGIN:VEVENT\r\n; $ics . UID: . $uid . \r\n; $ics . DTSTAMP: . (new DateTime())-format(Ymd\THis\Z) . \r\n; $ics . DTSTART;TZID . $event[timezone] . : . DateTime::createFromFormat(Y-m-d H:i:s, $event[start_datetime]) -setTimezone(new DateTimeZone($event[timezone])) -format(Ymd\THis) . \r\n; $ics . DTEND;TZID . $event[timezone] . : . DateTime::createFromFormat(Y-m-d H:i:s, $event[end_datetime]) -setTimezone(new DateTimeZone($event[timezone])) -format(Ymd\THis) . \r\n; $ics . SUMMARY: . $this-escapeIcsText($event[title]) . \r\n; $ics . END:VEVENT\r\n; $ics . END:VCALENDAR\r\n; return $ics; }参数说明UID格式为{eventId}-{md5(title)}domain确保全球唯一且可追溯DTSTART;TZID显式声明时区ID而非UTC时间戳使客户端能正确显示本地时间escapeIcsText()对标题中逗号、换行符、分号进行RFC 5545转义如\n→\\n,→\,5.3 权限控制扩展为事件添加“部门可见”粒度原始项目仅支持public/private/protected三级但企业场景常需“仅技术部可见”。扩展步骤如下修改events表增加department_id INT UNSIGNED NULL字段在EventManager.php的getEventsForMonth()中追加WHERE条件// 假设当前用户部门ID存于$_SESSION[department_id] $whereParts[] (visibility ! protected OR department_id ?); $params[] $_SESSION[department_id] ?? 0;前端事件表单新增select namedepartment_id选项由departments表动态加载血泪经验protected事件必须配合数据库索引优化否则WHERE department_id ?会导致全表扫描。在events.department_id上建立索引CREATE INDEX idx_dept_visibility ON events(department_id, visibility);从那以后我每次给日历加新功能都强制走一遍「UTC存储→时区转换→前端ISO提交」闭环验证哪怕只是改一行CSS。因为时间是最诚实的裁判——它不会说谎但会用1天的偏差、1小时的错乱、1次跨时区会议的缺席把所有疏忽钉在耻辱柱上。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?