Skip to content

Behavior Change: Duration events displayed as individual days instead of grouped entry (v9 → v10) #588

@SvenJuergens

Description

@SvenJuergens

hi @froemken

During a TYPO3 update from 12 to 13, we also updated events2 from 9 to 10. Among other things, we noticed that the behaviour had changed in one area.

Each individual day of events with the type ‘duration’ is now displayed, whereas previously these were displayed as a single element in the list view. It is possible to merge them using the ‘settings.mergeRecurringEvents’ option, but this also affects the other event types.
The customer would like to have duration events displayed as one event as before and the others as individual days.
Hence the question: was this change in behaviour intentional or a mistake in the refactoring?

The cause appears to be the following

The behavior change was introduced in commit de50b214 ("TYPO3 13 compatibility", PR #560).

Version 9 Implementation (DayRelationService.php)

protected function getSortDayTime(
    \DateTimeImmutable $day,
    int $hour,
    int $minute,
    array $eventRecord,
): \DateTimeImmutable {
    if ($eventRecord['event_type'] === 'duration') {
        [$hour, $minute] = $this->getHourAndMinuteFromTime($this->firstTimeRecordForCurrentDateTime);
        return $this->getDayTime($this->firstDateTime, $hour, $minute);  // ← Uses first date
    }
    return $this->getDayTime($day, $hour, $minute);
}

The key aspect is $this->firstDateTime - a class property that stored the first date of the duration event and was used for all days.

Version 10 Implementation (DayRecordBuilderService.php)

protected function getSortDayTime(
    DateTimeResult $dateTimeResult,
    DayGeneratorResult $dayGeneratorResult,
    TimeResult $timeResult,
): \DateTimeImmutable {
    if ($dayGeneratorResult->getEventRecord()['event_type'] === 'duration') {
        $timeResult = $dateTimeResult->getFirstTimeResult() ?? $timeResult;
        // ↑ Only the time is adjusted, not the date
    }
    return $dateTimeResult->getDate()->modify(sprintf(  // ← Uses current day's date
        '+%d hour +%d minute',
        $timeResult->getHour(),
        $timeResult->getMinute(),
    ));
}

The refactoring to make the service stateless removed the $this->firstDateTime property. However, the logic to use the first date for duration events was not fully migrated to the new architecture.

Notably, the PHPDoc comment in the current code still documents the expected (old) behavior:

/**
 * Get timestamp which is the same for all event days of type duration
 * Instead of getDayTime this method will return the same timestamp for all days in the event
 *
 * Day: 17.01.2017 00:00:00 + 8h + 30m  = 17.01.2017 08:30:00
 * Day: 18.01.2017 00:00:00 + 10h + 15m = 17.01.2017 08:30:00  ← All should be 17.01
 * Day: 19.01.2017 00:00:00 + 9h + 25m  = 17.01.2017 08:30:00
 * Day: 20.01.2017 00:00:00 + 14h + 45m = 17.01.2017 08:30:00
 */

To restore the previous behavior, the getSortDayTime method should use $dayGeneratorResult->getFirstDateTimeResult() for duration events:

diff --git a/Classes/Service/DayRecordBuilderService.php b/Classes/Service/DayRecordBuilderService.php
--- a/Classes/Service/DayRecordBuilderService.php
+++ b/Classes/Service/DayRecordBuilderService.php
@@ -106,14 +106,22 @@ readonly class DayRecordBuilderService
     protected function getSortDayTime(
         DateTimeResult $dateTimeResult,
         DayGeneratorResult $dayGeneratorResult,
         TimeResult $timeResult,
     ): \DateTimeImmutable {
         if ($dayGeneratorResult->getEventRecord()['event_type'] === 'duration') {
-            $timeResult = $dateTimeResult->getFirstTimeResult() ?? $timeResult;
+            // For duration events, use the FIRST date and its first time record
+            // to ensure all days of the duration event have the same sort_day_time.
+            // This allows grouping/merging of duration event days in listings.
+            $firstDateTimeResult = $dayGeneratorResult->getFirstDateTimeResult();
+            if ($firstDateTimeResult !== null) {
+                $timeResult = $firstDateTimeResult->getFirstTimeResult() ?? $timeResult;
+                return $firstDateTimeResult->getDate()->modify(sprintf(
+                    '+%d hour +%d minute',
+                    $timeResult->getHour(),
+                    $timeResult->getMinute(),
+                ));
+            }
         }

         return $dateTimeResult->getDate()->modify(sprintf(
             '+%d hour +%d minute',
             $timeResult->getHour(),
             $timeResult->getMinute(),
         ));
     }

When I now use events2::rebuild, the behaviour is the same as before.

So what do you think?

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions