diff --git a/shared/lib/Mail/Service/ServiceEntityMutableInterface.php b/shared/lib/Mail/Service/ServiceEntityMutableInterface.php index 9736cd0..ef144e3 100644 --- a/shared/lib/Mail/Service/ServiceEntityMutableInterface.php +++ b/shared/lib/Mail/Service/ServiceEntityMutableInterface.php @@ -79,25 +79,23 @@ interface ServiceEntityMutableInterface extends ServiceBaseInterface { * * @since 2025.05.01 * - * @param string|int $sourceCollection Source collection identifier - * @param string|int $targetCollection Target collection identifier - * @param string|int ...$identifiers Entity identifiers to copy + * @param string|int $target Target collection identifier + * @param array> $sources Source entities to move (collection identifier => [entity identifier]) * - * @return array Map of source identifier => new identifier + * @return array List of moved entity identifiers */ - public function entityCopy(string|int $sourceCollection, string|int $targetCollection, string|int ...$identifiers): array; + public function entityCopy(string|int $target, array $sources): array; /** * Moves entities to another collection * * @since 2025.05.01 - * - * @param string|int $sourceCollection Source collection identifier - * @param string|int $targetCollection Target collection identifier - * @param string|int ...$identifiers Entity identifiers to move + * + * @param string|int $target Target collection identifier + * @param array> $sources Source entities to move (collection identifier => [entity identifier]) * * @return array List of moved entity identifiers */ - public function entityMove(string|int $sourceCollection, string|int $targetCollection, string|int ...$identifiers): array; + public function entityMove(string|int $target, array $sources): array; } diff --git a/shared/lib/Resource/Identifier/CollectionIdentifier.php b/shared/lib/Resource/Identifier/CollectionIdentifier.php new file mode 100644 index 0000000..4f9ed5f --- /dev/null +++ b/shared/lib/Resource/Identifier/CollectionIdentifier.php @@ -0,0 +1,39 @@ + + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace KTXF\Resource\Identifier; + +/** + * Collection-level resource identifier (depth 3) + * + * Format: {provider}:{service}:{collection} + */ +class CollectionIdentifier extends ServiceIdentifier implements CollectionIdentifierInterface { + + public function __construct( + string $provider, + string $service, + private readonly string $_collection, + ) { + parent::__construct($provider, $service); + } + + public function collection(): string { + return $this->_collection; + } + + public function depth(): int { + return 3; + } + + public function __toString(): string { + return parent::__toString() . self::SEPARATOR . $this->_collection; + } + +} diff --git a/shared/lib/Resource/Identifier/CollectionIdentifierInterface.php b/shared/lib/Resource/Identifier/CollectionIdentifierInterface.php new file mode 100644 index 0000000..a8a4c20 --- /dev/null +++ b/shared/lib/Resource/Identifier/CollectionIdentifierInterface.php @@ -0,0 +1,20 @@ + + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace KTXF\Resource\Identifier; + +/** + * Collection-level identifier (provider + service + collection) + */ +interface CollectionIdentifierInterface extends ServiceIdentifierInterface { + + /** The collection segment (e.g. "inbox") */ + public function collection(): string; + +} diff --git a/shared/lib/Resource/Identifier/EntityIdentifier.php b/shared/lib/Resource/Identifier/EntityIdentifier.php new file mode 100644 index 0000000..5266c54 --- /dev/null +++ b/shared/lib/Resource/Identifier/EntityIdentifier.php @@ -0,0 +1,40 @@ + + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace KTXF\Resource\Identifier; + +/** + * Entity-level resource identifier (depth 4) + * + * Format: {provider}:{service}:{collection}:{entity} + */ +class EntityIdentifier extends CollectionIdentifier implements EntityIdentifierInterface { + + public function __construct( + string $provider, + string $service, + string $collection, + private readonly string $_entity, + ) { + parent::__construct($provider, $service, $collection); + } + + public function entity(): string { + return $this->_entity; + } + + public function depth(): int { + return 4; + } + + public function __toString(): string { + return parent::__toString() . self::SEPARATOR . $this->_entity; + } + +} diff --git a/shared/lib/Resource/Identifier/EntityIdentifierInterface.php b/shared/lib/Resource/Identifier/EntityIdentifierInterface.php new file mode 100644 index 0000000..21bb571 --- /dev/null +++ b/shared/lib/Resource/Identifier/EntityIdentifierInterface.php @@ -0,0 +1,20 @@ + + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace KTXF\Resource\Identifier; + +/** + * Entity-level identifier (provider + service + collection + entity) + */ +interface EntityIdentifierInterface extends CollectionIdentifierInterface { + + /** The entity segment (e.g. "1001") */ + public function entity(): string; + +} diff --git a/shared/lib/Resource/Identifier/ResourceIdentifier.php b/shared/lib/Resource/Identifier/ResourceIdentifier.php new file mode 100644 index 0000000..edf929c --- /dev/null +++ b/shared/lib/Resource/Identifier/ResourceIdentifier.php @@ -0,0 +1,57 @@ + + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace KTXF\Resource\Identifier; + +/** + * Provider-level resource identifier (depth 1) + * + * Format: {provider} + */ +class ResourceIdentifier implements ResourceIdentifierInterface { + + public const SEPARATOR = ':'; + + public function __construct( + private readonly string $provider, + ) { + } + + public function provider(): string { + return $this->provider; + } + + public function depth(): int { + return 1; + } + + public function __toString(): string { + return $this->provider; + } + + /** + * Parse a colon-separated identifier string and return the appropriate level class + * + * @return ResourceIdentifier|ServiceIdentifier|CollectionIdentifier|EntityIdentifier + */ + public static function fromString(string $identifier): ResourceIdentifierInterface { + $parts = explode(self::SEPARATOR, $identifier, 4); + if (count($parts) < 1 || $parts[0] === '') { + throw new \InvalidArgumentException("Invalid resource identifier: {$identifier}"); + } + + return match (count($parts)) { + 4 => new EntityIdentifier($parts[0], $parts[1], $parts[2], $parts[3]), + 3 => new CollectionIdentifier($parts[0], $parts[1], $parts[2]), + 2 => new ServiceIdentifier($parts[0], $parts[1]), + default => new self($parts[0]), + }; + } + +} diff --git a/shared/lib/Resource/Identifier/ResourceIdentifierInterface.php b/shared/lib/Resource/Identifier/ResourceIdentifierInterface.php new file mode 100644 index 0000000..ee5ddc7 --- /dev/null +++ b/shared/lib/Resource/Identifier/ResourceIdentifierInterface.php @@ -0,0 +1,26 @@ + + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace KTXF\Resource\Identifier; + +/** + * Top-level identifier for resources (provider level) + */ +interface ResourceIdentifierInterface extends \Stringable { + + /** The provider segment (e.g. "imap") */ + public function provider(): string; + + /** Number of segments present (1–4) */ + public function depth(): int; + + /** Canonical string form: provider[:service[:collection[:entity]]] */ + public function __toString(): string; + +} diff --git a/shared/lib/Resource/Identifier/ResourceIdentifiers.php b/shared/lib/Resource/Identifier/ResourceIdentifiers.php new file mode 100644 index 0000000..e0c1c2a --- /dev/null +++ b/shared/lib/Resource/Identifier/ResourceIdentifiers.php @@ -0,0 +1,154 @@ + + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace KTXF\Resource\Identifier; + +/** + * Typed collection of resource identifiers + * + * Accepts an array of identifier strings (e.g. ["imap:account1:inbox:1001", "imap:account1:sent"]) + * and provides filtering/search helpers. + */ +class ResourceIdentifiers implements ResourceIdentifiersInterface { + + /** @var ResourceIdentifierInterface[] */ + private array $identifiers = []; + + /** Create a collection from an array of identifier strings */ + public static function fromArray(array $strings): static { + $collection = new static(); + foreach ($strings as $string) { + if (!is_string($string)) { + throw new \InvalidArgumentException('Each identifier must be a string'); + } + $collection->add(ResourceIdentifier::fromString($string)); + } + return $collection; + } + + public function add(ResourceIdentifierInterface $identifier): void { + $this->identifiers[] = $identifier; + } + + /** @return ResourceIdentifierInterface[] */ + public function all(): array { + return $this->identifiers; + } + + public function count(): int { + return count($this->identifiers); + } + + public function getIterator(): \ArrayIterator { + return new \ArrayIterator($this->identifiers); + } + + /** Filter identifiers by depth level (1–4) */ + public function byDepth(int $depth): static { + $filtered = new static(); + foreach ($this->identifiers as $id) { + if ($id->depth() === $depth) { + $filtered->add($id); + } + } + return $filtered; + } + + /** Filter identifiers by provider */ + public function byProvider(string $provider): static { + $filtered = new static(); + foreach ($this->identifiers as $id) { + if ($id->provider() === $provider) { + $filtered->add($id); + } + } + return $filtered; + } + + /** Filter identifiers by service (only identifiers with depth >= 2) */ + public function byService(string $service): static { + $filtered = new static(); + foreach ($this->identifiers as $id) { + if ($id instanceof ServiceIdentifierInterface && $id->service() === $service) { + $filtered->add($id); + } + } + return $filtered; + } + + /** Filter identifiers by collection (only identifiers with depth >= 3) */ + public function byCollection(string $collection): static { + $filtered = new static(); + foreach ($this->identifiers as $id) { + if ($id instanceof CollectionIdentifierInterface && $id->collection() === $collection) { + $filtered->add($id); + } + } + return $filtered; + } + + /** Filter identifiers by entity (only identifiers with depth == 4) */ + public function byEntity(string $entity): static { + $filtered = new static(); + foreach ($this->identifiers as $id) { + if ($id instanceof EntityIdentifierInterface && $id->entity() === $entity) { + $filtered->add($id); + } + } + return $filtered; + } + + /** Get unique provider names */ + public function providers(): array { + $values = []; + foreach ($this->identifiers as $id) { + $values[$id->provider()] = true; + } + return array_keys($values); + } + + /** Get unique service names */ + public function services(): array { + $values = []; + foreach ($this->identifiers as $id) { + if ($id instanceof ServiceIdentifierInterface) { + $values[$id->service()] = true; + } + } + return array_keys($values); + } + + /** Get unique collection names */ + public function collections(): array { + $values = []; + foreach ($this->identifiers as $id) { + if ($id instanceof CollectionIdentifierInterface) { + $values[$id->collection()] = true; + } + } + return array_keys($values); + } + + /** Get unique entity names */ + public function entities(): array { + $values = []; + foreach ($this->identifiers as $id) { + if ($id instanceof EntityIdentifierInterface) { + $values[$id->entity()] = true; + } + } + return array_keys($values); + } + + /** Check if the collection is empty */ + public function isEmpty(): bool { + return count($this->identifiers) === 0; + } + +} diff --git a/shared/lib/Resource/Identifier/ResourceIdentifiersInterface.php b/shared/lib/Resource/Identifier/ResourceIdentifiersInterface.php new file mode 100644 index 0000000..c59c3dd --- /dev/null +++ b/shared/lib/Resource/Identifier/ResourceIdentifiersInterface.php @@ -0,0 +1,50 @@ + + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace KTXF\Resource\Identifier; + +/** + * A typed collection of resource identifiers with search and filter capabilities + */ +interface ResourceIdentifiersInterface extends \Countable, \IteratorAggregate { + + /** Add an identifier to the collection */ + public function add(ResourceIdentifierInterface $identifier): void; + + /** Get all identifiers */ + public function all(): array; + + /** Filter identifiers by depth level (1–4) */ + public function byDepth(int $depth): static; + + /** Filter identifiers by provider */ + public function byProvider(string $provider): static; + + /** Filter identifiers by service (requires depth >= 2) */ + public function byService(string $service): static; + + /** Filter identifiers by collection (requires depth >= 3) */ + public function byCollection(string $collection): static; + + /** Filter identifiers by entity (requires depth == 4) */ + public function byEntity(string $entity): static; + + /** Get unique provider names */ + public function providers(): array; + + /** Get unique service names */ + public function services(): array; + + /** Get unique collection names */ + public function collections(): array; + + /** Get unique entity names */ + public function entities(): array; + +} diff --git a/shared/lib/Resource/Identifier/ServiceIdentifier.php b/shared/lib/Resource/Identifier/ServiceIdentifier.php new file mode 100644 index 0000000..dda1437 --- /dev/null +++ b/shared/lib/Resource/Identifier/ServiceIdentifier.php @@ -0,0 +1,38 @@ + + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace KTXF\Resource\Identifier; + +/** + * Service-level resource identifier (depth 2) + * + * Format: {provider}:{service} + */ +class ServiceIdentifier extends ResourceIdentifier implements ServiceIdentifierInterface { + + public function __construct( + string $provider, + private readonly string $_service, + ) { + parent::__construct($provider); + } + + public function service(): string { + return $this->_service; + } + + public function depth(): int { + return 2; + } + + public function __toString(): string { + return parent::__toString() . self::SEPARATOR . $this->_service; + } + +} diff --git a/shared/lib/Resource/Identifier/ServiceIdentifierInterface.php b/shared/lib/Resource/Identifier/ServiceIdentifierInterface.php new file mode 100644 index 0000000..fa90374 --- /dev/null +++ b/shared/lib/Resource/Identifier/ServiceIdentifierInterface.php @@ -0,0 +1,20 @@ + + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace KTXF\Resource\Identifier; + +/** + * Service-level identifier (provider + service) + */ +interface ServiceIdentifierInterface extends ResourceIdentifierInterface { + + /** The service segment (e.g. "account1") */ + public function service(): string; + +}