* SPDX-License-Identifier: AGPL-3.0-or-later */ namespace KTXM\ChronoManager\Export; use Generator; use KTXF\Chrono\Entity\EventObject; use KTXF\Chrono\Entity\JournalObject; use KTXF\Chrono\Entity\TaskObject; use KTXF\Resource\Identifier\CollectionIdentifier; use KTXF\Resource\Identifier\EntityIdentifier; use KTXF\Resource\Identifier\ResourceIdentifiers; use KTXM\ChronoManager\Manager; use KTXM\ChronoManager\Conversion\IcalEncoder; use Psr\Log\LoggerInterface; use Throwable; /** Exports Chrono entities as a streamed RFC 5545 iCalendar document. */ class ExportService { public function __construct( private readonly Manager $manager, private readonly IcalEncoder $encoder, private readonly LoggerInterface $logger, ) {} /** * @param ResourceIdentifiers $sources collection and/or entity identifiers * @return Generator iCalendar document chunks */ public function export(ResourceIdentifiers $sources, string $tenantId, string $userId): Generator { yield from $this->encoder->toIcs($this->entities($sources, $tenantId, $userId)); } /** @return Generator */ private function entities(ResourceIdentifiers $sources, string $tenantId, string $userId): Generator { $collections = []; $entities = []; foreach ($sources as $source) { if ($source instanceof EntityIdentifier) { $entities[] = $source; } elseif ($source instanceof CollectionIdentifier) { $collections[] = $source; } } if ($collections !== []) { $listed = $this->manager->entityListBulk($tenantId, $userId, new ResourceIdentifiers($collections)); foreach ($listed as $services) { foreach ($services as $collectionSets) { foreach ($collectionSets as $set) { foreach ((array)$set as $entity) { if (($hydrated = $this->hydrate($entity)) !== null) { yield $hydrated; } } } } } } if ($entities !== []) { foreach ($this->manager->entityFetchBulk($tenantId, $userId, ...$entities) as $entity) { if (($hydrated = $this->hydrate($entity)) !== null) { yield $hydrated; } } } } /** Skip-and-continue on per-entity failure, matching import's error-tolerant posture. */ private function hydrate(mixed $entity): EventObject|TaskObject|JournalObject|null { if (!is_object($entity)) { return null; } try { $hydrated = $entity->getProperties(); // DAV-created entities may carry no urid; the provider identifier // keeps exported UIDs stable in that case. $hydrated->urid ??= (string)$entity->identifier(); return $hydrated; } catch (Throwable $t) { $this->logger->warning('Skipping entity during export', ['exception' => $t]); return null; } } }