Virtual ics file

Hey there,
recently I was working with calendar files for events, and serving them by returning a snippet via route works fine.

However, I’d like to implement these ics files as virtual files which is much cleaner on the frontend and allows for stuff like niceSize() (custom file class etc), but how would I get the virtual file content (which is a string, basically) to represent the file? Has anybody done this before? I don’t see how new File() would be of any use there …

Would I implement something like public function content() … ?


I know, I could write my content to disk via File::create, but there has to be another way to achieve this …

I think it’s not that easy. Of course you could assign your string value to content, and then return that somehow, but you wouldn’t be able to do a file_get_contents unless you store the string in a data uri. And yes, you can find a way to estimate the future size of such a file through php://memory. But I wonder if there is really anything to be gained by this.

I once did this, a bit in a hurry:


use Kirby\Cms\Page;
use Kirby\Cms\File;
use Jsvrcek\ICS\Model\Calendar;
use Jsvrcek\ICS\Model\CalendarEvent;
use Jsvrcek\ICS\Model\Description\Location;

use Jsvrcek\ICS\Utility\Formatter;
use Jsvrcek\ICS\CalendarStream;
use Jsvrcek\ICS\CalendarExport;

class ICSFile extends File {
  private $eventStart = null;
  private $eventEnd = null;
  private $eventName = null;
  private $wholeDay = false;
  private $eventLocation = null;
  private $timezone = null;

  // this needs to exist or deleting the page won't work
  public function delete(): bool
    return true;
  public function __construct($eventPage) {
    $this->eventStart = new DateTime($eventPage->date(), $this->timezone);
    if($eventPage->enddate()->isEmpty()) {
      $this->eventEnd = clone $this->eventStart;
      if($this->eventStart->format('H:i:s') === '00:00:00') {
        $this->wholeDay = true;
    } else {
      $this->eventEnd = new DateTime($eventPage->enddate(), $this->timezone);

    $this->eventName = $eventPage->title();
    $this->eventLocation = $eventPage->place();
    $this->eventUrl = $eventPage->url() . '.ics';
    $this->timezone = new DateTimeZone('Europe/Zurich');

      'filename' => 'calendar.ics',
      'url' => $this->eventUrl,
      'template' => 'attachment',
      'parent' => $eventPage,
      'content' => []

  public function writeContent(array $data, string $languageCode = null): bool {
    // not supported

  public function read() {
    $event = new CalendarEvent();
    $location = new Location();



    $calendar = new Calendar();
    $calendarExport = new CalendarExport(new CalendarStream, new Formatter());

    return $calendarExport->getStream();

class EventPage extends Page {
  public function files() {
    $files = parent::files();

    $virtualFile = new ICSFile($this);
    return $files;

It uses this GitHub - jasvrcek/ICS: Object-oriented php library for creating .ics iCal files to format the ICS file - you probably have your own solution, but I guess you’ll get the idea


Totally cool, thanks so much for sharing!

1 Like

I moved the calendar stream & export code from read() to __construct() and saved it as $this->calendar, then created these two (rather primitive) methods:

 * Returns the raw size of the file
 * @return int
public function size(): int
    return strlen($this->calendar);

 * Returns the file size in a human-readable format
 * @return string
public function niceSize(): string
    $size = $this->size();

    # the math magic
    $size = round($size / pow(1024, ($unit = floor(log($size, 1024)))), 2);

    return $size . ' ' . Kirby\Toolkit\F::$units[$unit];

… to provide low-level filesize. Thanks again, @rasteiner !

note that this creates the “file” (the export) every time the page is loaded. Even if you don’t access it, or its size. It’s probably not a problem, but if you want to be a bit more conservative, you could keep the code in read and instead cache it there. Like:

  public function read()
    if($this->calendar) return $this->calendar;
    //else create $this->calendar and return it

  public function size(): int
    return strlen($this->read());

  // etc...
1 Like

Didn’t think about that oO

Mhh, using an a tag (with download attribute, etc) doesn’t provide a working download link for $file->url() - what am I doing wrong? I didn’t change anything except adding the two functions as stated above - how did you manage to let people download your calendar file?

Uhm… It has been a while since I wrote that. Don’t really remember.

I’ll check it tomorrow and let you know

Oh… completely forgot about this. I have a content representation for the page the calendar event is bound to. That loads and echoes the file:


header('Content-type: text/calendar; charset=utf-8');
header('Content-Disposition: inline; filename=' . $page->slug() . '.ics');

echo $page->file('calendar.ics')->read();

You probably also could create a route that matches any “.ics” file, resolves and then loads it.

Yeah, I figured going this way would be feasible - @bnomei pointed me in that direction on Discord, thanks to you both!

1 Like

If you want the file to work in the panel, I guess you could overwrite the url function to make it point to the correct address

Nah, it works as it is, thx to your help - I’ll leave it as it is :wink: