mirror of
https://devops.lemonos.cn/lawson/FendxPHP.git
synced 2026-06-15 23:12:49 +08:00
feat(database): 添加用户角色权限系统及相关监控功能
- 创建用户表(users)包含基本信息和认证字段 - 创建角色表(roles)用于权限控制 - 创建权限表(permissions)定义系统权限 - 创建用户角色关联表(user_roles)建立用户与角色关系 - 创建角色权限关联表(role_permissions)建立角色与权限关系 - 创建迁移记录表(migrations)追踪数据库变更 - 添加AdminController提供管理员面板功能 - 实现系统监控、配置管理、缓存清理等功能 - 添加AOP切面编程支持的各种通知类型 - 实现告警管理AlertManager支持多渠道告警 - 添加文档注解接口规范
This commit is contained in:
@@ -0,0 +1,896 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Fendx\I18n\Timezone\DST;
|
||||
|
||||
use Fendx\I18n\Timezone\DST\Calculator\DSTCalculator;
|
||||
use Fendx\I18n\Timezone\DST\Detector\DSTDetector;
|
||||
use Fendx\I18n\Timezone\DST\Transition\DSTTransition;
|
||||
|
||||
class DaylightSavingTimeHandler
|
||||
{
|
||||
protected DSTCalculator $calculator;
|
||||
protected DSTDetector $detector;
|
||||
protected DSTTransition $transition;
|
||||
protected array $config = [];
|
||||
protected array $dstRules = [];
|
||||
protected array $transitionCache = [];
|
||||
|
||||
public function __construct(array $config = [])
|
||||
{
|
||||
$this->config = array_merge($this->getDefaultConfig(), $config);
|
||||
$this->calculator = new DSTCalculator($this->config);
|
||||
$this->detector = new DSTDetector($this->config);
|
||||
$this->transition = new DSTTransition($this->config);
|
||||
$this->dstRules = $this->loadDSTRules();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if DST is active for timezone at specific datetime.
|
||||
*/
|
||||
public function isDSTActive(string $timezone, \DateTimeInterface $datetime = null): bool
|
||||
{
|
||||
$datetime = $datetime ?? new \DateTime();
|
||||
|
||||
if (!$this->isValidTimezone($timezone)) {
|
||||
throw new \InvalidArgumentException("Invalid timezone: {$timezone}");
|
||||
}
|
||||
|
||||
$tz = new \DateTimeZone($timezone);
|
||||
$dt = new \DateTime($datetime->format('Y-m-d H:i:s'), $tz);
|
||||
|
||||
return $dt->format('I') === '1';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get DST information for timezone at specific datetime.
|
||||
*/
|
||||
public function getDSTInfo(string $timezone, \DateTimeInterface $datetime = null): array
|
||||
{
|
||||
$datetime = $datetime ?? new \DateTime();
|
||||
|
||||
if (!$this->isValidTimezone($timezone)) {
|
||||
throw new \InvalidArgumentException("Invalid timezone: {$timezone}");
|
||||
}
|
||||
|
||||
$tz = new \DateTimeZone($timezone);
|
||||
$dt = new \DateTime($datetime->format('Y-m-d H:i:s'), $tz);
|
||||
|
||||
$isDST = $dt->format('I') === '1';
|
||||
$offset = $tz->getOffset($dt);
|
||||
$abbreviation = $dt->format('T');
|
||||
|
||||
return [
|
||||
'timezone' => $timezone,
|
||||
'datetime' => $datetime->format('Y-m-d H:i:s'),
|
||||
'is_dst' => $isDST,
|
||||
'offset' => $offset,
|
||||
'offset_hours' => $offset / 3600,
|
||||
'abbreviation' => $abbreviation,
|
||||
'dst_offset' => $isDST ? $this->getDSTOffset($timezone) : 0,
|
||||
'standard_offset' => $isDST ? $offset - $this->getDSTOffset($timezone) : $offset
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get DST transitions for timezone in specific year.
|
||||
*/
|
||||
public function getDSTTransitions(string $timezone, int $year = null): array
|
||||
{
|
||||
$year = $year ?? (int) date('Y');
|
||||
|
||||
if (!$this->isValidTimezone($timezone)) {
|
||||
throw new \InvalidArgumentException("Invalid timezone: {$timezone}");
|
||||
}
|
||||
|
||||
$cacheKey = "{$timezone}_{$year}";
|
||||
|
||||
if (isset($this->transitionCache[$cacheKey])) {
|
||||
return $this->transitionCache[$cacheKey];
|
||||
}
|
||||
|
||||
$tz = new \DateTimeZone($timezone);
|
||||
|
||||
// Get transitions for the entire year
|
||||
$transitions = $tz->getTransitions(
|
||||
strtotime("{$year}-01-01"),
|
||||
strtotime("{$year}-12-31 23:59:59")
|
||||
);
|
||||
|
||||
$dstTransitions = [];
|
||||
|
||||
foreach ($transitions as $transition) {
|
||||
if ($transition['dst'] !== $transition['isdst']) {
|
||||
$dstTransitions[] = [
|
||||
'timestamp' => $transition['ts'],
|
||||
'datetime' => date('Y-m-d H:i:s', $transition['ts']),
|
||||
'offset' => $transition['offset'],
|
||||
'is_dst' => $transition['dst'],
|
||||
'abbreviation' => $transition['abbr'],
|
||||
'type' => $transition['dst'] ? 'start' : 'end',
|
||||
'offset_change' => $this->calculateOffsetChange($transition, $tz, $year)
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
$this->transitionCache[$cacheKey] = $dstTransitions;
|
||||
|
||||
return $dstTransitions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get next DST transition for timezone.
|
||||
*/
|
||||
public function getNextDSTTransition(string $timezone, \DateTimeInterface $datetime = null): ?array
|
||||
{
|
||||
$datetime = $datetime ?? new \DateTime();
|
||||
|
||||
$transitions = $this->getDSTTransitions($timezone, (int) $datetime->format('Y'));
|
||||
|
||||
$timestamp = $datetime->getTimestamp();
|
||||
|
||||
foreach ($transitions as $transition) {
|
||||
if ($transition['timestamp'] > $timestamp) {
|
||||
return $transition;
|
||||
}
|
||||
}
|
||||
|
||||
// Check next year if no transitions found this year
|
||||
$nextYear = (int) $datetime->format('Y') + 1;
|
||||
$nextYearTransitions = $this->getDSTTransitions($timezone, $nextYear);
|
||||
|
||||
return !empty($nextYearTransitions) ? $nextYearTransitions[0] : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get previous DST transition for timezone.
|
||||
*/
|
||||
public function getPreviousDSTTransition(string $timezone, \DateTimeInterface $datetime = null): ?array
|
||||
{
|
||||
$datetime = $datetime ?? new \DateTime();
|
||||
|
||||
$transitions = $this->getDSTTransitions($timezone, (int) $datetime->format('Y'));
|
||||
|
||||
$timestamp = $datetime->getTimestamp();
|
||||
$previousTransition = null;
|
||||
|
||||
foreach ($transitions as $transition) {
|
||||
if ($transition['timestamp'] < $timestamp) {
|
||||
$previousTransition = $transition;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Check previous year if no transitions found this year
|
||||
if ($previousTransition === null) {
|
||||
$previousYear = (int) $datetime->format('Y') - 1;
|
||||
$previousYearTransitions = $this->getDSTTransitions($timezone, $previousYear);
|
||||
|
||||
return !empty($previousYearTransitions) ? end($previousYearTransitions) : null;
|
||||
}
|
||||
|
||||
return $previousTransition;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get DST offset for timezone.
|
||||
*/
|
||||
public function getDSTOffset(string $timezone): int
|
||||
{
|
||||
if (!$this->isValidTimezone($timezone)) {
|
||||
throw new \InvalidArgumentException("Invalid timezone: {$timezone}");
|
||||
}
|
||||
|
||||
// Most DST offsets are 1 hour (3600 seconds)
|
||||
// Some exceptions exist (like Lord Howe Island with 30 minutes)
|
||||
$exceptions = [
|
||||
'Australia/Lord_Howe' => 1800, // 30 minutes
|
||||
'Antarctica/Macquarie' => 1800, // 30 minutes
|
||||
];
|
||||
|
||||
return $exceptions[$timezone] ?? 3600;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if timezone observes DST.
|
||||
*/
|
||||
public function observesDST(string $timezone): bool
|
||||
{
|
||||
if (!$this->isValidTimezone($timezone)) {
|
||||
throw new \InvalidArgumentException("Invalid timezone: {$timezone}");
|
||||
}
|
||||
|
||||
// Check if timezone has any DST transitions in current year
|
||||
$transitions = $this->getDSTTransitions($timezone);
|
||||
|
||||
return !empty($transitions);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get DST periods for timezone in specific year.
|
||||
*/
|
||||
public function getDSTPeriods(string $timezone, int $year = null): array
|
||||
{
|
||||
$year = $year ?? (int) date('Y');
|
||||
$transitions = $this->getDSTTransitions($timezone, $year);
|
||||
|
||||
$periods = [];
|
||||
$startTransition = null;
|
||||
|
||||
foreach ($transitions as $transition) {
|
||||
if ($transition['type'] === 'start') {
|
||||
$startTransition = $transition;
|
||||
} elseif ($transition['type'] === 'end' && $startTransition) {
|
||||
$periods[] = [
|
||||
'start' => $startTransition,
|
||||
'end' => $transition,
|
||||
'duration' => $transition['timestamp'] - $startTransition['timestamp'],
|
||||
'duration_hours' => ($transition['timestamp'] - $startTransition['timestamp']) / 3600
|
||||
];
|
||||
$startTransition = null;
|
||||
}
|
||||
}
|
||||
|
||||
return $periods;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert datetime with DST awareness.
|
||||
*/
|
||||
public function convertWithDST(\DateTimeInterface $datetime, string $fromTimezone, string $toTimezone): \DateTime
|
||||
{
|
||||
if (!$this->isValidTimezone($fromTimezone)) {
|
||||
throw new \InvalidArgumentException("Invalid source timezone: {$fromTimezone}");
|
||||
}
|
||||
|
||||
if (!$this->isValidTimezone($toTimezone)) {
|
||||
throw new \InvalidArgumentException("Invalid target timezone: {$toTimezone}");
|
||||
}
|
||||
|
||||
$fromTz = new \DateTimeZone($fromTimezone);
|
||||
$toTz = new \DateTimeZone($toTimezone);
|
||||
|
||||
// Create DateTime in source timezone
|
||||
$sourceDt = new \DateTime($datetime->format('Y-m-d H:i:s'), $fromTz);
|
||||
|
||||
// Convert to target timezone (PHP handles DST automatically)
|
||||
$targetDt = $sourceDt->setTimezone($toTz);
|
||||
|
||||
// Add DST metadata
|
||||
$targetDt->dstInfo = [
|
||||
'source_dst' => $this->isDSTActive($fromTimezone, $datetime),
|
||||
'target_dst' => $this->isDSTActive($toTimezone, $targetDt),
|
||||
'source_offset' => $fromTz->getOffset($datetime),
|
||||
'target_offset' => $toTz->getOffset($targetDt),
|
||||
'offset_change' => $toTz->getOffset($targetDt) - $fromTz->getOffset($datetime)
|
||||
];
|
||||
|
||||
return $targetDt;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle ambiguous or non-existent times during DST transitions.
|
||||
*/
|
||||
public function handleAmbiguousTime(\DateTimeInterface $datetime, string $timezone, string $preference = 'standard'): \DateTime
|
||||
{
|
||||
if (!$this->isValidTimezone($timezone)) {
|
||||
throw new \InvalidArgumentException("Invalid timezone: {$timezone}");
|
||||
}
|
||||
|
||||
$tz = new \DateTimeZone($timezone);
|
||||
$dt = new \DateTime($datetime->format('Y-m-d H:i:s'), $tz);
|
||||
|
||||
// Check if this is an ambiguous time (during fall back transition)
|
||||
$previousTransition = $this->getPreviousDSTTransition($timezone, $datetime);
|
||||
|
||||
if ($previousTransition && $previousTransition['type'] === 'end') {
|
||||
$transitionDateTime = new \DateTime($previousTransition['datetime']);
|
||||
$transitionDateTime->setTimezone($tz);
|
||||
|
||||
// Check if our datetime is within the ambiguous hour
|
||||
$diff = $datetime->getTimestamp() - $transitionDateTime->getTimestamp();
|
||||
|
||||
if ($diff >= 0 && $diff < 3600) { // Within the ambiguous hour
|
||||
switch ($preference) {
|
||||
case 'standard':
|
||||
// Use standard time (first occurrence)
|
||||
return $dt;
|
||||
case 'dst':
|
||||
// Use DST time (second occurrence)
|
||||
$dt->modify('+1 hour');
|
||||
return $dt;
|
||||
case 'earlier':
|
||||
// Use earlier time
|
||||
return $dt;
|
||||
case 'later':
|
||||
// Use later time
|
||||
$dt->modify('+1 hour');
|
||||
return $dt;
|
||||
default:
|
||||
throw new \InvalidArgumentException("Invalid preference: {$preference}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $dt;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle non-existent time during DST transition.
|
||||
*/
|
||||
public function handleNonExistentTime(\DateTimeInterface $datetime, string $timezone, string $strategy = 'forward'): \DateTime
|
||||
{
|
||||
if (!$this->isValidTimezone($timezone)) {
|
||||
throw new \InvalidArgumentException("Invalid timezone: {$timezone}");
|
||||
}
|
||||
|
||||
$tz = new \DateTimeZone($timezone);
|
||||
|
||||
// Check if this is a non-existent time (during spring forward transition)
|
||||
$previousTransition = $this->getPreviousDSTTransition($timezone, $datetime);
|
||||
|
||||
if ($previousTransition && $previousTransition['type'] === 'start') {
|
||||
$transitionDateTime = new \DateTime($previousTransition['datetime']);
|
||||
$transitionDateTime->setTimezone($tz);
|
||||
|
||||
// Check if our datetime is within the skipped hour
|
||||
$diff = $datetime->getTimestamp() - $transitionDateTime->getTimestamp();
|
||||
|
||||
if ($diff >= 0 && $diff < 3600) { // Within the skipped hour
|
||||
switch ($strategy) {
|
||||
case 'forward':
|
||||
// Move forward to the next valid time
|
||||
return new \DateTime($previousTransition['datetime'], $tz);
|
||||
case 'backward':
|
||||
// Move backward to the previous valid time
|
||||
$dt = new \DateTime($previousTransition['datetime'], $tz);
|
||||
$dt->modify('-1 hour');
|
||||
return $dt;
|
||||
case 'adjust':
|
||||
// Adjust to the nearest valid time
|
||||
return new \DateTime($previousTransition['datetime'], $tz);
|
||||
default:
|
||||
throw new \InvalidArgumentException("Invalid strategy: {$strategy}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return new \DateTime($datetime->format('Y-m-d H:i:s'), $tz);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get DST rules for timezone.
|
||||
*/
|
||||
public function getDSTRules(string $timezone): array
|
||||
{
|
||||
if (!$this->isValidTimezone($timezone)) {
|
||||
throw new \InvalidArgumentException("Invalid timezone: {$timezone}");
|
||||
}
|
||||
|
||||
return $this->dstRules[$timezone] ?? $this->generateDSTRules($timezone);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate DST-aware time difference.
|
||||
*/
|
||||
public function calculateTimeDifference(\DateTimeInterface $start, \DateTimeInterface $end, string $timezone): array
|
||||
{
|
||||
if (!$this->isValidTimezone($timezone)) {
|
||||
throw new \InvalidArgumentException("Invalid timezone: {$timezone}");
|
||||
}
|
||||
|
||||
$tz = new \DateTimeZone($timezone);
|
||||
$startDt = new \DateTime($start->format('Y-m-d H:i:s'), $tz);
|
||||
$endDt = new \DateTime($end->format('Y-m-d H:i:s'), $tz);
|
||||
|
||||
$interval = $endDt->diff($startDt);
|
||||
|
||||
// Check if DST transitions occurred between the two times
|
||||
$transitions = $this->getDSTTransitions($timezone, (int) $start->format('Y'));
|
||||
$dstTransitionsInRange = [];
|
||||
|
||||
foreach ($transitions as $transition) {
|
||||
if ($transition['timestamp'] > $start->getTimestamp() &&
|
||||
$transition['timestamp'] < $end->getTimestamp()) {
|
||||
$dstTransitionsInRange[] = $transition;
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'interval' => $interval,
|
||||
'total_seconds' => $end->getTimestamp() - $start->getTimestamp(),
|
||||
'dst_transitions' => $dstTransitionsInRange,
|
||||
'dst_adjusted_seconds' => $this->calculateDSTAdjustedDifference($start, $end, $timezone),
|
||||
'has_dst_transition' => !empty($dstTransitionsInRange)
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get DST-aware business hours calculation.
|
||||
*/
|
||||
public function calculateBusinessHours(\DateTimeInterface $datetime, string $timezone, array $businessHours = null): array
|
||||
{
|
||||
$businessHours = $businessHours ?? $this->config['business_hours'];
|
||||
|
||||
if (!$this->isValidTimezone($timezone)) {
|
||||
throw new \InvalidArgumentException("Invalid timezone: {$timezone}");
|
||||
}
|
||||
|
||||
$tz = new \DateTimeZone($timezone);
|
||||
$dt = new \DateTime($datetime->format('Y-m-d H:i:s'), $tz);
|
||||
|
||||
$isDST = $this->isDSTActive($timezone, $dt);
|
||||
$hour = (int) $dt->format('H');
|
||||
$dayOfWeek = (int) $dt->format('w');
|
||||
|
||||
// Adjust business hours for DST if needed
|
||||
$adjustedBusinessHours = $businessHours;
|
||||
if ($isDST && $this->config['adjust_business_hours_for_dst']) {
|
||||
$adjustedBusinessHours = $this->adjustBusinessHoursForDST($businessHours, $timezone);
|
||||
}
|
||||
|
||||
$isBusinessHours = in_array($dayOfWeek, $adjustedBusinessHours['weekdays']) &&
|
||||
$hour >= $adjustedBusinessHours['start_hour'] &&
|
||||
$hour < $adjustedBusinessHours['end_hour'];
|
||||
|
||||
return [
|
||||
'datetime' => $dt,
|
||||
'timezone' => $timezone,
|
||||
'is_dst' => $isDST,
|
||||
'is_business_hours' => $isBusinessHours,
|
||||
'business_hours' => $adjustedBusinessHours,
|
||||
'next_business_hour' => $this->getNextBusinessHour($dt, $timezone, $adjustedBusinessHours),
|
||||
'previous_business_hour' => $this->getPreviousBusinessHour($dt, $timezone, $adjustedBusinessHours)
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate DST configuration.
|
||||
*/
|
||||
public function validateDSTConfig(array $config): array
|
||||
{
|
||||
$errors = [];
|
||||
$warnings = [];
|
||||
|
||||
// Validate timezone
|
||||
if (isset($config['timezone']) && !$this->isValidTimezone($config['timezone'])) {
|
||||
$errors[] = "Invalid timezone: {$config['timezone']}";
|
||||
}
|
||||
|
||||
// Validate business hours
|
||||
if (isset($config['business_hours'])) {
|
||||
$bh = $config['business_hours'];
|
||||
if (!isset($bh['start_hour']) || !is_int($bh['start_hour']) || $bh['start_hour'] < 0 || $bh['start_hour'] > 23) {
|
||||
$errors[] = "Invalid start_hour in business_hours";
|
||||
}
|
||||
|
||||
if (!isset($bh['end_hour']) || !is_int($bh['end_hour']) || $bh['end_hour'] < 0 || $bh['end_hour'] > 24) {
|
||||
$errors[] = "Invalid end_hour in business_hours";
|
||||
}
|
||||
|
||||
if (isset($bh['start_hour']) && isset($bh['end_hour']) && $bh['start_hour'] >= $bh['end_hour']) {
|
||||
$warnings[] = "Business hours start_hour should be less than end_hour";
|
||||
}
|
||||
}
|
||||
|
||||
// Validate preference settings
|
||||
if (isset($config['ambiguous_time_preference'])) {
|
||||
$validPreferences = ['standard', 'dst', 'earlier', 'later'];
|
||||
if (!in_array($config['ambiguous_time_preference'], $validPreferences)) {
|
||||
$errors[] = "Invalid ambiguous_time_preference";
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($config['non_existent_time_strategy'])) {
|
||||
$validStrategies = ['forward', 'backward', 'adjust'];
|
||||
if (!in_array($config['non_existent_time_strategy'], $validStrategies)) {
|
||||
$errors[] = "Invalid non_existent_time_strategy";
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'valid' => empty($errors),
|
||||
'errors' => $errors,
|
||||
'warnings' => $warnings
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate DST report for timezone.
|
||||
*/
|
||||
public function generateDSTReport(string $timezone, int $year = null): array
|
||||
{
|
||||
$year = $year ?? (int) date('Y');
|
||||
|
||||
if (!$this->isValidTimezone($timezone)) {
|
||||
throw new \InvalidArgumentException("Invalid timezone: {$timezone}");
|
||||
}
|
||||
|
||||
$transitions = $this->getDSTTransitions($timezone, $year);
|
||||
$periods = $this->getDSTPeriods($timezone, $year);
|
||||
$observesDST = $this->observesDST($timezone);
|
||||
|
||||
$report = [
|
||||
'timezone' => $timezone,
|
||||
'year' => $year,
|
||||
'observes_dst' => $observesDST,
|
||||
'total_transitions' => count($transitions),
|
||||
'total_periods' => count($periods),
|
||||
'dst_offset' => $this->getDSTOffset($timezone),
|
||||
'current_status' => $this->getDSTInfo($timezone),
|
||||
'transitions' => $transitions,
|
||||
'periods' => $periods
|
||||
];
|
||||
|
||||
if ($observesDST) {
|
||||
$report['statistics'] = [
|
||||
'total_dst_hours' => $this->calculateTotalDSTHours($timezone, $year),
|
||||
'dst_percentage' => $this->calculateDSTPercentage($timezone, $year),
|
||||
'longest_dst_period' => $this->getLongestDSTPeriod($timezone, $year),
|
||||
'shortest_dst_period' => $this->getShortestDSTPeriod($timezone, $year)
|
||||
];
|
||||
}
|
||||
|
||||
return $report;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if timezone is valid.
|
||||
*/
|
||||
protected function isValidTimezone(string $timezone): bool
|
||||
{
|
||||
return in_array($timezone, \DateTimeZone::listIdentifiers());
|
||||
}
|
||||
|
||||
/**
|
||||
* Load DST rules.
|
||||
*/
|
||||
protected function loadDSTRules(): array
|
||||
{
|
||||
return [
|
||||
// US DST rules (historical and current)
|
||||
'America/New_York' => [
|
||||
'start_month' => 3,
|
||||
'start_day' => 'second_sunday',
|
||||
'start_time' => '02:00',
|
||||
'end_month' => 11,
|
||||
'end_day' => 'first_sunday',
|
||||
'end_time' => '02:00',
|
||||
'offset' => 3600
|
||||
],
|
||||
'America/Chicago' => [
|
||||
'start_month' => 3,
|
||||
'start_day' => 'second_sunday',
|
||||
'start_time' => '02:00',
|
||||
'end_month' => 11,
|
||||
'end_day' => 'first_sunday',
|
||||
'end_time' => '02:00',
|
||||
'offset' => 3600
|
||||
],
|
||||
'America/Denver' => [
|
||||
'start_month' => 3,
|
||||
'start_day' => 'second_sunday',
|
||||
'start_time' => '02:00',
|
||||
'end_month' => 11,
|
||||
'end_day' => 'first_sunday',
|
||||
'end_time' => '02:00',
|
||||
'offset' => 3600
|
||||
],
|
||||
'America/Los_Angeles' => [
|
||||
'start_month' => 3,
|
||||
'start_day' => 'second_sunday',
|
||||
'start_time' => '02:00',
|
||||
'end_month' => 11,
|
||||
'end_day' => 'first_sunday',
|
||||
'end_time' => '02:00',
|
||||
'offset' => 3600
|
||||
],
|
||||
// European DST rules
|
||||
'Europe/London' => [
|
||||
'start_month' => 3,
|
||||
'start_day' => 'last_sunday',
|
||||
'start_time' => '01:00',
|
||||
'end_month' => 10,
|
||||
'end_day' => 'last_sunday',
|
||||
'end_time' => '01:00',
|
||||
'offset' => 3600
|
||||
],
|
||||
'Europe/Paris' => [
|
||||
'start_month' => 3,
|
||||
'start_day' => 'last_sunday',
|
||||
'start_time' => '01:00',
|
||||
'end_month' => 10,
|
||||
'end_day' => 'last_sunday',
|
||||
'end_time' => '01:00',
|
||||
'offset' => 3600
|
||||
],
|
||||
'Europe/Berlin' => [
|
||||
'start_month' => 3,
|
||||
'start_day' => 'last_sunday',
|
||||
'start_time' => '01:00',
|
||||
'end_month' => 10,
|
||||
'end_day' => 'last_sunday',
|
||||
'end_time' => '01:00',
|
||||
'offset' => 3600
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate DST rules for timezone.
|
||||
*/
|
||||
protected function generateDSTRules(string $timezone): array
|
||||
{
|
||||
// Try to infer rules from transitions
|
||||
$transitions = $this->getDSTTransitions($timezone);
|
||||
|
||||
if (empty($transitions)) {
|
||||
return [
|
||||
'observes_dst' => false
|
||||
];
|
||||
}
|
||||
|
||||
// Analyze transitions to generate rules
|
||||
$rules = ['observes_dst' => true];
|
||||
|
||||
// This is a simplified rule generation
|
||||
// In practice, you'd want more sophisticated analysis
|
||||
foreach ($transitions as $transition) {
|
||||
$dt = new \DateTime($transition['datetime']);
|
||||
|
||||
if ($transition['type'] === 'start') {
|
||||
$rules['start_month'] = (int) $dt->format('m');
|
||||
$rules['start_day'] = 'unknown'; // Would need more analysis
|
||||
$rules['start_time'] = $dt->format('H:i');
|
||||
} elseif ($transition['type'] === 'end') {
|
||||
$rules['end_month'] = (int) $dt->format('m');
|
||||
$rules['end_day'] = 'unknown'; // Would need more analysis
|
||||
$rules['end_time'] = $dt->format('H:i');
|
||||
}
|
||||
}
|
||||
|
||||
$rules['offset'] = $this->getDSTOffset($timezone);
|
||||
|
||||
return $rules;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate offset change for transition.
|
||||
*/
|
||||
protected function calculateOffsetChange(array $transition, \DateTimeZone $tz, int $year): int
|
||||
{
|
||||
// Get offset before and after transition
|
||||
$before = new \DateTime($transition['datetime'], $tz);
|
||||
$before->modify('-1 second');
|
||||
|
||||
$after = new \DateTime($transition['datetime'], $tz);
|
||||
$after->modify('+1 second');
|
||||
|
||||
return $after->getOffset() - $before->getOffset();
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate DST-adjusted time difference.
|
||||
*/
|
||||
protected function calculateDSTAdjustedDifference(\DateTimeInterface $start, \DateTimeInterface $end, string $timezone): int
|
||||
{
|
||||
$tz = new \DateTimeZone($timezone);
|
||||
$startDt = new \DateTime($start->format('Y-m-d H:i:s'), $tz);
|
||||
$endDt = new \DateTime($end->format('Y-m-d H:i:s'), $tz);
|
||||
|
||||
// Calculate actual difference in seconds
|
||||
return $endDt->getTimestamp() - $startDt->getTimestamp();
|
||||
}
|
||||
|
||||
/**
|
||||
* Adjust business hours for DST.
|
||||
*/
|
||||
protected function adjustBusinessHoursForDST(array $businessHours, string $timezone): array
|
||||
{
|
||||
// Some businesses may want to adjust their hours during DST
|
||||
// This is a placeholder for more complex logic
|
||||
return $businessHours;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get next business hour.
|
||||
*/
|
||||
protected function getNextBusinessHour(\DateTime $datetime, string $timezone, array $businessHours): \DateTime
|
||||
{
|
||||
$next = clone $datetime;
|
||||
$dayOfWeek = (int) $next->format('w');
|
||||
|
||||
// Move to next business day if needed
|
||||
while (!in_array($dayOfWeek, $businessHours['weekdays'])) {
|
||||
$next->modify('+1 day');
|
||||
$dayOfWeek = (int) $next->format('w');
|
||||
}
|
||||
|
||||
// Set to start of business hours
|
||||
$next->setTime($businessHours['start_hour'], 0, 0);
|
||||
|
||||
return $next;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get previous business hour.
|
||||
*/
|
||||
protected function getPreviousBusinessHour(\DateTime $datetime, string $timezone, array $businessHours): \DateTime
|
||||
{
|
||||
$previous = clone $datetime;
|
||||
$dayOfWeek = (int) $previous->format('w');
|
||||
|
||||
// Move to previous business day if needed
|
||||
while (!in_array($dayOfWeek, $businessHours['weekdays'])) {
|
||||
$previous->modify('-1 day');
|
||||
$dayOfWeek = (int) $previous->format('w');
|
||||
}
|
||||
|
||||
// Set to end of business hours
|
||||
$previous->setTime($businessHours['end_hour'] - 1, 59, 59);
|
||||
|
||||
return $previous;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate total DST hours in year.
|
||||
*/
|
||||
protected function calculateTotalDSTHours(string $timezone, int $year): int
|
||||
{
|
||||
$periods = $this->getDSTPeriods($timezone, $year);
|
||||
$totalHours = 0;
|
||||
|
||||
foreach ($periods as $period) {
|
||||
$totalHours += $period['duration_hours'];
|
||||
}
|
||||
|
||||
return (int) $totalHours;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate DST percentage for year.
|
||||
*/
|
||||
protected function calculateDSTPercentage(string $timezone, int $year): float
|
||||
{
|
||||
$totalHours = $this->calculateTotalDSTHours($timezone, $year);
|
||||
$yearHours = ($this->isLeapYear($year) ? 366 : 365) * 24;
|
||||
|
||||
return ($totalHours / $yearHours) * 100;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if year is leap year.
|
||||
*/
|
||||
protected function isLeapYear(int $year): bool
|
||||
{
|
||||
return ($year % 4 === 0 && $year % 100 !== 0) || ($year % 400 === 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get longest DST period.
|
||||
*/
|
||||
protected function getLongestDSTPeriod(string $timezone, int $year): ?array
|
||||
{
|
||||
$periods = $this->getDSTPeriods($timezone, $year);
|
||||
|
||||
if (empty($periods)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$longest = $periods[0];
|
||||
|
||||
foreach ($periods as $period) {
|
||||
if ($period['duration_hours'] > $longest['duration_hours']) {
|
||||
$longest = $period;
|
||||
}
|
||||
}
|
||||
|
||||
return $longest;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get shortest DST period.
|
||||
*/
|
||||
protected function getShortestDSTPeriod(string $timezone, int $year): ?array
|
||||
{
|
||||
$periods = $this->getDSTPeriods($timezone, $year);
|
||||
|
||||
if (empty($periods)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$shortest = $periods[0];
|
||||
|
||||
foreach ($periods as $period) {
|
||||
if ($period['duration_hours'] < $shortest['duration_hours']) {
|
||||
$shortest = $period;
|
||||
}
|
||||
}
|
||||
|
||||
return $shortest;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear transition cache.
|
||||
*/
|
||||
public function clearCache(): void
|
||||
{
|
||||
$this->transitionCache = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get default configuration.
|
||||
*/
|
||||
protected function getDefaultConfig(): array
|
||||
{
|
||||
return [
|
||||
'business_hours' => [
|
||||
'weekdays' => [1, 2, 3, 4, 5], // Monday to Friday
|
||||
'start_hour' => 9,
|
||||
'end_hour' => 17
|
||||
],
|
||||
'adjust_business_hours_for_dst' => false,
|
||||
'ambiguous_time_preference' => 'standard',
|
||||
'non_existent_time_strategy' => 'forward',
|
||||
'cache_enabled' => true,
|
||||
'cache_ttl' => 3600
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get configuration.
|
||||
*/
|
||||
public function getConfig(): array
|
||||
{
|
||||
return $this->config;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set configuration.
|
||||
*/
|
||||
public function setConfig(array $config): void
|
||||
{
|
||||
$this->config = array_merge($this->config, $config);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create DST handler instance.
|
||||
*/
|
||||
public static function create(array $config = []): self
|
||||
{
|
||||
return new self($config);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create for US timezones.
|
||||
*/
|
||||
public static function forUS(): self
|
||||
{
|
||||
return new self([
|
||||
'business_hours' => [
|
||||
'weekdays' => [1, 2, 3, 4, 5],
|
||||
'start_hour' => 9,
|
||||
'end_hour' => 17
|
||||
],
|
||||
'ambiguous_time_preference' => 'standard',
|
||||
'non_existent_time_strategy' => 'forward'
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create for European timezones.
|
||||
*/
|
||||
public static function forEurope(): self
|
||||
{
|
||||
return new self([
|
||||
'business_hours' => [
|
||||
'weekdays' => [1, 2, 3, 4, 5],
|
||||
'start_hour' => 9,
|
||||
'end_hour' => 17
|
||||
],
|
||||
'ambiguous_time_preference' => 'standard',
|
||||
'non_existent_time_strategy' => 'forward'
|
||||
]);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user