refactor: use unified service provider desing

Signed-off-by: Sebastian Krupinski <krupinski01@gmail.com>
This commit is contained in:
2026-06-25 23:41:26 -04:00
parent d13c3f4ac6
commit 4b4e7a3ae0
5 changed files with 282 additions and 168 deletions
+21 -10
View File
@@ -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);
}
}
+20 -11
View File
@@ -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);
+7 -7
View File
@@ -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;
}
+218 -124
View File
@@ -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,18 +34,16 @@ 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 = [
self::CAPABILITY_COLLECTION_LIST => true,
@@ -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)) {
$this->store->collectionDestroyById($this->serviceTenantId, $this->serviceUserId, $id);
unset($this->serviceCollectionCache[$id]);
return true;
}
return false;
}
// 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();
}
$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;
}
// 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}'");
}
// 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");
}
// validate entity identifier
if (!is_string($identifier)) {
throw new InvalidParameterException("Invalid: Entity identifier '$identifier' 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->collectionExtant($collection) === false) {
throw new InvalidParameterException("Invalid: Collection identifier '$collection' does not exist or does not belong to user '{$this->serviceUserId}'");
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) {
throw new InvalidParameterException("Invalid: Entity identifier '$identifier' does not exist or does not belong to collection '$collection' or user '{$this->serviceUserId}'");
$results[$key] = ['disposition' => 'error', 'destination' => null, 'mutation' => $target];
continue;
}
// 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);
$results[$key] = ['disposition' => 'deleted', 'destination' => null, 'mutation' => $target];
}
return $results;
}
return $entity;
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<string, array{disposition: string, destination: ?CollectionIdentifierInterface, mutation: EntityIdentifierInterface}>
*/
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;
}
}
+4 -4
View File
@@ -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,
];
}