├── src ├── Exception.php ├── Event │ ├── Exception │ │ ├── NotFound.php │ │ ├── InvalidEvent.php │ │ └── NoProviderFound.php │ ├── Provider │ │ ├── ProviderInterface.php │ │ ├── Psr16CacheProvider.php │ │ ├── AggregateProvider.php │ │ └── ArrayProvider.php │ ├── EventInterface.php │ ├── EventTrait.php │ ├── Event.php │ ├── Collection │ │ ├── CollectionInterface.php │ │ ├── ArrayCollection.php │ │ └── IndexedCollection.php │ └── EventManager.php ├── Period │ ├── Exception │ │ ├── NotADay.php │ │ ├── NotAWeek.php │ │ ├── NotAYear.php │ │ ├── NotAMinute.php │ │ ├── NotAMonth.php │ │ ├── NotASecond.php │ │ ├── NotAnHour.php │ │ ├── NotImplemented.php │ │ ├── NotAWeekday.php │ │ └── InvalidArgument.php │ ├── IterablePeriod.php │ ├── Second.php │ ├── PeriodFactoryInterface.php │ ├── Day.php │ ├── Year.php │ ├── Minute.php │ ├── Week.php │ ├── Hour.php │ ├── Range.php │ ├── PeriodInterface.php │ ├── PeriodFactory.php │ ├── Month.php │ └── PeriodAbstract.php ├── DayOfWeek.php ├── Bridge │ ├── Symfony │ │ └── Bundle │ │ │ ├── CalendRBundle.php │ │ │ ├── DependencyInjection │ │ │ ├── Compiler │ │ │ │ └── EventProviderPass.php │ │ │ ├── CalendRExtension.php │ │ │ └── Configuration.php │ │ │ └── Resources │ │ │ └── config │ │ │ └── services.php │ ├── Doctrine │ │ └── ORM │ │ │ └── EventRepository.php │ └── Twig │ │ └── CalendRExtension.php └── Calendar.php ├── LICENSE ├── CHANGELOG.md ├── composer.json └── README.md /src/Exception.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | interface IterablePeriod extends \Traversable 14 | { 15 | } 16 | -------------------------------------------------------------------------------- /src/Event/Provider/ProviderInterface.php: -------------------------------------------------------------------------------- 1 | begin, new \DateInterval('PT1S'), $this->end); 13 | } 14 | 15 | #[\Override] 16 | public static function isValid(\DateTimeInterface $start): bool 17 | { 18 | return '000000' === $start->format('u'); 19 | } 20 | 21 | #[\Override] 22 | public function __toString(): string 23 | { 24 | return $this->format('s'); 25 | } 26 | 27 | #[\Override] 28 | public static function getDateInterval(): \DateInterval 29 | { 30 | return new \DateInterval('PT1S'); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Bridge/Symfony/Bundle/CalendRBundle.php: -------------------------------------------------------------------------------- 1 | registerForAutoconfiguration(ProviderInterface::class)->addTag(EventProviderPass::TAG); 19 | } 20 | 21 | $container->addCompilerPass(new EventProviderPass()); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Event/EventTrait.php: -------------------------------------------------------------------------------- 1 | getBegin() <= $datetime && $datetime < $this->getEnd(); 18 | } 19 | 20 | public function containsPeriod(PeriodInterface $period): bool 21 | { 22 | return $this->getBegin() <= $period->getBegin() && $this->getEnd() >= $period->getEnd(); 23 | } 24 | 25 | public function isDuring(PeriodInterface $period): bool 26 | { 27 | return $this->getBegin() >= $period->getBegin() && $this->getEnd() < $period->getEnd(); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Period/PeriodFactoryInterface.php: -------------------------------------------------------------------------------- 1 | getDefinition(EventManager::class); 20 | 21 | foreach ($container->findTaggedServiceIds(self::TAG) as $id => $attributes) { 22 | /** @var string $providerAlias */ 23 | $providerAlias = $attributes[0]['alias'] ?? $id; 24 | 25 | $eventManager->addMethodCall('addProvider', [$providerAlias, new Reference($id)]); 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012 Yohan Giarelli 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is furnished 8 | to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /src/Event/Event.php: -------------------------------------------------------------------------------- 1 | diff($end)->invert) { 25 | throw new InvalidEvent('Events usually start before they end'); 26 | } 27 | 28 | $this->begin = \DateTimeImmutable::createFromInterface($start); 29 | $this->end = \DateTimeImmutable::createFromInterface($end); 30 | } 31 | 32 | #[\Override] 33 | public function getBegin(): \DateTimeInterface 34 | { 35 | return $this->begin; 36 | } 37 | 38 | #[\Override] 39 | public function getEnd(): \DateTimeInterface 40 | { 41 | return $this->end; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/Event/Collection/CollectionInterface.php: -------------------------------------------------------------------------------- 1 | 16 | */ 17 | interface CollectionInterface extends \Countable, \Traversable 18 | { 19 | /** 20 | * Adds an event to the collection. 21 | */ 22 | public function add(EventInterface $event): void; 23 | 24 | /** 25 | * Removes an event from the collection. 26 | */ 27 | public function remove(EventInterface $event): void; 28 | 29 | /** 30 | * Return all events;. 31 | * 32 | * @return list 33 | */ 34 | public function all(): array; 35 | 36 | /** 37 | * Returns if there is events corresponding to $index period. 38 | */ 39 | public function has(PeriodInterface|\DateTimeInterface|string $index): bool; 40 | 41 | /** 42 | * Find events in the collection. 43 | * 44 | * @return list 45 | */ 46 | public function find(PeriodInterface|\DateTimeInterface|string $index): array; 47 | } 48 | -------------------------------------------------------------------------------- /src/Event/Provider/Psr16CacheProvider.php: -------------------------------------------------------------------------------- 1 | namespace) { 26 | $cacheKey = $this->namespace.'.'.$cacheKey; 27 | } 28 | 29 | if ($this->cache->has($cacheKey)) { 30 | /** @var list $result */ 31 | $result = $this->cache->get($cacheKey); 32 | 33 | return $result; 34 | } 35 | 36 | $events = $this->provider->getEvents($begin, $end, $options); 37 | $this->cache->set($cacheKey, $events, $this->lifetime); 38 | 39 | return $events; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Event/Provider/AggregateProvider.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | private array $providers = []; 13 | 14 | /** 15 | * @param ProviderInterface[] $providers 16 | * 17 | * @throws \InvalidArgumentException 18 | */ 19 | public function __construct(array $providers) 20 | { 21 | foreach ($providers as $provider) { 22 | if (!$provider instanceof ProviderInterface) { 23 | throw new \InvalidArgumentException('Providers must implement CalendR\\Event\\ProviderInterface'); 24 | } 25 | 26 | $this->providers[] = $provider; 27 | } 28 | } 29 | 30 | public function add(ProviderInterface $provider): void 31 | { 32 | $this->providers[] = $provider; 33 | } 34 | 35 | #[\Override] 36 | public function getEvents(\DateTimeInterface $begin, \DateTimeInterface $end, array $options = []): array 37 | { 38 | $events = []; 39 | 40 | foreach ($this->providers as $provider) { 41 | $events = array_merge($events, $provider->getEvents($begin, $end, $options)); 42 | } 43 | 44 | return $events; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/Period/Day.php: -------------------------------------------------------------------------------- 1 | 9 | * @implements IterablePeriod 10 | */ 11 | final class Day extends PeriodAbstract implements \IteratorAggregate, \Stringable, IterablePeriod 12 | { 13 | #[\Override] 14 | public function getDatePeriod(): \DatePeriod 15 | { 16 | return new \DatePeriod($this->begin, new \DateInterval('P1D'), $this->end); 17 | } 18 | 19 | #[\Override] 20 | public function getIterator(): \Generator 21 | { 22 | $current = $this->getFactory()->createHour($this->begin); 23 | while ($this->contains($current->getBegin())) { 24 | /* No need to explicit key as whe start to 0 */ 25 | yield $current; 26 | 27 | $current = $current->getNext(); 28 | } 29 | } 30 | 31 | #[\Override] 32 | public function __toString(): string 33 | { 34 | return $this->format('l'); 35 | } 36 | 37 | #[\Override] 38 | public static function isValid(\DateTimeInterface $start): bool 39 | { 40 | return '00:00:00' === $start->format('H:i:s'); 41 | } 42 | 43 | #[\Override] 44 | public static function getDateInterval(): \DateInterval 45 | { 46 | return new \DateInterval('P1D'); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/Period/Year.php: -------------------------------------------------------------------------------- 1 | 11 | * @implements IterablePeriod 12 | */ 13 | class Year extends PeriodAbstract implements \IteratorAggregate, \Stringable, IterablePeriod 14 | { 15 | #[\Override] 16 | public function getDatePeriod(): \DatePeriod 17 | { 18 | return new \DatePeriod($this->begin, new \DateInterval('P1D'), $this->end); 19 | } 20 | 21 | #[\Override] 22 | public static function isValid(\DateTimeInterface $start): bool 23 | { 24 | return '01-01 00:00:00' === $start->format('d-m H:i:s'); 25 | } 26 | 27 | #[\Override] 28 | public function getIterator(): \Generator 29 | { 30 | $current = $this->getFactory()->createMonth($this->begin); 31 | while ($this->contains($current->getBegin())) { 32 | yield (int) $current->getBegin()->format('m') => $current; 33 | 34 | $current = $current->getNext(); 35 | } 36 | } 37 | 38 | #[\Override] 39 | public function __toString(): string 40 | { 41 | return $this->format('Y'); 42 | } 43 | 44 | #[\Override] 45 | public static function getDateInterval(): \DateInterval 46 | { 47 | return new \DateInterval('P1Y'); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/Event/Provider/ArrayProvider.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | final class ArrayProvider implements ProviderInterface, \IteratorAggregate, \Countable 13 | { 14 | /** 15 | * @var EventInterface[] 16 | */ 17 | protected array $events = []; 18 | 19 | #[\Override] 20 | public function getEvents(\DateTimeInterface $begin, \DateTimeInterface $end, array $options = []): array 21 | { 22 | $events = []; 23 | foreach ($this->events as $event) { 24 | if ($event->getBegin() < $end && $event->getEnd() > $begin) { 25 | $events[] = $event; 26 | } 27 | } 28 | 29 | return $events; 30 | } 31 | 32 | public function add(EventInterface $event): void 33 | { 34 | $this->events[] = $event; 35 | } 36 | 37 | /** 38 | * @return EventInterface[] 39 | */ 40 | public function all(): array 41 | { 42 | return $this->events; 43 | } 44 | 45 | #[\Override] 46 | public function getIterator(): \ArrayIterator 47 | { 48 | return new \ArrayIterator($this->events); 49 | } 50 | 51 | #[\Override] 52 | public function count(): int 53 | { 54 | return \count($this->events); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/Period/Minute.php: -------------------------------------------------------------------------------- 1 | 9 | * @implements IterablePeriod 10 | */ 11 | final class Minute extends PeriodAbstract implements \IteratorAggregate, \Stringable, IterablePeriod 12 | { 13 | #[\Override] 14 | public function getDatePeriod(): \DatePeriod 15 | { 16 | return new \DatePeriod($this->begin, new \DateInterval('PT1S'), $this->end); 17 | } 18 | 19 | #[\Override] 20 | public function getIterator(): \Traversable 21 | { 22 | $current = $this->getFactory()->createSecond($this->begin); 23 | while ($this->contains($current->getBegin())) { 24 | /* No need to explicit key as whe start to 0 */ 25 | yield $current; 26 | 27 | $current = $this->getFactory()->createSecond($current->getBegin()->modify('+1 second')); 28 | } 29 | } 30 | 31 | #[\Override] 32 | public function __toString(): string 33 | { 34 | return $this->format('i'); 35 | } 36 | 37 | #[\Override] 38 | public static function isValid(\DateTimeInterface $start): bool 39 | { 40 | return '00' === $start->format('s'); 41 | } 42 | 43 | #[\Override] 44 | public static function getDateInterval(): \DateInterval 45 | { 46 | return new \DateInterval('PT1M'); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/Period/Week.php: -------------------------------------------------------------------------------- 1 | 9 | * @implements IterablePeriod 10 | */ 11 | final class Week extends PeriodAbstract implements \IteratorAggregate, \Stringable, IterablePeriod 12 | { 13 | public function getNumber(): int 14 | { 15 | return (int) $this->begin->format('W'); 16 | } 17 | 18 | #[\Override] 19 | public function getDatePeriod(): \DatePeriod 20 | { 21 | return new \DatePeriod($this->begin, new \DateInterval('P1D'), $this->end); 22 | } 23 | 24 | #[\Override] 25 | public function getIterator(): \Generator 26 | { 27 | $current = $this->getFactory()->createDay($this->begin); 28 | while ($this->contains($current->getBegin())) { 29 | yield $current->getBegin()->format('d-m-Y') => $current; 30 | 31 | $current = $current->getNext(); 32 | } 33 | } 34 | 35 | #[\Override] 36 | public function __toString(): string 37 | { 38 | return $this->format('W'); 39 | } 40 | 41 | #[\Override] 42 | public static function isValid(\DateTimeInterface $start): bool 43 | { 44 | return '00:00:00' === $start->format('H:i:s'); 45 | } 46 | 47 | #[\Override] 48 | public static function getDateInterval(): \DateInterval 49 | { 50 | return new \DateInterval('P1W'); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Bridge/Symfony/Bundle/Resources/config/services.php: -------------------------------------------------------------------------------- 1 | services() 15 | ->set(EventManager::class) 16 | ->public(); 17 | 18 | $container->services() 19 | ->set(PeriodFactory::class) 20 | ->public(); 21 | 22 | $container->services() 23 | ->set(Calendar::class) 24 | ->arg('$factory', service(PeriodFactory::class)) 25 | ->arg('$eventManager', service(EventManager::class)) 26 | ->public(); 27 | 28 | $container->services() 29 | ->set(CalendRExtension::class) 30 | ->arg('$factory', service(Calendar::class)) 31 | ->tag('twig.extension'); 32 | 33 | $container->services() 34 | ->alias('calendr', Calendar::class) 35 | ->public(); 36 | 37 | $container->services() 38 | ->alias('calendr.factory', PeriodFactory::class) 39 | ->public(); 40 | 41 | $container->services() 42 | ->alias('calendr.event_manager', EventManager::class) 43 | ->public(); 44 | }; 45 | -------------------------------------------------------------------------------- /src/Bridge/Symfony/Bundle/DependencyInjection/CalendRExtension.php: -------------------------------------------------------------------------------- 1 | getConfiguration($configs, $container); 20 | \assert(null !== $configuration); 21 | 22 | /** @var array{periods: array{default_first_weekday: DayOfWeek|int}} $config */ 23 | $config = $this->processConfiguration($configuration, $configs); 24 | 25 | $loader = new PhpFileLoader($container, new FileLocator(__DIR__.'/../Resources/config')); 26 | $loader->load('services.php'); 27 | 28 | $defaultFirstWeekday = $config['periods']['default_first_weekday']; 29 | if (!($defaultFirstWeekday instanceof DayOfWeek)) { 30 | $defaultFirstWeekday = DayOfWeek::from($defaultFirstWeekday); // @codeCoverageIgnore 31 | } 32 | 33 | $container 34 | ->getDefinition(Calendar::class) 35 | ->addMethodCall('setFirstWeekday', [$defaultFirstWeekday]); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | CHANGELOG 2 | ========= 3 | 4 | 4.0.0 5 | ----- 6 | 7 | * Simplified date comparisons 8 | * Removed all internal usage of \DateTime in favor of \DateTimeImmutable 9 | * Added enum DayOfWeek 10 | * Renamed Indexed and Basic classes 11 | * Renamed Cache provider and made it use PSR-16 instead of doctrine/cache 12 | * Added full type and generic types (psalm based) 13 | * Made Factory optional again, non-week based period does not need it at all. 14 | * Simplified iterators via \IteratorAggregate 15 | * Moved CI to GitHub Actions & Dagger 16 | * Dropped support for PHP < 8.2 17 | * Dropped support for Twig < 3 18 | 19 | 20 | 2.2.0 21 | ----- 22 | 23 | * Added `CalendR\Bridge` namespace and deprecated `CalendR\Extension` (no BC-break) 24 | * Removing `CalendR` directories to use full PSR-4 structure 25 | * Introduced built-in Symfony Bundle 26 | 27 | 2.1.2 28 | ----- 29 | 30 | * Made compatible with Symfony 4 / Twig 2 31 | * Deprecated Silex Service provider 32 | * Fixed a bug in events 33 | * Removed custom test bootstrap and migrated to PSR-4 34 | 35 | 2.1.0 36 | ----- 37 | * Made compatible with PHP 7.1+ 38 | * Updated to PHPUnit >= 4 39 | 40 | 2.0.0 41 | ----- 42 | 43 | * Non-strict mode does not exists anymore (use the factory / calendar to create periods) 44 | * No more google calendar provider (hard to maintain and a bit out of the scope of this library) 45 | * Removed first-monday / last-sunday in favor of first weekday / last weekday 46 | * Factory argument is now mandatory for period instantiation 47 | -------------------------------------------------------------------------------- /src/Period/Hour.php: -------------------------------------------------------------------------------- 1 | 11 | * @implements IterablePeriod 12 | */ 13 | final class Hour extends PeriodAbstract implements \IteratorAggregate, \Stringable, IterablePeriod 14 | { 15 | #[\Override] 16 | public function getDatePeriod(): \DatePeriod 17 | { 18 | return new \DatePeriod($this->begin, new \DateInterval('PT1M'), $this->end); 19 | } 20 | 21 | #[\Override] 22 | public static function isValid(\DateTimeInterface $start): bool 23 | { 24 | return '00:00' === $start->format('i:s'); 25 | } 26 | 27 | #[\Override] 28 | public function getIterator(): \Generator 29 | { 30 | $current = $this->getFactory()->createMinute($this->begin); 31 | while ($this->contains($current->getBegin())) { 32 | /* No need to explicit key as whe start to 0 */ 33 | yield $current; 34 | 35 | $current = $current->getNext(); 36 | } 37 | } 38 | 39 | #[\Override] 40 | public function __toString(): string 41 | { 42 | return $this->format('G'); 43 | } 44 | 45 | #[\Override] 46 | public static function getDateInterval(): \DateInterval 47 | { 48 | return new \DateInterval('PT1H'); 49 | } 50 | 51 | #[\Override] 52 | protected function createInvalidException(): NotAnHour 53 | { 54 | return new NotAnHour(); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/Bridge/Symfony/Bundle/DependencyInjection/Configuration.php: -------------------------------------------------------------------------------- 1 | getRootNode() 20 | ->children() 21 | ->arrayNode('periods') 22 | ->addDefaultsIfNotSet() 23 | ->children() 24 | ->enumNode('default_first_weekday'); 25 | 26 | if (method_exists($enumNode, 'enumFqcn')) { 27 | $enumNode 28 | ->enumFqcn(DayOfWeek::class) 29 | ->defaultValue(DayOfWeek::MONDAY); 30 | } else { 31 | // @codeCoverageIgnoreStart 32 | $enumNode 33 | ->values(array_map(fn (DayOfWeek $dayOfWeek) => $dayOfWeek->value, DayOfWeek::cases())) 34 | ->defaultValue(DayOfWeek::MONDAY->value) 35 | ->validate() 36 | ->ifNotInArray(array_map(static fn (DayOfWeek $d) => $d->value, DayOfWeek::cases())) 37 | ->thenInvalid('Day must be be between 0 (Sunday) and 6 (Saturday)') 38 | ->end(); 39 | // @codeCoverageIgnoreEnd 40 | } 41 | 42 | return $treeBuilder; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Bridge/Doctrine/ORM/EventRepository.php: -------------------------------------------------------------------------------- 1 | createQueryBuilderForGetEvent($options); 18 | 19 | return $qb 20 | ->andWhere( 21 | $qb->expr()->andX( 22 | $qb->expr()->lt($this->getBeginFieldName(), ':end'), 23 | $qb->expr()->gt($this->getEndFieldName(), ':begin'), 24 | ) 25 | ) 26 | ->setParameter(':begin', $begin) 27 | ->setParameter(':end', $end); 28 | } 29 | 30 | public function getEventsQuery(\DateTimeInterface $begin, \DateTimeInterface $end, array $options = []): AbstractQuery 31 | { 32 | return $this->getEventsQueryBuilder($begin, $end, $options)->getQuery(); 33 | } 34 | 35 | public function getEvents(\DateTimeInterface $begin, \DateTimeInterface $end, array $options = []): array 36 | { 37 | return $this->getEventsQuery($begin, $end, $options)->getResult(); 38 | } 39 | 40 | public function createQueryBuilderForGetEvent(array $options): QueryBuilder 41 | { 42 | return $this->createQueryBuilder('evt'); 43 | } 44 | 45 | abstract public function getBeginFieldName(): string; 46 | 47 | abstract public function getEndFieldName(): string; 48 | } 49 | -------------------------------------------------------------------------------- /src/Period/Range.php: -------------------------------------------------------------------------------- 1 | factory = $factory; 14 | $this->begin = \DateTimeImmutable::createFromInterface($begin); 15 | $this->end = \DateTimeImmutable::createFromInterface($end); 16 | } 17 | 18 | #[\Override] 19 | public static function isValid(\DateTimeInterface $start): bool 20 | { 21 | throw new NotImplemented('Range period doesn\'t support isValid().'); 22 | } 23 | 24 | #[\Override] 25 | public function getNext(): self 26 | { 27 | $diff = $this->begin->diff($this->end); 28 | $begin = $this->begin->add($diff); 29 | $end = $this->end->add($diff); 30 | 31 | return new self($begin, $end, $this->factory); 32 | } 33 | 34 | #[\Override] 35 | public function getPrevious(): self 36 | { 37 | $diff = $this->begin->diff($this->end); 38 | $begin = $this->begin->sub($diff); 39 | $end = $this->end->sub($diff); 40 | 41 | return new self($begin, $end, $this->factory); 42 | } 43 | 44 | #[\Override] 45 | public function getDatePeriod(): \DatePeriod 46 | { 47 | return new \DatePeriod($this->begin, $this->begin->diff($this->end), $this->end); 48 | } 49 | 50 | /** 51 | * @throws NotImplemented 52 | */ 53 | #[\Override] 54 | public static function getDateInterval(): \DateInterval 55 | { 56 | throw new NotImplemented('Range period doesn\'t support getDateInterval().'); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "yohang/calendr", 3 | "description": "Object Oriented calendar management", 4 | "keywords": [ 5 | "date", 6 | "calendar", 7 | "events" 8 | ], 9 | "homepage": "https://github.com/yohang/CalendR", 10 | "type": "library", 11 | "license": "MIT", 12 | "authors": [ 13 | { 14 | "name": "Yohan GIARELLI", 15 | "email": "yohan@giarel.li", 16 | "homepage": "http://yohan.giarel.li" 17 | } 18 | ], 19 | "require": { 20 | "php": ">=8.2" 21 | }, 22 | "require-dev": { 23 | "doctrine/doctrine-bundle": ">=2.7.2,<4.0", 24 | "doctrine/orm": "^2.9.3|^3.0.0", 25 | "friendsofphp/php-cs-fixer": "^3.90", 26 | "infection/infection": "^0.31.9", 27 | "phpspec/prophecy-phpunit": "^2.3.0", 28 | "phpunit/phpunit": "^11.5.44", 29 | "psr/simple-cache": ">=0.2.0,<4", 30 | "rector/rector": "^2.2", 31 | "symfony/browser-kit": ">=6.4,<9.0", 32 | "symfony/config": ">=4.4,<9.0", 33 | "symfony/css-selector": ">=6.4,<9.0", 34 | "symfony/dependency-injection": ">=4.4,<9.0", 35 | "symfony/framework-bundle": ">=6.4,<9.0", 36 | "symfony/http-kernel": ">=5.4,<9.0", 37 | "symfony/twig-bundle": ">=4.4,<9.0", 38 | "symfony/var-dumper": ">=4.4,<9.0", 39 | "twig/twig": "^3.3.2", 40 | "vimeo/psalm": "^6.13@dev" 41 | }, 42 | "autoload": { 43 | "psr-4": { 44 | "CalendR\\": "src/" 45 | } 46 | }, 47 | "autoload-dev": { 48 | "psr-4": { 49 | "CalendR\\Test\\": "tests/", 50 | "App\\": "tests/Bridge/Symfony/Fixtures/" 51 | } 52 | }, 53 | "extra": { 54 | "branch-alias": { 55 | "dev-master": "4.0.x-dev" 56 | } 57 | }, 58 | "config": { 59 | "allow-plugins": { 60 | "infection/extension-installer": true 61 | }, 62 | "sort-packages": true 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/Bridge/Twig/CalendRExtension.php: -------------------------------------------------------------------------------- 1 | 25 | */ 26 | #[\Override] 27 | public function getFunctions(): array 28 | { 29 | return [ 30 | new TwigFunction('calendr_year', $this->getYear(...)), 31 | new TwigFunction('calendr_month', $this->getMonth(...)), 32 | new TwigFunction('calendr_week', $this->getWeek(...)), 33 | new TwigFunction('calendr_day', $this->getDay(...)), 34 | new TwigFunction('calendr_events', $this->getEvents(...)), 35 | ]; 36 | } 37 | 38 | public function getYear(\DateTimeInterface|int $yearOrStart): Year 39 | { 40 | return $this->factory->getYear($yearOrStart); 41 | } 42 | 43 | public function getMonth(\DateTimeInterface|int $yearOrStart, ?int $month = null): Month 44 | { 45 | return $this->factory->getMonth($yearOrStart, $month); 46 | } 47 | 48 | public function getWeek(\DateTimeInterface|int $yearOrStart, ?int $week = null): Week 49 | { 50 | return $this->factory->getWeek($yearOrStart, $week); 51 | } 52 | 53 | public function getDay(\DateTimeInterface|int $yearOrStart, ?int $month = null, ?int $day = null): Day 54 | { 55 | return $this->factory->getDay($yearOrStart, $month, $day); 56 | } 57 | 58 | public function getEvents(PeriodInterface $period, array $options = []): CollectionInterface 59 | { 60 | return $this->factory->getEvents($period, $options); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/Event/Collection/ArrayCollection.php: -------------------------------------------------------------------------------- 1 | 15 | * @implements CollectionInterface 16 | */ 17 | final class ArrayCollection implements CollectionInterface, \IteratorAggregate 18 | { 19 | /** 20 | * @param list $events 21 | */ 22 | public function __construct( 23 | protected array $events = [], 24 | ) { 25 | } 26 | 27 | #[\Override] 28 | public function add(EventInterface $event): void 29 | { 30 | $this->events[] = $event; 31 | } 32 | 33 | #[\Override] 34 | public function remove(EventInterface $event): void 35 | { 36 | foreach ($this->events as $key => $internalEvent) { 37 | if ($event === $internalEvent) { 38 | unset($this->events[$key]); 39 | } 40 | } 41 | } 42 | 43 | #[\Override] 44 | public function all(): array 45 | { 46 | return $this->events; 47 | } 48 | 49 | #[\Override] 50 | public function has(PeriodInterface|\DateTimeInterface|string $index): bool 51 | { 52 | return \count($this->find($index)) > 0; 53 | } 54 | 55 | #[\Override] 56 | public function find(PeriodInterface|\DateTimeInterface|string $index): array 57 | { 58 | $result = []; 59 | foreach ($this->events as $event) { 60 | if ($index instanceof PeriodInterface && $index->containsEvent($event)) { 61 | $result[] = $event; 62 | } elseif ($index instanceof \DateTime && $event->contains($index)) { 63 | $result[] = $event; 64 | } 65 | } 66 | 67 | return $result; 68 | } 69 | 70 | #[\Override] 71 | public function count(): int 72 | { 73 | return \count($this->events); 74 | } 75 | 76 | #[\Override] 77 | public function getIterator(): \Traversable 78 | { 79 | return new \ArrayIterator($this->events); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/Period/PeriodInterface.php: -------------------------------------------------------------------------------- 1 | firstWeekday = $firstWeekday; 68 | } 69 | 70 | #[\Override] 71 | public function getFirstWeekday(): DayOfWeek 72 | { 73 | return $this->firstWeekday; 74 | } 75 | 76 | #[\Override] 77 | public function findFirstDayOfWeek(\DateTimeInterface $dateTime): \DateTimeImmutable 78 | { 79 | $day = \DateTimeImmutable::createFromInterface($dateTime); 80 | $delta = ((int) $day->format('w') - $this->getFirstWeekday()->value + 7) % 7; 81 | 82 | return $day->sub(new \DateInterval(\sprintf('P%sD', $delta))); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/Period/Month.php: -------------------------------------------------------------------------------- 1 | 9 | * @implements IterablePeriod 10 | */ 11 | final class Month extends PeriodAbstract implements \IteratorAggregate, \Stringable, IterablePeriod 12 | { 13 | #[\Override] 14 | public function getDatePeriod(): \DatePeriod 15 | { 16 | return new \DatePeriod($this->begin, new \DateInterval('P1D'), $this->end); 17 | } 18 | 19 | /** 20 | * Returns a Day array. 21 | * 22 | * @return array 23 | */ 24 | public function getDays(): array 25 | { 26 | $days = []; 27 | foreach ($this->getDatePeriod() as $date) { 28 | $days[] = $this->getFactory()->createDay($date); 29 | } 30 | 31 | return $days; 32 | } 33 | 34 | /** 35 | * Returns the first day of the first week of month. 36 | * First day of week is configurable via {@link PeriodFactory}. 37 | */ 38 | public function getFirstDayOfFirstWeek(): \DateTimeImmutable 39 | { 40 | return $this->getFactory()->findFirstDayOfWeek($this->begin); 41 | } 42 | 43 | /** 44 | * Returns a Range period beginning at the first day of first week of this month, 45 | * and ending at the last day of the last week of this month. 46 | */ 47 | public function getExtendedMonth(): PeriodInterface 48 | { 49 | return $this->getFactory()->createRange($this->getFirstDayOfFirstWeek(), $this->getLastDayOfLastWeek()); 50 | } 51 | 52 | /** 53 | * Returns the last day of last week of month 54 | * First day of week is configurable via {@link PeriodFactory}. 55 | */ 56 | public function getLastDayOfLastWeek(): \DateTimeImmutable 57 | { 58 | $lastDay = $this->end->sub(new \DateInterval('P1D')); 59 | 60 | return $this->getFactory()->findFirstDayOfWeek($lastDay)->add(new \DateInterval('P6D')); 61 | } 62 | 63 | #[\Override] 64 | public function getIterator(): \Generator 65 | { 66 | $current = $this->getFactory()->createWeek($this->getFirstDayOfFirstWeek()); 67 | while ($this->getExtendedMonth()->contains($current->getBegin())) { 68 | yield (int) $current->getBegin()->format('W') => $current; 69 | 70 | $current = $current->getNext(); 71 | } 72 | } 73 | 74 | #[\Override] 75 | public function __toString(): string 76 | { 77 | return $this->format('F'); 78 | } 79 | 80 | #[\Override] 81 | public static function isValid(\DateTimeInterface $start): bool 82 | { 83 | return '01 00:00:00' === $start->format('d H:i:s'); 84 | } 85 | 86 | #[\Override] 87 | public static function getDateInterval(): \DateInterval 88 | { 89 | return new \DateInterval('P1M'); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/Event/EventManager.php: -------------------------------------------------------------------------------- 1 | 20 | */ 21 | protected array $providers = []; 22 | 23 | /** 24 | * The callable used to instantiate the event collection. 25 | * 26 | * @var callable():CollectionInterface 27 | */ 28 | protected $collectionInstantiator; 29 | 30 | /** 31 | * @param iterable $providers 32 | * @param callable():CollectionInterface|null $collectionInstantiator 33 | */ 34 | public function __construct( 35 | iterable $providers = [], 36 | ?callable $collectionInstantiator = null, 37 | ) { 38 | $this->collectionInstantiator = $collectionInstantiator ?? static fn (): ArrayCollection => new ArrayCollection(); 39 | 40 | foreach ($providers as $name => $provider) { 41 | $this->addProvider($name, $provider); 42 | } 43 | } 44 | 45 | /** 46 | * find events that match the given period (during or over). 47 | * 48 | * @param array{providers?: list|string} $options 49 | * 50 | * @throws NoProviderFound 51 | */ 52 | public function find(PeriodInterface $period, array $options = []): CollectionInterface 53 | { 54 | if (0 === \count($this->providers)) { 55 | throw new NoProviderFound(); 56 | } 57 | 58 | // Check if there's a provider option provided, used to filter the used providers 59 | $providers = $options['providers'] ?? []; 60 | if (!\is_array($providers)) { 61 | $providers = [$providers]; 62 | } 63 | 64 | // Instantiate an event collection 65 | $collectionInstantiator = $this->collectionInstantiator; 66 | $collection = $collectionInstantiator(); 67 | foreach ($this->providers as $name => $provider) { 68 | if (\count($providers) > 0 && !\in_array($name, $providers, true)) { 69 | continue; 70 | } 71 | 72 | // Add matching events to the collection 73 | foreach ($provider->getEvents($period->getBegin(), $period->getEnd(), $options) as $event) { 74 | if ($period->containsEvent($event)) { 75 | $collection->add($event); 76 | } 77 | } 78 | } 79 | 80 | return $collection; 81 | } 82 | 83 | public function addProvider(string $name, ProviderInterface $provider): void 84 | { 85 | $this->providers[$name] = $provider; 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/Period/PeriodAbstract.php: -------------------------------------------------------------------------------- 1 | createInvalidException(); 26 | } 27 | 28 | $this->begin = \DateTimeImmutable::createFromInterface($begin); 29 | $this->end = $this->begin->add($this->getDateInterval()); 30 | } 31 | 32 | #[\Override] 33 | public function contains(\DateTimeInterface $date): bool 34 | { 35 | return $this->begin <= $date && $date < $this->end; 36 | } 37 | 38 | #[\Override] 39 | public function equals(PeriodInterface $period): bool 40 | { 41 | return 42 | $period instanceof static 43 | && $this->begin->format('Y-m-d-H-i-s') === $period->getBegin()->format('Y-m-d-H-i-s'); 44 | } 45 | 46 | #[\Override] 47 | public function includes(PeriodInterface $period, bool $strict = true): bool 48 | { 49 | if ($strict) { 50 | return $this->getBegin() <= $period->getBegin() && $this->getEnd() >= $period->getEnd(); 51 | } 52 | 53 | return $this->getBegin() < $period->getEnd() && $this->getEnd() > $period->getBegin(); 54 | } 55 | 56 | #[\Override] 57 | public function containsEvent(EventInterface $event): bool 58 | { 59 | return $this->getBegin() <= $event->getEnd() && $this->getEnd() > $event->getBegin(); 60 | } 61 | 62 | #[\Override] 63 | public function format(string $format): string 64 | { 65 | return $this->begin->format($format); 66 | } 67 | 68 | #[\Override] 69 | public function isCurrent(): bool 70 | { 71 | return $this->contains(new \DateTimeImmutable()); 72 | } 73 | 74 | /** 75 | * @psalm-suppress UnsafeInstantiation 76 | * 77 | * @throws Exception 78 | */ 79 | #[\Override] 80 | public function getNext(): PeriodInterface 81 | { 82 | return new static($this->end, $this->factory); 83 | } 84 | 85 | /** 86 | * @psalm-suppress UnsafeInstantiation 87 | * 88 | * @throws Exception 89 | */ 90 | #[\Override] 91 | public function getPrevious(): PeriodInterface 92 | { 93 | $start = $this->begin->sub(static::getDateInterval()); 94 | 95 | return new static($start, $this->factory); 96 | } 97 | 98 | #[\Override] 99 | public function getBegin(): \DateTimeImmutable 100 | { 101 | return $this->begin; 102 | } 103 | 104 | #[\Override] 105 | public function getEnd(): \DateTimeImmutable 106 | { 107 | return $this->end; 108 | } 109 | 110 | protected function getFactory(): PeriodFactoryInterface 111 | { 112 | if (null === $this->factory) { 113 | $this->factory = new PeriodFactory(); 114 | } 115 | 116 | return $this->factory; 117 | } 118 | 119 | /** 120 | * @psalm-suppress InvalidStringClass 121 | */ 122 | protected function createInvalidException(): Exception 123 | { 124 | $class = 'CalendR\Period\Exception\NotA'.(new \ReflectionClass($this))->getShortName(); 125 | $exception = new $class(); 126 | \assert($exception instanceof Exception); 127 | 128 | return $exception; 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /src/Event/Collection/IndexedCollection.php: -------------------------------------------------------------------------------- 1 | 15 | * @implements \IteratorAggregate 16 | */ 17 | final class IndexedCollection implements CollectionInterface, \IteratorAggregate 18 | { 19 | /** 20 | * @var array> 21 | */ 22 | protected array $events = []; 23 | 24 | /** 25 | * Event count. 26 | */ 27 | protected int $count = 0; 28 | 29 | /** 30 | * The function used to index events. 31 | * Takes a \DateTimeInterface in parameter and must return an array index for this value. 32 | * 33 | * By default: 34 | * ```php 35 | * function(\DateTimeInterface $dateTime) { 36 | * return $dateTime->format('Y-m-d'); 37 | * } 38 | * ``` 39 | * 40 | * @var callable(\DateTimeInterface):string 41 | */ 42 | protected $indexFunction; 43 | 44 | /** 45 | * @param list $events 46 | * @param callable(\DateTimeInterface):string|null $callable 47 | */ 48 | public function __construct(array $events = [], ?callable $callable = null) 49 | { 50 | $this->indexFunction = $callable ?? static fn (\DateTimeInterface $dateTime): string => $dateTime->format('Y-m-d'); 51 | 52 | foreach ($events as $event) { 53 | $this->add($event); 54 | } 55 | } 56 | 57 | /** 58 | * Adds an event to the collection. 59 | */ 60 | #[\Override] 61 | public function add(EventInterface $event): void 62 | { 63 | $index = $this->computeIndex($event); 64 | if (isset($this->events[$index])) { 65 | $this->events[$index][] = $event; 66 | } else { 67 | $this->events[$index] = [$event]; 68 | } 69 | 70 | ++$this->count; 71 | } 72 | 73 | #[\Override] 74 | public function remove(EventInterface $event): void 75 | { 76 | $index = $this->computeIndex($event); 77 | if (isset($this->events[$index])) { 78 | foreach ($this->events[$index] as $key => $internalEvent) { 79 | if ($event === $internalEvent) { 80 | unset($this->events[$index][$key]); 81 | --$this->count; 82 | } 83 | } 84 | } 85 | } 86 | 87 | #[\Override] 88 | public function has(PeriodInterface|\DateTimeInterface|string $index): bool 89 | { 90 | return 0 < \count($this->find($index)); 91 | } 92 | 93 | #[\Override] 94 | public function find(PeriodInterface|\DateTimeInterface|string $index): array 95 | { 96 | if ($index instanceof PeriodInterface) { 97 | $index = $index->getBegin(); 98 | } 99 | if ($index instanceof \DateTimeInterface) { 100 | $index = $this->computeIndex(\DateTimeImmutable::createFromInterface($index)); 101 | } 102 | 103 | return $this->events[$index] ?? []; 104 | } 105 | 106 | #[\Override] 107 | public function all(): array 108 | { 109 | $results = []; 110 | 111 | foreach ($this->events as $events) { 112 | $results = array_merge($results, $events); 113 | } 114 | 115 | return $results; 116 | } 117 | 118 | private function computeIndex(EventInterface|\DateTimeImmutable $toCompute): string 119 | { 120 | if ($toCompute instanceof EventInterface) { 121 | $toCompute = $toCompute->getBegin(); 122 | } 123 | 124 | $function = $this->indexFunction; 125 | 126 | return $function($toCompute); 127 | } 128 | 129 | #[\Override] 130 | public function count(): int 131 | { 132 | return $this->count; 133 | } 134 | 135 | #[\Override] 136 | public function getIterator(): \Traversable 137 | { 138 | return new \ArrayIterator($this->events); 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /src/Calendar.php: -------------------------------------------------------------------------------- 1 | getFactory()->createYear($yearOrStart); 41 | } 42 | 43 | public function getMonth(\DateTimeInterface|int $yearOrStart, ?int $month = null): Month 44 | { 45 | if (!$yearOrStart instanceof \DateTimeInterface) { 46 | $yearOrStart = new \DateTimeImmutable(\sprintf('%s-%s-01', $yearOrStart, $month ?? '')); 47 | } 48 | 49 | return $this->getFactory()->createMonth($yearOrStart); 50 | } 51 | 52 | public function getWeek(\DateTimeInterface|int $yearOrStart, ?int $week = null): Week 53 | { 54 | $factory = $this->getFactory(); 55 | 56 | if (!$yearOrStart instanceof \DateTimeInterface) { 57 | $yearOrStart = new \DateTimeImmutable(\sprintf('%s-W%s', $yearOrStart, str_pad((string) $week, 2, '0', \STR_PAD_LEFT))); 58 | } 59 | 60 | return $factory->createWeek($factory->findFirstDayOfWeek($yearOrStart)); 61 | } 62 | 63 | public function getDay(\DateTimeInterface|int $yearOrStart, ?int $month = null, ?int $day = null): Day 64 | { 65 | if (!$yearOrStart instanceof \DateTimeInterface) { 66 | $yearOrStart = new \DateTimeImmutable(\sprintf('%s-%s-%s', $yearOrStart, $month ?? '', $day ?? '')); 67 | } 68 | 69 | return $this->getFactory()->createDay($yearOrStart); 70 | } 71 | 72 | public function getHour(\DateTimeInterface|int $yearOrStart, ?int $month = null, ?int $day = null, ?int $hour = null): Hour 73 | { 74 | if (!$yearOrStart instanceof \DateTimeInterface) { 75 | $yearOrStart = new \DateTimeImmutable(\sprintf('%s-%s-%s %s:00', $yearOrStart, $month ?? '', $day ?? '', $hour ?? '')); 76 | } 77 | 78 | return $this->getFactory()->createHour($yearOrStart); 79 | } 80 | 81 | public function getMinute(\DateTimeInterface|int $yearOrStart, ?int $month = null, ?int $day = null, ?int $hour = null, ?int $minute = null): Minute 82 | { 83 | if (!$yearOrStart instanceof \DateTimeInterface) { 84 | $yearOrStart = new \DateTimeImmutable(\sprintf('%s-%s-%s %s:%s', $yearOrStart, $month ?? '', $day ?? '', $hour ?? '', $minute ?? '')); 85 | } 86 | 87 | return $this->getFactory()->createMinute($yearOrStart); 88 | } 89 | 90 | public function getSecond(\DateTimeInterface|int $yearOrStart, ?int $month = null, ?int $day = null, ?int $hour = null, ?int $minute = null, ?int $second = null): Second 91 | { 92 | if (!$yearOrStart instanceof \DateTimeInterface) { 93 | $yearOrStart = new \DateTimeImmutable( 94 | \sprintf('%s-%s-%s %s:%s:%s', $yearOrStart, $month ?? '', $day ?? '', $hour ?? '', $minute ?? '', $second ?? '') 95 | ); 96 | } 97 | 98 | return $this->getFactory()->createSecond($yearOrStart); 99 | } 100 | 101 | /** 102 | * @psalm-suppress MixedArgumentTypeCoercion 103 | * 104 | * @throws NoProviderFound 105 | */ 106 | public function getEvents(PeriodInterface $period, array $options = []): CollectionInterface 107 | { 108 | return $this->getEventManager()->find($period, $options); 109 | } 110 | 111 | public function getFactory(): PeriodFactoryInterface 112 | { 113 | return $this->factory; 114 | } 115 | 116 | public function getEventManager(): EventManager 117 | { 118 | return $this->eventManager; 119 | } 120 | 121 | public function setFirstWeekday(DayOfWeek $firstWeekday): void 122 | { 123 | $this->getFactory()->setFirstWeekday($firstWeekday); 124 | } 125 | 126 | public function getFirstWeekday(): DayOfWeek 127 | { 128 | return $this->factory->getFirstWeekday(); 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CalendR 2 | 3 | **A modern, object-oriented calendar management library for PHP 8.2+.** 4 | 5 | CalendR provides a clean, immutable, and iterable API to manipulate time periods (Years, Months, Weeks, Days...) and manage associated events. 6 | 7 | [![CI Status](https://github.com/yohang/CalendR/actions/workflows/ci.yml/badge.svg)](https://github.com/yohang/CalendR/actions/workflows/ci.yml) 8 | [![Coverage Status](https://coveralls.io/repos/github/yohang/CalendR/badge.svg?branch=main)](https://coveralls.io/github/yohang/CalendR?branch=main) 9 | [![Mutation testing badge](https://img.shields.io/endpoint?style=flat&url=https%3A%2F%2Fbadge-api.stryker-mutator.io%2Fgithub.com%2Fyohang%2FCalendR%2Fmain)](https://dashboard.stryker-mutator.io/reports/github.com/yohang/CalendR/main) 10 | 11 | ## ✨ Features 12 | 13 | * **Object-Oriented Periods:** Manipulate `Year`, `Month`, `Week`, `Day` as objects, not strings or timestamps. 14 | * **Fully Iterable:** Iterate over a Year to get Months, or a Month to get Days, using native `foreach` loops. 15 | * **Immutable by Design:** Based on `DateTimeImmutable`, ensuring safe date manipulations. 16 | * **Event Management:** Fetch and aggregate events from multiple sources (Doctrine, API, etc.) for any period. 17 | * **Zero Dependencies:** The core library has no external dependencies. 18 | * **Framework Integrations:** Includes a Symfony Bundle and Twig extensions. 19 | 20 | ## 📦 Installation 21 | 22 | ```bash 23 | composer require yohang/calendr 24 | ``` 25 | 26 | ## 🚀 Usage 27 | 28 | ### 1. Navigating Time 29 | 30 | The Calendar class is your main entry point. It acts as a factory to create periods configured with your preferences (e.g., first day of the week). 31 | 32 | ```php 33 | getYear(2025); 41 | 42 | foreach ($year as $month) { 43 | echo $month->format('F Y') . "\n"; 44 | 45 | // Iterate over days in that month 46 | foreach ($month as $day) { 47 | // ... 48 | } 49 | } 50 | ``` 51 | 52 | ### 2. Working with Periods 53 | 54 | Every period object implements PeriodInterface, providing powerful methods: 55 | 56 | ```php 57 | getMonth(2025, 1); // January 2025 60 | 61 | // Check containment 62 | if ($month->contains(new \DateTimeImmutable('2025-01-15'))) { 63 | echo "We are in the middle of the month!"; 64 | } 65 | 66 | // Navigation 67 | $nextMonth = $month->getNext(); // February 2025 68 | $prevMonth = $month->getPrevious(); // December 2024 69 | 70 | // DatePeriod compatibility 71 | foreach ($month->getDatePeriod() as $date) { 72 | // $date is a DateTimeImmutable 73 | } 74 | ``` 75 | 76 | ### 3. Managing Events 77 | 78 | CalendR can attach events to any period. You need to configure an EventManager with one or more ProviderInterface. 79 | 80 | #### Implement your Event 81 | 82 | ```php 83 | begin; } 95 | public function getEnd(): \DateTimeInterface { return $this->end; } 96 | public function getUid(): string { return uniqid(); } 97 | } 98 | ``` 99 | 100 | #### Create a Provider 101 | 102 | ```php 103 | getEventManager(); 125 | $manager->addProvider('my_source', new MyProvider()); 126 | 127 | $month = $calendar->getMonth(2025, 1); 128 | $events = $manager->find($month); 129 | 130 | foreach ($events as $event) { 131 | // ... 132 | } 133 | ``` 134 | 135 | ## 🧩 Symfony & Twig Integration 136 | 137 | If you use Symfony, the bundle is automatically configured. 138 | 139 | ```yaml 140 | # config/packages/calendr.yaml 141 | calendr: 142 | periods: 143 | default_first_weekday: 1 # Monday 144 | ``` 145 | 146 | ### Twig Functions 147 | 148 | You can access periods directly in your templates: 149 | 150 | ```twig 151 | {# Iterate over days of the current month #} 152 | {% set month = calendr_month(2025, 1) %} 153 | 154 | 155 | {% for week in month %} 156 | 157 | {% for day in week %} 158 | 166 | {% endfor %} 167 | 168 | {% endfor %} 169 |
159 | {{ day.begin|date('d') }} 160 | 161 | {# Fetch events for this specific day #} 162 | {% for event in calendr_events(day) %} 163 | {{ event.uid }} 164 | {% endfor %} 165 |
170 | ``` 171 | --------------------------------------------------------------------------------