Files
2026-07-17 19:13:08 -04:00

94 lines
3.3 KiB
PHP

<?php
declare(strict_types=1);
/**
* SPDX-FileCopyrightText: Sebastian Krupinski <krupinski01@gmail.com>
* 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<string> 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<EventObject|TaskObject|JournalObject> */
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;
}
}
}