diff --git a/CHANGELOG.md b/CHANGELOG.md index 25f70e07..3e3e28e9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- Event organizer [#260](https://github.com/markuspoerschke/iCal/pull/260) + ## [2.1.0] - 2021-04-21 ### Fixed diff --git a/composer.lock b/composer.lock index e4ea5fa9..11aab2d6 100644 --- a/composer.lock +++ b/composer.lock @@ -241,79 +241,6 @@ ], "time": "2021-03-30T17:13:30+00:00" }, - { - "name": "composer/package-versions-deprecated", - "version": "1.11.99.1", - "source": { - "type": "git", - "url": "https://github.com/composer/package-versions-deprecated.git", - "reference": "7413f0b55a051e89485c5cb9f765fe24bb02a7b6" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/composer/package-versions-deprecated/zipball/7413f0b55a051e89485c5cb9f765fe24bb02a7b6", - "reference": "7413f0b55a051e89485c5cb9f765fe24bb02a7b6", - "shasum": "" - }, - "require": { - "composer-plugin-api": "^1.1.0 || ^2.0", - "php": "^7 || ^8" - }, - "replace": { - "ocramius/package-versions": "1.11.99" - }, - "require-dev": { - "composer/composer": "^1.9.3 || ^2.0@dev", - "ext-zip": "^1.13", - "phpunit/phpunit": "^6.5 || ^7" - }, - "type": "composer-plugin", - "extra": { - "class": "PackageVersions\\Installer", - "branch-alias": { - "dev-master": "1.x-dev" - } - }, - "autoload": { - "psr-4": { - "PackageVersions\\": "src/PackageVersions" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Marco Pivetta", - "email": "ocramius@gmail.com" - }, - { - "name": "Jordi Boggiano", - "email": "j.boggiano@seld.be" - } - ], - "description": "Composer plugin that provides efficient querying for installed package versions (no runtime IO)", - "support": { - "issues": "https://github.com/composer/package-versions-deprecated/issues", - "source": "https://github.com/composer/package-versions-deprecated/tree/1.11.99.1" - }, - "funding": [ - { - "url": "https://packagist.com", - "type": "custom" - }, - { - "url": "https://github.com/composer", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/composer/composer", - "type": "tidelift" - } - ], - "time": "2020-11-11T10:22:58+00:00" - }, { "name": "composer/semver", "version": "3.2.4", @@ -1707,6 +1634,68 @@ }, "time": "2020-12-20T10:01:03+00:00" }, + { + "name": "ocramius/package-versions", + "version": "2.1.0", + "source": { + "type": "git", + "url": "https://github.com/Ocramius/PackageVersions.git", + "reference": "a7e35c34bc166a5684a1e2f13da7b1d6a821349d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Ocramius/PackageVersions/zipball/a7e35c34bc166a5684a1e2f13da7b1d6a821349d", + "reference": "a7e35c34bc166a5684a1e2f13da7b1d6a821349d", + "shasum": "" + }, + "require": { + "composer-runtime-api": "^2.0.0", + "php": "^7.4.7 || ~8.0.0" + }, + "replace": { + "composer/package-versions-deprecated": "*" + }, + "require-dev": { + "composer/composer": "^2.0.0@dev", + "doctrine/coding-standard": "^8.1.0", + "ext-zip": "^1.15.0", + "infection/infection": "dev-master#8d6c4d6b15ec58d3190a78b7774a5d604ec1075a", + "phpunit/phpunit": "~9.3.11", + "vimeo/psalm": "^4.0.1" + }, + "type": "library", + "autoload": { + "psr-4": { + "PackageVersions\\": "src/PackageVersions" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Marco Pivetta", + "email": "ocramius@gmail.com" + } + ], + "description": "Provides efficient querying for installed package versions (no runtime IO)", + "support": { + "issues": "https://github.com/Ocramius/PackageVersions/issues", + "source": "https://github.com/Ocramius/PackageVersions/tree/2.1.0" + }, + "funding": [ + { + "url": "https://github.com/Ocramius", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/ocramius/package-versions", + "type": "tidelift" + } + ], + "time": "2020-10-21T13:48:04+00:00" + }, { "name": "ondram/ci-detector", "version": "3.5.1", diff --git a/examples/example1.php b/examples/example1.php index 99546a5b..fc51e051 100644 --- a/examples/example1.php +++ b/examples/example1.php @@ -18,8 +18,10 @@ use Eluceo\iCal\Domain\ValueObject\Alarm; use Eluceo\iCal\Domain\ValueObject\Attachment; use Eluceo\iCal\Domain\ValueObject\DateTime; +use Eluceo\iCal\Domain\ValueObject\EmailAddress; use Eluceo\iCal\Domain\ValueObject\GeographicPosition; use Eluceo\iCal\Domain\ValueObject\Location; +use Eluceo\iCal\Domain\ValueObject\Organizer; use Eluceo\iCal\Domain\ValueObject\TimeSpan; use Eluceo\iCal\Domain\ValueObject\Uri; use Eluceo\iCal\Presentation\Factory\CalendarFactory; @@ -31,6 +33,10 @@ $event ->setSummary('Christmas Eve') ->setDescription('Lorem Ipsum Dolor...') + ->setOrganizer(new Organizer( + new EmailAddress('john.doe@example.com'), + 'John Doe' + )) ->setLocation( (new Location('Neuschwansteinstraße 20, 87645 Schwangau', 'Schloss Neuschwanstein')) ->withGeographicPosition(new GeographicPosition(47.557579, 10.749704)) diff --git a/src/Domain/Entity/Event.php b/src/Domain/Entity/Event.php index d356928a..220d4501 100644 --- a/src/Domain/Entity/Event.php +++ b/src/Domain/Entity/Event.php @@ -15,6 +15,7 @@ use Eluceo\iCal\Domain\ValueObject\Attachment; use Eluceo\iCal\Domain\ValueObject\Location; use Eluceo\iCal\Domain\ValueObject\Occurrence; +use Eluceo\iCal\Domain\ValueObject\Organizer; use Eluceo\iCal\Domain\ValueObject\Timestamp; use Eluceo\iCal\Domain\ValueObject\UniqueIdentifier; @@ -26,6 +27,7 @@ class Event private ?string $description = null; private ?Occurrence $occurrence = null; private ?Location $location = null; + private ?Organizer $organizer = null; /** * @var array @@ -150,6 +152,25 @@ public function hasLocation(): bool return $this->location !== null; } + public function getOrganizer(): Organizer + { + assert($this->organizer !== null); + + return $this->organizer; + } + + public function setOrganizer(?Organizer $organizer): self + { + $this->organizer = $organizer; + + return $this; + } + + public function hasOrganizer(): bool + { + return $this->organizer !== null; + } + /** * @return Alarm[] */ diff --git a/src/Domain/ValueObject/EmailAddress.php b/src/Domain/ValueObject/EmailAddress.php new file mode 100644 index 00000000..4b9b1f66 --- /dev/null +++ b/src/Domain/ValueObject/EmailAddress.php @@ -0,0 +1,38 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Eluceo\iCal\Domain\ValueObject; + +use InvalidArgumentException; + +final class EmailAddress +{ + private string $emailAddress; + + public function __construct(string $emailAddress) + { + if (!filter_var($emailAddress, FILTER_VALIDATE_EMAIL)) { + throw new InvalidArgumentException("$emailAddress is no valid e-mail address"); + } + + $this->emailAddress = $emailAddress; + } + + public function getEmailAddress(): string + { + return $this->emailAddress; + } + + public function toUri(): Uri + { + return new Uri('mailto:' . urlencode($this->emailAddress)); + } +} diff --git a/src/Domain/ValueObject/Organizer.php b/src/Domain/ValueObject/Organizer.php new file mode 100644 index 00000000..f29be6af --- /dev/null +++ b/src/Domain/ValueObject/Organizer.php @@ -0,0 +1,83 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Eluceo\iCal\Domain\ValueObject; + +final class Organizer +{ + private EmailAddress $emailAddress; + private ?string $displayName; + + /** + * The e-mail address of another user that is acting on behalf of the "Organizer". + */ + private ?EmailAddress $sentBy; + + /** + * To specify reference to a directory entry associated wit the calendar user specified by the property. + * + * @see https://tools.ietf.org/html/rfc5545#section-3.2.6 + */ + private ?Uri $directoryEntry; + + public function __construct( + EmailAddress $emailAddress, + ?string $displayName = null, + ?Uri $directoryEntry = null, + ?EmailAddress $sentBy = null + ) { + $this->emailAddress = $emailAddress; + $this->displayName = $displayName; + $this->directoryEntry = $directoryEntry; + $this->sentBy = $sentBy; + } + + public function getEmailAddress(): EmailAddress + { + return $this->emailAddress; + } + + public function hasDisplayName(): bool + { + return $this->displayName !== null; + } + + public function getDisplayName(): string + { + assert($this->displayName !== null); + + return $this->displayName; + } + + public function isSentInBehalfOf(): bool + { + return $this->sentBy !== null; + } + + public function getSentBy(): EmailAddress + { + assert($this->sentBy !== null); + + return $this->sentBy; + } + + public function hasDirectoryEntry(): bool + { + return $this->directoryEntry !== null; + } + + public function getDirectoryEntry(): Uri + { + assert($this->directoryEntry !== null); + + return $this->directoryEntry; + } +} diff --git a/src/Presentation/Factory/EventFactory.php b/src/Presentation/Factory/EventFactory.php index b1227c14..bf2039ef 100644 --- a/src/Presentation/Factory/EventFactory.php +++ b/src/Presentation/Factory/EventFactory.php @@ -18,6 +18,7 @@ use Eluceo\iCal\Domain\ValueObject\Attachment; use Eluceo\iCal\Domain\ValueObject\MultiDay; use Eluceo\iCal\Domain\ValueObject\Occurrence; +use Eluceo\iCal\Domain\ValueObject\Organizer; use Eluceo\iCal\Domain\ValueObject\SingleDay; use Eluceo\iCal\Domain\ValueObject\TimeSpan; use Eluceo\iCal\Presentation\Component; @@ -91,6 +92,10 @@ protected function getProperties(Event $event): Generator yield from $this->getLocationProperties($event); } + if ($event->hasOrganizer()) { + yield $this->getOrganizerProperty($event->getOrganizer()); + } + foreach ($event->getAttachments() as $attachment) { yield from $this->getAttachmentProperties($attachment); } @@ -179,4 +184,23 @@ private function getAttachmentProperties(Attachment $attachment): Generator ); } } + + private function getOrganizerProperty(Organizer $organizer): Property + { + $parameters = []; + + if ($organizer->hasDisplayName()) { + $parameters[] = new Parameter('CN', new TextValue($organizer->getDisplayName())); + } + + if ($organizer->hasDirectoryEntry()) { + $parameters[] = new Parameter('DIR', new UriValue($organizer->getDirectoryEntry())); + } + + if ($organizer->isSentInBehalfOf()) { + $parameters[] = new Parameter('SENT-BY', new UriValue($organizer->getSentBy()->toUri())); + } + + return new Property('ORGANIZER', new UriValue($organizer->getEmailAddress()->toUri()), $parameters); + } } diff --git a/tests/Unit/Presentation/Factory/EventFactoryTest.php b/tests/Unit/Presentation/Factory/EventFactoryTest.php index 4b3d591b..14946ca3 100644 --- a/tests/Unit/Presentation/Factory/EventFactoryTest.php +++ b/tests/Unit/Presentation/Factory/EventFactoryTest.php @@ -18,9 +18,11 @@ use Eluceo\iCal\Domain\ValueObject\BinaryContent; use Eluceo\iCal\Domain\ValueObject\Date; use Eluceo\iCal\Domain\ValueObject\DateTime; +use Eluceo\iCal\Domain\ValueObject\EmailAddress; use Eluceo\iCal\Domain\ValueObject\GeographicPosition; use Eluceo\iCal\Domain\ValueObject\Location; use Eluceo\iCal\Domain\ValueObject\MultiDay; +use Eluceo\iCal\Domain\ValueObject\Organizer; use Eluceo\iCal\Domain\ValueObject\SingleDay; use Eluceo\iCal\Domain\ValueObject\TimeSpan; use Eluceo\iCal\Domain\ValueObject\Timestamp; @@ -148,6 +150,22 @@ public function testFileAttachments() ]); } + public function testOrganizer() + { + $event = (new Event()) + ->setOrganizer(new Organizer( + new EmailAddress('test@example.com'), + 'Test Display Name', + new Uri('example://directory-entry'), + new EmailAddress('sendby@example.com') + )); + + self::assertEventRendersCorrect($event, [ + 'ORGANIZER;CN=Test Display Name;DIR=example://directory-entry;SENT-BY=mailto', + ' :sendby%40example.com:mailto:test%40example.com', + ]); + } + private static function assertEventRendersCorrect(Event $event, array $expected) { $resultAsString = (string) (new EventFactory())->createComponent($event); diff --git a/website/docs/component-event.md b/website/docs/component-event.md index d6bf0aba..00aad268 100644 --- a/website/docs/component-event.md +++ b/website/docs/component-event.md @@ -39,6 +39,7 @@ The following sections explain the properties of the domain object: - [Description](#description) - [Occurrence](#occurrence) - [Location](#location) +- [Organizer](#organizer) - [Attachments](#attachments) ### Unique Identifier @@ -209,6 +210,30 @@ $event = new Event(); $event->setLocation($location); ``` +### Organizer + +The Organizer defines the person who organises the event. +The property consists of at least an email address. +Optional a display name, or a directory entry (as used in LDAP for example) can be added. +In case the event was sent in behalf of another person, then the `sendBy` attribute will contain the email address. + +```php +use Eluceo\iCal\Domain\ValueObject\Organizer; +use Eluceo\iCal\Domain\ValueObject\Uri; +use Eluceo\iCal\Domain\ValueObject\EmailAddress; +use Eluceo\iCal\Domain\Entity\Event; + +$organizer = new Organizer( + new EmailAddress('test@example.org'), + 'John Doe', + new Uri('ldap://example.com:6666/o=ABC%20Industries,c=US???(cn=Jim%20Dolittle)'), + new EmailAddress('sender@example.com') +); + +$event = new Event(); +$event->setOrganizer($organizer); +``` + ### Attachments A document can be associated with an event. diff --git a/website/docs/maturity-matrix.md b/website/docs/maturity-matrix.md index 4d924438..65efd05e 100644 --- a/website/docs/maturity-matrix.md +++ b/website/docs/maturity-matrix.md @@ -44,7 +44,7 @@ See [RFC 5545 section 3.6.1](https://tools.ietf.org/html/rfc5545#section-3.6.1). | geo | ✔ | | last-mod | ✖ | | location | ✔ | -| organizer | ✖ | +| organizer | ✔ | | priority | ✖ | | seq | ✖ | | status | ✖ |