From 4b4e7a3ae029110e1c3b5529f4e3e6e39b3439e8 Mon Sep 17 00:00:00 2001 From: Sebastian Krupinski Date: Thu, 25 Jun 2026 23:41:26 -0400 Subject: [PATCH] refactor: use unified service provider desing Signed-off-by: Sebastian Krupinski --- .../Personal/CollectionProperties.php | 31 +- lib/Providers/Personal/CollectionResource.php | 31 +- lib/Providers/Personal/EntityResource.php | 14 +- lib/Providers/Personal/PersonalService.php | 366 +++++++++++------- lib/Providers/Provider.php | 8 +- 5 files changed, 282 insertions(+), 168 deletions(-) diff --git a/lib/Providers/Personal/CollectionProperties.php b/lib/Providers/Personal/CollectionProperties.php index 9fba333..cae3012 100644 --- a/lib/Providers/Personal/CollectionProperties.php +++ b/lib/Providers/Personal/CollectionProperties.php @@ -12,31 +12,42 @@ namespace KTXM\ProviderLocalChrono\Providers\Personal; use KTXF\Chrono\Collection\CollectionContent; use KTXF\Chrono\Collection\CollectionPropertiesMutableAbstract; +/** + * Collection Properties Implementation + */ class CollectionProperties extends CollectionPropertiesMutableAbstract { /** * Converts store document values into collection properties. */ public function fromStore(array $data): static { - $this->data[self::JSON_PROPERTY_CONTENTS] = CollectionContent::Event->value; + $this->data[self::PROPERTY_CONTENT] = [ + CollectionContent::Event->value, + CollectionContent::Task->value, + CollectionContent::Journal->value + ]; if (isset($data['label'])) { - $this->data[self::JSON_PROPERTY_LABEL] = (string) $data['label']; + $this->data[self::PROPERTY_LABEL] = (string) $data['label']; } if (array_key_exists('description', $data)) { - $this->data[self::JSON_PROPERTY_DESCRIPTION] = $data['description']; + $this->data[self::PROPERTY_DESCRIPTION] = $data['description']; } - if (array_key_exists('priority', $data)) { - $this->data[self::JSON_PROPERTY_PRIORITY] = $data['priority'] !== null ? (int) $data['priority'] : null; + if (array_key_exists('rank', $data)) { + $this->data[self::PROPERTY_RANK] = $data['rank'] !== null ? (int) $data['rank'] : null; } if (array_key_exists('visibility', $data)) { - $this->data[self::JSON_PROPERTY_VISIBILITY] = $data['visibility'] !== null ? (bool) $data['visibility'] : null; + $this->data[self::PROPERTY_VISIBILITY] = $data['visibility'] !== null ? (bool) $data['visibility'] : null; } if (array_key_exists('color', $data)) { - $this->data[self::JSON_PROPERTY_COLOR] = $data['color']; + $this->data[self::PROPERTY_COLOR] = $data['color']; } if (isset($data['content'])) { - $this->data[self::JSON_PROPERTY_CONTENTS] = (string) $data['content']; + $content = is_array($data['content']) ? $data['content'] : [$data['content']]; + $this->data[self::PROPERTY_CONTENT] = array_values(array_map( + static fn($item) => $item instanceof CollectionContent ? $item->value : (string) $item, + $content + )); } return $this; @@ -51,10 +62,10 @@ class CollectionProperties extends CollectionPropertiesMutableAbstract { return array_filter([ 'label' => $this->getLabel(), 'description' => $this->getDescription(), - 'priority' => $this->getPriority(), + 'rank' => $this->getRank(), 'visibility' => $this->getVisibility(), 'color' => $this->getColor(), - 'content' => $content instanceof CollectionContent ? $content->value : null, + 'content' => $content !== [] ? array_map(static fn(CollectionContent $item) => $item->value, $content) : null, ], static fn($value) => $value !== null); } } diff --git a/lib/Providers/Personal/CollectionResource.php b/lib/Providers/Personal/CollectionResource.php index 332a87f..c0f1b33 100644 --- a/lib/Providers/Personal/CollectionResource.php +++ b/lib/Providers/Personal/CollectionResource.php @@ -10,6 +10,7 @@ declare(strict_types=1); namespace KTXM\ProviderLocalChrono\Providers\Personal; use KTXF\Chrono\Collection\CollectionMutableAbstract; +use KTXF\Resource\Identifier\CollectionIdentifier; class CollectionResource extends CollectionMutableAbstract { @@ -24,6 +25,14 @@ class CollectionResource extends CollectionMutableAbstract { parent::__construct($provider, $service); } + protected function nodeIdentifier(): CollectionIdentifier { + return new CollectionIdentifier( + $this->data[static::PROPERTY_PROVIDER], + $this->data[static::PROPERTY_SERVICE], + (string) $this->data[static::PROPERTY_IDENTIFIER] + ); + } + public function fromStore(array|object $data): self { if (is_object($data)) { @@ -31,23 +40,23 @@ class CollectionResource extends CollectionMutableAbstract { } if (isset($data['cid'])) { - $this->data[self::JSON_PROPERTY_IDENTIFIER] = (string) $data['cid']; + $this->data[self::PROPERTY_IDENTIFIER] = (string) $data['cid']; } elseif (isset($data['_id'])) { if (is_object($data['_id']) && method_exists($data['_id'], '__toString')) { - $this->data[self::JSON_PROPERTY_IDENTIFIER] = (string) $data['_id']; + $this->data[self::PROPERTY_IDENTIFIER] = (string) $data['_id']; } elseif (is_array($data['_id']) && isset($data['_id']['$oid'])) { - $this->data[self::JSON_PROPERTY_IDENTIFIER] = (string) $data['_id']['$oid']; + $this->data[self::PROPERTY_IDENTIFIER] = (string) $data['_id']['$oid']; } else { - $this->data[self::JSON_PROPERTY_IDENTIFIER] = (string) $data['_id']; + $this->data[self::PROPERTY_IDENTIFIER] = (string) $data['_id']; } } $this->userId = $data['uid'] ?? null; $this->collectionUuid = isset($data['uuid']) ? (string) $data['uuid'] : null; $this->getProperties()->fromStore($data); - $this->data[self::JSON_PROPERTY_CREATED] = $data['createdOn'] ?? $data['created'] ?? null; - $this->data[self::JSON_PROPERTY_MODIFIED] = $data['modifiedOn'] ?? $data['modified'] ?? null; + $this->data[self::PROPERTY_CREATED] = $data['createdOn'] ?? $data['created'] ?? null; + $this->data[self::PROPERTY_MODIFIED] = $data['modifiedOn'] ?? $data['modified'] ?? null; $this->collectionEnabled = $data['enabled'] ?? true; - $this->data[self::JSON_PROPERTY_SIGNATURE] = isset($data['signature']) ? md5((string) $data['signature']) : null; + $this->data[self::PROPERTY_SIGNATURE] = isset($data['signature']) ? md5((string) $data['signature']) : null; return $this; } @@ -61,12 +70,12 @@ class CollectionResource extends CollectionMutableAbstract { 'uuid' => $this->collectionUuid, 'label' => $properties->getLabel(), 'description' => $properties->getDescription(), - 'priority' => $properties->getPriority(), + 'rank' => $properties->getRank(), 'visibility' => $properties->getVisibility(), 'color' => $properties->getColor(), - 'createdOn' => $this->data[self::JSON_PROPERTY_CREATED] ?? null, - 'modifiedOn' => $this->data[self::JSON_PROPERTY_MODIFIED] ?? null, - 'signature' => $this->data[self::JSON_PROPERTY_SIGNATURE] ?? null, + 'createdOn' => $this->data[self::PROPERTY_CREATED] ?? null, + 'modifiedOn' => $this->data[self::PROPERTY_MODIFIED] ?? null, + 'signature' => $this->data[self::PROPERTY_SIGNATURE] ?? null, 'enabled' => $this->collectionEnabled, ], static fn($value) => $value !== null); diff --git a/lib/Providers/Personal/EntityResource.php b/lib/Providers/Personal/EntityResource.php index 8bc5451..10d46da 100644 --- a/lib/Providers/Personal/EntityResource.php +++ b/lib/Providers/Personal/EntityResource.php @@ -30,14 +30,14 @@ class EntityResource extends EntityMutableAbstract { $document = (array) $document; } - $this->data[self::JSON_PROPERTY_IDENTIFIER] = $document['eid'] ?? null; + $this->data[self::PROPERTY_IDENTIFIER] = $document['eid'] ?? null; $this->tenantId = $document['tid'] ?? null; $this->userId = $document['uid'] ?? null; - $this->data[self::JSON_PROPERTY_COLLECTION] = $document['cid'] ?? null; - $this->data[self::JSON_PROPERTY_CREATED] = $document['createdOn'] ?? null; - $this->data[self::JSON_PROPERTY_MODIFIED] = $document['modifiedOn'] ?? null; + $this->data[self::PROPERTY_COLLECTION] = $document['cid'] ?? null; + $this->data[self::PROPERTY_CREATED] = $document['createdOn'] ?? null; + $this->data[self::PROPERTY_MODIFIED] = $document['modifiedOn'] ?? null; $this->getProperties()->fromStore($document); - $this->data[self::JSON_PROPERTY_SIGNATURE] = md5(json_encode($this->getDataJson())); + $this->data[self::PROPERTY_SIGNATURE] = md5(json_encode($this->getDataJson())); $this->entityDataObject = null; return $this; @@ -49,7 +49,7 @@ class EntityResource extends EntityMutableAbstract { 'uid' => $this->userId, 'cid' => $this->collection(), 'eid' => $this->identifier(), - 'createdOn' => $this->data[self::JSON_PROPERTY_CREATED] ?? date('c'), + 'createdOn' => $this->data[self::PROPERTY_CREATED] ?? date('c'), 'modifiedOn' => date('c'), ], static fn($value) => $value !== null); @@ -85,7 +85,7 @@ class EntityResource extends EntityMutableAbstract { { $this->entityDataObject = null; $this->getProperties()->setDataRaw($value); - $this->data[self::JSON_PROPERTY_SIGNATURE] = md5(json_encode($value)); + $this->data[self::PROPERTY_SIGNATURE] = md5(json_encode($value)); return $this; } diff --git a/lib/Providers/Personal/PersonalService.php b/lib/Providers/Personal/PersonalService.php index f6d78b6..821e9a4 100644 --- a/lib/Providers/Personal/PersonalService.php +++ b/lib/Providers/Personal/PersonalService.php @@ -9,9 +9,11 @@ declare(strict_types=1); namespace KTXM\ProviderLocalChrono\Providers\Personal; +use Generator; use KTXF\Chrono\Collection\CollectionBaseInterface; -use KTXF\Chrono\Collection\CollectionMutableInterface; -use KTXF\Chrono\Entity\EntityMutableInterface; +use KTXF\Chrono\Collection\CollectionPropertiesBaseInterface; +use KTXF\Chrono\Entity\EntityBaseInterface; +use KTXF\Chrono\Entity\EntityPropertiesMutableInterface; use KTXF\Chrono\Service\ServiceBaseInterface; use KTXF\Chrono\Service\ServiceCollectionMutableInterface; use KTXF\Chrono\Service\ServiceEntityMutableInterface; @@ -20,6 +22,10 @@ use KTXF\Resource\Delta\DeltaCollection; use KTXF\Resource\Exceptions\InvalidParameterException; use KTXF\Resource\Filter\Filter; use KTXF\Resource\Filter\IFilter; +use KTXF\Resource\Identifier\CollectionIdentifier; +use KTXF\Resource\Identifier\CollectionIdentifierInterface; +use KTXF\Resource\Identifier\EntityIdentifier; +use KTXF\Resource\Identifier\EntityIdentifierInterface; use KTXF\Resource\Range\IRange; use KTXF\Resource\Range\RangeDate; use KTXF\Resource\Range\RangeTally; @@ -28,20 +34,18 @@ use KTXF\Resource\Sort\ISort; use KTXF\Resource\Sort\Sort; use KTXM\ProviderLocalChrono\Store\Personal\Store; -class PersonalService implements ServiceBaseInterface, ServiceCollectionMutableInterface, ServiceEntityMutableInterface -{ - public const JSON_TYPE = ServiceBaseInterface::JSON_TYPE; +class PersonalService implements ServiceBaseInterface, ServiceCollectionMutableInterface, ServiceEntityMutableInterface { private const PROVIDER_IDENTIFIER = 'default'; private const SERVICE_IDENTIFIER = 'personal'; private const SERVICE_LABEL = 'Personal Calendar Service'; - private array $serviceCollectionCache = []; - private ?string $serviceTenantId = null; - private ?string $serviceUserId = null; - private ?bool $serviceEnabled = true; + protected array $serviceCollectionCache = []; + protected ?string $serviceTenantId = null; + protected ?string $serviceUserId = null; + protected ?bool $serviceEnabled = true; - private array $serviceAbilities = [ + private array $serviceAbilities = [ self::CAPABILITY_COLLECTION_LIST => true, self::CAPABILITY_COLLECTION_LIST_FILTER => [ self::CAPABILITY_COLLECTION_FILTER_LABEL => 's:100:256:256', @@ -77,8 +81,10 @@ class PersonalService implements ServiceBaseInterface, ServiceCollectionMutableI self::CAPABILITY_ENTITY_EXTANT => true, self::CAPABILITY_ENTITY_FETCH => true, self::CAPABILITY_ENTITY_CREATE => true, - self::CAPABILITY_ENTITY_UPDATE => true, + self::CAPABILITY_ENTITY_MODIFY => true, self::CAPABILITY_ENTITY_DELETE => true, + self::CAPABILITY_ENTITY_MOVE => true, + self::CAPABILITY_ENTITY_COPY => true, ]; public function __construct( @@ -93,23 +99,20 @@ class PersonalService implements ServiceBaseInterface, ServiceCollectionMutableI public function jsonSerialize(): array { return array_filter([ - self::JSON_PROPERTY_TYPE => self::JSON_TYPE, - self::JSON_PROPERTY_PROVIDER => self::PROVIDER_IDENTIFIER, - self::JSON_PROPERTY_IDENTIFIER => self::SERVICE_IDENTIFIER, - self::JSON_PROPERTY_LABEL => self::SERVICE_LABEL, - self::JSON_PROPERTY_ENABLED => $this->serviceEnabled, - self::JSON_PROPERTY_CAPABILITIES => $this->serviceAbilities, - self::JSON_PROPERTY_LOCATION => null, - self::JSON_PROPERTY_IDENTITY => null, - self::JSON_PROPERTY_AUXILIARY => [], + self::PROPERTY_TYPE => self::JSON_TYPE, + self::PROPERTY_PROVIDER => self::PROVIDER_IDENTIFIER, + self::PROPERTY_IDENTIFIER => self::SERVICE_IDENTIFIER, + self::PROPERTY_LABEL => self::SERVICE_LABEL, + self::PROPERTY_ENABLED => $this->serviceEnabled, + self::PROPERTY_CAPABILITIES => $this->serviceAbilities, + self::PROPERTY_LOCATION => null, + self::PROPERTY_IDENTITY => null, + self::PROPERTY_AUXILIARY => [], ], fn($v) => $v !== null); } public function capable(string $value): bool { - if (isset($this->serviceAbilities[$value])) { - return (bool)$this->serviceAbilities[$value]; - } - return false; + return isset($this->serviceAbilities[$value]); } public function capabilities(): array { @@ -154,8 +157,6 @@ class PersonalService implements ServiceBaseInterface, ServiceCollectionMutableI return []; } - // Collection operations - public function collectionList(string|int|null $location, ?IFilter $filter = null, ?ISort $sort = null): array { $entries = $this->store->collectionList($this->serviceTenantId, $this->serviceUserId, $filter, $sort); $this->serviceCollectionCache = $entries; @@ -170,6 +171,23 @@ class PersonalService implements ServiceBaseInterface, ServiceCollectionMutableI return new Sort($this->serviceAbilities[self::CAPABILITY_COLLECTION_LIST_SORT] ?? []); } + public function collectionFetch(string|int $identifier): ?CollectionBaseInterface { + // determine if collection is cached + if (isset($this->serviceCollectionCache[$identifier])) { + return $this->serviceCollectionCache[$identifier]; + } + // retrieve from store + $collection = $this->store->collectionFetch($this->serviceTenantId, $this->serviceUserId, (string)$identifier); + if ($collection !== null) { + $collectionIdentifier = $collection->identifier(); + if ($collectionIdentifier !== null) { + $this->serviceCollectionCache[(string) $collectionIdentifier] = $collection; + } + return $collection; + } + return null; + } + public function collectionExtant(string|int $location, string|int ...$identifiers): array { $resolvedIdentifiers = $identifiers !== [] ? $identifiers : [$location]; $response = []; @@ -194,34 +212,31 @@ class PersonalService implements ServiceBaseInterface, ServiceCollectionMutableI return $response; } - public function collectionFetch(string|int $identifier): ?CollectionBaseInterface { - // determine if collection is cached - if (isset($this->serviceCollectionCache[$identifier])) { - return $this->serviceCollectionCache[$identifier]; - } - // retrieve from store - $collection = $this->store->collectionFetch($this->serviceTenantId, $this->serviceUserId, (string)$identifier); - if ($collection !== null) { - $collectionIdentifier = $collection->identifier(); - if ($collectionIdentifier !== null) { - $this->serviceCollectionCache[(string) $collectionIdentifier] = $collection; - } - return $collection; - } - return null; + /** + * Determine whether a single collection exists and belongs to the current user. + * + * @param string|int $collection collection identifier + * + * @return bool + */ + private function collectionAccessible(string|int $collection): bool { + $response = $this->collectionExtant($collection); + return ($response[$collection] ?? false) === true; } public function collectionFresh(): CollectionResource { return new CollectionResource(); } - public function collectionCreate(string|int|null $location, CollectionMutableInterface $collection, array $options = []): CollectionBaseInterface { + public function collectionCreate(CollectionIdentifierInterface|null $target, CollectionPropertiesBaseInterface $properties, array $options = []): CollectionBaseInterface { // convert collection to a native type if needed - if (!($collection instanceof CollectionResource)) { - $nativeCollection = new CollectionResource(); - $nativeCollection->jsonDeserialize($collection->jsonSerialize()); + if (!($properties instanceof CollectionResource)) { + $nativeCollection = $this->collectionFresh(); + $nativeCollection->jsonDeserialize([ + CollectionResource::PROPERTY_PROPERTIES => $properties->jsonSerialize(), + ]); } else { - $nativeCollection = clone $collection; + $nativeCollection = clone $properties; } // create collection in store $result = $this->store->collectionCreate($this->serviceTenantId, $this->serviceUserId, $nativeCollection); @@ -233,26 +248,24 @@ class PersonalService implements ServiceBaseInterface, ServiceCollectionMutableI return $result; } - public function collectionUpdate(string|int $id, CollectionMutableInterface $collection): CollectionBaseInterface { - // validate id - if (!is_string($id)) { - throw new InvalidParameterException("Invalid: Collection identifier '$id' is not valid"); - } + public function collectionUpdate(CollectionIdentifierInterface $target, CollectionPropertiesBaseInterface $properties): CollectionBaseInterface { + $id = $target->collection(); // validate ownership - if ($this->collectionExtant($id) === false) { + if ($this->collectionAccessible($id) === false) { throw new InvalidParameterException("Invalid: Collection identifier '$id' does not exist or does not belong to user '{$this->serviceUserId}'"); } // convert collection to a native type if needed - if (!($collection instanceof CollectionResource)) { + if (!($properties instanceof CollectionResource)) { $nativeCollection = new CollectionResource(); - $data = $collection->jsonSerialize(); - $data[CollectionResource::JSON_PROPERTY_IDENTIFIER] = $id; - $nativeCollection->jsonDeserialize($data); + $nativeCollection->jsonDeserialize([ + CollectionResource::PROPERTY_IDENTIFIER => $id, + CollectionResource::PROPERTY_PROPERTIES => $properties->jsonSerialize(), + ]); } else { - $nativeCollection = clone $collection; + $nativeCollection = clone $properties; if ($nativeCollection->identifier() === null) { $data = $nativeCollection->jsonSerialize(); - $data[CollectionResource::JSON_PROPERTY_IDENTIFIER] = $id; + $data[CollectionResource::PROPERTY_IDENTIFIER] = $id; $nativeCollection->jsonDeserialize($data); } } @@ -261,39 +274,48 @@ class PersonalService implements ServiceBaseInterface, ServiceCollectionMutableI return $result; } - public function collectionDelete(string|int $id, bool $force = false, bool $recursive = false): bool { - // validate id - if (!is_string($id)) { - throw new InvalidParameterException("Invalid: Collection identifier '$id' is not valid"); - } + public function collectionDelete(CollectionIdentifierInterface $target, bool $force = false): CollectionBaseInterface | true { + $id = $target->collection(); // validate ownership - if ($this->collectionExtant($id) === false) { + if ($this->collectionAccessible($id) === false) { throw new InvalidParameterException("Invalid: Collection identifier '$id' does not exist or does not belong to user '{$this->serviceUserId}'"); } // destroy collection in store - if ($this->store->collectionDestroyById($this->serviceTenantId, $this->serviceUserId, $id)) { - unset($this->serviceCollectionCache[$id]); - return true; - } - return false; + $this->store->collectionDestroyById($this->serviceTenantId, $this->serviceUserId, $id); + unset($this->serviceCollectionCache[$id]); + return true; } // Entity operations - public function entityList(string|int $collection, ?IFilter $filter = null, ?ISort $sort = null, ?IRange $range = null, ?array $options = null): array { + public function entityListBulk(string|int $collection, ?IFilter $filter = null, ?ISort $sort = null, ?IRange $range = null, ?array $properties = null): array { // validate id if (!is_string($collection)) { throw new InvalidParameterException("Invalid: Collection identifier '$collection' is not valid"); } // validate ownership - if ($this->collectionExtant($collection) === false) { + if ($this->collectionAccessible($collection) === false) { throw new InvalidParameterException("Invalid: Collection identifier '$collection' does not exist or does not belong to user '{$this->serviceUserId}'"); } // retrieve entities from store - $entries = $this->store->entityList($this->serviceTenantId, $this->serviceUserId, $collection, $filter, $sort, $range, $options); + $entries = $this->store->entityList($this->serviceTenantId, $this->serviceUserId, $collection, $filter, $sort, $range, $properties); return $entries ?? []; } + public function entityListStream(string|int $collection, ?IFilter $filter = null, ?ISort $sort = null, ?IRange $range = null, ?array $properties = null): Generator { + // validate id + if (!is_string($collection)) { + throw new InvalidParameterException("Invalid: Collection identifier '$collection' is not valid"); + } + // validate ownership + if ($this->collectionAccessible($collection) === false) { + throw new InvalidParameterException("Invalid: Collection identifier '$collection' does not exist or does not belong to user '{$this->serviceUserId}'"); + } + // retrieve entities from store and yield one by one + $entries = $this->store->entityList($this->serviceTenantId, $this->serviceUserId, $collection, $filter, $sort, $range, $properties); + yield from ($entries ?? []); + } + public function entityListFilter(): Filter { return new Filter($this->serviceAbilities[self::CAPABILITY_ENTITY_LIST_FILTER] ?? []); } @@ -319,7 +341,7 @@ class PersonalService implements ServiceBaseInterface, ServiceCollectionMutableI throw new InvalidParameterException("Invalid: Collection identifier '$collection' is not valid"); } // validate ownership - if ($this->collectionExtant($collection) === false) { + if ($this->collectionAccessible($collection) === false) { throw new InvalidParameterException("Invalid: Collection identifier '$collection' does not exist or does not belong to user '{$this->serviceUserId}'"); } // retrieve entity status from store @@ -332,7 +354,7 @@ class PersonalService implements ServiceBaseInterface, ServiceCollectionMutableI throw new InvalidParameterException("Invalid: Collection identifier '$collection' is not valid"); } // validate ownership - if ($this->collectionExtant($collection) === false) { + if ($this->collectionAccessible($collection) === false) { throw new InvalidParameterException("Invalid: Collection identifier '$collection' does not exist or does not belong to user '{$this->serviceUserId}'"); } // retrieve entity delta from store @@ -346,57 +368,73 @@ class PersonalService implements ServiceBaseInterface, ServiceCollectionMutableI ); } - public function entityFetch(string|int $collection, string|int ...$identifiers): array { - // validate id - if (!is_string($collection)) { - throw new InvalidParameterException("Invalid: Collection identifier '$collection' is not valid"); + public function entityFetchBulk(EntityIdentifierInterface ...$identifiers): array { + // group identifiers by collection + $byCollection = []; + foreach ($identifiers as $identifier) { + $byCollection[$identifier->collection()][] = $identifier->entity(); } - // validate ownership - if ($this->collectionExtant($collection) === false) { - throw new InvalidParameterException("Invalid: Collection identifier '$collection' does not exist or does not belong to user '{$this->serviceUserId}'"); + $results = []; + foreach ($byCollection as $collection => $entityIds) { + // skip collections that don't exist or don't belong to user + if ($this->collectionAccessible($collection) === false) { + continue; + } + $entries = $this->store->entityFetch($this->serviceTenantId, $this->serviceUserId, $collection, ...$entityIds); + if ($entries) { + $results = array_merge($results, $entries); + } + } + return $results; + } + + public function entityFetchStream(EntityIdentifierInterface ...$identifiers): Generator { + // group identifiers by collection + $byCollection = []; + foreach ($identifiers as $identifier) { + $byCollection[$identifier->collection()][] = $identifier->entity(); + } + foreach ($byCollection as $collection => $entityIds) { + // skip collections that don't exist or don't belong to user + if ($this->collectionAccessible($collection) === false) { + continue; + } + $entries = $this->store->entityFetch($this->serviceTenantId, $this->serviceUserId, $collection, ...$entityIds); + if ($entries) { + yield from $entries; + } } - // retrieve entity from store - $entries = $this->store->entityFetch($this->serviceTenantId, $this->serviceUserId, $collection, ...$identifiers); - return $entries ?? []; } public function entityFresh(): EntityResource { return new EntityResource(); } - public function entityCreate(string|int $collection, EntityMutableInterface $entity, array $options = []): EntityResource { - // validate collection identifier - if (!is_string($collection)) { - throw new InvalidParameterException("Invalid: Collection identifier '$collection' is not valid"); - } + public function entityCreate(CollectionIdentifierInterface $target, EntityPropertiesMutableInterface $properties, array $options = []): EntityBaseInterface { + $collection = $target->collection(); // validate collection extant and ownership - if ($this->collectionExtant($collection) === false) { + if ($this->collectionAccessible($collection) === false) { throw new InvalidParameterException("Invalid: Collection identifier '$collection' does not exist or does not belong to user '{$this->serviceUserId}'"); } // convert entity to a native type if needed - if (!($entity instanceof EntityResource)) { + if (!($properties instanceof EntityResource)) { $nativeEntity = $this->entityFresh(); - $nativeEntity->jsonDeserialize($entity->jsonSerialize()); + $nativeEntity->jsonDeserialize([ + EntityResource::PROPERTY_PROPERTIES => $properties->jsonSerialize(), + ]); } else { - $nativeEntity = clone $entity; + $nativeEntity = clone $properties; } // create entity in store (store will handle userId and collection) $result = $this->store->entityCreate($this->serviceTenantId, $this->serviceUserId, $collection, $nativeEntity); - return $result; } - public function entityUpdate(string|int $collection, string|int $identifier, EntityMutableInterface $entity): EntityResource { - // validate collection identifier - if (!is_string($collection)) { - throw new InvalidParameterException("Invalid: Collection identifier '$collection' is not valid"); - } - // validate entity identifier - if (!is_string($identifier)) { - throw new InvalidParameterException("Invalid: Entity identifier '$identifier' is not valid"); - } + public function entityModify(EntityIdentifierInterface $target, EntityPropertiesMutableInterface $properties): EntityBaseInterface { + $collection = $target->collection(); + $identifier = $target->entity(); // validate collection extant and ownership - if ($this->collectionExtant($collection) === false) { + if ($this->collectionAccessible($collection) === false) { throw new InvalidParameterException("Invalid: Collection identifier '$collection' does not exist or does not belong to user '{$this->serviceUserId}'"); } // validate entity extant and ownership @@ -404,47 +442,103 @@ class PersonalService implements ServiceBaseInterface, ServiceCollectionMutableI if (!isset($extant[$identifier]) || $extant[$identifier] === false) { throw new InvalidParameterException("Invalid: Entity identifier '$identifier' does not exist or does not belong to collection '$collection' or user '{$this->serviceUserId}'"); } - // convert entity to a native type if needed - if (!($entity instanceof EntityResource)) { + if (!($properties instanceof EntityResource)) { $nativeEntity = $this->entityFresh(); - $nativeEntity->jsonDeserialize($entity->jsonSerialize()); + $nativeEntity->jsonDeserialize([ + EntityResource::PROPERTY_PROPERTIES => $properties->jsonSerialize(), + ]); } else { - $nativeEntity = clone $entity; + $nativeEntity = clone $properties; } - // modify entity in store $result = $this->store->entityModify($this->serviceTenantId, $this->serviceUserId, $collection, $identifier, $nativeEntity); - return $result; } - public function entityDelete(string|int $collection, string|int $identifier): EntityResource { - // validate collection identifier - if (!is_string($collection)) { - throw new InvalidParameterException("Invalid: Collection identifier '$collection' is not valid"); + public function entityDelete(EntityIdentifierInterface ...$targets): array { + $results = []; + foreach ($targets as $target) { + $key = (string) $target; + $collection = $target->collection(); + $identifier = $target->entity(); + // validate collection extant and ownership + if ($this->collectionAccessible($collection) === false) { + $results[$key] = ['disposition' => 'error', 'destination' => null, 'mutation' => $target]; + continue; + } + // validate entity extant and ownership + $extant = $this->store->entityExtant($this->serviceTenantId, $this->serviceUserId, $collection, $identifier); + if (!isset($extant[$identifier]) || $extant[$identifier] === false) { + $results[$key] = ['disposition' => 'error', 'destination' => null, 'mutation' => $target]; + continue; + } + // destroy entity in store + $this->store->entityDestroyById($this->serviceTenantId, $this->serviceUserId, $collection, $identifier); + $results[$key] = ['disposition' => 'deleted', 'destination' => null, 'mutation' => $target]; } - // validate entity identifier - if (!is_string($identifier)) { - throw new InvalidParameterException("Invalid: Entity identifier '$identifier' is not valid"); - } - // validate collection extant and ownership - if ($this->collectionExtant($collection) === false) { - throw new InvalidParameterException("Invalid: Collection identifier '$collection' does not exist or does not belong to user '{$this->serviceUserId}'"); - } - // validate entity extant and ownership - $extant = $this->store->entityExtant($this->serviceTenantId, $this->serviceUserId, $collection, $identifier); - if (!isset($extant[$identifier]) || $extant[$identifier] === false) { - throw new InvalidParameterException("Invalid: Entity identifier '$identifier' does not exist or does not belong to collection '$collection' or user '{$this->serviceUserId}'"); - } - // fetch entity before destruction to return it - $entities = $this->store->entityFetch($this->serviceTenantId, $this->serviceUserId, $collection, $identifier); - $entity = $entities[$identifier] ?? $this->entityFresh(); - - // destroy entity in store - $this->store->entityDestroyById($this->serviceTenantId, $this->serviceUserId, $collection, $identifier); - - return $entity; + return $results; } - + + public function entityMove(CollectionIdentifierInterface $target, EntityIdentifierInterface ...$sources): array { + return $this->entityRelocate($target, true, ...$sources); + } + + public function entityCopy(CollectionIdentifierInterface $target, EntityIdentifierInterface ...$sources): array { + return $this->entityRelocate($target, false, ...$sources); + } + + /** + * Relocate (move or copy) entities into a target collection. + * + * The store assigns a fresh identifier to each relocated entity, so the + * mutation identifier reflects the entity's new location. + * + * @param CollectionIdentifierInterface $target destination collection identifier + * @param bool $remove true to move (delete the source), false to copy + * @param EntityIdentifierInterface ...$sources source entity identifiers + * + * @return array + */ + private function entityRelocate(CollectionIdentifierInterface $target, bool $remove, EntityIdentifierInterface ...$sources): array { + $results = []; + $disposition = $remove ? 'moved' : 'copied'; + $targetCollection = $target->collection(); + // validate target collection extant and ownership + $targetValid = $this->collectionAccessible($targetCollection); + foreach ($sources as $source) { + $key = (string) $source; + $sourceCollection = $source->collection(); + $identifier = $source->entity(); + // validate target collection + if ($targetValid === false) { + $results[$key] = ['disposition' => 'error', 'destination' => null, 'mutation' => $source]; + continue; + } + // validate source collection extant and ownership + if ($this->collectionAccessible($sourceCollection) === false) { + $results[$key] = ['disposition' => 'error', 'destination' => null, 'mutation' => $source]; + continue; + } + // fetch source entity (scoped to user; also confirms the entity exists) + $entries = $this->store->entityFetch($this->serviceTenantId, $this->serviceUserId, $sourceCollection, $identifier); + $entity = $entries[$identifier] ?? null; + if ($entity === null) { + $results[$key] = ['disposition' => 'error', 'destination' => null, 'mutation' => $source]; + continue; + } + // create a copy in the target collection (store assigns a fresh identifier) + $created = $this->store->entityCreate($this->serviceTenantId, $this->serviceUserId, $targetCollection, $entity); + // remove the source entity when moving + if ($remove) { + $this->store->entityDestroyById($this->serviceTenantId, $this->serviceUserId, $sourceCollection, $identifier); + } + // construct destination and mutation identifiers + $destination = new CollectionIdentifier($this->provider(), $this->identifier(), $targetCollection); + $mutation = new EntityIdentifier($this->provider(), $this->identifier(), $targetCollection, (string) $created->identifier()); + $results[$key] = ['disposition' => $disposition, 'destination' => $destination, 'mutation' => $mutation]; + } + return $results; + } + } diff --git a/lib/Providers/Provider.php b/lib/Providers/Provider.php index 56109c0..1e3e7a4 100644 --- a/lib/Providers/Provider.php +++ b/lib/Providers/Provider.php @@ -38,10 +38,10 @@ class Provider implements ProviderBaseInterface public function jsonSerialize(): array { return [ - self::JSON_PROPERTY_TYPE => self::JSON_TYPE, - self::JSON_PROPERTY_IDENTIFIER => self::PROVIDER_IDENTIFIER, - self::JSON_PROPERTY_LABEL => self::PROVIDER_LABEL, - self::JSON_PROPERTY_CAPABILITIES => $this->providerAbilities, + self::PROPERTY_TYPE => self::JSON_TYPE, + self::PROPERTY_IDENTIFIER => self::PROVIDER_IDENTIFIER, + self::PROPERTY_LABEL => self::PROVIDER_LABEL, + self::PROPERTY_CAPABILITIES => $this->providerAbilities, ]; }