feat: entity move #42

Merged
Sebastian merged 1 commits from feat/entity-move into main 2026-03-28 00:41:55 +00:00
11 changed files with 472 additions and 10 deletions

View File

@@ -79,25 +79,23 @@ interface ServiceEntityMutableInterface extends ServiceBaseInterface {
* *
* @since 2025.05.01 * @since 2025.05.01
* *
* @param string|int $sourceCollection Source collection identifier * @param string|int $target Target collection identifier
* @param string|int $targetCollection Target collection identifier * @param array<string|int,array<string|int>> $sources Source entities to move (collection identifier => [entity identifier])
* @param string|int ...$identifiers Entity identifiers to copy
* *
* @return array<string|int,string|int> Map of source identifier => new identifier * @return array<string|int,bool> 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 * Moves entities to another collection
* *
* @since 2025.05.01 * @since 2025.05.01
* *
* @param string|int $sourceCollection Source collection identifier * @param string|int $target Target collection identifier
* @param string|int $targetCollection Target collection identifier * @param array<string|int,array<string|int>> $sources Source entities to move (collection identifier => [entity identifier])
* @param string|int ...$identifiers Entity identifiers to move
* *
* @return array<string|int,bool> List of moved entity identifiers * @return array<string|int,bool> 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;
} }

View File

@@ -0,0 +1,39 @@
<?php
declare(strict_types=1);
/**
* SPDX-FileCopyrightText: Sebastian Krupinski <krupinski01@gmail.com>
* 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;
}
}

View File

@@ -0,0 +1,20 @@
<?php
declare(strict_types=1);
/**
* SPDX-FileCopyrightText: Sebastian Krupinski <krupinski01@gmail.com>
* 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;
}

View File

@@ -0,0 +1,40 @@
<?php
declare(strict_types=1);
/**
* SPDX-FileCopyrightText: Sebastian Krupinski <krupinski01@gmail.com>
* 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;
}
}

View File

@@ -0,0 +1,20 @@
<?php
declare(strict_types=1);
/**
* SPDX-FileCopyrightText: Sebastian Krupinski <krupinski01@gmail.com>
* 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;
}

View File

@@ -0,0 +1,57 @@
<?php
declare(strict_types=1);
/**
* SPDX-FileCopyrightText: Sebastian Krupinski <krupinski01@gmail.com>
* 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]),
};
}
}

View File

@@ -0,0 +1,26 @@
<?php
declare(strict_types=1);
/**
* SPDX-FileCopyrightText: Sebastian Krupinski <krupinski01@gmail.com>
* 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 (14) */
public function depth(): int;
/** Canonical string form: provider[:service[:collection[:entity]]] */
public function __toString(): string;
}

View File

@@ -0,0 +1,154 @@
<?php
declare(strict_types=1);
/**
* SPDX-FileCopyrightText: Sebastian Krupinski <krupinski01@gmail.com>
* 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 (14) */
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;
}
}

View File

@@ -0,0 +1,50 @@
<?php
declare(strict_types=1);
/**
* SPDX-FileCopyrightText: Sebastian Krupinski <krupinski01@gmail.com>
* 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 (14) */
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;
}

View File

@@ -0,0 +1,38 @@
<?php
declare(strict_types=1);
/**
* SPDX-FileCopyrightText: Sebastian Krupinski <krupinski01@gmail.com>
* 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;
}
}

View File

@@ -0,0 +1,20 @@
<?php
declare(strict_types=1);
/**
* SPDX-FileCopyrightText: Sebastian Krupinski <krupinski01@gmail.com>
* 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;
}