From 86c93e8d3e709282f05900372162dd7e4ba3a01d Mon Sep 17 00:00:00 2001 From: Sebastian Krupinski Date: Sun, 17 May 2026 17:49:01 -0400 Subject: [PATCH] refactor: service entity list and fetch Signed-off-by: Sebastian Krupinski --- lib/Client/Command/StoreCommand.php | 6 +- lib/Providers/Provider.php | 1 - lib/Providers/Service.php | 89 +++++++++++++++++------------ 3 files changed, 57 insertions(+), 39 deletions(-) diff --git a/lib/Client/Command/StoreCommand.php b/lib/Client/Command/StoreCommand.php index 7a40a73..9187c8b 100644 --- a/lib/Client/Command/StoreCommand.php +++ b/lib/Client/Command/StoreCommand.php @@ -28,9 +28,9 @@ final class StoreCommand implements CommandInterface */ public function __construct( FetchTarget|string|SequenceSet|null $target = null, - private readonly array $flags = [], - private readonly string $action = '', - private readonly bool $silent = true, + private array $flags = [], + private string $action = '', + private bool $silent = true, ) { $resolvedTarget = match (true) { $target instanceof FetchTarget => $target, diff --git a/lib/Providers/Provider.php b/lib/Providers/Provider.php index 39b9df9..a572230 100644 --- a/lib/Providers/Provider.php +++ b/lib/Providers/Provider.php @@ -27,7 +27,6 @@ use KTXM\ProviderImap\Stores\ServiceStore; class Provider implements ProviderBaseInterface, ProviderServiceMutateInterface, ProviderServiceDiscoverInterface, ProviderServiceTestInterface { - public const JSON_TYPE = ProviderBaseInterface::JSON_TYPE; protected const PROVIDER_IDENTIFIER = 'imap'; protected const PROVIDER_LABEL = 'IMAP Mail Provider'; protected const PROVIDER_DESCRIPTION = 'Provides mail services via the IMAP protocol'; diff --git a/lib/Providers/Service.php b/lib/Providers/Service.php index f6b5023..f916de3 100644 --- a/lib/Providers/Service.php +++ b/lib/Providers/Service.php @@ -11,7 +11,6 @@ namespace KTXM\ProviderImap\Providers; use Generator; use KTXF\Mail\Collection\CollectionBaseInterface; -use KTXF\Mail\Collection\CollectionMutableInterface; use KTXF\Mail\Collection\CollectionPropertiesBaseInterface; use KTXF\Mail\Object\Address; use KTXF\Mail\Object\AddressInterface; @@ -40,6 +39,7 @@ use KTXM\ProviderImap\Service\Remote\RemoteService; use KTXM\ProviderImap\Providers\CollectionResource; use KTXF\Mail\Collection\CollectionRoles; use KTXF\Mail\Object\MessagePropertiesMutableInterface; +use KTXF\Resource\Identifier\EntityIdentifierInterface; use KTXM\ProviderImap\Providers\EntityResource; /** @@ -498,7 +498,7 @@ class Service implements ServiceBaseInterface, ServiceMutableInterface, ServiceC } $result = match ($deleteMode) { - 'soft' => $this->collectionMove($target, new CollectionIdentifier($target->provider(), $target->service(), $deleteTarget)), + 'soft' => $this->collectionMove(new CollectionIdentifier($target->provider(), $target->service(), $deleteTarget), $target), 'hard' => $this->mailService->collectionDestroy((string) $target->collection()), }; return $result; @@ -520,8 +520,9 @@ class Service implements ServiceBaseInterface, ServiceMutableInterface, ServiceC $sourceDelimiter = $sourceMailbox->delimiter() ?? '/'; $targetDelimiter = $targetMailbox->delimiter() ?? '/'; - $targetPath = rtrim($targetMailbox->name(), $targetDelimiter) . $targetDelimiter . end(explode($sourceDelimiter, $sourceMailbox->name())); - $mutatedMailbox = $this->mailService->collectionRename($sourceMailbox->name(), $targetPath); + $extantPath = $sourceMailbox->name(); + $freshPath = rtrim($targetMailbox->name(), $targetDelimiter) . $targetDelimiter . end(explode($sourceDelimiter, $extantPath)); + $mutatedMailbox = $this->mailService->collectionRename($extantPath, $freshPath); $collection = $this->collectionFresh(); $collection->fromImap($mutatedMailbox, ['delimiter' => $targetDelimiter]); @@ -530,9 +531,9 @@ class Service implements ServiceBaseInterface, ServiceMutableInterface, ServiceC // ── Entity operations ───────────────────────────────────────────────────── - public function entityList(string|int $collection, ?IFilter $filter = null, ?ISort $sort = null, ?IRange $range = null, ?array $properties = null): array + public function entityListBulk(string|int $collection, ?IFilter $filter = null, ?ISort $sort = null, ?IRange $range = null, ?array $properties = null): array { - return iterator_to_array($this->entityList((string) $collection, $filter, $sort, $range), true); + return iterator_to_array($this->entityListStream((string) $collection, $filter, $sort, $range), true); } public function entityListStream(string|int $collection, ?IFilter $filter = null, ?ISort $sort = null, ?IRange $range = null, ?array $properties = null): Generator @@ -542,7 +543,7 @@ class Service implements ServiceBaseInterface, ServiceMutableInterface, ServiceC foreach ($this->mailService->entityList((string) $collection, $filter, $sort, $range) as $identifier => $message) { $resource = $this->entityFresh(); $resource->fromImap($message, $collection); - yield $identifier => $resource; + yield $resource->urn() => $resource; } } @@ -564,12 +565,25 @@ class Service implements ServiceBaseInterface, ServiceMutableInterface, ServiceC }; } - public function entityFetch(string|int $collection, string|int ...$identifiers): array + public function entityFetchBulk(EntityIdentifierInterface ...$identifiers): array + { + return iterator_to_array($this->entityFetchStream(...$identifiers), true); + } + + public function entityFetchStream(EntityIdentifierInterface ...$identifiers): Generator { $this->initialize(); - $uids = array_map('intval', $identifiers); - return $this->mailService->entityFetch((string) $collection, ...$uids); + $identifiers = $this->groupEntitiesByCollection(...$identifiers); + + foreach ($identifiers as $collection => $entities) { + $uids = array_keys($entities); + foreach ($this->mailService->entityFetch((string) $collection, ...$uids) as $uid => $message) { + $resource = $this->entityFresh(); + $resource->fromImap($message, $collection); + yield $resource->urn() => $resource; + } + } } public function entityDelta(string|int $collection, string $signature, string $detail = 'ids'): Delta @@ -605,6 +619,29 @@ class Service implements ServiceBaseInterface, ServiceMutableInterface, ServiceC throw new \RuntimeException('Entity modification is not supported in this service'); } + public function entityPatch(MessagePropertiesMutableInterface $properties, EntityIdentifier ...$targets): array + { + // validate identifiers and group by collection + $targets = $this->groupEntitiesByCollection(...$targets); + + // move entities on remote store and construct result map + $this->initialize(); + + $list = []; + + foreach ($targets as $targetCollection => $targetIdentifiers) { + $uids = array_keys($targetIdentifiers); + + $mutations = $this->mailService->entityPatch($targetCollection, $properties, ...$uids); + + foreach ($uids as $uid) { + $list[(string)$targetIdentifiers[$uid]] = ['disposition' => 'patched']; + } + } + + return $list; + } + public function entityDelete(EntityIdentifier ...$targets): array { // validate identifiers and group by collection @@ -643,6 +680,11 @@ class Service implements ServiceBaseInterface, ServiceMutableInterface, ServiceC $deleteTargetIdentifier = new CollectionIdentifier($this->provider(), (string) $this->identifier(), $deleteTargetNative); } + // if all targets are already in the delete target collection, we should hard delete instead of moving to avoid duplicates in the trash + if (array_keys($targets) === [$deleteTargetNative]) { + $deleteMode = 'hard'; + } + // entities need to be moved or deleted by collection $list = []; foreach ($targets as $sourceCollection => $sourceEntities) { @@ -658,11 +700,11 @@ class Service implements ServiceBaseInterface, ServiceMutableInterface, ServiceC }; foreach ($uids as $uid) { - $mutatedUid = $mutations[$uid] ?? null; + $mutatedUid = !isset($mutations[$uid]) || $mutations[$uid] === true ? null : $mutations[$uid]; $list[(string)$sourceEntities[$uid]] = [ 'disposition' => $deleteMode === 'soft' ? 'moved' : 'deleted', 'destination' => $deleteMode === 'soft' ? $deleteTargetIdentifier : null, - 'mutation' => $mutatedUid !== null ? new EntityIdentifier($this->provider(), $this->identifier(), $deleteTargetIdentifier->collection(), $mutatedUid) : null, + 'mutation' => $deleteMode === 'soft' && $mutatedUid !== null ? new EntityIdentifier($this->provider(), $this->identifier(), $deleteTargetIdentifier->collection(), $mutatedUid) : null, ]; } } @@ -670,29 +712,6 @@ class Service implements ServiceBaseInterface, ServiceMutableInterface, ServiceC return $list; } - public function entityPatch(MessagePropertiesMutableInterface $properties, EntityIdentifier ...$targets): array - { - // validate identifiers and group by collection - $targets = $this->groupEntitiesByCollection(...$targets); - - // move entities on remote store and construct result map - $this->initialize(); - - $list = []; - - foreach ($targets as $targetCollection => $targetIdentifiers) { - $uids = array_keys($targetIdentifiers); - - $mutations = $this->mailService->entityPatch($targetCollection, $properties, ...$uids); - - foreach ($uids as $uid) { - $list[(string)$targetIdentifiers[$uid]] = ['disposition' => 'patched']; - } - } - - return $list; - } - public function entityCopy(CollectionIdentifier $target, EntityIdentifier ...$sources): array { // validate target belongs to this service -- 2.39.5