From a8d9de20da7775bcb2528c639dd8c495f06fd5b3 Mon Sep 17 00:00:00 2001 From: Markus Poerschke Date: Thu, 1 Oct 2020 22:15:31 +0200 Subject: [PATCH] Add alarm component (#182) --- Makefile | 2 + docs/advanced/maturity-matrix.md | 34 ++++- examples/example1.php | 8 + examples/example2.php | 16 +- psalm.xml | 4 - src/Domain/Entity/Event.php | 30 +++- src/Domain/ValueObject/Alarm.php | 74 +++++++++ .../Alarm/AbsoluteDateTimeTrigger.php | 29 ++++ src/Domain/ValueObject/Alarm/Action.php | 16 ++ src/Domain/ValueObject/Alarm/AudioAction.php | 16 ++ .../ValueObject/Alarm/DisplayAction.php | 27 ++++ src/Domain/ValueObject/Alarm/EmailAction.php | 34 +++++ .../ValueObject/Alarm/RelativeTrigger.php | 65 ++++++++ src/Domain/ValueObject/Alarm/Trigger.php | 16 ++ src/Presentation/Calendar.php | 43 ------ src/Presentation/Component.php | 40 ++--- .../Component/Property/Parameter.php | 7 +- .../Property/Value/DurationValue.php | 77 ++++++++++ .../Component/Property/Value/IntegerValue.php | 20 +++ .../Component/Property/Value/TextValue.php | 4 +- src/Presentation/Factory/AlarmFactory.php | 108 ++++++++++++++ src/Presentation/Factory/CalendarFactory.php | 10 +- src/Presentation/Factory/EventFactory.php | 27 +++- tests/Unit/Presentation/CalendarTest.php | 112 -------------- .../Component/Property/ParameterTest.php | 2 +- .../Property/Value/DurationValueTest.php | 56 +++++++ .../Property/Value/IntegerValueTest.php | 24 +++ .../Presentation/Component/PropertyTest.php | 6 +- .../Presentation/Factory/AlarmFactoryTest.php | 141 ++++++++++++++++++ 29 files changed, 832 insertions(+), 216 deletions(-) create mode 100644 src/Domain/ValueObject/Alarm.php create mode 100644 src/Domain/ValueObject/Alarm/AbsoluteDateTimeTrigger.php create mode 100644 src/Domain/ValueObject/Alarm/Action.php create mode 100644 src/Domain/ValueObject/Alarm/AudioAction.php create mode 100644 src/Domain/ValueObject/Alarm/DisplayAction.php create mode 100644 src/Domain/ValueObject/Alarm/EmailAction.php create mode 100644 src/Domain/ValueObject/Alarm/RelativeTrigger.php create mode 100644 src/Domain/ValueObject/Alarm/Trigger.php delete mode 100644 src/Presentation/Calendar.php create mode 100644 src/Presentation/Component/Property/Value/DurationValue.php create mode 100644 src/Presentation/Component/Property/Value/IntegerValue.php create mode 100644 src/Presentation/Factory/AlarmFactory.php delete mode 100644 tests/Unit/Presentation/CalendarTest.php create mode 100644 tests/Unit/Presentation/Component/Property/Value/DurationValueTest.php create mode 100644 tests/Unit/Presentation/Component/Property/Value/IntegerValueTest.php create mode 100644 tests/Unit/Presentation/Factory/AlarmFactoryTest.php diff --git a/Makefile b/Makefile index 4c8f27c2..574bbaaf 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,8 @@ MAKEFLAGS += --warn-undefined-variables SHELL := bash PATH := $(PATH):$(CURDIR)/vendor/bin +PSALM_FLAGS := +PHPUNIT_FLAGS := .PHONY: test test: test-validate-composer test-code-style test-psalm test-phpunit test-examples test-composer-normalize test-phpmd diff --git a/docs/advanced/maturity-matrix.md b/docs/advanced/maturity-matrix.md index abc29c7e..13412dd3 100644 --- a/docs/advanced/maturity-matrix.md +++ b/docs/advanced/maturity-matrix.md @@ -29,7 +29,7 @@ See [RFC 5545 section 3.6](https://tools.ietf.org/html/rfc5545#section-3.6). | VJOURNAL | ✖ | | VFREEBUSY | ✖ | | VTIMEZONE | ✖ | -| VALARM | ✖ | +| VALARM | ✔ | ## Event Component @@ -69,3 +69,35 @@ See [RFC 5545 section 3.6.1](https://tools.ietf.org/html/rfc5545#section-3.6.1). | rdate | ✖ | | x-prop | (✔) | | iana-prop | (✔) | + +## Alarm Component + +See [RFC 5545 section 3.6.1](https://tools.ietf.org/html/rfc5545#section-3.6.6). + +### Audio + +| Property | Supported | +| --------- | :-------: | +| action | ✔ | +| trigger | ✔ | +| duration | ✔ | +| repeat | ✔ | +| attach | ✖ | +| x-prop | (✔) | +| iana-prop | (✔) | + +### Display + +| Property | Supported | +| ----------- | :-------: | +| action | ✔ | +| trigger | ✔ | +| description | ✔ | +| duration | ✔ | +| repeat | ✔ | +| x-prop | (✔) | +| iana-prop | (✔) | + +## Email + +Not yet supported. diff --git a/examples/example1.php b/examples/example1.php index caa07486..53286f0d 100644 --- a/examples/example1.php +++ b/examples/example1.php @@ -11,9 +11,11 @@ namespace Example; +use DateInterval; use DateTimeImmutable; use Eluceo\iCal\Domain\Entity\Calendar; use Eluceo\iCal\Domain\Entity\Event; +use Eluceo\iCal\Domain\ValueObject\Alarm; use Eluceo\iCal\Domain\ValueObject\DateTime; use Eluceo\iCal\Domain\ValueObject\TimeSpan; use Eluceo\iCal\Presentation\Factory\CalendarFactory; @@ -31,6 +33,12 @@ new DateTime(DateTimeImmutable::createFromFormat('Y-m-d H:i:s', '2030-12-24 14:30:00')) ) ) + ->addAlarm( + new Alarm( + new Alarm\DisplayAction('Reminder: the meeting starts in 15 minutes!'), + (new Alarm\RelativeTrigger(DateInterval::createFromDateString('-15 minutes')))->withRelationToEnd() + ) + ) ; // 2. Create Calendar domain entity. diff --git a/examples/example2.php b/examples/example2.php index a1cfc324..1147f0ce 100644 --- a/examples/example2.php +++ b/examples/example2.php @@ -18,6 +18,7 @@ use Eluceo\iCal\Presentation\Component\Property\Value\TextValue; use Eluceo\iCal\Presentation\Factory\CalendarFactory; use Eluceo\iCal\Presentation\Factory\EventFactory; +use Generator; require_once __DIR__ . '/../vendor/autoload.php'; @@ -36,20 +37,15 @@ public function getMyCustomProperty(): string // that can add the additional property to the presentation component. class CustomEventFactory extends EventFactory { - public function createComponent(Event $event): Component + protected function getProperties(Event $event): Generator { - $component = parent::createComponent($event); - + yield from parent::getProperties($event); if ($event instanceof CustomEvent) { - $component = $component->withProperty( - new Property( - 'X-CUSTOM', - new TextValue($event->getMyCustomProperty()) - ) + yield new Property( + 'X-CUSTOM', + new TextValue($event->getMyCustomProperty()) ); } - - return $component; } } diff --git a/psalm.xml b/psalm.xml index 408036f8..75bbb1e3 100644 --- a/psalm.xml +++ b/psalm.xml @@ -12,8 +12,4 @@ - - - - diff --git a/src/Domain/Entity/Event.php b/src/Domain/Entity/Event.php index 9049be4e..b3c44b0f 100644 --- a/src/Domain/Entity/Event.php +++ b/src/Domain/Entity/Event.php @@ -11,6 +11,7 @@ namespace Eluceo\iCal\Domain\Entity; +use Eluceo\iCal\Domain\ValueObject\Alarm; use Eluceo\iCal\Domain\ValueObject\Location; use Eluceo\iCal\Domain\ValueObject\Occurrence; use Eluceo\iCal\Domain\ValueObject\Timestamp; @@ -25,6 +26,11 @@ class Event private ?Occurrence $occurrence = null; private ?Location $location = null; + /** + * @var array + */ + private array $alarms = []; + public function __construct(?UniqueIdentifier $uniqueIdentifier = null) { $this->uniqueIdentifier = $uniqueIdentifier ?? UniqueIdentifier::createRandom(); @@ -48,12 +54,10 @@ public function touch(?Timestamp $dateTime = null): self return $this; } - /** - * @psalm-suppress InvalidNullableReturnType - * @psalm-suppress NullableReturnStatement - */ public function getSummary(): string { + assert($this->summary !== null); + return $this->summary; } @@ -76,12 +80,10 @@ public function unsetSummary(): self return $this; } - /** - * @psalm-suppress InvalidNullableReturnType - * @psalm-suppress NullableReturnStatement - */ public function getDescription(): string { + assert($this->description !== null); + return $this->description; } @@ -145,4 +147,16 @@ public function hasLocation(): bool { return $this->location !== null; } + + public function getAlarms(): array + { + return $this->alarms; + } + + public function addAlarm(Alarm $alarm): self + { + $this->alarms[] = $alarm; + + return $this; + } } diff --git a/src/Domain/ValueObject/Alarm.php b/src/Domain/ValueObject/Alarm.php new file mode 100644 index 00000000..a3769c2a --- /dev/null +++ b/src/Domain/ValueObject/Alarm.php @@ -0,0 +1,74 @@ + + * + * 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 DateInterval; +use Eluceo\iCal\Domain\ValueObject\Alarm\Action; +use Eluceo\iCal\Domain\ValueObject\Alarm\Trigger; + +class Alarm +{ + private Action $action; + private Trigger $trigger; + + private int $repeatCount = 0; + private ?DateInterval $repeatInterval = null; + + public function __construct(Action $action, Trigger $trigger) + { + $this->action = $action; + $this->trigger = $trigger; + } + + public function getAction(): Action + { + return $this->action; + } + + public function getTrigger(): Trigger + { + return $this->trigger; + } + + public function isRepeated(): bool + { + return $this->repeatCount > 0; + } + + public function withRepeat(int $repeatCount, DateInterval $repeatInterval): self + { + $this->repeatCount = $repeatCount; + $this->repeatInterval = $repeatInterval; + + return $this; + } + + public function withoutRepeat(): self + { + $this->repeatCount = 0; + $this->repeatInterval = null; + + return $this; + } + + public function getRepeatCount(): int + { + return $this->repeatCount; + } + + public function getRepeatInterval(): DateInterval + { + assert($this->repeatInterval !== null); + + return $this->repeatInterval; + } +} diff --git a/src/Domain/ValueObject/Alarm/AbsoluteDateTimeTrigger.php b/src/Domain/ValueObject/Alarm/AbsoluteDateTimeTrigger.php new file mode 100644 index 00000000..afa5c28a --- /dev/null +++ b/src/Domain/ValueObject/Alarm/AbsoluteDateTimeTrigger.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\Alarm; + +use Eluceo\iCal\Domain\ValueObject\Timestamp; + +final class AbsoluteDateTimeTrigger extends Trigger +{ + private Timestamp $dateTime; + + public function __construct(Timestamp $dateTime) + { + $this->dateTime = $dateTime; + } + + public function getDateTime(): Timestamp + { + return $this->dateTime; + } +} diff --git a/src/Domain/ValueObject/Alarm/Action.php b/src/Domain/ValueObject/Alarm/Action.php new file mode 100644 index 00000000..a91c8d5d --- /dev/null +++ b/src/Domain/ValueObject/Alarm/Action.php @@ -0,0 +1,16 @@ + + * + * 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\Alarm; + +abstract class Action +{ +} diff --git a/src/Domain/ValueObject/Alarm/AudioAction.php b/src/Domain/ValueObject/Alarm/AudioAction.php new file mode 100644 index 00000000..9a2d0598 --- /dev/null +++ b/src/Domain/ValueObject/Alarm/AudioAction.php @@ -0,0 +1,16 @@ + + * + * 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\Alarm; + +final class AudioAction extends Action +{ +} diff --git a/src/Domain/ValueObject/Alarm/DisplayAction.php b/src/Domain/ValueObject/Alarm/DisplayAction.php new file mode 100644 index 00000000..bbe54745 --- /dev/null +++ b/src/Domain/ValueObject/Alarm/DisplayAction.php @@ -0,0 +1,27 @@ + + * + * 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\Alarm; + +final class DisplayAction extends Action +{ + private string $description; + + public function __construct(string $description) + { + $this->description = $description; + } + + public function getDescription(): string + { + return $this->description; + } +} diff --git a/src/Domain/ValueObject/Alarm/EmailAction.php b/src/Domain/ValueObject/Alarm/EmailAction.php new file mode 100644 index 00000000..c44d242d --- /dev/null +++ b/src/Domain/ValueObject/Alarm/EmailAction.php @@ -0,0 +1,34 @@ + + * + * 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\Alarm; + +final class EmailAction extends Action +{ + private string $summary; + private string $description; + + public function __construct(string $summary, string $description) + { + $this->summary = $summary; + $this->description = $description; + } + + public function getDescription(): string + { + return $this->description; + } + + public function getSummary(): string + { + return $this->summary; + } +} diff --git a/src/Domain/ValueObject/Alarm/RelativeTrigger.php b/src/Domain/ValueObject/Alarm/RelativeTrigger.php new file mode 100644 index 00000000..18e3ccf6 --- /dev/null +++ b/src/Domain/ValueObject/Alarm/RelativeTrigger.php @@ -0,0 +1,65 @@ + + * + * 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\Alarm; + +use DateInterval; + +final class RelativeTrigger extends Trigger +{ + /** + * The trigger is either related to the start or end of an event. + * + * If the value is true, then the trigger is related to the start, + * which is the default value. + * + * If the value is false, then the trigger is related to the end. + */ + private bool $relatedToStart = true; + + private DateInterval $duration; + + public function __construct(DateInterval $duration) + { + $this->duration = $duration; + } + + public function isRelatedToStart(): bool + { + return $this->relatedToStart; + } + + public function isRelatedToEnd(): bool + { + return !$this->relatedToStart; + } + + public function getDuration(): DateInterval + { + return $this->duration; + } + + public function withRelationToEnd(): self + { + $new = clone $this; + $new->relatedToStart = false; + + return $new; + } + + public function withRelationToStart(): self + { + $new = clone $this; + $new->relatedToStart = true; + + return $new; + } +} diff --git a/src/Domain/ValueObject/Alarm/Trigger.php b/src/Domain/ValueObject/Alarm/Trigger.php new file mode 100644 index 00000000..625d43f2 --- /dev/null +++ b/src/Domain/ValueObject/Alarm/Trigger.php @@ -0,0 +1,16 @@ + + * + * 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\Alarm; + +abstract class Trigger +{ +} diff --git a/src/Presentation/Calendar.php b/src/Presentation/Calendar.php deleted file mode 100644 index a8108070..00000000 --- a/src/Presentation/Calendar.php +++ /dev/null @@ -1,43 +0,0 @@ - - * - * This source file is subject to the MIT license that is bundled - * with this source code in the file LICENSE. - */ - -namespace Eluceo\iCal\Presentation; - -use Eluceo\iCal\Presentation\Component\Property; -use Generator; - -class Calendar extends Component -{ - /** - * @var iterable - */ - private iterable $components = []; - - /** - * @param iterable $components - * @param Property[] $properties - */ - public static function createCalendar(iterable $components = [], array $properties = []): self - { - $new = new self('VCALENDAR', $properties); - $new->components = $components; - - return $new; - } - - protected function getContentLinesGenerator(): Generator - { - yield from parent::getContentLinesGenerator(); - foreach ($this->components as $component) { - yield from $component->getContentLines(); - } - } -} diff --git a/src/Presentation/Component.php b/src/Presentation/Component.php index 7404b16b..ad230d5b 100644 --- a/src/Presentation/Component.php +++ b/src/Presentation/Component.php @@ -17,27 +17,33 @@ class Component implements IteratorAggregate { + private string $componentName; + /** - * @var array + * @var Property[] */ private array $properties = []; - private string $componentName; /** - * @param Property[] $properties + * @var iterable */ - public function __construct(string $componentName, array $properties = []) + private iterable $components = []; + + /** + * @param Property[] $properties + * @param iterable $components + */ + public function __construct(string $componentName, array $properties = [], iterable $components = []) { - $this->componentName = strtoupper($componentName); - foreach ($properties as $property) { - $this->addProperty($property); - } + $this->componentName = $componentName; + $this->properties = $properties; + $this->components = $components; } public function withProperty(Property $property): self { $new = clone $this; - $new->addProperty($property); + $new->properties[] = $property; return $new; } @@ -64,16 +70,12 @@ protected function getContentLines(): Generator protected function getContentLinesGenerator(): Generator { - yield from array_map( - fn (string $string) => new ContentLine($string), - array_map('strval', $this->properties) - ); - } - - private function addProperty(Property $property): self - { - $this->properties[] = $property; + foreach ($this->properties as $property) { + yield new ContentLine((string) $property); + } - return $this; + foreach ($this->components as $component) { + yield from $component->getContentLines(); + } } } diff --git a/src/Presentation/Component/Property/Parameter.php b/src/Presentation/Component/Property/Parameter.php index 6db69596..d11f1ef5 100644 --- a/src/Presentation/Component/Property/Parameter.php +++ b/src/Presentation/Component/Property/Parameter.php @@ -16,17 +16,12 @@ final class Parameter private string $name; private Value $value; - private function __construct(string $name, Value $value) + public function __construct(string $name, Value $value) { $this->name = strtoupper($name); $this->value = $value; } - public static function create(string $name, Value $value): self - { - return new static($name, $value); - } - public function __toString(): string { return $this->name . '=' . $this->value; diff --git a/src/Presentation/Component/Property/Value/DurationValue.php b/src/Presentation/Component/Property/Value/DurationValue.php new file mode 100644 index 00000000..cb53e6d7 --- /dev/null +++ b/src/Presentation/Component/Property/Value/DurationValue.php @@ -0,0 +1,77 @@ + + * + * 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 DateInterval; +use DateTimeImmutable; +use Eluceo\iCal\Presentation\Component\Property\Value; + +final class DurationValue extends Value +{ + private DateInterval $duration; + + public function __construct(DateInterval $duration) + { + $this->duration = $duration; + } + + public function __toString(): string + { + $duration = $this->getNormalizedDateInterval(); + $durationAsString = $duration->invert === 1 ? '-P' : 'P'; + + $days = abs($duration->days); + if ($days > 0) { + $durationAsString .= $days . 'D'; + } + + $hours = abs($duration->h); + $minutes = abs($duration->i); + $seconds = abs($duration->s); + if ($hours > 0 || $minutes > 0 || $seconds > 0) { + $durationAsString .= 'T'; + + if ($hours > 0) { + $durationAsString .= $hours . 'H'; + } + + if ($minutes > 0) { + $durationAsString .= $minutes . 'M'; + } + + if ($seconds > 0) { + $durationAsString .= $seconds . 'S'; + } + } + + return $durationAsString; + } + + /** + * Normalizes the date interval. + * + * If the date interval is created from string, + * then interval and days property are empty. + * + * Only date intervals that are created as a result from a diff + * of two dates contains the correct values. + * + * @see https://www.php.net/manual/de/class.dateinterval.php + */ + private function getNormalizedDateInterval(): DateInterval + { + $baseDate = (new DateTimeImmutable())->setTimestamp(0); + $nextDate = $baseDate->sub($this->duration); + + return $nextDate->diff($baseDate); + } +} diff --git a/src/Presentation/Component/Property/Value/IntegerValue.php b/src/Presentation/Component/Property/Value/IntegerValue.php new file mode 100644 index 00000000..a9d7e834 --- /dev/null +++ b/src/Presentation/Component/Property/Value/IntegerValue.php @@ -0,0 +1,20 @@ + + * + * 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; + +class IntegerValue extends TextValue +{ + public function __construct(int $value) + { + parent::__construct((string) $value); + } +} diff --git a/src/Presentation/Component/Property/Value/TextValue.php b/src/Presentation/Component/Property/Value/TextValue.php index f02ff14d..62082927 100644 --- a/src/Presentation/Component/Property/Value/TextValue.php +++ b/src/Presentation/Component/Property/Value/TextValue.php @@ -16,7 +16,7 @@ /** * @see https://tools.ietf.org/html/rfc5545#section-3.3.11 */ -final class TextValue extends Value +class TextValue extends Value { /** * ESCAPED-CHAR as defined in section 3.3.11. @@ -78,7 +78,7 @@ public function __toString(): string $value = $this->value; $value = str_replace(array_keys(self::ESCAPED_CHARACTERS), array_values(self::ESCAPED_CHARACTERS), $value); - $value = str_replace(static::FORBIDDEN_CHARACTERS, '', $value); + $value = str_replace(self::FORBIDDEN_CHARACTERS, '', $value); return $value; } diff --git a/src/Presentation/Factory/AlarmFactory.php b/src/Presentation/Factory/AlarmFactory.php new file mode 100644 index 00000000..58ffd3b4 --- /dev/null +++ b/src/Presentation/Factory/AlarmFactory.php @@ -0,0 +1,108 @@ + + * + * 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\ValueObject\Alarm; +use Eluceo\iCal\Domain\ValueObject\Alarm\AbsoluteDateTimeTrigger; +use Eluceo\iCal\Domain\ValueObject\Alarm\Action; +use Eluceo\iCal\Domain\ValueObject\Alarm\AudioAction; +use Eluceo\iCal\Domain\ValueObject\Alarm\DisplayAction; +use Eluceo\iCal\Domain\ValueObject\Alarm\EmailAction; +use Eluceo\iCal\Domain\ValueObject\Alarm\RelativeTrigger; +use Eluceo\iCal\Domain\ValueObject\Alarm\Trigger; +use Eluceo\iCal\Presentation\Component; +use Eluceo\iCal\Presentation\Component\Property; +use Eluceo\iCal\Presentation\Component\Property\Parameter; +use Eluceo\iCal\Presentation\Component\Property\Value\DateTimeValue; +use Eluceo\iCal\Presentation\Component\Property\Value\DurationValue; +use Eluceo\iCal\Presentation\Component\Property\Value\IntegerValue; +use Eluceo\iCal\Presentation\Component\Property\Value\TextValue; +use Generator; + +/** + * @SuppressWarnings("CouplingBetweenObjects") + */ +class AlarmFactory +{ + public function createComponent(Alarm $alarm): Component + { + return new Component('VALARM', iterator_to_array($this->getProperties($alarm), false)); + } + + /** + * @return Generator + */ + private function getProperties(Alarm $alarm): Generator + { + yield from $this->getActionProperties($alarm->getAction()); + yield from $this->getTriggerProperties($alarm->getTrigger()); + yield from $this->getRepeatProperties($alarm); + } + + /** + * @return Generator + */ + private function getRepeatProperties(Alarm $alarm): Generator + { + if (!$alarm->isRepeated()) { + return; + } + + yield new Property('REPEAT', new IntegerValue($alarm->getRepeatCount())); + yield new Property('DURATION', new DurationValue($alarm->getRepeatInterval())); + } + + /** + * @return Generator + */ + private function getTriggerProperties(Trigger $trigger): Generator + { + if ($trigger instanceof AbsoluteDateTimeTrigger) { + yield new Property( + 'TRIGGER', + new DateTimeValue($trigger->getDateTime()), + [ + new Parameter('VALUE', new TextValue('DATE-TIME')), + ] + ); + } + + if ($trigger instanceof RelativeTrigger) { + yield new Property( + 'TRIGGER', + new DurationValue($trigger->getDuration()), + $trigger->isRelatedToEnd() ? [new Parameter('RELATED', new TextValue('END'))] : [] + ); + } + } + + /** + * @return Generator + */ + private function getActionProperties(Action $action): Generator + { + if ($action instanceof AudioAction) { + yield new Property('ACTION', new TextValue('AUDIO')); + } + + if ($action instanceof EmailAction) { + yield new Property('ACTION', new TextValue('EMAIL')); + yield new Property('SUMMARY', new TextValue($action->getSummary())); + yield new Property('DESCRIPTION', new TextValue($action->getDescription())); + } + + if ($action instanceof DisplayAction) { + yield new Property('ACTION', new TextValue('DISPLAY')); + yield new Property('DESCRIPTION', new TextValue($action->getDescription())); + } + } +} diff --git a/src/Presentation/Factory/CalendarFactory.php b/src/Presentation/Factory/CalendarFactory.php index 28d3f1ad..60025ace 100644 --- a/src/Presentation/Factory/CalendarFactory.php +++ b/src/Presentation/Factory/CalendarFactory.php @@ -11,8 +11,8 @@ namespace Eluceo\iCal\Presentation\Factory; -use Eluceo\iCal\Domain\Entity\Calendar as CalendarEntity; -use Eluceo\iCal\Presentation\Calendar; +use Eluceo\iCal\Domain\Entity\Calendar; +use Eluceo\iCal\Presentation\Component; use Eluceo\iCal\Presentation\Component\Property; use Eluceo\iCal\Presentation\Component\Property\Value\TextValue; use Generator; @@ -26,18 +26,18 @@ public function __construct(?EventFactory $eventFactory = null) $this->eventFactory = $eventFactory ?? new EventFactory(); } - public function createCalendar(CalendarEntity $calendar): Calendar + public function createCalendar(Calendar $calendar): Component { $components = $this->eventFactory->createComponents($calendar->getEvents()); $properties = iterator_to_array($this->getProperties($calendar), false); - return Calendar::createCalendar($components, $properties); + return new Component('VCALENDAR', $properties, $components); } /** * @return Generator */ - private function getProperties(CalendarEntity $calendar): Generator + private function getProperties(Calendar $calendar): Generator { /* @see https://www.ietf.org/rfc/rfc5545.html#section-3.7.3 */ yield new Property('PRODID', new TextValue($calendar->getProductIdentifier())); diff --git a/src/Presentation/Factory/EventFactory.php b/src/Presentation/Factory/EventFactory.php index 3df71299..0f2138a7 100644 --- a/src/Presentation/Factory/EventFactory.php +++ b/src/Presentation/Factory/EventFactory.php @@ -14,6 +14,7 @@ use DateInterval; use Eluceo\iCal\Domain\Collection\Events; use Eluceo\iCal\Domain\Entity\Event; +use Eluceo\iCal\Domain\ValueObject\Alarm; use Eluceo\iCal\Domain\ValueObject\MultiDay; use Eluceo\iCal\Domain\ValueObject\Occurrence; use Eluceo\iCal\Domain\ValueObject\SingleDay; @@ -31,6 +32,13 @@ */ class EventFactory { + private AlarmFactory $alarmFactory; + + public function __construct(?AlarmFactory $alarmFactory = null) + { + $this->alarmFactory = $alarmFactory ?? new AlarmFactory(); + } + /** * @return Generator */ @@ -43,13 +51,17 @@ final public function createComponents(Events $events): Generator public function createComponent(Event $event): Component { - return new Component('VEVENT', iterator_to_array($this->getProperties($event), false)); + return new Component( + 'VEVENT', + iterator_to_array($this->getProperties($event), false), + iterator_to_array($this->getComponents($event), false) + ); } /** * @return Generator */ - private function getProperties(Event $event): Generator + protected function getProperties(Event $event): Generator { yield new Property('UID', new TextValue((string) $event->getUniqueIdentifier())); yield new Property('DTSTAMP', new DateTimeValue($event->getTouchedAt())); @@ -71,6 +83,17 @@ private function getProperties(Event $event): Generator } } + /** + * @return Generator + */ + protected function getComponents(Event $event): Generator + { + yield from array_map( + fn (Alarm $alarm) => $this->alarmFactory->createComponent($alarm), + $event->getAlarms() + ); + } + /** * @return Generator */ diff --git a/tests/Unit/Presentation/CalendarTest.php b/tests/Unit/Presentation/CalendarTest.php deleted file mode 100644 index 3e0f9e1c..00000000 --- a/tests/Unit/Presentation/CalendarTest.php +++ /dev/null @@ -1,112 +0,0 @@ - - * - * 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; - -use Eluceo\iCal\Presentation\Calendar; -use Eluceo\iCal\Presentation\Component; -use Eluceo\iCal\Presentation\Component\Property; -use Eluceo\iCal\Presentation\Component\Property\Value\TextValue; -use Eluceo\iCal\Presentation\ContentLine; -use PHPUnit\Framework\TestCase; - -class CalendarTest extends TestCase -{ - public function testEmptyCalendarToString() - { - $expected = implode(ContentLine::LINE_SEPARATOR, [ - 'BEGIN:VCALENDAR', - 'END:VCALENDAR', - '', - ]); - - self::assertSame($expected, (string) Calendar::createCalendar()); - } - - public function testWithSingleComponentToString() - { - $expected = implode(ContentLine::LINE_SEPARATOR, [ - 'BEGIN:VCALENDAR', - 'BEGIN:VEVENT', - 'END:VEVENT', - 'END:VCALENDAR', - '', - ]); - - $components = [ - new Component('VEVENT'), - ]; - - $calendar = Calendar::createCalendar($components); - - self::assertSame($expected, (string) $calendar); - } - - public function testWithMultipleComponentsToString() - { - $expected = implode(ContentLine::LINE_SEPARATOR, [ - 'BEGIN:VCALENDAR', - 'BEGIN:VEVENT', - 'UID:event1', - 'END:VEVENT', - 'BEGIN:VEVENT', - 'UID:event2', - 'END:VEVENT', - 'END:VCALENDAR', - '', - ]); - - $components = [ - new Component( - 'VEVENT', - [ - new Property('UID', new TextValue('event1')), - ] - ), - new Component( - 'VEVENT', - [ - new Property('UID', new TextValue('event2')), - ] - ), - ]; - - $calendar = Calendar::createCalendar($components); - - self::assertSame($expected, (string) $calendar); - } - - public function testRenderOwnPropertiesBeforeComponents() - { - $expected = implode(ContentLine::LINE_SEPARATOR, [ - 'BEGIN:VCALENDAR', - 'TEST:value', - 'TEST2:value2', - 'BEGIN:VEVENT', - 'END:VEVENT', - 'END:VCALENDAR', - '', - ]); - - $properties = [ - new Property('TEST', new TextValue('value')), - new Property('TEST2', new TextValue('value2')), - ]; - - $components = [ - new Component('VEVENT'), - ]; - - $calendar = Calendar::createCalendar($components, $properties); - - self::assertSame($expected, (string) $calendar); - } -} diff --git a/tests/Unit/Presentation/Component/Property/ParameterTest.php b/tests/Unit/Presentation/Component/Property/ParameterTest.php index 8e92635e..ad38711d 100644 --- a/tests/Unit/Presentation/Component/Property/ParameterTest.php +++ b/tests/Unit/Presentation/Component/Property/ParameterTest.php @@ -19,7 +19,7 @@ class ParameterTest extends TestCase { public function testParameterToString() { - $parameter = Parameter::create('TEST', new TextValue('lorem ipsum')); + $parameter = new Parameter('TEST', new TextValue('lorem ipsum')); self::assertSame('TEST=lorem ipsum', (string) $parameter); } } diff --git a/tests/Unit/Presentation/Component/Property/Value/DurationValueTest.php b/tests/Unit/Presentation/Component/Property/Value/DurationValueTest.php new file mode 100644 index 00000000..5f92fa63 --- /dev/null +++ b/tests/Unit/Presentation/Component/Property/Value/DurationValueTest.php @@ -0,0 +1,56 @@ + + * + * 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 DateInterval; +use Eluceo\iCal\Presentation\Component\Property\Value\DurationValue; +use PHPUnit\Framework\TestCase; + +class DurationValueTest extends TestCase +{ + /** + * @dataProvider provideTestData + */ + public function testDurationToString(DateInterval $duration, string $expected) + { + $actual = (new DurationValue($duration))->__toString(); + self::assertSame($expected, $actual); + } + + public function provideTestData() + { + yield '30 days' => [ + new DateInterval('P30D'), + 'P30D', + ]; + + yield '-30 days' => [ + DateInterval::createFromDateString('-30 days'), + '-P30D', + ]; + + yield 'time based' => [ + new DateInterval('PT10H20M30S'), + 'PT10H20M30S', + ]; + + yield '-15 minutes' => [ + DateInterval::createFromDateString('-15 minutes'), + '-PT15M', + ]; + + yield 'days and time' => [ + new DateInterval('P1MT10H'), + 'P31DT10H', + ]; + } +} diff --git a/tests/Unit/Presentation/Component/Property/Value/IntegerValueTest.php b/tests/Unit/Presentation/Component/Property/Value/IntegerValueTest.php new file mode 100644 index 00000000..6b2bb490 --- /dev/null +++ b/tests/Unit/Presentation/Component/Property/Value/IntegerValueTest.php @@ -0,0 +1,24 @@ + + * + * 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\IntegerValue; +use PHPUnit\Framework\TestCase; + +class IntegerValueTest extends TestCase +{ + public function testIntegerIsRendered() + { + $actual = (new IntegerValue(123))->__toString(); + self::assertSame('123', $actual); + } +} diff --git a/tests/Unit/Presentation/Component/PropertyTest.php b/tests/Unit/Presentation/Component/PropertyTest.php index f17208a2..b987c9c2 100644 --- a/tests/Unit/Presentation/Component/PropertyTest.php +++ b/tests/Unit/Presentation/Component/PropertyTest.php @@ -40,7 +40,7 @@ public function provideTestData() 'LOREM', new TextValue('Ipsum'), [ - Parameter::create('TEST', new TextValue('value')), + new Parameter('TEST', new TextValue('value')), ], 'LOREM:TEST=value:Ipsum', ]; @@ -49,8 +49,8 @@ public function provideTestData() 'LOREM', new TextValue('Ipsum'), [ - Parameter::create('TEST', new TextValue('value')), - Parameter::create('TEST2', new TextValue('value2')), + new Parameter('TEST', new TextValue('value')), + new Parameter('TEST2', new TextValue('value2')), ], 'LOREM:TEST=value;TEST2=value2:Ipsum', ]; diff --git a/tests/Unit/Presentation/Factory/AlarmFactoryTest.php b/tests/Unit/Presentation/Factory/AlarmFactoryTest.php new file mode 100644 index 00000000..c29b87a3 --- /dev/null +++ b/tests/Unit/Presentation/Factory/AlarmFactoryTest.php @@ -0,0 +1,141 @@ + + * + * 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\Factory; + +use DateInterval; +use DateTimeImmutable as PhpDateTimeImmutable; +use Eluceo\iCal\Domain\ValueObject\Alarm; +use Eluceo\iCal\Domain\ValueObject\Alarm\AbsoluteDateTimeTrigger; +use Eluceo\iCal\Domain\ValueObject\Alarm\AudioAction; +use Eluceo\iCal\Domain\ValueObject\Alarm\EmailAction; +use Eluceo\iCal\Domain\ValueObject\DateTime; +use Eluceo\iCal\Presentation\ContentLine; +use Eluceo\iCal\Presentation\Factory\AlarmFactory; +use PHPUnit\Framework\TestCase; + +class AlarmFactoryTest extends TestCase +{ + public function testAudioAlarm() + { + $alarm = new Alarm( + new AudioAction(), + new AbsoluteDateTimeTrigger(new DateTime(new PhpDateTimeImmutable('2020-09-30 00:00:00'))) + ); + + $expected = implode(ContentLine::LINE_SEPARATOR, [ + 'BEGIN:VALARM', + 'ACTION:AUDIO', + 'TRIGGER:VALUE=DATE-TIME:20200930T000000', + 'END:VALARM', + ]); + + $actual = (string) (new AlarmFactory())->createComponent($alarm); + + self::assertSame( + $expected, + trim($actual) + ); + } + + public function testEmailAlarm() + { + $alarm = new Alarm( + new EmailAction('Summary Text', 'Description Text'), + new AbsoluteDateTimeTrigger(new DateTime(new PhpDateTimeImmutable('2020-09-30 00:00:00'))) + ); + + $expected = implode(ContentLine::LINE_SEPARATOR, [ + 'BEGIN:VALARM', + 'ACTION:EMAIL', + 'SUMMARY:Summary Text', + 'DESCRIPTION:Description Text', + 'TRIGGER:VALUE=DATE-TIME:20200930T000000', + 'END:VALARM', + ]); + + $actual = (string) (new AlarmFactory())->createComponent($alarm); + + self::assertSame( + $expected, + trim($actual) + ); + } + + public function testDisplayAlarm() + { + $alarm = new Alarm( + new Alarm\DisplayAction('Description Text'), + new AbsoluteDateTimeTrigger(new DateTime(new PhpDateTimeImmutable('2020-09-30 00:00:00'))) + ); + + $expected = implode(ContentLine::LINE_SEPARATOR, [ + 'BEGIN:VALARM', + 'ACTION:DISPLAY', + 'DESCRIPTION:Description Text', + 'TRIGGER:VALUE=DATE-TIME:20200930T000000', + 'END:VALARM', + ]); + + $actual = (string) (new AlarmFactory())->createComponent($alarm); + + self::assertSame( + $expected, + trim($actual) + ); + } + + public function testRelativeTrigger() + { + $alarm = new Alarm( + new AudioAction(), + new Alarm\RelativeTrigger(new DateInterval('P1D')) + ); + + $expected = implode(ContentLine::LINE_SEPARATOR, [ + 'BEGIN:VALARM', + 'ACTION:AUDIO', + 'TRIGGER:P1D', + 'END:VALARM', + ]); + + $actual = (string) (new AlarmFactory())->createComponent($alarm); + + self::assertSame( + $expected, + trim($actual) + ); + } + + public function testRepeat() + { + $alarm = (new Alarm( + new AudioAction(), + new AbsoluteDateTimeTrigger(new DateTime(new PhpDateTimeImmutable('2020-09-30 00:00:00'))) + ))->withRepeat(3, new DateInterval('P1D')); + + $expected = implode(ContentLine::LINE_SEPARATOR, [ + 'BEGIN:VALARM', + 'ACTION:AUDIO', + 'TRIGGER:VALUE=DATE-TIME:20200930T000000', + 'REPEAT:3', + 'DURATION:P1D', + 'END:VALARM', + ]); + + $actual = (string) (new AlarmFactory())->createComponent($alarm); + + self::assertSame( + $expected, + trim($actual) + ); + } +}