Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
tests/fixtures/mergephp.ical eol=crlf
1 change: 1 addition & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
"ext-mbstring": "*",
"ext-readline": "*",
"ext-simplexml": "*",
"eluceo/ical": "^2.15",
"league/commonmark": "^2.3",
"nette/php-generator": "^4.0.0",
"psr/log": "^3.0.0",
Expand Down
66 changes: 65 additions & 1 deletion composer.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

24 changes: 23 additions & 1 deletion src/Builder/MeetupCollection.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,14 @@

namespace MergePHP\Website\Builder;

use ArrayAccess;
use Countable;
use DateTimeImmutable;
use DateTimeZone;
use Iterator;
use MergePHP\Website\Exception\NotImplementedException;

class MeetupCollection implements Iterator, Countable
class MeetupCollection implements Iterator, Countable, ArrayAccess
{
/** @var MeetupEntry[] */
private array $array = [];
Expand Down Expand Up @@ -87,4 +89,24 @@ public function withOnlyFuture(): array
return $meetupEntry->instance->getDateTime() > $NOW;
}));
}

public function offsetExists(mixed $offset): bool
{
return array_key_exists($offset, $this->array);
}

public function offsetGet(mixed $offset): ?MeetupEntry
{
return $this->array[$offset] ?? null;
}

public function offsetSet(mixed $offset, mixed $value): void
{
throw new NotImplementedException('Setting values directly is not allowed');
}

public function offsetUnset(mixed $offset): void
{
throw new NotImplementedException('Unsetting values directly is not allowed');
}
}
113 changes: 113 additions & 0 deletions src/Builder/Processor/ICalProcessor.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
<?php

declare(strict_types=1);

namespace MergePHP\Website\Builder\Processor;

use DateInterval;
use DateTimeZone;
use Eluceo\iCal\Domain\Entity\Calendar;
use Eluceo\iCal\Domain\Entity\Event;
use Eluceo\iCal\Domain\Entity\TimeZone;
use Eluceo\iCal\Domain\Enum\EventStatus;
use Eluceo\iCal\Domain\ValueObject\DateTime;
use Eluceo\iCal\Domain\ValueObject\EmailAddress;
use Eluceo\iCal\Domain\ValueObject\Location;
use Eluceo\iCal\Domain\ValueObject\Organizer;
use Eluceo\iCal\Domain\ValueObject\TimeSpan;
use Eluceo\iCal\Domain\ValueObject\Timestamp;
use Eluceo\iCal\Domain\ValueObject\UniqueIdentifier;
use Eluceo\iCal\Domain\ValueObject\Uri;
use Eluceo\iCal\Presentation\Factory\CalendarFactory;
use Generator;
use League\CommonMark\CommonMarkConverter;
use MergePHP\Website\Builder\MeetupCollection;
use Psr\Log\LoggerInterface;
use RuntimeException;

class ICalProcessor extends AbstractProcessor
{
public function __construct(
protected LoggerInterface $logger,
protected string $outputDirectory,
protected MeetupCollection $meetups,
) {
parent::__construct($logger, $this->outputDirectory);
}

public function run(): void
{
$this->logger->info('Building iCal feed');
$converter = new CommonMarkConverter();

$calendar = new Calendar($this->generateEvents($converter));

$this->meetups->sort();
$oldestTimestamp = $this->meetups[0]->instance->getDateTime();
$newestTimestamp = $this->meetups[count($this->meetups) - 1]->instance->getDateTime();
$calendar->addTimeZone(
TimeZone::createFromPhpDateTimeZone(
new DateTimeZone('America/New_York'),
$oldestTimestamp,
$newestTimestamp,
)
);

$componentFactory = new CalendarFactory();
$calendar = $componentFactory->createCalendar($calendar);

$outputFilename = "$this->outputDirectory/mergephp.ical";
$bytes = file_put_contents($outputFilename, (string)$calendar);

if ($bytes === false) {
throw new RuntimeException("Could not write $outputFilename");
}
$this->logger->info("Saved $bytes bytes to $outputFilename");
}

protected function generateEvents(CommonMarkConverter $converter): Generator
{
foreach ($this->meetups as $meetup) {
$this->logger->debug("Generating iCal event for {$meetup->instance->getTitle()}");

$permalink = 'https://www.mergephp.com' . $meetup->instance->getPermalink();
$description = $converter->convert($meetup->instance->getDescription());
$description = trim(strip_tags((string)$description));

yield (new Event(
new UniqueIdentifier($permalink),
))
->setSummary(
$meetup->instance->getTitle(),
)
->setDescription(
$description,
)
->setOccurrence(
new TimeSpan(
new DateTime($meetup->instance->getDateTime(), true),
new DateTime($meetup->instance->getDateTime()->add(new DateInterval('PT1H')), true),
),
)
->setLocation(
new Location($meetup->instance->getYouTubeLink() ?: 'https://www.mergephp.com/'),
)
->setOrganizer(
new Organizer(new EmailAddress('[email protected]'), $meetup->instance->getSpeakerName()),
)
->setUrl(
new Uri($permalink),
)
->setLastModified(
new Timestamp($meetup->modifiedTimestamp),
)
->touch(
new Timestamp($meetup->modifiedTimestamp),
)
->setStatus(
(new EventStatus())->CONFIRMED(),
)
;
}
}
}
2 changes: 2 additions & 0 deletions src/Builder/SiteBuilderService.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
use FilesystemIterator;
use Lcobucci\Clock\SystemClock;
use MergePHP\Website\Builder\Processor\ArchiveProcessor;
use MergePHP\Website\Builder\Processor\ICalProcessor;
use MergePHP\Website\Builder\Processor\MeetupProcessor;
use MergePHP\Website\Builder\Processor\HomepageProcessor;
use MergePHP\Website\Builder\Processor\MissingLinkProcessor;
Expand Down Expand Up @@ -61,6 +62,7 @@ public function build(): void
(new ArchiveProcessor($this->logger, $this->outputDirectory, $collection, $this->twig, $twigData))->run();
(new SitemapProcessor($this->logger, $this->outputDirectory, $clock))->run();
(new RSSFeedProcessor($this->logger, $this->outputDirectory, $collection))->run();
(new ICalProcessor($this->logger, $this->outputDirectory, $collection))->run();
(new MissingLinkProcessor($this->logger, $this->outputDirectory, $collection))->run();
$this->logger->info('Finished successfully');
}
Expand Down
9 changes: 9 additions & 0 deletions src/Exception/NotImplementedException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<?php

namespace MergePHP\Website\Exception;

use LogicException;

class NotImplementedException extends LogicException
{
}
1 change: 1 addition & 0 deletions templates/header.twig.html
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
<a class="btn btn-outline-primary mr-2" title="YouTube" href="https://www.youtube.com/c/MergePHP"><i class="fab fa-youtube"></i></a>
<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>
<a class="btn btn-outline-primary mr-2" title="Mastodon" href="https://phpc.social/@merge"><i class="fab fa-mastodon"></i></a>
<a class="btn btn-outline-primary mr-2" title="iCal Feed" href="/mergephp.ical"><i class="fa fa-calendar"></i></a>
<a class="btn btn-outline-primary mr-2" title="RSS" href="/atom.xml"><i class="fa fa-rss"></i></a>
</div>
</div>
Expand Down
69 changes: 69 additions & 0 deletions tests/Builder/Processor/ICalProcessorTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
<?php

declare(strict_types=1);

namespace Tests\Builder\Processor;

use DateTimeImmutable;
use DateTimeZone;
use MergePHP\Website\AbstractMeetup;
use MergePHP\Website\Builder\MeetupCollection;
use MergePHP\Website\Builder\MeetupEntry;
use MergePHP\Website\Builder\Processor\ICalProcessor;
use org\bovigo\vfs\vfsStream;
use PHPUnit\Framework\TestCase;
use Psr\Log\NullLogger;

class ICalProcessorTest extends TestCase
{
public function setUp(): void
{
$this->directory = vfsStream::setup();
parent::setUp();
}

public function testItGeneratesAnIcalFeed(): void
{
$collection = new MeetupCollection();
$collection->append(
new MeetupEntry(
new class extends AbstractMeetup
{
public function getTitle(): string
{
return 'Meetup Title';
}

public function getDescription(): string
{
return <<<END
This description spans multiple lines and uses the heredoc syntax
This description spans multiple lines and uses the heredoc syntax
END;
}

public function getDateTime(): DateTimeImmutable
{
return new DateTimeImmutable('2000-01-01 00:00:00', new DateTimeZone('America/New_York'));
}

public function getSpeakerName(): string
{
return 'Speaker Name';
}

public function getSpeakerBio(): string
{
return 'Speaker has no bio';
}
},
new DateTimeImmutable('2001-01-01 01:01:01', new DateTimeZone('America/New_York')),
)
);

$processor = new ICalProcessor(new NullLogger(), 'vfs://root', $collection);
$processor->run();

$this->assertFileEquals(__DIR__ . '/../../fixtures/mergephp.ical', 'vfs://root/mergephp.ical');
}
}
28 changes: 28 additions & 0 deletions tests/fixtures/mergephp.ical
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
BEGIN:VCALENDAR
PRODID:-//eluceo/ical//2.0/EN
VERSION:2.0
CALSCALE:GREGORIAN
BEGIN:VEVENT
UID:https://www.mergephp.com/meetups/2000/01/01/meetup-title.html
DTSTAMP:20010101T060101Z
LAST-MODIFIED:20010101T060101Z
SUMMARY:Meetup Title
DESCRIPTION:This description spans multiple lines and uses the heredoc synt
ax\nThis description spans multiple lines and uses the heredoc syntax
URL:https://www.mergephp.com/meetups/2000/01/01/meetup-title.html
DTSTART;TZID=America/New_York:20000101T000000
DTEND;TZID=America/New_York:20000101T010000
LOCATION:https://www.mergephp.com/
ORGANIZER;CN=Speaker Name:mailto:[email protected]
STATUS:CONFIRMED
END:VEVENT
BEGIN:VTIMEZONE
TZID:America/New_York
BEGIN:STANDARD
DTSTART:20000101T000000
TZNAME:EST
TZOFFSETTO:-0500
TZOFFSETFROM:-0500
END:STANDARD
END:VTIMEZONE
END:VCALENDAR