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)
+ );
+ }
+}