Skip to content

Commit fdd8204

Browse files
committed
Add iCal feed
Fixes #12
1 parent 2c87852 commit fdd8204

File tree

10 files changed

+312
-2
lines changed

10 files changed

+312
-2
lines changed

.gitattributes

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
tests/fixtures/mergephp.ical eol=crlf

composer.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
"ext-mbstring": "*",
2020
"ext-readline": "*",
2121
"ext-simplexml": "*",
22+
"eluceo/ical": "^2.15",
2223
"league/commonmark": "^2.3",
2324
"nette/php-generator": "^4.0.0",
2425
"psr/log": "^3.0.0",

composer.lock

Lines changed: 65 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/Builder/MeetupCollection.php

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,14 @@
44

55
namespace MergePHP\Website\Builder;
66

7+
use ArrayAccess;
78
use Countable;
89
use DateTimeImmutable;
910
use DateTimeZone;
1011
use Iterator;
12+
use MergePHP\Website\Exception\NotImplementedException;
1113

12-
class MeetupCollection implements Iterator, Countable
14+
class MeetupCollection implements Iterator, Countable, ArrayAccess
1315
{
1416
/** @var MeetupEntry[] */
1517
private array $array = [];
@@ -87,4 +89,24 @@ public function withOnlyFuture(): array
8789
return $meetupEntry->instance->getDateTime() > $NOW;
8890
}));
8991
}
92+
93+
public function offsetExists(mixed $offset): bool
94+
{
95+
return array_key_exists($offset, $this->array);
96+
}
97+
98+
public function offsetGet(mixed $offset): ?MeetupEntry
99+
{
100+
return $this->array[$offset] ?? null;
101+
}
102+
103+
public function offsetSet(mixed $offset, mixed $value): void
104+
{
105+
throw new NotImplementedException('Setting values directly is not allowed');
106+
}
107+
108+
public function offsetUnset(mixed $offset): void
109+
{
110+
throw new NotImplementedException('Unsetting values directly is not allowed');
111+
}
90112
}
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace MergePHP\Website\Builder\Processor;
6+
7+
use DateInterval;
8+
use DateTimeZone;
9+
use Eluceo\iCal\Domain\Entity\Calendar;
10+
use Eluceo\iCal\Domain\Entity\Event;
11+
use Eluceo\iCal\Domain\Entity\TimeZone;
12+
use Eluceo\iCal\Domain\Enum\EventStatus;
13+
use Eluceo\iCal\Domain\ValueObject\DateTime;
14+
use Eluceo\iCal\Domain\ValueObject\EmailAddress;
15+
use Eluceo\iCal\Domain\ValueObject\Location;
16+
use Eluceo\iCal\Domain\ValueObject\Organizer;
17+
use Eluceo\iCal\Domain\ValueObject\TimeSpan;
18+
use Eluceo\iCal\Domain\ValueObject\Timestamp;
19+
use Eluceo\iCal\Domain\ValueObject\UniqueIdentifier;
20+
use Eluceo\iCal\Domain\ValueObject\Uri;
21+
use Eluceo\iCal\Presentation\Factory\CalendarFactory;
22+
use Generator;
23+
use League\CommonMark\CommonMarkConverter;
24+
use MergePHP\Website\Builder\MeetupCollection;
25+
use Psr\Log\LoggerInterface;
26+
use RuntimeException;
27+
28+
class ICalProcessor extends AbstractProcessor
29+
{
30+
public function __construct(
31+
protected LoggerInterface $logger,
32+
protected string $outputDirectory,
33+
protected MeetupCollection $meetups,
34+
) {
35+
parent::__construct($logger, $this->outputDirectory);
36+
}
37+
38+
public function run(): void
39+
{
40+
$this->logger->info('Building iCal feed');
41+
$converter = new CommonMarkConverter();
42+
43+
$calendar = new Calendar($this->generateEvents($converter));
44+
45+
$this->meetups->sort();
46+
$oldestTimestamp = $this->meetups[0]->instance->getDateTime();
47+
$newestTimestamp = $this->meetups[count($this->meetups) - 1]->instance->getDateTime();
48+
$calendar->addTimeZone(
49+
TimeZone::createFromPhpDateTimeZone(
50+
new DateTimeZone('America/New_York'),
51+
$oldestTimestamp,
52+
$newestTimestamp,
53+
)
54+
);
55+
56+
$componentFactory = new CalendarFactory();
57+
$calendar = $componentFactory->createCalendar($calendar);
58+
59+
$outputFilename = "$this->outputDirectory/mergephp.ical";
60+
$bytes = file_put_contents($outputFilename, (string)$calendar);
61+
62+
if ($bytes === false) {
63+
throw new RuntimeException("Could not write $outputFilename");
64+
}
65+
$this->logger->info("Saved $bytes bytes to $outputFilename");
66+
}
67+
68+
protected function generateEvents(CommonMarkConverter $converter): Generator
69+
{
70+
foreach ($this->meetups as $meetup) {
71+
$this->logger->debug("Generating iCal event for {$meetup->instance->getTitle()}");
72+
73+
$permalink = 'https://www.mergephp.com' . $meetup->instance->getPermalink();
74+
$description = $converter->convert($meetup->instance->getDescription());
75+
$description = trim(strip_tags((string)$description));
76+
77+
yield (new Event(
78+
new UniqueIdentifier($permalink),
79+
))
80+
->setSummary(
81+
$meetup->instance->getTitle(),
82+
)
83+
->setDescription(
84+
$description,
85+
)
86+
->setOccurrence(
87+
new TimeSpan(
88+
new DateTime($meetup->instance->getDateTime(), true),
89+
new DateTime($meetup->instance->getDateTime()->add(new DateInterval('PT1H')), true),
90+
),
91+
)
92+
->setLocation(
93+
new Location($meetup->instance->getYouTubeLink() ?: 'https://www.mergephp.com/'),
94+
)
95+
->setOrganizer(
96+
new Organizer(new EmailAddress('[email protected]'), $meetup->instance->getSpeakerName()),
97+
)
98+
->setUrl(
99+
new Uri($permalink),
100+
)
101+
->setLastModified(
102+
new Timestamp($meetup->modifiedTimestamp),
103+
)
104+
->touch(
105+
new Timestamp($meetup->modifiedTimestamp),
106+
)
107+
->setStatus(
108+
(new EventStatus())->CONFIRMED(),
109+
)
110+
;
111+
}
112+
}
113+
}

src/Builder/SiteBuilderService.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
use FilesystemIterator;
99
use Lcobucci\Clock\SystemClock;
1010
use MergePHP\Website\Builder\Processor\ArchiveProcessor;
11+
use MergePHP\Website\Builder\Processor\ICalProcessor;
1112
use MergePHP\Website\Builder\Processor\MeetupProcessor;
1213
use MergePHP\Website\Builder\Processor\HomepageProcessor;
1314
use MergePHP\Website\Builder\Processor\MissingLinkProcessor;
@@ -61,6 +62,7 @@ public function build(): void
6162
(new ArchiveProcessor($this->logger, $this->outputDirectory, $collection, $this->twig, $twigData))->run();
6263
(new SitemapProcessor($this->logger, $this->outputDirectory, $clock))->run();
6364
(new RSSFeedProcessor($this->logger, $this->outputDirectory, $collection))->run();
65+
(new ICalProcessor($this->logger, $this->outputDirectory, $collection))->run();
6466
(new MissingLinkProcessor($this->logger, $this->outputDirectory, $collection))->run();
6567
$this->logger->info('Finished successfully');
6668
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<?php
2+
3+
namespace MergePHP\Website\Exception;
4+
5+
use LogicException;
6+
7+
class NotImplementedException extends LogicException
8+
{
9+
}

templates/header.twig.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
<a class="btn btn-outline-primary mr-2" title="YouTube" href="https://www.youtube.com/c/MergePHP"><i class="fab fa-youtube"></i></a>
3939
<a class="btn btn-outline-primary mr-2" title="LinkedIn" href="https://www.linkedin.com/company/mergephp/"><i class="fab fa-linkedin-in"></i></a>
4040
<a class="btn btn-outline-primary mr-2" title="Mastodon" href="https://phpc.social/@merge"><i class="fab fa-mastodon"></i></a>
41+
<a class="btn btn-outline-primary mr-2" title="iCal Feed" href="/mergephp.ical"><i class="fa fa-calendar"></i></a>
4142
<a class="btn btn-outline-primary mr-2" title="RSS" href="/atom.xml"><i class="fa fa-rss"></i></a>
4243
</div>
4344
</div>
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Tests\Builder\Processor;
6+
7+
use DateTimeImmutable;
8+
use DateTimeZone;
9+
use MergePHP\Website\AbstractMeetup;
10+
use MergePHP\Website\Builder\MeetupCollection;
11+
use MergePHP\Website\Builder\MeetupEntry;
12+
use MergePHP\Website\Builder\Processor\ICalProcessor;
13+
use org\bovigo\vfs\vfsStream;
14+
use PHPUnit\Framework\TestCase;
15+
use Psr\Log\NullLogger;
16+
17+
class ICalProcessorTest extends TestCase
18+
{
19+
public function setUp(): void
20+
{
21+
$this->directory = vfsStream::setup();
22+
parent::setUp();
23+
}
24+
25+
public function testItGeneratesAnIcalFeed(): void
26+
{
27+
$collection = new MeetupCollection();
28+
$collection->append(
29+
new MeetupEntry(
30+
new class extends AbstractMeetup
31+
{
32+
public function getTitle(): string
33+
{
34+
return 'Meetup Title';
35+
}
36+
37+
public function getDescription(): string
38+
{
39+
return <<<END
40+
This description spans multiple lines and uses the heredoc syntax
41+
This description spans multiple lines and uses the heredoc syntax
42+
END;
43+
}
44+
45+
public function getDateTime(): DateTimeImmutable
46+
{
47+
return new DateTimeImmutable('2000-01-01 00:00:00', new DateTimeZone('America/New_York'));
48+
}
49+
50+
public function getSpeakerName(): string
51+
{
52+
return 'Speaker Name';
53+
}
54+
55+
public function getSpeakerBio(): string
56+
{
57+
return 'Speaker has no bio';
58+
}
59+
},
60+
new DateTimeImmutable('2001-01-01 01:01:01', new DateTimeZone('America/New_York')),
61+
)
62+
);
63+
64+
$processor = new ICalProcessor(new NullLogger(), 'vfs://root', $collection);
65+
$processor->run();
66+
67+
$this->assertFileEquals(__DIR__ . '/../../fixtures/mergephp.ical', 'vfs://root/mergephp.ical');
68+
}
69+
}

tests/fixtures/mergephp.ical

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
BEGIN:VCALENDAR
2+
PRODID:-//eluceo/ical//2.0/EN
3+
VERSION:2.0
4+
CALSCALE:GREGORIAN
5+
BEGIN:VEVENT
6+
UID:https://www.mergephp.com/meetups/2000/01/01/meetup-title.html
7+
DTSTAMP:20010101T060101Z
8+
LAST-MODIFIED:20010101T060101Z
9+
SUMMARY:Meetup Title
10+
DESCRIPTION:This description spans multiple lines and uses the heredoc synt
11+
ax\nThis description spans multiple lines and uses the heredoc syntax
12+
URL:https://www.mergephp.com/meetups/2000/01/01/meetup-title.html
13+
DTSTART;TZID=America/New_York:20000101T000000
14+
DTEND;TZID=America/New_York:20000101T010000
15+
LOCATION:https://www.mergephp.com/
16+
ORGANIZER;CN=Speaker Name:mailto:[email protected]
17+
STATUS:CONFIRMED
18+
END:VEVENT
19+
BEGIN:VTIMEZONE
20+
TZID:America/New_York
21+
BEGIN:STANDARD
22+
DTSTART:20000101T000000
23+
TZNAME:EST
24+
TZOFFSETTO:-0500
25+
TZOFFSETFROM:-0500
26+
END:STANDARD
27+
END:VTIMEZONE
28+
END:VCALENDAR

0 commit comments

Comments
 (0)