refactor: use unified provider desing

Signed-off-by: Sebastian Krupinski <krupinski01@gmail.com>
This commit is contained in:
2026-06-20 20:31:17 -04:00
parent a3f2fa0101
commit d7c35a5d49
6 changed files with 271 additions and 170 deletions
@@ -21,29 +21,29 @@ class CollectionProperties extends CollectionPropertiesMutableAbstract {
* Converts store document values into collection properties.
*/
public function fromStore(array $data): static {
$this->data[self::JSON_PROPERTY_CONTENT] = [
$this->data[self::PROPERTY_CONTENT] = [
CollectionContent::Individual->value,
CollectionContent::Organization->value,
CollectionContent::Group->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_CONTENT] = (string) $data['content'];
$this->data[self::PROPERTY_CONTENT] = (string) $data['content'];
}
return $this;
@@ -58,7 +58,7 @@ 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,
+20 -11
View File
@@ -10,6 +10,7 @@ declare(strict_types=1);
namespace KTXM\ProviderLocalPeople\Providers\Personal;
use KTXF\People\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;
}
+225 -134
View File
@@ -9,9 +9,11 @@ declare(strict_types=1);
namespace KTXM\ProviderLocalPeople\Providers\Personal;
use Generator;
use KTXF\People\Collection\CollectionBaseInterface;
use KTXF\People\Collection\CollectionMutableInterface;
use KTXF\People\Entity\EntityMutableInterface;
use KTXF\People\Collection\CollectionPropertiesBaseInterface;
use KTXF\People\Entity\EntityBaseInterface;
use KTXF\People\Entity\EntityPropertiesMutableInterface;
use KTXF\People\Service\ServiceBaseInterface;
use KTXF\People\Service\ServiceCollectionMutableInterface;
use KTXF\People\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\RangeTally;
use KTXF\Resource\Range\RangeType;
@@ -29,11 +35,9 @@ use KTXM\ProviderLocalPeople\Store\Personal\Store;
class PersonalService implements ServiceBaseInterface, ServiceCollectionMutableInterface, ServiceEntityMutableInterface {
public const JSON_TYPE = ServiceBaseInterface::JSON_TYPE;
private const PROVIDER_IDENTIFIER = 'default';
private const SERVICE_IDENTIFIER = 'personal';
private const SERVICE_LABEL = 'Personal Calendar Service';
private const SERVICE_LABEL = 'Personal Contacts Service';
protected array $serviceCollectionCache = [];
protected ?string $serviceTenantId = null;
@@ -75,8 +79,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(
@@ -91,23 +97,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 {
@@ -152,8 +155,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;
@@ -168,6 +169,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 = [];
@@ -192,34 +210,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);
@@ -231,26 +246,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);
}
}
@@ -259,39 +272,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] ?? []);
}
@@ -305,9 +327,6 @@ class PersonalService implements ServiceBaseInterface, ServiceCollectionMutableI
if ($type === RangeType::TALLY) {
return new RangeTally();
}
if ($type === RangeType::DATE) {
return new RangeDate();
}
throw new InvalidParameterException("Invalid: Entity range of type '{$type->value}' is not valid");
}
@@ -317,7 +336,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
@@ -330,7 +349,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
@@ -344,57 +363,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
@@ -402,47 +437,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 Entity)) {
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): EntityMutableInterface {
// 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<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 -6
View File
@@ -16,7 +16,6 @@ use KTXM\ProviderLocalPeople\Providers\Personal\PersonalService;
class Provider implements ProviderBaseInterface {
public const JSON_TYPE = ProviderBaseInterface::JSON_TYPE;
protected const PROVIDER_IDENTIFIER = 'default';
protected const PROVIDER_LABEL = 'Default People Provider';
protected const PROVIDER_DESCRIPTION = 'Provides local people storage';
@@ -36,10 +35,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,
];
}
@@ -82,7 +81,6 @@ class Provider implements ProviderBaseInterface {
return $this->providerAbilities;
}
protected function serviceInstancePersonal(string $tenantId, string $userId): PersonalService {
$service = $this->container->get(PersonalService::class);
$service->initialize($tenantId, $userId);
+6 -3
View File
@@ -257,7 +257,8 @@ class Store {
['tid' => $tenantId, 'uid' => $userId, 'cid' => $cid],
['$set' => $data]
);
return $entity;
// return the canonical post-update collection so provider/service and full state are populated
return $this->collectionFetch($tenantId, $userId, (string) $cid) ?? $entity;
}
/**
@@ -501,8 +502,10 @@ class Store {
// Chronicle the modification (operation 2)
$this->chronicleDocument($tenantId, $collectionId, $identifier, 2);
}
return $entity;
// return the canonical post-update entity so provider/service and full state are populated
$entries = $this->entityFetch($tenantId, $userId, $collectionId, $identifier);
return $entries[$identifier] ?? $entity;
}
/**