diff --git a/examples/example4.php b/examples/example4.php new file mode 100644 index 00000000..692449af --- /dev/null +++ b/examples/example4.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 Example; + +use DateTimeImmutable; +use Eluceo\iCal\Domain\Entity\Calendar; +use Eluceo\iCal\Domain\Entity\Event; +use Eluceo\iCal\Domain\Entity\RecurrenceRule; +use Eluceo\iCal\Domain\Enum\RecurrenceFrequency; +use Eluceo\iCal\Domain\Enum\RecurrenceWeekDay; +use Eluceo\iCal\Domain\ValueObject\DateTime; +use Eluceo\iCal\Domain\ValueObject\Recurrence\By; +use Eluceo\iCal\Domain\ValueObject\Recurrence\By\Day; +use Eluceo\iCal\Domain\ValueObject\Recurrence\By\SetPosition; +use Eluceo\iCal\Domain\ValueObject\Recurrence\Count; +use Eluceo\iCal\Domain\ValueObject\Recurrence\Exclusion; +use Eluceo\iCal\Domain\ValueObject\Recurrence\Frequency; +use Eluceo\iCal\Domain\ValueObject\Recurrence\Until; +use Eluceo\iCal\Domain\ValueObject\TimeSpan; +use Eluceo\iCal\Presentation\Factory\CalendarFactory; + +require_once __DIR__ . '/../vendor/autoload.php'; + +// 1. Create the recurrence rule. +$rrule = new RecurrenceRule(); +$rrule + ->setFrequency(new Frequency(RecurrenceFrequency::monthly())) + ->setCount(new Count(3)) + ->setBy(new By([ + new Day([ + RecurrenceWeekDay::tuesday(), + RecurrenceWeekDay::wednesday(), + RecurrenceWeekDay::thursday() + ]), + new SetPosition([3]) + ])) + ->setExclusions(new Exclusion([ + new DateTime(new DateTimeImmutable('now'),true), + new DateTime(new DateTimeImmutable('2023-10-23T10:00:00'),true), + ])) + ->setUntil(new Until(new DateTime(new DateTimeImmutable('2023-12-31T10:00:00'),true))); + +// 2. Create Event domain entity. +$event = new Event(); +$event + ->setSummary('Stand-up meeting') + ->setOccurrence( + new TimeSpan( + new DateTime(DateTimeImmutable::createFromFormat('Y-m-d H:i:s', '2023-10-15 10:00:00'), true), + new DateTime(DateTimeImmutable::createFromFormat('Y-m-d H:i:s', '2023-10-15 10:30:00'), true) + ) + ) + ->setRecurrenceRule($rrule) +; + +// 3. Create Calendar domain entity. +$calendar = new Calendar([$event]); + +// 4. Transform domain entity into an iCalendar component +$componentFactory = new CalendarFactory(); +$calendarComponent = $componentFactory->createCalendar($calendar); + +// 5. Set HTTP headers. +header('Content-Type: text/calendar; charset=utf-8'); +header('Content-Disposition: attachment; filename="cal.ics"'); + +// 6. Output. +echo $calendarComponent; diff --git a/src/Domain/Entity/Event.php b/src/Domain/Entity/Event.php index 641ec825..5c881132 100644 --- a/src/Domain/Entity/Event.php +++ b/src/Domain/Entity/Event.php @@ -34,6 +34,7 @@ class Event private ?Organizer $organizer = null; private ?Timestamp $lastModified = null; private ?EventStatus $status = null; + private ?RecurrenceRule $rrule = null; /** * @var array @@ -347,4 +348,30 @@ public function unsetStatus(): self return $this; } + + public function getRecurrenceRule(): RecurrenceRule + { + assert($this->rrule !== null); + + return $this->rrule; + } + + public function hasRecurrenceRule(): bool + { + return $this->rrule !== null; + } + + public function setRecurrenceRule(RecurrenceRule $rrule): self + { + $this->rrule = $rrule; + + return $this; + } + + public function unsetRecurrenceRule(): self + { + $this->rrule = null; + + return $this; + } } diff --git a/src/Domain/Entity/RecurrenceRule.php b/src/Domain/Entity/RecurrenceRule.php new file mode 100644 index 00000000..e52ec97d --- /dev/null +++ b/src/Domain/Entity/RecurrenceRule.php @@ -0,0 +1,217 @@ + + * + * 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\ValueObject\DateTime; +use Eluceo\iCal\Domain\ValueObject\Recurrence\By; +use Eluceo\iCal\Domain\ValueObject\Recurrence\Count; +use Eluceo\iCal\Domain\ValueObject\Recurrence\Exclusion; +use Eluceo\iCal\Domain\ValueObject\Recurrence\Frequency; +use Eluceo\iCal\Domain\ValueObject\Recurrence\Interval; +use Eluceo\iCal\Domain\ValueObject\Recurrence\Until; +use Eluceo\iCal\Domain\ValueObject\Recurrence\WeekStart; +use Eluceo\iCal\Presentation\Component\Property\Value\DateTimeValue; + +class RecurrenceRule +{ + private ?Until $until = null; + private ?Frequency $frequency = null; + private ?Count $count = null; + private ?Interval $interval = null; + private ?WeekStart $weekStart = null; + private ?By $by = null; + private ?Exclusion $exclusions = null; + + public function getUntil(): ?Until + { + return $this->until; + } + + public function hasUntil(): bool + { + return $this->until !== null; + } + + public function setUntil(?Until $until): self + { + $this->until = $until; + return $this; + } + + public function unsetUntil(): self + { + $this->until = null; + return $this; + } + + public function getFrequency(): ?Frequency + { + return $this->frequency; + } + + public function hasFrequency(): bool + { + return $this->frequency !== null; + } + + public function setFrequency(?Frequency $frequency): self + { + $this->frequency = $frequency; + return $this; + } + + public function unsetFrequency(): self + { + $this->frequency = null; + return $this; + } + + public function getCount(): ?Count + { + return $this->count; + } + + public function hasCount(): bool + { + return $this->count !== null; + } + + public function setCount(?Count $count): self + { + $this->count = $count; + return $this; + } + + public function unsetCount(): self + { + $this->count = null; + return $this; + } + + public function getInterval(): ?Interval + { + return $this->interval; + } + + public function hasInterval(): bool + { + return $this->interval !== null; + } + + public function setInterval(?Interval $interval): self + { + $this->interval = $interval; + return $this; + } + + public function unsetInterval(): self + { + $this->interval = null; + return $this; + } + + public function getWeekStart(): ?WeekStart + { + return $this->weekStart; + } + + public function hasWeekStart(): bool + { + return $this->weekStart !== null; + } + + public function setWeekStart(?WeekStart $weekStart): self + { + $this->weekStart = $weekStart; + return $this; + } + + public function unsetWeekStart(): self + { + $this->weekStart = null; + return $this; + } + + public function getBy(): ?By + { + return $this->by; + } + + public function hasBy(): bool + { + return !empty($this->by); + } + + public function setBy(By $by): self + { + $this->by = $by; + return $this; + } + + public function unsetBy(): self + { + $this->by = null; + return $this; + } + + public function getExclusions(): ?Exclusion + { + return $this->exclusions; + } + + public function hasExclusions(): bool + { + return !empty($this->exclusions); + } + + public function setExclusions(Exclusion $exclusions): self + { + $this->exclusions = $exclusions; + return $this; + } + + public function unsetExclusions(): self + { + $this->exclusions = null; + return $this; + } + + public function __toString(): string + { + if ( + !$this->hasUntil() + && !$this->hasFrequency() + && !$this->hasCount() + && !$this->hasInterval() + && !$this->hasInterval() + && !$this->hasWeekStart() + && !$this->hasBy() + ) { + return ''; + } + + $parts = [ + $this->getUntil(), + $this->getFrequency(), + $this->getCount(), + $this->getInterval(), + $this->getWeekStart(), + $this->getBy() + ]; + + return implode(';', array_map('strval', + array_filter($parts, static function ($part) { + return !empty($part); + }) + )); + } +} diff --git a/src/Domain/Enum/RecurrenceEnum.php b/src/Domain/Enum/RecurrenceEnum.php new file mode 100644 index 00000000..51f60222 --- /dev/null +++ b/src/Domain/Enum/RecurrenceEnum.php @@ -0,0 +1,37 @@ + + * + * 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; + +use InvalidArgumentException; +use ReflectionClass; + +abstract class RecurrenceEnum +{ + protected string $value; + + public function __construct(string $value) + { + $constants = (new ReflectionClass(static::class))->getConstants(); + if (!in_array($value, $constants, true)) { + $allowedValues = implode(', ', $constants); + throw new InvalidArgumentException( + "Value must be one of: {$allowedValues}" + ); + } + $this->value = $value; + } + + public function __toString(): string + { + return strtoupper($this->value); + } +} diff --git a/src/Domain/Enum/RecurrenceFrequency.php b/src/Domain/Enum/RecurrenceFrequency.php new file mode 100644 index 00000000..44af9692 --- /dev/null +++ b/src/Domain/Enum/RecurrenceFrequency.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 RecurrenceFrequency extends RecurrenceEnum +{ + public const SECONDLY = 'secondly'; + public const MINUTELY = 'minutely'; + public const HOURLY = 'hourly'; + public const DAILY = 'daily'; + public const WEEKLY = 'weekly'; + public const MONTHLY = 'monthly'; + public const YEARLY = 'yearly'; + + public static function secondly(): self + { + return new self(self::SECONDLY); + } + + public static function minutely(): self + { + return new self(self::MINUTELY); + } + + public static function hourly(): self + { + return new self(self::HOURLY); + } + + public static function daily(): self + { + return new self(self::DAILY); + } + + public static function weekly(): self + { + return new self(self::WEEKLY); + } + + public static function monthly(): self + { + return new self(self::MONTHLY); + } + + public function yearly(): self + { + return new self(self::YEARLY); + } +} diff --git a/src/Domain/Enum/RecurrenceWeekDay.php b/src/Domain/Enum/RecurrenceWeekDay.php new file mode 100644 index 00000000..fc73b685 --- /dev/null +++ b/src/Domain/Enum/RecurrenceWeekDay.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\Domain\Enum; + +final class RecurrenceWeekDay extends RecurrenceEnum +{ + public const SUNDAY = 'SU'; + public const MONDAY = 'MO'; + public const TUESDAY = 'TU'; + public const WEDNESDAY = 'WE'; + public const THURSDAY = 'TH'; + public const FRIDAY = 'FR'; + public const SATURDAY = 'SA'; + + protected ?int $offset = null; + + + public function __construct(string $value, ?int $offset = null) + { + parent::__construct($value); + if ($offset !== null) { + if ($offset < -53 || $offset > 53) { + throw new \InvalidArgumentException('Day offsets must be between -53 and 53'); + } + $this->offset = $offset; + } + } + + public static function sunday(?int $offset = null): self + { + return new self(self::SUNDAY, $offset); + } + + public static function monday(?int $offset = null): self + { + return new self(self::MONDAY, $offset); + } + + public static function tuesday(?int $offset = null): self + { + return new self(self::TUESDAY, $offset); + } + + public static function wednesday(?int $offset = null): self + { + return new self(self::WEDNESDAY, $offset); + } + + public static function thursday(?int $offset = null): self + { + return new self(self::THURSDAY, $offset); + } + + public static function friday(?int $offset = null): self + { + return new self(self::FRIDAY, $offset); + } + + public static function saturday(?int $offset = null): self + { + return new self(self::SATURDAY, $offset); + } + + public function __toString(): string + { + return ($this->offset !== null ? (string)$this->offset : '') . parent::__toString(); + } +} diff --git a/src/Domain/ValueObject/Recurrence/By.php b/src/Domain/ValueObject/Recurrence/By.php new file mode 100644 index 00000000..40ed8528 --- /dev/null +++ b/src/Domain/ValueObject/Recurrence/By.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\ValueObject\Recurrence; + +use InvalidArgumentException; +use TypeError; + +final class By +{ + /** @var array */ + private array $by; + + public function __construct(array $by) + { + foreach ($by as $checkBy) { + try { + if (!in_array(get_class($checkBy), [ + By\Second::class, + By\Minute::class, + By\Hour::class, + By\Day::class, + By\MonthDay::class, + By\Month::class, + By\YearDay::class, + By\SetPosition::class, + By\WeekNumber::class + ])) { + $this->throwInvalid(); + } + } catch (TypeError $e) { + $this->throwInvalid(); + } + } + + $this->by = $by; + } + + private function throwInvalid(): void + { + throw new InvalidArgumentException('Values must be one of: Day, Hour, Minute, Month, MonthDay, Second, SetPosition, WeekNumber, YearDay'); + } + + public function __toString(): string + { + return implode(';', array_map(static function ($by) { + return (string)$by; + }, $this->by)); + } +} diff --git a/src/Domain/ValueObject/Recurrence/By/Day.php b/src/Domain/ValueObject/Recurrence/By/Day.php new file mode 100644 index 00000000..145528a5 --- /dev/null +++ b/src/Domain/ValueObject/Recurrence/By/Day.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\ValueObject\Recurrence\By; + +use Eluceo\iCal\Domain\Enum\RecurrenceWeekDay; + +class Day +{ + private array $days; + + /** + * Accepts one or more recurrence days + * + * For example: + * + * [RecurrenceWeekDay::monday(), RecurrenceWeekDay::sunday(-3)] + * + * If the offset is given, it must be between -53 and 53. + * + * @param array $days + */ + public function __construct(array $days) + { + foreach ($days as $day) { + if (!is_a($day, RecurrenceWeekDay::class)) { + throw new \InvalidArgumentException('Day must be an instance of RecurrenceWeekDay'); + } + } + + $this->days = $days; + } + + public function __toString(): string + { + return 'BYDAY=' . implode(',', $this->days); + } +} diff --git a/src/Domain/ValueObject/Recurrence/By/Hour.php b/src/Domain/ValueObject/Recurrence/By/Hour.php new file mode 100644 index 00000000..b987d044 --- /dev/null +++ b/src/Domain/ValueObject/Recurrence/By/Hour.php @@ -0,0 +1,38 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Eluceo\iCal\Domain\ValueObject\Recurrence\By; + +use InvalidArgumentException; + +final class Hour +{ + /** @var array */ + private array $hours; + + public function __construct(array $value) + { + $value = array_map('intval', $value); + + foreach ($value as $hour) { + if ($hour < 0 || $hour > 23) { + throw new InvalidArgumentException('Hour values must be between 0 and 23'); + } + } + + $this->hours = $value; + } + + public function __toString(): string + { + return 'BYHOUR=' . implode(',', $this->hours); + } +} diff --git a/src/Domain/ValueObject/Recurrence/By/Minute.php b/src/Domain/ValueObject/Recurrence/By/Minute.php new file mode 100644 index 00000000..8c0f9f0f --- /dev/null +++ b/src/Domain/ValueObject/Recurrence/By/Minute.php @@ -0,0 +1,38 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Eluceo\iCal\Domain\ValueObject\Recurrence\By; + +use InvalidArgumentException; + +final class Minute +{ + /** @var array */ + private array $minutes; + + public function __construct(array $value) + { + $value = array_map('intval', $value); + + foreach ($value as $minute) { + if ($minute < 0 || $minute > 59) { + throw new InvalidArgumentException('Minute values must be between 0 and 59'); + } + } + + $this->minutes = $value; + } + + public function __toString(): string + { + return 'BYMINUTE=' . implode(',', $this->minutes); + } +} diff --git a/src/Domain/ValueObject/Recurrence/By/Month.php b/src/Domain/ValueObject/Recurrence/By/Month.php new file mode 100644 index 00000000..79447e7d --- /dev/null +++ b/src/Domain/ValueObject/Recurrence/By/Month.php @@ -0,0 +1,38 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Eluceo\iCal\Domain\ValueObject\Recurrence\By; + +use InvalidArgumentException; + +final class Month +{ + /** @var array */ + private array $months; + + public function __construct(array $value) + { + $value = array_map('intval', $value); + + foreach ($value as $month) { + if ($month < 1 || $month > 12) { + throw new InvalidArgumentException('Month values must be between 1 and 12'); + } + } + + $this->months = $value; + } + + public function __toString(): string + { + return 'BYMONTH=' . implode(',', $this->months); + } +} diff --git a/src/Domain/ValueObject/Recurrence/By/MonthDay.php b/src/Domain/ValueObject/Recurrence/By/MonthDay.php new file mode 100644 index 00000000..21b82204 --- /dev/null +++ b/src/Domain/ValueObject/Recurrence/By/MonthDay.php @@ -0,0 +1,38 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Eluceo\iCal\Domain\ValueObject\Recurrence\By; + +use InvalidArgumentException; + +final class MonthDay +{ + /** @var array */ + private array $monthDays; + + public function __construct(array $value) + { + $value = array_map('intval', $value); + + foreach ($value as $monthDay) { + if ($monthDay < -31 || $monthDay > 31 || $monthDay === 0) { + throw new InvalidArgumentException('Month day values must be between 1 and 31, or -1 and -31'); + } + } + + $this->monthDays = $value; + } + + public function __toString(): string + { + return 'BYMONTHDAY=' . implode(',', $this->monthDays); + } +} diff --git a/src/Domain/ValueObject/Recurrence/By/Second.php b/src/Domain/ValueObject/Recurrence/By/Second.php new file mode 100644 index 00000000..de0f5d6e --- /dev/null +++ b/src/Domain/ValueObject/Recurrence/By/Second.php @@ -0,0 +1,38 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Eluceo\iCal\Domain\ValueObject\Recurrence\By; + +use InvalidArgumentException; + +final class Second +{ + /** @var array */ + private array $seconds; + + public function __construct(array $value) + { + $value = array_map('intval', $value); + + foreach ($value as $second) { + if ($second < 0 || $second > 60) { + throw new InvalidArgumentException('Second values must be between 0 and 60'); + } + } + + $this->seconds = $value; + } + + public function __toString(): string + { + return 'BYSECONDS=' . implode(',', $this->seconds); + } +} diff --git a/src/Domain/ValueObject/Recurrence/By/SetPosition.php b/src/Domain/ValueObject/Recurrence/By/SetPosition.php new file mode 100644 index 00000000..5172cebb --- /dev/null +++ b/src/Domain/ValueObject/Recurrence/By/SetPosition.php @@ -0,0 +1,38 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Eluceo\iCal\Domain\ValueObject\Recurrence\By; + +use InvalidArgumentException; + +final class SetPosition +{ + /** @var array */ + private array $positions; + + public function __construct(array $value) + { + $value = array_map('intval', $value); + + foreach ($value as $position) { + if ($position < -366 || $position > 366 || $position === 0) { + throw new InvalidArgumentException('Position values must be between 1 and 366, or -1 and -366'); + } + } + + $this->positions = $value; + } + + public function __toString(): string + { + return 'BYSETPOS=' . implode(',', $this->positions); + } +} diff --git a/src/Domain/ValueObject/Recurrence/By/WeekNumber.php b/src/Domain/ValueObject/Recurrence/By/WeekNumber.php new file mode 100644 index 00000000..888de4af --- /dev/null +++ b/src/Domain/ValueObject/Recurrence/By/WeekNumber.php @@ -0,0 +1,38 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Eluceo\iCal\Domain\ValueObject\Recurrence\By; + +use InvalidArgumentException; + +final class WeekNumber +{ + /** @var array */ + private array $weekNumbers; + + public function __construct(array $value) + { + $value = array_map('intval', $value); + + foreach ($value as $weekNumber) { + if ($weekNumber < -53 || $weekNumber > 53 || $weekNumber === 0) { + throw new InvalidArgumentException('Week number values must be between 1 and 53, or -1 and -53'); + } + } + + $this->weekNumbers = $value; + } + + public function __toString(): string + { + return 'BYWEEKNO=' . implode(',', $this->weekNumbers); + } +} diff --git a/src/Domain/ValueObject/Recurrence/By/YearDay.php b/src/Domain/ValueObject/Recurrence/By/YearDay.php new file mode 100644 index 00000000..cdb583b8 --- /dev/null +++ b/src/Domain/ValueObject/Recurrence/By/YearDay.php @@ -0,0 +1,38 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Eluceo\iCal\Domain\ValueObject\Recurrence\By; + +use InvalidArgumentException; + +final class YearDay +{ + /** @var array */ + private array $yearDays; + + public function __construct(array $value) + { + $value = array_map('intval', $value); + + foreach ($value as $yearDay) { + if ($yearDay < -366 || $yearDay > 366 || $yearDay === 0) { + throw new InvalidArgumentException('Year day values must be between 1 and 366, or -1 and -366'); + } + } + + $this->yearDays = $value; + } + + public function __toString(): string + { + return 'BYYEARDAY=' . implode(',', $this->yearDays); + } +} diff --git a/src/Domain/ValueObject/Recurrence/Count.php b/src/Domain/ValueObject/Recurrence/Count.php new file mode 100644 index 00000000..636b6f15 --- /dev/null +++ b/src/Domain/ValueObject/Recurrence/Count.php @@ -0,0 +1,33 @@ + + * + * 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\Recurrence; + +use InvalidArgumentException; + +final class Count +{ + private int $count; + + public function __construct(int $count) + { + if ($count < 1) { + throw new InvalidArgumentException('Count must be greater than 0'); + } + + $this->count = $count; + } + + public function __toString(): string + { + return "COUNT={$this->count}"; + } +} diff --git a/src/Domain/ValueObject/Recurrence/Exclusion.php b/src/Domain/ValueObject/Recurrence/Exclusion.php new file mode 100644 index 00000000..c04fdd64 --- /dev/null +++ b/src/Domain/ValueObject/Recurrence/Exclusion.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\Domain\ValueObject\Recurrence; + +use Eluceo\iCal\Domain\ValueObject\DateTime; +use Eluceo\iCal\Presentation\Component\Property\Value\DateTimeValue; +use InvalidArgumentException; + +class Exclusion +{ + /** @var array */ + private array $exclusions; + + public function __construct(array $value) + { + foreach ($value as $exclusion) { + if (!$exclusion instanceof DateTime) { + throw new InvalidArgumentException('Values must be DateTime objects'); + } + } + $this->exclusions = $value; + } + + public function __toString(): string + { + return implode(',', array_map(static function ($ex) { + return (string)(new DateTimeValue($ex)); + }, $this->exclusions)); + } +} diff --git a/src/Domain/ValueObject/Recurrence/Frequency.php b/src/Domain/ValueObject/Recurrence/Frequency.php new file mode 100644 index 00000000..4add587a --- /dev/null +++ b/src/Domain/ValueObject/Recurrence/Frequency.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\Recurrence; + +use Eluceo\iCal\Domain\Enum\RecurrenceFrequency; + +final class Frequency +{ + private RecurrenceFrequency $frequency; + + public function __construct(RecurrenceFrequency $frequency) + { + $this->frequency = $frequency; + } + + public function __toString(): string + { + return "FREQ={$this->frequency}"; + } +} diff --git a/src/Domain/ValueObject/Recurrence/Interval.php b/src/Domain/ValueObject/Recurrence/Interval.php new file mode 100644 index 00000000..f99a81e5 --- /dev/null +++ b/src/Domain/ValueObject/Recurrence/Interval.php @@ -0,0 +1,33 @@ + + * + * 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\Recurrence; + +use InvalidArgumentException; + +final class Interval +{ + private int $interval; + + public function __construct(int $interval) + { + if ($interval < 1) { + throw new InvalidArgumentException('Interval must be greater than 0'); + } + + $this->interval = $interval; + } + + public function __toString(): string + { + return "INTERVAL={$this->interval}"; + } +} diff --git a/src/Domain/ValueObject/Recurrence/Until.php b/src/Domain/ValueObject/Recurrence/Until.php new file mode 100644 index 00000000..475af6b8 --- /dev/null +++ b/src/Domain/ValueObject/Recurrence/Until.php @@ -0,0 +1,30 @@ + + * + * 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\Recurrence; + +use Eluceo\iCal\Domain\ValueObject\DateTime; +use Eluceo\iCal\Presentation\Component\Property\Value\DateTimeValue; + +class Until +{ + private DateTime $exclusions; + + public function __construct(DateTime $value) + { + $this->exclusions = $value; + } + + public function __toString(): string + { + return 'UNTIL=' . (new DateTimeValue($this->exclusions)); + } +} diff --git a/src/Domain/ValueObject/Recurrence/WeekStart.php b/src/Domain/ValueObject/Recurrence/WeekStart.php new file mode 100644 index 00000000..4584cad3 --- /dev/null +++ b/src/Domain/ValueObject/Recurrence/WeekStart.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\Recurrence; + +use Eluceo\iCal\Domain\Enum\RecurrenceWeekDay; + +final class WeekStart +{ + private RecurrenceWeekDay $weekStart; + + public function __construct(RecurrenceWeekDay $weekStart) + { + $this->weekStart = $weekStart; + } + + public function __toString(): string + { + return "WKST={$this->weekStart}"; + } +} diff --git a/src/Presentation/Component/Property/Value/RawTextValue.php b/src/Presentation/Component/Property/Value/RawTextValue.php new file mode 100644 index 00000000..25499aa7 --- /dev/null +++ b/src/Presentation/Component/Property/Value/RawTextValue.php @@ -0,0 +1,30 @@ + + * + * 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; +use Eluceo\iCal\Presentation\Component\Property\Value; + +class RawTextValue extends Value +{ + private string $value; + + public function __construct(string $value) + { + $this->value = $value; + } + + public function __toString(): string + { + return $this->value; + } +} diff --git a/src/Presentation/Factory/EventFactory.php b/src/Presentation/Factory/EventFactory.php index 88c8794f..e32b3c83 100644 --- a/src/Presentation/Factory/EventFactory.php +++ b/src/Presentation/Factory/EventFactory.php @@ -32,6 +32,7 @@ use Eluceo\iCal\Presentation\Component\Property\Value\GeoValue; use Eluceo\iCal\Presentation\Component\Property\Value\IntegerValue; use Eluceo\iCal\Presentation\Component\Property\Value\ListValue; +use Eluceo\iCal\Presentation\Component\Property\Value\RawTextValue; use Eluceo\iCal\Presentation\Component\Property\Value\TextValue; use Eluceo\iCal\Presentation\Component\Property\Value\UriValue; use Generator; @@ -127,6 +128,13 @@ protected function getProperties(Event $event): Generator foreach ($event->getAttachments() as $attachment) { yield from $this->getAttachmentProperties($attachment); } + + if ($event->hasRecurrenceRule()) { + yield new Property('RRULE', new RawTextValue((string)$event->getRecurrenceRule())); + if ($event->getRecurrenceRule()->hasExclusions()) { + yield new Property('EXDATE', new RawTextValue((string)$event->getRecurrenceRule()->getExclusions())); + } + } } /** diff --git a/tests/Unit/Domain/Entity/RecurrenceRuleTest.php b/tests/Unit/Domain/Entity/RecurrenceRuleTest.php new file mode 100644 index 00000000..e56db14c --- /dev/null +++ b/tests/Unit/Domain/Entity/RecurrenceRuleTest.php @@ -0,0 +1,51 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Eluceo\iCal\Unit\Domain\Entity; + +use DateTime as PhpDateTime; +use Eluceo\iCal\Domain\Enum\RecurrenceFrequency; +use Eluceo\iCal\Domain\Enum\RecurrenceWeekDay; +use Eluceo\iCal\Domain\ValueObject\Recurrence\By\Day; +use Eluceo\iCal\Domain\ValueObject\Recurrence\Until; +use PHPUnit\Framework\TestCase; +use Eluceo\iCal\Domain\Entity\RecurrenceRule; +use Eluceo\iCal\Domain\ValueObject\DateTime; +use Eluceo\iCal\Domain\ValueObject\Recurrence\By; +use Eluceo\iCal\Domain\ValueObject\Recurrence\Count; +use Eluceo\iCal\Domain\ValueObject\Recurrence\Exclusion; +use Eluceo\iCal\Domain\ValueObject\Recurrence\Frequency; +use Eluceo\iCal\Domain\ValueObject\Recurrence\Interval; +use Eluceo\iCal\Domain\ValueObject\Recurrence\WeekStart; + +class RecurrenceRuleTest extends TestCase +{ + public function testToStringWithAllPropertiesSet(): void + { + $recurrenceRule = (new RecurrenceRule()) + ->setUntil(new Until(new DateTime(new PhpDateTime('2023-12-31T12:00:00'), true))) + ->setFrequency(new Frequency(RecurrenceFrequency::daily())) + ->setCount(new Count(5)) + ->setInterval(new Interval(2)) + ->setWeekStart(new WeekStart(RecurrenceWeekDay::monday())) + ->setBy(new By([new Day([RecurrenceWeekDay::thursday(-1)])])) + ->setExclusions(new Exclusion([new DateTime(new PhpDateTime('2023-12-31T12:00:00'), true)])); + + $expected = 'UNTIL=20231231T120000;FREQ=DAILY;COUNT=5;INTERVAL=2;WKST=MO;BYDAY=-1TH'; + $this->assertSame($expected, $recurrenceRule->__toString()); + } + + public function testToStringWithNoPropertiesSet(): void + { + $recurrenceRule = new RecurrenceRule(); + $this->assertSame('', $recurrenceRule->__toString()); + } +} diff --git a/tests/Unit/Domain/ValueObject/Recurrence/By/DayTest.php b/tests/Unit/Domain/ValueObject/Recurrence/By/DayTest.php new file mode 100644 index 00000000..49d3fb8e --- /dev/null +++ b/tests/Unit/Domain/ValueObject/Recurrence/By/DayTest.php @@ -0,0 +1,62 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Eluceo\iCal\Unit\Domain\ValueObject\Recurrence\By; + +use PHPUnit\Framework\TestCase; +use Eluceo\iCal\Domain\ValueObject\Recurrence\By\Day; +use Eluceo\iCal\Domain\Enum\RecurrenceWeekDay; + +class DayTest extends TestCase +{ + public function testConstructorWithSingleDay(): void + { + $day = new Day([RecurrenceWeekDay::monday()]); + + $this->assertInstanceOf(Day::class, $day); + $this->assertSame('BYDAY=MO', $day->__toString()); + } + + public function testConstructorWithMultipleDays(): void + { + $days = new Day([RecurrenceWeekDay::monday(), RecurrenceWeekDay::sunday()]); + + $this->assertInstanceOf(Day::class, $days); + $this->assertSame('BYDAY=MO,SU', $days->__toString()); + } + + public function testConstructorWithDayOffsets(): void + { + $days = new Day([ + RecurrenceWeekDay::monday(5), + RecurrenceWeekDay::sunday(-3), + ]); + + $this->assertInstanceOf(Day::class, $days); + $this->assertSame('BYDAY=5MO,-3SU', $days->__toString()); + } + + public function testConstructorWithInvalidOffsetValue(): void + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('Day offsets must be between -53 and 53'); + + new Day([RecurrenceWeekDay::monday(-54)]); + } + + public function testConstructorWithInvalidDayType(): void + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('Day must be an instance of RecurrenceWeekDay'); + + new Day(['InvalidDay']); + } +} diff --git a/tests/Unit/Domain/ValueObject/Recurrence/By/HourTest.php b/tests/Unit/Domain/ValueObject/Recurrence/By/HourTest.php new file mode 100644 index 00000000..aa91455d --- /dev/null +++ b/tests/Unit/Domain/ValueObject/Recurrence/By/HourTest.php @@ -0,0 +1,51 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Eluceo\iCal\Unit\Domain\ValueObject\Recurrence\By; + +use PHPUnit\Framework\TestCase; +use Eluceo\iCal\Domain\ValueObject\Recurrence\By\Hour; +use InvalidArgumentException; + +class HourTest extends TestCase +{ + public function testConstructorWithSingleHour(): void + { + $hour = new Hour([8]); + + $this->assertInstanceOf(Hour::class, $hour); + $this->assertSame('BYHOUR=8', $hour->__toString()); + } + + public function testConstructorWithMultipleHours(): void + { + $hours = new Hour([8, 16, 12]); + + $this->assertInstanceOf(Hour::class, $hours); + $this->assertSame('BYHOUR=8,16,12', $hours->__toString()); + } + + public function testConstructorWithInvalidHourValue(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Hour values must be between 0 and 23'); + + new Hour([25]); + } + + public function testConstructorWithInvalidHourArrayValue(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Hour values must be between 0 and 23'); + + new Hour([8, 25, 16]); + } +} diff --git a/tests/Unit/Domain/ValueObject/Recurrence/By/MinuteTest.php b/tests/Unit/Domain/ValueObject/Recurrence/By/MinuteTest.php new file mode 100644 index 00000000..332c16e2 --- /dev/null +++ b/tests/Unit/Domain/ValueObject/Recurrence/By/MinuteTest.php @@ -0,0 +1,51 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Eluceo\iCal\Unit\Domain\ValueObject\Recurrence\By; + +use PHPUnit\Framework\TestCase; +use Eluceo\iCal\Domain\ValueObject\Recurrence\By\Minute; +use InvalidArgumentException; + +class MinuteTest extends TestCase +{ + public function testConstructorWithSingleMinute(): void + { + $minute = new Minute([15]); + + $this->assertInstanceOf(Minute::class, $minute); + $this->assertSame('BYMINUTE=15', $minute->__toString()); + } + + public function testConstructorWithMultipleMinutes(): void + { + $minutes = new Minute([15, 30, 45]); + + $this->assertInstanceOf(Minute::class, $minutes); + $this->assertSame('BYMINUTE=15,30,45', $minutes->__toString()); + } + + public function testConstructorWithInvalidMinuteValue(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Minute values must be between 0 and 59'); + + new Minute([60]); + } + + public function testConstructorWithInvalidMinuteArrayValue(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Minute values must be between 0 and 59'); + + new Minute([15, 60, 45]); + } +} diff --git a/tests/Unit/Domain/ValueObject/Recurrence/By/MonthDayTest.php b/tests/Unit/Domain/ValueObject/Recurrence/By/MonthDayTest.php new file mode 100644 index 00000000..50f547f1 --- /dev/null +++ b/tests/Unit/Domain/ValueObject/Recurrence/By/MonthDayTest.php @@ -0,0 +1,51 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Eluceo\iCal\Unit\Domain\ValueObject\Recurrence\By; + +use PHPUnit\Framework\TestCase; +use Eluceo\iCal\Domain\ValueObject\Recurrence\By\MonthDay; +use InvalidArgumentException; + +class MonthDayTest extends TestCase +{ + public function testConstructorWithSingleMonthDay(): void + { + $monthDay = new MonthDay([15]); + + $this->assertInstanceOf(MonthDay::class, $monthDay); + $this->assertSame('BYMONTHDAY=15', $monthDay->__toString()); + } + + public function testConstructorWithMultipleMonthDays(): void + { + $monthDays = new MonthDay([15, -1, -31]); + + $this->assertInstanceOf(MonthDay::class, $monthDays); + $this->assertSame('BYMONTHDAY=15,-1,-31', $monthDays->__toString()); + } + + public function testConstructorWithInvalidMonthDayValue(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Month day values must be between 1 and 31, or -1 and -31'); + + new MonthDay([0]); + } + + public function testConstructorWithInvalidMonthDayArrayValue(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Month day values must be between 1 and 31, or -1 and -31'); + + new MonthDay([15, 32, -33]); + } +} diff --git a/tests/Unit/Domain/ValueObject/Recurrence/By/MonthTest.php b/tests/Unit/Domain/ValueObject/Recurrence/By/MonthTest.php new file mode 100644 index 00000000..2f8e939d --- /dev/null +++ b/tests/Unit/Domain/ValueObject/Recurrence/By/MonthTest.php @@ -0,0 +1,51 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Eluceo\iCal\Unit\Domain\ValueObject\Recurrence\By; + +use PHPUnit\Framework\TestCase; +use Eluceo\iCal\Domain\ValueObject\Recurrence\By\Month; +use InvalidArgumentException; + +class MonthTest extends TestCase +{ + public function testConstructorWithSingleMonth(): void + { + $month = new Month([5]); + + $this->assertInstanceOf(Month::class, $month); + $this->assertSame('BYMONTH=5', $month->__toString()); + } + + public function testConstructorWithMultipleMonths(): void + { + $months = new Month([5, 8, 12]); + + $this->assertInstanceOf(Month::class, $months); + $this->assertSame('BYMONTH=5,8,12', $months->__toString()); + } + + public function testConstructorWithInvalidMonthValue(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Month values must be between 1 and 12'); + + new Month([0]); + } + + public function testConstructorWithInvalidMonthArrayValue(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Month values must be between 1 and 12'); + + new Month([5, 0, 12]); + } +} diff --git a/tests/Unit/Domain/ValueObject/Recurrence/By/SecondTest.php b/tests/Unit/Domain/ValueObject/Recurrence/By/SecondTest.php new file mode 100644 index 00000000..85e6aa3d --- /dev/null +++ b/tests/Unit/Domain/ValueObject/Recurrence/By/SecondTest.php @@ -0,0 +1,51 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Eluceo\iCal\Unit\Domain\ValueObject\Recurrence\By; + +use PHPUnit\Framework\TestCase; +use Eluceo\iCal\Domain\ValueObject\Recurrence\By\Second; +use InvalidArgumentException; + +class SecondTest extends TestCase +{ + public function testConstructorWithSingleSecond(): void + { + $second = new Second([30]); + + $this->assertInstanceOf(Second::class, $second); + $this->assertSame('BYSECONDS=30', $second->__toString()); + } + + public function testConstructorWithMultipleSeconds(): void + { + $seconds = new Second([15, 30, 45]); + + $this->assertInstanceOf(Second::class, $seconds); + $this->assertSame('BYSECONDS=15,30,45', $seconds->__toString()); + } + + public function testConstructorWithInvalidSecondValue(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Second values must be between 0 and 60'); + + new Second([61]); + } + + public function testConstructorWithInvalidSecondArrayValue(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Second values must be between 0 and 60'); + + new Second([15, 61, 45]); + } +} diff --git a/tests/Unit/Domain/ValueObject/Recurrence/By/SetPositionTest.php b/tests/Unit/Domain/ValueObject/Recurrence/By/SetPositionTest.php new file mode 100644 index 00000000..06ea4f57 --- /dev/null +++ b/tests/Unit/Domain/ValueObject/Recurrence/By/SetPositionTest.php @@ -0,0 +1,51 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Eluceo\iCal\Unit\Domain\ValueObject\Recurrence\By; + +use PHPUnit\Framework\TestCase; +use Eluceo\iCal\Domain\ValueObject\Recurrence\By\SetPosition; +use InvalidArgumentException; + +class SetPositionTest extends TestCase +{ + public function testConstructorWithSinglePosition(): void + { + $position = new SetPosition([15]); + + $this->assertInstanceOf(SetPosition::class, $position); + $this->assertSame('BYSETPOS=15', $position->__toString()); + } + + public function testConstructorWithMultiplePositions(): void + { + $positions = new SetPosition([15, -1, -366]); + + $this->assertInstanceOf(SetPosition::class, $positions); + $this->assertSame('BYSETPOS=15,-1,-366', $positions->__toString()); + } + + public function testConstructorWithInvalidPositionValue(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Position values must be between 1 and 366, or -1 and -366'); + + new SetPosition([0]); + } + + public function testConstructorWithInvalidPositionArrayValue(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Position values must be between 1 and 366, or -1 and -366'); + + new SetPosition([15, 367, -367]); + } +} diff --git a/tests/Unit/Domain/ValueObject/Recurrence/By/WeekNumberTest.php b/tests/Unit/Domain/ValueObject/Recurrence/By/WeekNumberTest.php new file mode 100644 index 00000000..2916b4e1 --- /dev/null +++ b/tests/Unit/Domain/ValueObject/Recurrence/By/WeekNumberTest.php @@ -0,0 +1,51 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Eluceo\iCal\Unit\Domain\ValueObject\Recurrence\By; + +use PHPUnit\Framework\TestCase; +use Eluceo\iCal\Domain\ValueObject\Recurrence\By\WeekNumber; +use InvalidArgumentException; + +class WeekNumberTest extends TestCase +{ + public function testConstructorWithSingleWeekNumber(): void + { + $weekNumber = new WeekNumber([15]); + + $this->assertInstanceOf(WeekNumber::class, $weekNumber); + $this->assertSame('BYWEEKNO=15', $weekNumber->__toString()); + } + + public function testConstructorWithMultipleWeekNumbers(): void + { + $weekNumbers = new WeekNumber([15, -1, -53]); + + $this->assertInstanceOf(WeekNumber::class, $weekNumbers); + $this->assertSame('BYWEEKNO=15,-1,-53', $weekNumbers->__toString()); + } + + public function testConstructorWithInvalidWeekNumberValue(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Week number values must be between 1 and 53, or -1 and -53'); + + new WeekNumber([0]); + } + + public function testConstructorWithInvalidWeekNumberArrayValue(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Week number values must be between 1 and 53, or -1 and -53'); + + new WeekNumber([15, 54, -54]); + } +} diff --git a/tests/Unit/Domain/ValueObject/Recurrence/By/YearDayTest.php b/tests/Unit/Domain/ValueObject/Recurrence/By/YearDayTest.php new file mode 100644 index 00000000..402608c6 --- /dev/null +++ b/tests/Unit/Domain/ValueObject/Recurrence/By/YearDayTest.php @@ -0,0 +1,51 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Eluceo\iCal\Unit\Domain\ValueObject\Recurrence\By; + +use PHPUnit\Framework\TestCase; +use Eluceo\iCal\Domain\ValueObject\Recurrence\By\YearDay; +use InvalidArgumentException; + +class YearDayTest extends TestCase +{ + public function testConstructorWithSingleYearDay(): void + { + $yearDay = new YearDay([100]); + + $this->assertInstanceOf(YearDay::class, $yearDay); + $this->assertSame('BYYEARDAY=100', $yearDay->__toString()); + } + + public function testConstructorWithMultipleYearDays(): void + { + $yearDays = new YearDay([100, -1, -366]); + + $this->assertInstanceOf(YearDay::class, $yearDays); + $this->assertSame('BYYEARDAY=100,-1,-366', $yearDays->__toString()); + } + + public function testConstructorWithInvalidYearDayValue(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Year day values must be between 1 and 366, or -1 and -366'); + + new YearDay([0]); + } + + public function testConstructorWithInvalidYearDayArrayValue(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Year day values must be between 1 and 366, or -1 and -366'); + + new YearDay([100, 367, -367]); + } +} diff --git a/tests/Unit/Domain/ValueObject/Recurrence/ByTest.php b/tests/Unit/Domain/ValueObject/Recurrence/ByTest.php new file mode 100644 index 00000000..1aa229b9 --- /dev/null +++ b/tests/Unit/Domain/ValueObject/Recurrence/ByTest.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\Unit\Domain\ValueObject\Recurrence; + +use Eluceo\iCal\Domain\Enum\RecurrenceWeekDay; +use PHPUnit\Framework\TestCase; +use Eluceo\iCal\Domain\ValueObject\Recurrence\By; +use Eluceo\iCal\Domain\ValueObject\Recurrence\By\Second; +use Eluceo\iCal\Domain\ValueObject\Recurrence\By\Minute; +use Eluceo\iCal\Domain\ValueObject\Recurrence\By\Hour; +use Eluceo\iCal\Domain\ValueObject\Recurrence\By\Day; +use Eluceo\iCal\Domain\ValueObject\Recurrence\By\MonthDay; +use Eluceo\iCal\Domain\ValueObject\Recurrence\By\Month; +use Eluceo\iCal\Domain\ValueObject\Recurrence\By\YearDay; +use Eluceo\iCal\Domain\ValueObject\Recurrence\By\SetPosition; +use Eluceo\iCal\Domain\ValueObject\Recurrence\By\WeekNumber; +use InvalidArgumentException; + +class ByTest extends TestCase +{ + public function testConstructorWithValidByValues(): void + { + $byArray = [ + new Second([15]), + new Minute([30]), + new Hour([4]), + new Day([RecurrenceWeekDay::monday()]), + new MonthDay([15]), + new Month([6]), + new YearDay([200]), + new SetPosition([3]), + new WeekNumber([10]), + ]; + + $by = new By($byArray); + + $this->assertInstanceOf(By::class, $by); + $expectedString = implode(';', array_map(static function ($byValue) { + return (string)$byValue; + }, $byArray)); + $this->assertSame($expectedString, $by->__toString()); + } + + public function testConstructorWithInvalidByValues(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Values must be one of: Day, Hour, Minute, Month, MonthDay, Second, SetPosition, WeekNumber, YearDay'); + + $byArray = [ + new Second([15]), + 'InvalidValue', + ]; + + new By($byArray); + } +} diff --git a/tests/Unit/Domain/ValueObject/Recurrence/CountTest.php b/tests/Unit/Domain/ValueObject/Recurrence/CountTest.php new file mode 100644 index 00000000..b6cebb41 --- /dev/null +++ b/tests/Unit/Domain/ValueObject/Recurrence/CountTest.php @@ -0,0 +1,43 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Eluceo\iCal\Unit\Domain\ValueObject\Recurrence; + +use PHPUnit\Framework\TestCase; +use Eluceo\iCal\Domain\ValueObject\Recurrence\Count; +use InvalidArgumentException; + +class CountTest extends TestCase +{ + public function testConstructorWithValidCount(): void + { + $count = new Count(5); + + $this->assertInstanceOf(Count::class, $count); + $this->assertSame('COUNT=5', $count->__toString()); + } + + public function testConstructorWithInvalidCountValue(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Count must be greater than 0'); + + new Count(0); + } + + public function testConstructorWithNegativeCountValue(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Count must be greater than 0'); + + new Count(-5); + } +} diff --git a/tests/Unit/Domain/ValueObject/Recurrence/ExclusionTest.php b/tests/Unit/Domain/ValueObject/Recurrence/ExclusionTest.php new file mode 100644 index 00000000..291af057 --- /dev/null +++ b/tests/Unit/Domain/ValueObject/Recurrence/ExclusionTest.php @@ -0,0 +1,51 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Eluceo\iCal\Unit\Domain\ValueObject\Recurrence; + +use DateTime as PhpDateTime; +use PHPUnit\Framework\TestCase; +use Eluceo\iCal\Domain\ValueObject\Recurrence\Exclusion; +use Eluceo\iCal\Domain\ValueObject\DateTime; +use InvalidArgumentException; + +class ExclusionTest extends TestCase +{ + public function testConstructorWithSingleExclusion(): void + { + $exclusion = new Exclusion([new DateTime( + new PhpDateTime('2023-12-31T12:00:00'), + true + )]); + + $this->assertInstanceOf(Exclusion::class, $exclusion); + $this->assertSame('20231231T120000', $exclusion->__toString()); + } + + public function testConstructorWithMultipleExclusions(): void + { + $exclusions = new Exclusion([ + new DateTime(new PhpDateTime('2023-12-31T12:00:00'), true), + new DateTime(new PhpDateTime('2023-12-30T14:30:00'), true), + ]); + + $this->assertInstanceOf(Exclusion::class, $exclusions); + $this->assertSame('20231231T120000,20231230T143000', $exclusions->__toString()); + } + + public function testConstructorWithInvalidExclusionValue(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Values must be DateTime objects'); + + new Exclusion([new DateTime(new PhpDateTime('2023-12-31T12:00:00'), true), 'invalidValue']); + } +} diff --git a/tests/Unit/Domain/ValueObject/Recurrence/FrequencyTest.php b/tests/Unit/Domain/ValueObject/Recurrence/FrequencyTest.php new file mode 100644 index 00000000..d2582663 --- /dev/null +++ b/tests/Unit/Domain/ValueObject/Recurrence/FrequencyTest.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\Unit\Domain\ValueObject\Recurrence; + +use PHPUnit\Framework\TestCase; +use Eluceo\iCal\Domain\ValueObject\Recurrence\Frequency; +use Eluceo\iCal\Domain\Enum\RecurrenceFrequency; + +class FrequencyTest extends TestCase +{ + public function testConstructorWithValidFrequency(): void + { + $frequency = new Frequency(RecurrenceFrequency::DAILY()); + + $this->assertInstanceOf(Frequency::class, $frequency); + $this->assertSame('FREQ=DAILY', $frequency->__toString()); + } +} diff --git a/tests/Unit/Domain/ValueObject/Recurrence/IntervalTest.php b/tests/Unit/Domain/ValueObject/Recurrence/IntervalTest.php new file mode 100644 index 00000000..a1763ebc --- /dev/null +++ b/tests/Unit/Domain/ValueObject/Recurrence/IntervalTest.php @@ -0,0 +1,43 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Eluceo\iCal\Unit\Domain\ValueObject\Recurrence; + +use PHPUnit\Framework\TestCase; +use Eluceo\iCal\Domain\ValueObject\Recurrence\Interval; +use InvalidArgumentException; + +class IntervalTest extends TestCase +{ + public function testConstructorWithValidInterval(): void + { + $interval = new Interval(2); + + $this->assertInstanceOf(Interval::class, $interval); + $this->assertSame('INTERVAL=2', $interval->__toString()); + } + + public function testConstructorWithInvalidIntervalValue(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Interval must be greater than 0'); + + new Interval(0); + } + + public function testConstructorWithNegativeIntervalValue(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Interval must be greater than 0'); + + new Interval(-2); + } +} diff --git a/tests/Unit/Domain/ValueObject/Recurrence/UntilTest.php b/tests/Unit/Domain/ValueObject/Recurrence/UntilTest.php new file mode 100644 index 00000000..69ca4d62 --- /dev/null +++ b/tests/Unit/Domain/ValueObject/Recurrence/UntilTest.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\Unit\Domain\ValueObject\Recurrence; + +use DateTime as PhpDateTime; +use PHPUnit\Framework\TestCase; +use Eluceo\iCal\Domain\ValueObject\Recurrence\Until; +use Eluceo\iCal\Domain\ValueObject\DateTime; + +class UntilTest extends TestCase +{ + public function testConstructorWithValidDateTime(): void + { + $dateTime = new DateTime(new PhpDateTime('2023-12-31T12:00:00'), true); + $until = new Until($dateTime); + + $this->assertInstanceOf(Until::class, $until); + $this->assertSame('UNTIL=20231231T120000', $until->__toString()); + } +} diff --git a/tests/Unit/Domain/ValueObject/Recurrence/WeekStartTest.php b/tests/Unit/Domain/ValueObject/Recurrence/WeekStartTest.php new file mode 100644 index 00000000..ef3a0fe9 --- /dev/null +++ b/tests/Unit/Domain/ValueObject/Recurrence/WeekStartTest.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\Unit\Domain\ValueObject\Recurrence; + +use PHPUnit\Framework\TestCase; +use Eluceo\iCal\Domain\ValueObject\Recurrence\WeekStart; +use Eluceo\iCal\Domain\Enum\RecurrenceWeekDay; + +class WeekStartTest extends TestCase +{ + public function testConstructorWithValidWeekStart(): void + { + $weekStart = new WeekStart(RecurrenceWeekDay::MONDAY()); + + $this->assertInstanceOf(WeekStart::class, $weekStart); + $this->assertSame('WKST=MO', $weekStart->__toString()); + } +}