From a5c57b279716970f0bd9515f71d995ae46bd2ba7 Mon Sep 17 00:00:00 2001 From: giorginogreg Date: Fri, 29 Oct 2021 23:36:55 +0200 Subject: [PATCH] Added attendee Object (#333) --- .gitignore | 1 + CHANGELOG.md | 39 ++- src/Domain/Entity/Attendee.php | 271 +++++++++++++++ src/Domain/Entity/Event.php | 35 ++ src/Domain/Enum/CalendarUserType.php | 46 +++ src/Domain/Enum/ParticipationStatus.php | 58 ++++ src/Domain/Enum/RoleType.php | 40 +++ src/Domain/Enum/TimeZoneTransitionType.php | 4 +- src/Domain/ValueObject/Member.php | 29 ++ .../Component/Property/Value/BooleanValue.php | 29 ++ .../Property/Value/QuotedUriValue.php | 29 ++ src/Presentation/Factory/AttendeeFactory.php | 181 ++++++++++ src/Presentation/Factory/EventFactory.php | 12 +- .../Property/Value/BooleanValueTest.php | 39 +++ .../Presentation/Factory/EventFactoryTest.php | 309 +++++++++++++++++- website/docs/component-event.md | 44 +++ website/docs/maturity-matrix.md | 2 +- 17 files changed, 1144 insertions(+), 24 deletions(-) create mode 100644 src/Domain/Entity/Attendee.php create mode 100644 src/Domain/Enum/CalendarUserType.php create mode 100644 src/Domain/Enum/ParticipationStatus.php create mode 100644 src/Domain/Enum/RoleType.php create mode 100644 src/Domain/ValueObject/Member.php create mode 100644 src/Presentation/Component/Property/Value/BooleanValue.php create mode 100644 src/Presentation/Component/Property/Value/QuotedUriValue.php create mode 100644 src/Presentation/Factory/AttendeeFactory.php create mode 100644 tests/Unit/Presentation/Component/Property/Value/BooleanValueTest.php diff --git a/.gitignore b/.gitignore index 827a45fb..fbf94bbf 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ /.idea +/.vscode /*.cache /build /Makefile.local diff --git a/CHANGELOG.md b/CHANGELOG.md index 73471628..05799be1 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 Attendee property [#333](https://github.com/markuspoerschke/iCal/pull/314) + ### Fixed - PHPStan complains about wrongly typed hinted constructor argument of `Calendar` class. [#281](https://github.com/markuspoerschke/iCal/issues/281) @@ -203,25 +207,24 @@ Please check the [upgrade guide](UPGRADE.md) on how to upgrade from version `0.* - **Breaking Change:** Changed signature of the `Event::setOrganizer` method. Now there is is only one parameter that must be an instance of `Property\Organizer`. - Updated install section in README.md [#54](https://github.com/markuspoerschke/iCal/pull/53) -[0.9.0]: https://github.com/markuspoerschke/iCal/compare/0.8.0...0.9.0 -[0.10.0]: https://github.com/markuspoerschke/iCal/compare/0.9.0...0.10.0 -[0.10.1]: https://github.com/markuspoerschke/iCal/compare/0.10.0...0.10.1 -[0.11.0]: https://github.com/markuspoerschke/iCal/compare/0.10.1...0.11.0 -[0.11.1]: https://github.com/markuspoerschke/iCal/compare/0.11.0...0.11.1 -[0.11.2]: https://github.com/markuspoerschke/iCal/compare/0.11.1...0.11.2 -[0.11.3]: https://github.com/markuspoerschke/iCal/compare/0.11.2...0.11.3 -[0.11.4]: https://github.com/markuspoerschke/iCal/compare/0.11.3...0.11.4 -[0.11.5]: https://github.com/markuspoerschke/iCal/compare/0.11.4...0.11.5 -[0.12.0]: https://github.com/markuspoerschke/iCal/compare/0.11.0...0.12.0 -[0.12.1]: https://github.com/markuspoerschke/iCal/compare/0.12.0...0.12.1 -[0.13.0]: https://github.com/markuspoerschke/iCal/compare/0.12.1...0.13.0 -[0.14.0]: https://github.com/markuspoerschke/iCal/compare/0.13.0...0.14.0 -[0.15.0]: https://github.com/markuspoerschke/iCal/compare/0.14.0...0.15.0 -[0.15.1]: https://github.com/markuspoerschke/iCal/compare/0.15.0...0.15.1 -[0.16.0]: https://github.com/markuspoerschke/iCal/compare/0.15.1...0.16.0 [unreleased]: https://github.com/markuspoerschke/iCal/compare/2.3.0...HEAD [2.3.0]: https://github.com/markuspoerschke/iCal/compare/2.2.0...2.3.0 [2.2.0]: https://github.com/markuspoerschke/iCal/compare/2.1.0...2.2.0 -[2.0.0]: https://github.com/markuspoerschke/iCal/compare/0.16.0...2.0.0 -[unreleased]: https://github.com/markuspoerschke/iCal/compare/2.1.0...HEAD [2.1.0]: https://github.com/markuspoerschke/iCal/compare/2.0.0...2.1.0 +[2.0.0]: https://github.com/markuspoerschke/iCal/compare/0.16.0...2.0.0 +[0.16.0]: https://github.com/markuspoerschke/iCal/compare/0.15.1...0.16.0 +[0.15.1]: https://github.com/markuspoerschke/iCal/compare/0.15.0...0.15.1 +[0.15.0]: https://github.com/markuspoerschke/iCal/compare/0.14.0...0.15.0 +[0.14.0]: https://github.com/markuspoerschke/iCal/compare/0.13.0...0.14.0 +[0.13.0]: https://github.com/markuspoerschke/iCal/compare/0.12.1...0.13.0 +[0.12.1]: https://github.com/markuspoerschke/iCal/compare/0.12.0...0.12.1 +[0.12.0]: https://github.com/markuspoerschke/iCal/compare/0.11.0...0.12.0 +[0.11.5]: https://github.com/markuspoerschke/iCal/compare/0.11.4...0.11.5 +[0.11.4]: https://github.com/markuspoerschke/iCal/compare/0.11.3...0.11.4 +[0.11.3]: https://github.com/markuspoerschke/iCal/compare/0.11.2...0.11.3 +[0.11.2]: https://github.com/markuspoerschke/iCal/compare/0.11.1...0.11.2 +[0.11.1]: https://github.com/markuspoerschke/iCal/compare/0.11.0...0.11.1 +[0.11.0]: https://github.com/markuspoerschke/iCal/compare/0.10.1...0.11.0 +[0.10.0]: https://github.com/markuspoerschke/iCal/compare/0.9.0...0.10.0 +[0.10.1]: https://github.com/markuspoerschke/iCal/compare/0.10.0...0.10.1 +[0.9.0]: https://github.com/markuspoerschke/iCal/compare/0.8.0...0.9.0 diff --git a/src/Domain/Entity/Attendee.php b/src/Domain/Entity/Attendee.php new file mode 100644 index 00000000..f6b636a3 --- /dev/null +++ b/src/Domain/Entity/Attendee.php @@ -0,0 +1,271 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Eluceo\iCal\Domain\Entity; + +use Eluceo\iCal\Domain\Enum\CalendarUserType; +use Eluceo\iCal\Domain\Enum\ParticipationStatus; +use Eluceo\iCal\Domain\Enum\RoleType; +use Eluceo\iCal\Domain\ValueObject\EmailAddress; +use Eluceo\iCal\Domain\ValueObject\Member; +use Eluceo\iCal\Domain\ValueObject\Uri; + +/** + * @see https://datatracker.ietf.org/doc/html/rfc5545#section-3.8.4.1 + */ +final class Attendee +{ + private EmailAddress $emailAddress; + private ?CalendarUserType $calendarUserType = null; + private ?RoleType $role = null; + private ?ParticipationStatus $participationStatus = null; + private ?bool $rsvp = null; + private ?string $displayName = null; + private ?Uri $directoryEntry = null; + private ?string $language = null; + + /** + * @var array + */ + private array $members = []; + + /** + * @var array + */ + private array $delegatedTo = []; + + /** + * @var array + */ + private array $delegatedFrom = []; + + /** + * @var array + */ + private array $sentBy = []; + + public function __construct( + EmailAddress $emailAddress + ) { + $this->emailAddress = $emailAddress; + } + + public function getEmailAddress(): EmailAddress + { + return $this->emailAddress; + } + + public function setCalendarUserType(CalendarUserType $calendarUserType): self + { + $this->calendarUserType = $calendarUserType; + + return $this; + } + + public function hasCalendarUserType(): bool + { + return $this->calendarUserType !== null; + } + + public function getCalendarUserType(): CalendarUserType + { + assert($this->calendarUserType !== null); + + return $this->calendarUserType; + } + + public function hasMembers(): bool + { + return !empty($this->members); + } + + public function addMember(Member $member): self + { + $this->members[] = $member; + + return $this; + } + + /** + * @return array + */ + public function getMembers(): array + { + return $this->members; + } + + public function setRole(RoleType $role): self + { + $this->role = $role; + + return $this; + } + + public function hasRole(): bool + { + return $this->role !== null; + } + + public function getRole(): RoleType + { + assert($this->role !== null); + + return $this->role; + } + + public function setParticipationStatus(ParticipationStatus $participationStatus): self + { + $this->participationStatus = $participationStatus; + + return $this; + } + + public function hasParticipationStatus(): bool + { + return $this->participationStatus !== null; + } + + public function getParticipationStatus(): ParticipationStatus + { + assert($this->participationStatus !== null); + + return $this->participationStatus; + } + + public function setResponseNeededFromAttendee(bool $res): self + { + $this->rsvp = $res; + + return $this; + } + + public function isRSVPenabled(): bool + { + return $this->rsvp !== null; + } + + public function setDisplayName(string $displayName): self + { + $this->displayName = $displayName; + + return $this; + } + + public function hasDisplayName(): bool + { + return $this->displayName !== null; + } + + public function getDisplayName(): string + { + assert($this->displayName !== null); + + return $this->displayName; + } + + public function addDelegatedTo(EmailAddress $delegatedTo): self + { + $this->delegatedTo[] = $delegatedTo; + + return $this; + } + + public function hasDelegatedTo(): bool + { + return !empty($this->delegatedTo); + } + + /** + * @return array + */ + public function getDelegatedTo(): array + { + return $this->delegatedTo; + } + + public function addDelegatedFrom(EmailAddress $delegatedFrom): self + { + $this->delegatedFrom[] = $delegatedFrom; + + return $this; + } + + public function hasDelegatedFrom(): bool + { + return !empty($this->delegatedFrom); + } + + /** + * @return array + */ + public function getDelegatedFrom(): array + { + return $this->delegatedFrom; + } + + public function addSentBy(EmailAddress $sentBy): self + { + $this->sentBy[] = $sentBy; + + return $this; + } + + public function hasSentBy(): bool + { + return !empty($this->sentBy); + } + + /** + * @return array + */ + public function getSentBy(): array + { + return $this->sentBy; + } + + public function hasDirectoryEntryReference(): bool + { + return $this->directoryEntry !== null; + } + + public function getDirectoryEntryReference(): Uri + { + assert($this->directoryEntry !== null); + + return $this->directoryEntry; + } + + public function setDirectoryEntryReference(Uri $directoryEntry): self + { + $this->directoryEntry = $directoryEntry; + + return $this; + } + + public function hasLanguage(): bool + { + return $this->language !== null; + } + + public function getLanguage(): string + { + assert($this->language !== null); + + return $this->language; + } + + public function setLanguage(string $language): self + { + $this->language = $language; + + return $this; + } +} diff --git a/src/Domain/Entity/Event.php b/src/Domain/Entity/Event.php index 09c96aea..8bbace12 100644 --- a/src/Domain/Entity/Event.php +++ b/src/Domain/Entity/Event.php @@ -32,6 +32,11 @@ class Event private ?Organizer $organizer = null; private ?Timestamp $lastModified = null; + /** + * @var array + */ + private array $attendees = []; + /** * @var array */ @@ -248,4 +253,34 @@ public function setLastModified(?Timestamp $lastModified): self return $this; } + + public function hasAttendee(): bool + { + return !empty($this->attendees); + } + + public function addAttendee(Attendee $attendee): self + { + $this->attendees[] = $attendee; + + return $this; + } + + /** + * @param Attendee[] $attendees + */ + public function setAttendees(array $attendees): self + { + $this->attendees = $attendees; + + return $this; + } + + /** + * @return Attendee[] + */ + public function getAttendees(): array + { + return $this->attendees; + } } diff --git a/src/Domain/Enum/CalendarUserType.php b/src/Domain/Enum/CalendarUserType.php new file mode 100644 index 00000000..723b2a8a --- /dev/null +++ b/src/Domain/Enum/CalendarUserType.php @@ -0,0 +1,46 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Eluceo\iCal\Domain\Enum; + +final class CalendarUserType +{ + private static ?self $individual = null; + private static ?self $group = null; + private static ?self $resource = null; + private static ?self $room = null; + private static ?self $unknown = null; + + public static function INDIVIDUAL(): self + { + return self::$individual ??= new self(); + } + + public static function GROUP(): self + { + return self::$group ??= new self(); + } + + public static function RESOURCE(): self + { + return self::$resource ??= new self(); + } + + public static function ROOM(): self + { + return self::$room ??= new self(); + } + + public static function UNKNOWN(): self + { + return self::$unknown ??= new self(); + } +} diff --git a/src/Domain/Enum/ParticipationStatus.php b/src/Domain/Enum/ParticipationStatus.php new file mode 100644 index 00000000..3a011cd3 --- /dev/null +++ b/src/Domain/Enum/ParticipationStatus.php @@ -0,0 +1,58 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Eluceo\iCal\Domain\Enum; + +final class ParticipationStatus +{ + private static ?self $needsAction = null; + private static ?self $accepted = null; + private static ?self $declined = null; + private static ?self $tentative = null; + private static ?self $delegated = null; + private static ?self $completed = null; + private static ?self $inProcess = null; + + public static function NEEDS_ACTION(): ParticipationStatus + { + return self::$needsAction ??= new ParticipationStatus(); + } + + public static function ACCEPTED(): ParticipationStatus + { + return self::$accepted ??= new ParticipationStatus(); + } + + public static function DECLINED(): ParticipationStatus + { + return self::$declined ??= new ParticipationStatus(); + } + + public static function TENTATIVE(): ParticipationStatus + { + return self::$tentative ??= new ParticipationStatus(); + } + + public static function DELEGATED(): ParticipationStatus + { + return self::$delegated ??= new ParticipationStatus(); + } + + public static function COMPLETED(): ParticipationStatus + { + return self::$completed ??= new ParticipationStatus(); + } + + public static function IN_PROCESS(): ParticipationStatus + { + return self::$inProcess ??= new ParticipationStatus(); + } +} diff --git a/src/Domain/Enum/RoleType.php b/src/Domain/Enum/RoleType.php new file mode 100644 index 00000000..1293c04b --- /dev/null +++ b/src/Domain/Enum/RoleType.php @@ -0,0 +1,40 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Eluceo\iCal\Domain\Enum; + +final class RoleType +{ + private static ?self $chair = null; + private static ?self $reqParticipant = null; + private static ?self $optParticipant = null; + private static ?self $nonParticipant = null; + + public static function CHAIR(): self + { + return self::$chair ??= new RoleType(); + } + + public static function REQ_PARTICIPANT(): self + { + return self::$reqParticipant ??= new RoleType(); + } + + public static function OPT_PARTICIPANT(): self + { + return self::$optParticipant ??= new RoleType(); + } + + public static function NON_PARTICIPANT(): self + { + return self::$nonParticipant ??= new RoleType(); + } +} diff --git a/src/Domain/Enum/TimeZoneTransitionType.php b/src/Domain/Enum/TimeZoneTransitionType.php index 2b118b79..cc494d7e 100644 --- a/src/Domain/Enum/TimeZoneTransitionType.php +++ b/src/Domain/Enum/TimeZoneTransitionType.php @@ -18,11 +18,11 @@ final class TimeZoneTransitionType public static function DAYLIGHT(): self { - return self::$daylight = self::$daylight ?? new TimeZoneTransitionType(); + return self::$daylight ??= new TimeZoneTransitionType(); } public static function STANDARD(): self { - return self::$standard = self::$standard ?? new TimeZoneTransitionType(); + return self::$standard ??= new TimeZoneTransitionType(); } } diff --git a/src/Domain/ValueObject/Member.php b/src/Domain/ValueObject/Member.php new file mode 100644 index 00000000..6dc13d04 --- /dev/null +++ b/src/Domain/ValueObject/Member.php @@ -0,0 +1,29 @@ + + * + * 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 Member +{ + private ?EmailAddress $emailAddress = null; + + public function __construct(EmailAddress $emailAddress) + { + $this->emailAddress = $emailAddress; + } + + public function getEmailAddress(): EmailAddress + { + assert($this->emailAddress !== null); + + return $this->emailAddress; + } +} diff --git a/src/Presentation/Component/Property/Value/BooleanValue.php b/src/Presentation/Component/Property/Value/BooleanValue.php new file mode 100644 index 00000000..bf32f04a --- /dev/null +++ b/src/Presentation/Component/Property/Value/BooleanValue.php @@ -0,0 +1,29 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Eluceo\iCal\Presentation\Component\Property\Value; + +use Eluceo\iCal\Presentation\Component\Property\Value; + +final class BooleanValue extends Value +{ + private string $valueAsString; + + public function __construct(bool $value) + { + $this->valueAsString = $value ? 'TRUE' : 'FALSE'; + } + + public function __toString(): string + { + return $this->valueAsString; + } +} diff --git a/src/Presentation/Component/Property/Value/QuotedUriValue.php b/src/Presentation/Component/Property/Value/QuotedUriValue.php new file mode 100644 index 00000000..9512e9bb --- /dev/null +++ b/src/Presentation/Component/Property/Value/QuotedUriValue.php @@ -0,0 +1,29 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Eluceo\iCal\Presentation\Component\Property\Value; + +use Eluceo\iCal\Domain\ValueObject\Uri; + +class QuotedUriValue extends UriValue +{ + private string $uri; + + public function __construct(Uri $url) + { + $this->uri = $url->getUri(); + } + + public function __toString(): string + { + return '"' . $this->uri . '"'; + } +} diff --git a/src/Presentation/Factory/AttendeeFactory.php b/src/Presentation/Factory/AttendeeFactory.php new file mode 100644 index 00000000..ab37d339 --- /dev/null +++ b/src/Presentation/Factory/AttendeeFactory.php @@ -0,0 +1,181 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Eluceo\iCal\Presentation\Factory; + +use Eluceo\iCal\Domain\Entity\Attendee; +use Eluceo\iCal\Domain\Enum\CalendarUserType; +use Eluceo\iCal\Domain\Enum\ParticipationStatus; +use Eluceo\iCal\Domain\Enum\RoleType; +use Eluceo\iCal\Presentation\Component\Property; +use Eluceo\iCal\Presentation\Component\Property\Parameter; +use Eluceo\iCal\Presentation\Component\Property\Value\BooleanValue; +use Eluceo\iCal\Presentation\Component\Property\Value\ListValue; +use Eluceo\iCal\Presentation\Component\Property\Value\QuotedUriValue; +use Eluceo\iCal\Presentation\Component\Property\Value\TextValue; +use Eluceo\iCal\Presentation\Component\Property\Value\UriValue; +use UnexpectedValueException; + +class AttendeeFactory +{ + public function createProperty(Attendee $attendee): Property + { + return new Property('ATTENDEE', new UriValue($attendee->getEmailAddress()->toUri()), $this->getParameters($attendee)); + } + + /** + * @SuppressWarnings(PHPMD.NPathComplexity) + * @SuppressWarnings(PHPMD.CyclomaticComplexity) + * + * @return array + */ + private function getParameters(Attendee $attendee): array + { + $parameters = []; + + if ($attendee->hasCalendarUserType()) { + $parameters[] = new Parameter('CUTYPE', $this->getCalendarUserTypeValue($attendee->getCalendarUserType())); + } + + if ($attendee->hasMembers()) { + $listAddressesEmail = []; + foreach ($attendee->getMembers() as $member) { + $listAddressesEmail[] = new QuotedUriValue($member->getEmailAddress()->toUri()); + } + $parameters[] = new Parameter('MEMBER', new ListValue($listAddressesEmail)); + } + + if ($attendee->hasRole()) { + $parameters[] = new Parameter('ROLE', $this->getRoleTypeValue($attendee->getRole())); + } + + if ($attendee->hasParticipationStatus()) { + $parameters[] = new Parameter('PARTSTAT', $this->getParticipantStatusTextValue($attendee->getParticipationStatus())); + } + + if ($attendee->isRSVPenabled()) { + $parameters[] = new Parameter('RSVP', new BooleanValue(true)); + } + + if ($attendee->hasDelegatedTo()) { + $listAddressesEmail = []; + foreach ($attendee->getDelegatedTo() as $delegatedToAddress) { + $listAddressesEmail[] = new QuotedUriValue($delegatedToAddress->toUri()); + } + $parameters[] = new Parameter('DELEGATED-TO', new ListValue($listAddressesEmail)); + } + + if ($attendee->hasDelegatedFrom()) { + $listAddressesEmail = []; + foreach ($attendee->getDelegatedFrom() as $delegatedFromAddress) { + $listAddressesEmail[] = new QuotedUriValue($delegatedFromAddress->toUri()); + } + $parameters[] = new Parameter('DELEGATED-FROM', new ListValue($listAddressesEmail)); + } + + if ($attendee->hasSentBy()) { + $listAddressesEmail = []; + foreach ($attendee->getSentBy() as $sentByAddress) { + $listAddressesEmail[] = new QuotedUriValue($sentByAddress->toUri()); + } + $parameters[] = new Parameter('SENT-BY', new ListValue($listAddressesEmail)); + } + + if ($attendee->hasDisplayName()) { + $parameters[] = new Parameter('CN', new TextValue($attendee->getDisplayName())); + } + + if ($attendee->hasDirectoryEntryReference()) { + $parameters[] = new Parameter('DIR', new QuotedUriValue($attendee->getDirectoryEntryReference())); + } + + if ($attendee->hasLanguage()) { + $parameters[] = new Parameter('LANGUAGE', new TextValue($attendee->getLanguage())); + } + + return $parameters; + } + + private function getCalendarUserTypeValue(CalendarUserType $calendarUserType): TextValue + { + if ($calendarUserType === CalendarUserType::GROUP()) { + return new TextValue('GROUP'); + } + + if ($calendarUserType === CalendarUserType::INDIVIDUAL()) { + return new TextValue('INDIVIDUAL'); + } + + if ($calendarUserType === CalendarUserType::RESOURCE()) { + return new TextValue('RESOURCE'); + } + + if ($calendarUserType === CalendarUserType::ROOM()) { + return new TextValue('ROOM'); + } + + return new TextValue('UNKNOWN'); + } + + private function getRoleTypeValue(RoleType $roleType): TextValue + { + if ($roleType === RoleType::CHAIR()) { + return new TextValue('CHAIR'); + } + + if ($roleType === RoleType::REQ_PARTICIPANT()) { + return new TextValue('REQ-PARTICIPANT'); + } + + if ($roleType === RoleType::OPT_PARTICIPANT()) { + return new TextValue('OPT-PARTICIPANT'); + } + + if ($roleType === RoleType::NON_PARTICIPANT()) { + return new TextValue('NON-PARTICIPANT'); + } + + throw new UnexpectedValueException(sprintf('The enum %s resulted into an unknown role type value that is not yet implemented.', RoleType::class)); + } + + public function getParticipantStatusTextValue(ParticipationStatus $participationStatus): TextValue + { + if (ParticipationStatus::NEEDS_ACTION() === $participationStatus) { + return new TextValue('NEEDS-ACTION'); + } + + if (ParticipationStatus::ACCEPTED() === $participationStatus) { + return new TextValue('ACCEPTED'); + } + + if (ParticipationStatus::DECLINED() === $participationStatus) { + return new TextValue('DECLINED'); + } + + if (ParticipationStatus::TENTATIVE() === $participationStatus) { + return new TextValue('TENTATIVE'); + } + + if (ParticipationStatus::DELEGATED() === $participationStatus) { + return new TextValue('DELEGATED'); + } + + if (ParticipationStatus::COMPLETED() === $participationStatus) { + return new TextValue('COMPLETED'); + } + + if (ParticipationStatus::IN_PROCESS() === $participationStatus) { + return new TextValue('IN-PROCESS'); + } + + throw new UnexpectedValueException(sprintf('The enum %s resulted into an unknown role type value that is not yet implemented.', ParticipationStatus::class)); + } +} diff --git a/src/Presentation/Factory/EventFactory.php b/src/Presentation/Factory/EventFactory.php index be6cbe00..122b1ea7 100644 --- a/src/Presentation/Factory/EventFactory.php +++ b/src/Presentation/Factory/EventFactory.php @@ -40,13 +40,14 @@ class EventFactory { private AlarmFactory $alarmFactory; - private DateTimeFactory $dateTimeFactory; + private AttendeeFactory $attendeeFactory; - public function __construct(?AlarmFactory $alarmFactory = null, ?DateTimeFactory $dateTimeFactory = null) + public function __construct(?AlarmFactory $alarmFactory = null, ?DateTimeFactory $dateTimeFactory = null, ?AttendeeFactory $attendeeFactory = null) { $this->alarmFactory = $alarmFactory ?? new AlarmFactory(); $this->dateTimeFactory = $dateTimeFactory ?? new DateTimeFactory(); + $this->attendeeFactory = $attendeeFactory ?? new AttendeeFactory(); } /** @@ -71,6 +72,7 @@ public function createComponent(Event $event): Component /** * @return Generator * @SuppressWarnings(PHPMD.NPathComplexity) + * @SuppressWarnings(PHPMD.CyclomaticComplexity) */ protected function getProperties(Event $event): Generator { @@ -105,6 +107,12 @@ protected function getProperties(Event $event): Generator yield $this->getOrganizerProperty($event->getOrganizer()); } + if ($event->hasAttendee()) { + foreach ($event->getAttendees() as $attendee) { + yield $this->attendeeFactory->createProperty($attendee); + } + } + foreach ($event->getAttachments() as $attachment) { yield from $this->getAttachmentProperties($attachment); } diff --git a/tests/Unit/Presentation/Component/Property/Value/BooleanValueTest.php b/tests/Unit/Presentation/Component/Property/Value/BooleanValueTest.php new file mode 100644 index 00000000..71ae0832 --- /dev/null +++ b/tests/Unit/Presentation/Component/Property/Value/BooleanValueTest.php @@ -0,0 +1,39 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Eluceo\iCal\Test\Unit\Presentation\Component\Property\Value; + +use Eluceo\iCal\Presentation\Component\Property\Value\BooleanValue; +use PHPUnit\Framework\TestCase; + +class BooleanValueTest extends TestCase +{ + /** + * @dataProvider provideTestData + */ + public function testStringValueEscaping(bool $inputValue, string $expected) + { + self::assertSame($expected, (string) (new BooleanValue($inputValue))); + } + + public function provideTestData() + { + yield 'Test bool true is same' => [ + true, + 'TRUE', + ]; + + yield 'Test bool false is same' => [ + false, + 'FALSE', + ]; + } +} diff --git a/tests/Unit/Presentation/Factory/EventFactoryTest.php b/tests/Unit/Presentation/Factory/EventFactoryTest.php index 776bcaf7..e322785c 100644 --- a/tests/Unit/Presentation/Factory/EventFactoryTest.php +++ b/tests/Unit/Presentation/Factory/EventFactoryTest.php @@ -13,7 +13,11 @@ use DateTimeImmutable; use DateTimeZone; +use Eluceo\iCal\Domain\Entity\Attendee; use Eluceo\iCal\Domain\Entity\Event; +use Eluceo\iCal\Domain\Enum\CalendarUserType; +use Eluceo\iCal\Domain\Enum\ParticipationStatus; +use Eluceo\iCal\Domain\Enum\RoleType; use Eluceo\iCal\Domain\ValueObject\Attachment; use Eluceo\iCal\Domain\ValueObject\BinaryContent; use Eluceo\iCal\Domain\ValueObject\Date; @@ -21,6 +25,7 @@ use Eluceo\iCal\Domain\ValueObject\EmailAddress; use Eluceo\iCal\Domain\ValueObject\GeographicPosition; use Eluceo\iCal\Domain\ValueObject\Location; +use Eluceo\iCal\Domain\ValueObject\Member; use Eluceo\iCal\Domain\ValueObject\MultiDay; use Eluceo\iCal\Domain\ValueObject\Organizer; use Eluceo\iCal\Domain\ValueObject\SingleDay; @@ -32,7 +37,7 @@ use Eluceo\iCal\Presentation\Factory\EventFactory; use PHPUnit\Framework\TestCase; -class CalendarFactoryTest extends TestCase +class EventFactoryTest extends TestCase { public function testMinimalEvent() { @@ -178,6 +183,308 @@ public function testOrganizer() ]); } + public function testOneAttendee() + { + $event = (new Event()) + ->addAttendee(new Attendee( + new EmailAddress('test@example.com') + )); + + self::assertEventRendersCorrect($event, [ + 'ATTENDEE:mailto:test%40example.com', + ]); + } + + public function testMultipleAttendees() + { + $event = (new Event()) + ->addAttendee(new Attendee( + new EmailAddress('test@example.com') + )) + ->addAttendee(new Attendee( + new EmailAddress('test2@example.net') + )); + + self::assertEventRendersCorrect($event, [ + 'ATTENDEE:mailto:test%40example.com', + 'ATTENDEE:mailto:test2%40example.net', + ]); + } + + /* public function testAttendeeWithCN() + { + $event = (new Event()) + ->addAttendee(new Attendee( + new EmailAddress('test@example.com'), + null, + 'Test Display Name', + )); + + self::assertEventRendersCorrect($event, [ + 'ATTENDEE;CN=Test Display Name:mailto:test%40example.com', + ]); + } */ + + public function testAttendeeWithIndividualCUtype() + { + $attendee = new Attendee(new EmailAddress('test@example.com')); + $attendee->setCalendarUserType(CalendarUserType::INDIVIDUAL()); + + $event = (new Event()) + ->addAttendee($attendee); + + self::assertEventRendersCorrect($event, [ + 'ATTENDEE;CUTYPE=INDIVIDUAL:mailto:test%40example.com', + ]); + } + + public function testAttendeeWithGroupCUtype() + { + $attendee = new Attendee(new EmailAddress('test@example.com')); + $attendee->setCalendarUserType(CalendarUserType::GROUP()); + + $event = (new Event()) + ->addAttendee($attendee); + + self::assertEventRendersCorrect($event, [ + 'ATTENDEE;CUTYPE=GROUP:mailto:test%40example.com', + ]); + } + + public function testAttendeeWithResourceCUtype() + { + $attendee = new Attendee(new EmailAddress('test@example.com')); + $attendee->setCalendarUserType(CalendarUserType::RESOURCE()); + + $event = (new Event()) + ->addAttendee($attendee); + + self::assertEventRendersCorrect($event, [ + 'ATTENDEE;CUTYPE=RESOURCE:mailto:test%40example.com', + ]); + } + + public function testAttendeeWithRoomCUtype() + { + $attendee = new Attendee(new EmailAddress('test@example.com')); + $attendee->setCalendarUserType(CalendarUserType::ROOM()); + + $event = (new Event()) + ->addAttendee($attendee); + + self::assertEventRendersCorrect($event, [ + 'ATTENDEE;CUTYPE=ROOM:mailto:test%40example.com', + ]); + } + + public function testAttendeeWithUnknownCUtype() + { + $attendee = new Attendee(new EmailAddress('test@example.com')); + $attendee->setCalendarUserType(CalendarUserType::UNKNOWN()); + + $event = (new Event()) + ->addAttendee($attendee); + self::assertEventRendersCorrect($event, [ + 'ATTENDEE;CUTYPE=UNKNOWN:mailto:test%40example.com', + ]); + } + + public function testAttendeeWithOneMember() + { + $attendee = new Attendee(new EmailAddress('test@example.com')); + $attendee->setCalendarUserType(CalendarUserType::INDIVIDUAL()); + + $event = (new Event()) + ->addAttendee($attendee); + + self::assertEventRendersCorrect($event, [ + 'ATTENDEE;CUTYPE=INDIVIDUAL:mailto:test%40example.com', + ]); + } + + public function testAttendeeWithMultipleMembers() + { + $attendee = new Attendee(new EmailAddress('test@example.com')); + $attendee->setCalendarUserType(CalendarUserType::INDIVIDUAL()) + ->addMember(new Member(new EmailAddress('test@example.com'))) + ->addMember(new Member(new EmailAddress('test@example.net'))); + + $event = (new Event()) + ->addAttendee($attendee); + + self::assertEventRendersCorrect($event, [ + 'ATTENDEE;CUTYPE=INDIVIDUAL;MEMBER="mailto:test%40example.com","mailto:test%', + ' 40example.net":mailto:test%40example.com', + ]); + } + + public function testAttendeeWithChairRole() + { + $attendee = new Attendee(new EmailAddress('test@example.com')); + $attendee->setRole(RoleType::CHAIR()); + + $event = (new Event()) + ->addAttendee($attendee); + + self::assertEventRendersCorrect($event, [ + 'ATTENDEE;ROLE=CHAIR:mailto:test%40example.com', + ]); + } + + public function testAttendeeWithReqParticipantRole() + { + $attendee = new Attendee( + new EmailAddress('test@example.com'), + ); + $attendee->setRole(RoleType::REQ_PARTICIPANT()); + + $event = (new Event()) + ->addAttendee($attendee); + + self::assertEventRendersCorrect($event, [ + 'ATTENDEE;ROLE=REQ-PARTICIPANT:mailto:test%40example.com', + ]); + } + + public function testAttendeeWithParticipationStatusNeedsAction() + { + $attendee = new Attendee( + new EmailAddress('test@example.com'), + ); + + $attendee->setParticipationStatus(ParticipationStatus::NEEDS_ACTION()); + + $event = (new Event()) + ->addAttendee($attendee); + + self::assertEventRendersCorrect($event, [ + 'ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:test%40example.com', + ]); + } + + public function testAttendeeWithRSVP() + { + $attendee = new Attendee( + new EmailAddress('test@example.com'), + ); + + $attendee->setResponseNeededFromAttendee(true); + + $event = (new Event()) + ->addAttendee($attendee); + + self::assertEventRendersCorrect($event, [ + 'ATTENDEE;RSVP=TRUE:mailto:test%40example.com', + ]); + } + + public function testAttendeeWithDelegatedTo() + { + $attendee = new Attendee( + new EmailAddress('jsmith@example.com'), + ); + + $attendee->addDelegatedTo( + new EmailAddress('jdoe@example.com') + )->addDelegatedTo( + new EmailAddress('jqpublic@example.com') + ); + + $event = (new Event()) + ->addAttendee($attendee); + + self::assertEventRendersCorrect($event, [ + 'ATTENDEE;DELEGATED-TO="mailto:jdoe%40example.com","mailto:jqpublic%40exampl', + ' e.com":mailto:jsmith%40example.com', + ]); + } + + public function testAttendeeWithDelegatedFrom() + { + $attendee = new Attendee( + new EmailAddress('jdoe@example.com'), + ); + + $attendee->addDelegatedFrom( + new EmailAddress('jsmith@example.com') + ); + + $event = (new Event()) + ->addAttendee($attendee); + + self::assertEventRendersCorrect($event, [ + 'ATTENDEE;DELEGATED-FROM="mailto:jsmith%40example.com":mailto:jdoe%40example', + ' .com', + ]); + } + + public function testAttendeeWithSentBy() + { + $attendee = new Attendee( + new EmailAddress('jdoe@example.com'), + ); + + $attendee->addSentBy( + new EmailAddress('sray@example.com') + ); + + $event = (new Event()) + ->addAttendee($attendee); + + self::assertEventRendersCorrect($event, [ + 'ATTENDEE;SENT-BY="mailto:sray%40example.com":mailto:jdoe%40example.com', + ]); + } + + public function testAttendeeWithCommonName() + { + $attendee = new Attendee( + new EmailAddress('jdoe@example.com'), + ); + + $attendee->setDisplayName('Test Example'); + + $event = (new Event()) + ->addAttendee($attendee); + + self::assertEventRendersCorrect($event, [ + 'ATTENDEE;CN=Test Example:mailto:jdoe%40example.com', + ]); + } + + public function testAttendeeWithDirectoryEntryRef() + { + $attendee = new Attendee( + new EmailAddress('jdoe@example.com'), + ); + + $attendee->setDirectoryEntryReference(new Uri('ldap://example.com:6666/o=ABC%20Industries,c=US???(cn=Jim%20Dolittle)')); + + $event = (new Event()) + ->addAttendee($attendee); + + self::assertEventRendersCorrect($event, [ + 'ATTENDEE;DIR="ldap://example.com:6666/o=ABC%20Industries,c=US???(cn=Jim%20D', + ' olittle)":mailto:jdoe%40example.com', + ]); + } + + public function testAttendeeWithLanguage() + { + $attendee = new Attendee( + new EmailAddress('jdoe@example.com'), + ); + + $attendee->setLanguage('en-US'); + + $event = (new Event()) + ->addAttendee($attendee); + + self::assertEventRendersCorrect($event, [ + 'ATTENDEE;LANGUAGE=en-US:mailto:jdoe%40example.com', + ]); + } + public function testEventUrl() { $event = (new Event()) diff --git a/website/docs/component-event.md b/website/docs/component-event.md index 459fa74b..08a7e0c0 100644 --- a/website/docs/component-event.md +++ b/website/docs/component-event.md @@ -41,6 +41,7 @@ The following sections explain the properties of the domain object: - [Location](#location) - [Organizer](#organizer) - [Attachments](#attachments) +- [Attendee](#attendee) ### Unique Identifier @@ -273,3 +274,46 @@ $event = new Event(); $event->addAttachment($urlAttachment); $event->addAttachment($binaryContentAttachment); ``` + +### Attendee + +This property defines one or more attendee/s related to the event. +Calendar user type, group or list membership, participation role, participation status, RSVP expectation, delegatee, delegator, sent by, common name, or directory entry reference property parameters can be specified on this property. +Therefore are listed all the possible methods that you can call on the attendee + +```php +use Eluceo\iCal\Domain\Entity\Event; +use Eluceo\iCal\Domain\Enum\ParticipationStatus; +use Eluceo\iCal\Domain\Enum\RoleType; +use Eluceo\iCal\Domain\Entity\Attendee; +use Eluceo\iCal\Domain\ValueObject\BinaryContent; +use Eluceo\iCal\Domain\ValueObject\Uri; + +$attendee = new Attendee(new EmailAddress('jdoe@example.com')); +$attendee->setCalendarUserType(CalendarUserType::INDIVIDUAL) + ->addMember(new Member(new EmailAddress('test@example.com'))) + ->setRole(RoleType::CHAIR()) + ->setParticipationStatus( + ParticipationStatus::NEEDS_ACTION() + )->setResponseNeededFromAttendee(true) + ->addDelegatedTo( + new EmailAddress('jdoe@example.com') + )->addDelegatedTo( + new EmailAddress('jqpublic@example.com') + )->addDelegatedFrom( + new EmailAddress('jsmith@example.com') + )->addSentBy( + new EmailAddress('sray@example.com') + ) + ->setDisplayName('Test Example') + ->setDirectoryEntryReference( + new Uri('ldap://example.com:6666/o=ABC%20Industries,c=US???(cn=Jim%20Dolittle)') + )->setLanguage('en-US'); + +$event = (new Event()) + ->addAttendee($attendee); + +$event = new Event(); +$event->addAttachment($urlAttachment); +$event->addAttachment($binaryContentAttachment); +``` diff --git a/website/docs/maturity-matrix.md b/website/docs/maturity-matrix.md index 05550752..73b62dbe 100644 --- a/website/docs/maturity-matrix.md +++ b/website/docs/maturity-matrix.md @@ -56,7 +56,7 @@ See [RFC 5545 section 3.6.1](https://tools.ietf.org/html/rfc5545#section-3.6.1). | dtend | ✔ | | duration | ✖ | | attach | ✔ | -| attendee | ✖ | +| attendee | ✔ | | categories | ✖ | | comment | ✖ | | contact | ✖ |