feat: listen to user life cycle events

Signed-off-by: Sebastian Krupinski <krupinski01@gmail.com>
This commit is contained in:
2026-08-08 21:51:07 -04:00
parent c0f154eddb
commit 25c3043b82
5 changed files with 216 additions and 3 deletions
+58
View File
@@ -0,0 +1,58 @@
<?php
declare(strict_types=1);
namespace KTXM\ProviderLocalPeople\Listeners;
use KTXC\User\Event\UserCreatedEvent;
use KTXC\User\Event\UserDeletingEvent;
use KTXF\Resource\Identifier\CollectionIdentifier;
use KTXM\ProviderLocalPeople\Providers\Personal\PersonalService;
use KTXM\ProviderLocalPeople\Store\Personal\Store;
final class UserEventListener
{
private const PERSONAL_ADDRESS_BOOK_LABEL = 'Personal Address Book';
public function __construct(
private readonly Store $store,
private readonly PersonalService $service,
) {
}
public function onUserCreated(UserCreatedEvent $event): void
{
$tenantId = $event->tenantIdentifier();
$userId = $event->userIdentifier();
// A replayed lifecycle event must not create another default collection.
if ($this->store->collectionList($tenantId, $userId) !== []) {
return;
}
$collection = $this->store->collectionFresh()->setEnabled(true);
$collection->getProperties()
->setLabel(self::PERSONAL_ADDRESS_BOOK_LABEL)
->setVisibility(true);
$this->store->collectionCreate($tenantId, $userId, $collection);
}
public function onUserDeleting(UserDeletingEvent $event): void
{
$service = $this->service->initialize($event->tenantIdentifier(), $event->userIdentifier());
foreach ($service->collectionList(null) as $collection) {
$identifier = $collection->identifier();
if ($identifier === null) {
continue;
}
$service->collectionDelete(new CollectionIdentifier(
$collection->provider(),
(string) $collection->service(),
(string) $identifier,
), true);
}
}
}
+10
View File
@@ -3,9 +3,14 @@
namespace KTXM\ProviderLocalPeople;
use KTXC\Resource\ProviderManager;
use KTXC\User\Event\UserCreatedEvent;
use KTXC\User\Event\UserDeletingEvent;
use KTXF\Event\DeliveryMode;
use KTXF\Event\EventListenerRegistrarInterface;
use KTXF\Module\ModuleBrowserInterface;
use KTXF\Module\ModuleInstanceAbstract;
use KTXF\Resource\Provider\ProviderInterface;
use KTXM\ProviderLocalPeople\Listeners\UserEventListener;
use KTXM\ProviderLocalPeople\Providers\Provider;
/**
@@ -15,6 +20,7 @@ class Module extends ModuleInstanceAbstract implements ModuleBrowserInterface
{
public function __construct(
private readonly ProviderManager $providerManager,
private readonly EventListenerRegistrarInterface $events,
) {}
public function handle(): string
@@ -55,6 +61,10 @@ class Module extends ModuleInstanceAbstract implements ModuleBrowserInterface
public function boot(): void
{
// Register listeners
$this->events->listen($this->handle(), UserCreatedEvent::class, UserEventListener::class, 'onUserCreated', DeliveryMode::Deferred);
$this->events->listen($this->handle(), UserDeletingEvent::class, UserEventListener::class, 'onUserDeleting', DeliveryMode::Deferred);
// Register providers
$this->providerManager->register(ProviderInterface::TYPE_PEOPLE, 'default', Provider::class);
}
+5 -1
View File
@@ -279,7 +279,11 @@ class PersonalService implements ServiceBaseInterface, ServiceCollectionMutableI
throw new InvalidParameterException("Invalid: Collection identifier '$id' does not exist or does not belong to user '{$this->serviceUserId}'");
}
// destroy collection in store
$this->store->collectionDestroyById($this->serviceTenantId, $this->serviceUserId, $id);
$collection = $this->store->collectionFetch($this->serviceTenantId, $this->serviceUserId, $id);
if ($collection === null) {
throw new InvalidParameterException("Invalid: Collection identifier '$id' could not be retrieved");
}
$this->store->collectionDestroy($this->serviceTenantId, $this->serviceUserId, $collection);
unset($this->serviceCollectionCache[$id]);
return true;
}
+25 -2
View File
@@ -183,6 +183,7 @@ class Store {
return $list;
}
/**
* confirm if collections exist in data store
*
@@ -306,7 +307,12 @@ class Store {
if ($identifier === null) {
return $entity;
}
return $this->collectionDestroyById($tenantId, $userId, (string) $identifier) ? $entity : $entity;
$this->entityDestroyByCollectionId($tenantId, $userId, [(string) $identifier]);
$this->chronicleExpungeByCollectionId($tenantId, [(string) $identifier]);
$this->collectionDestroyById($tenantId, $userId, (string) $identifier);
return $entity;
}
/**
@@ -591,6 +597,21 @@ class Store {
return false;
}
/**
* Delete entities belonging to specific collections from the data store.
*
* @param string $tenantId tenant identifier
* @param string $userId user identifier
* @param array $identifiers collection identifiers
*/
private function entityDestroyByCollectionId(string $tenantId, string $userId, array $identifiers): void {
$this->_store->selectCollection($this->_EntityTable)->deleteMany([
'tid' => $tenantId,
'uid' => $userId,
'cid' => ['$in' => $identifiers],
]);
}
/**
* chronicle a operation to an entity to the data store
*
@@ -758,10 +779,12 @@ class Store {
* @since Release 1.0.0
*
* @param array $identifiers collection of identifiers
* @param string $tenantId tenant identifier
*/
private function chronicleExpungeByCollectionId(array $identifiers): void {
private function chronicleExpungeByCollectionId(string $tenantId, array $identifiers): void {
// Delete chronicle entries for the specified collection identifiers
$this->_store->selectCollection($this->_ChronicleTable)->deleteMany([
'tid' => $tenantId,
'cid' => ['$in' => $identifiers]
]);
}
+118
View File
@@ -0,0 +1,118 @@
<?php
declare(strict_types=1);
namespace KTXT\ProviderLocalPeople\Tests\Unit;
use KTXC\Event\EventListenerRegistry;
use KTXC\Resource\ProviderManager;
use KTXC\User\Event\UserCreatedEvent;
use KTXC\User\Event\UserDeletingEvent;
use KTXF\Event\DeliveryMode;
use KTXF\Resource\Provider\ProviderInterface;
use KTXF\Resource\Identifier\CollectionIdentifier;
use KTXM\ProviderLocalPeople\Listeners\UserEventListener;
use KTXM\ProviderLocalPeople\Module;
use KTXM\ProviderLocalPeople\Providers\Personal\CollectionResource;
use KTXM\ProviderLocalPeople\Providers\Personal\PersonalService;
use KTXM\ProviderLocalPeople\Providers\Provider;
use KTXM\ProviderLocalPeople\Store\Personal\Store;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\TestCase;
final class UserEventListenerTest extends TestCase
{
#[Test]
public function moduleRegistersDeferredUserLifecycleListeners(): void
{
$providers = $this->createMock(ProviderManager::class);
$providers->expects(self::once())
->method('register')
->with(ProviderInterface::TYPE_PEOPLE, 'default', Provider::class);
$events = new EventListenerRegistry();
(new Module($providers, $events))->boot();
$definitions = $events->definitions();
self::assertCount(2, $definitions);
self::assertSame(UserCreatedEvent::class, $definitions[0]->event);
self::assertSame(UserEventListener::class, $definitions[0]->service);
self::assertSame('onUserCreated', $definitions[0]->method);
self::assertSame(DeliveryMode::Deferred, $definitions[0]->delivery);
self::assertSame(UserDeletingEvent::class, $definitions[1]->event);
self::assertSame(UserEventListener::class, $definitions[1]->service);
self::assertSame('onUserDeleting', $definitions[1]->method);
self::assertSame(DeliveryMode::Deferred, $definitions[1]->delivery);
}
#[Test]
public function createsExactlyOnePersonalAddressBook(): void
{
$freshCollection = new CollectionResource();
$store = $this->createMock(Store::class);
$store->expects(self::exactly(2))
->method('collectionList')
->with('tenant-a', 'user-a')
->willReturnOnConsecutiveCalls([], [new CollectionResource()]);
$store->expects(self::once())
->method('collectionFresh')
->willReturn($freshCollection);
$store->expects(self::once())
->method('collectionCreate')
->with(
'tenant-a',
'user-a',
self::callback(static fn(CollectionResource $collection): bool =>
$collection->getProperties()->getLabel() === 'Personal Address Book'
&& $collection->getProperties()->getVisibility() === true
&& $collection->getEnabled()),
)
->willReturn(new CollectionResource());
$listener = new UserEventListener($store, $this->createStub(PersonalService::class));
$event = $this->userCreatedEvent();
$listener->onUserCreated($event);
$listener->onUserCreated($event);
self::assertTrue($freshCollection->getEnabled());
}
#[Test]
public function deletesPersonalAddressBooks(): void
{
$collection = (new CollectionResource())->fromStore(['cid' => 'address-book-a']);
$service = $this->createMock(PersonalService::class);
$service->expects(self::once())
->method('initialize')
->with('tenant-a', 'user-a')
->willReturnSelf();
$service->expects(self::once())
->method('collectionList')
->with(null)
->willReturn([$collection]);
$service->expects(self::once())
->method('collectionDelete')
->with(
self::callback(static fn(CollectionIdentifier $target): bool =>
$target->collection() === 'address-book-a'),
true,
)
->willReturn(true);
(new UserEventListener(
$this->createStub(Store::class),
$service,
))->onUserDeleting(UserDeletingEvent::fromUser([
'uid' => 'user-a',
'identity' => 'person@example.test',
], 'tenant-a'));
}
private function userCreatedEvent(): UserCreatedEvent
{
return UserCreatedEvent::fromUser([
'uid' => 'user-a',
'identity' => 'person@example.test',
], 'tenant-a');
}
}