feat: listen to user life cycle events
Signed-off-by: Sebastian Krupinski <krupinski01@gmail.com>
This commit is contained in:
@@ -0,0 +1,57 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace KTXM\ProviderLocalChrono\Listeners;
|
||||||
|
|
||||||
|
use KTXC\User\Event\UserCreatedEvent;
|
||||||
|
use KTXC\User\Event\UserDeletingEvent;
|
||||||
|
use KTXF\Resource\Identifier\CollectionIdentifier;
|
||||||
|
use KTXM\ProviderLocalChrono\Providers\Personal\PersonalService;
|
||||||
|
use KTXM\ProviderLocalChrono\Store\Personal\Store;
|
||||||
|
|
||||||
|
final class UserEventListener
|
||||||
|
{
|
||||||
|
private const PERSONAL_CALENDAR_LABEL = 'Personal';
|
||||||
|
|
||||||
|
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_CALENDAR_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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,15 +3,21 @@
|
|||||||
namespace KTXM\ProviderLocalChrono;
|
namespace KTXM\ProviderLocalChrono;
|
||||||
|
|
||||||
use KTXC\Resource\ProviderManager;
|
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\ModuleBrowserInterface;
|
||||||
use KTXF\Module\ModuleInstanceAbstract;
|
use KTXF\Module\ModuleInstanceAbstract;
|
||||||
use KTXF\Resource\Provider\ProviderInterface;
|
use KTXF\Resource\Provider\ProviderInterface;
|
||||||
|
use KTXM\ProviderLocalChrono\Listeners\UserEventListener;
|
||||||
use KTXM\ProviderLocalChrono\Providers\Provider;
|
use KTXM\ProviderLocalChrono\Providers\Provider;
|
||||||
|
|
||||||
class Module extends ModuleInstanceAbstract implements ModuleBrowserInterface
|
class Module extends ModuleInstanceAbstract implements ModuleBrowserInterface
|
||||||
{
|
{
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private readonly ProviderManager $providerManager,
|
private readonly ProviderManager $providerManager,
|
||||||
|
private readonly EventListenerRegistrarInterface $events,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public function handle(): string
|
public function handle(): string
|
||||||
@@ -52,6 +58,10 @@ class Module extends ModuleInstanceAbstract implements ModuleBrowserInterface
|
|||||||
|
|
||||||
public function boot(): void
|
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_CHRONO, 'default', Provider::class);
|
$this->providerManager->register(ProviderInterface::TYPE_CHRONO, 'default', Provider::class);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -282,7 +282,11 @@ class PersonalService implements ServiceBaseInterface, ServiceCollectionMutableI
|
|||||||
throw new InvalidParameterException("Invalid: Collection identifier '$id' does not exist or does not belong to user '{$this->serviceUserId}'");
|
throw new InvalidParameterException("Invalid: Collection identifier '$id' does not exist or does not belong to user '{$this->serviceUserId}'");
|
||||||
}
|
}
|
||||||
// destroy collection in store
|
// 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]);
|
unset($this->serviceCollectionCache[$id]);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -308,7 +308,12 @@ class Store {
|
|||||||
if ($identifier === null) {
|
if ($identifier === null) {
|
||||||
return $entity;
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -592,6 +597,21 @@ class Store {
|
|||||||
return false;
|
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
|
* chronicle a operation to an entity to the data store
|
||||||
*
|
*
|
||||||
@@ -758,11 +778,13 @@ class Store {
|
|||||||
*
|
*
|
||||||
* @since Release 1.0.0
|
* @since Release 1.0.0
|
||||||
*
|
*
|
||||||
|
* @param string $tenantId tenant identifier
|
||||||
* @param array $identifiers collection of identifiers
|
* @param array $identifiers collection of identifiers
|
||||||
*/
|
*/
|
||||||
private function chronicleExpungeByCollectionId(array $identifiers): void {
|
private function chronicleExpungeByCollectionId(string $tenantId, array $identifiers): void {
|
||||||
// Delete chronicle entries for the specified collection identifiers
|
// Delete chronicle entries for the specified collection identifiers
|
||||||
$this->_store->selectCollection($this->_ChronicleTable)->deleteMany([
|
$this->_store->selectCollection($this->_ChronicleTable)->deleteMany([
|
||||||
|
'tid' => $tenantId,
|
||||||
'cid' => ['$in' => $identifiers]
|
'cid' => ['$in' => $identifiers]
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,118 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace KTXT\ProviderLocalChrono\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\Identifier\CollectionIdentifier;
|
||||||
|
use KTXF\Resource\Provider\ProviderInterface;
|
||||||
|
use KTXM\ProviderLocalChrono\Listeners\UserEventListener;
|
||||||
|
use KTXM\ProviderLocalChrono\Module;
|
||||||
|
use KTXM\ProviderLocalChrono\Providers\Personal\CollectionResource;
|
||||||
|
use KTXM\ProviderLocalChrono\Providers\Personal\PersonalService;
|
||||||
|
use KTXM\ProviderLocalChrono\Providers\Provider;
|
||||||
|
use KTXM\ProviderLocalChrono\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_CHRONO, '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 createsExactlyOnePersonalCalendar(): 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'
|
||||||
|
&& $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 deletesPersonalCalendars(): void
|
||||||
|
{
|
||||||
|
$collection = (new CollectionResource())->fromStore(['cid' => 'calendar-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() === 'calendar-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');
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user