1
0
Files
breadcrumb-the-shire/modules/notifications/lib/Module/Notifications/Service/NotificationService.php
fs fb6e30a062 feat: add notifications module with bell UI, event listeners, and cleanup job
In-app notification system with topbar bell icon, dropdown panel,
mark-as-read/dismiss actions, and scheduled cleanup of old read
notifications. Includes event listeners for user.created and
user.deleted events, authorization policy, and full test coverage.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 22:09:44 +01:00

204 lines
5.3 KiB
PHP

<?php
namespace MintyPHP\Module\Notifications\Service;
use MintyPHP\Module\Notifications\Repository\NotificationRepositoryInterface;
class NotificationService
{
public function __construct(
private readonly NotificationRepositoryInterface $notificationRepository
) {
}
/** @return list<array<string, mixed>> */
public function listForUser(int $userId, int $limit = 50): array
{
if ($userId <= 0) {
return [];
}
return $this->notificationRepository->listByUser($userId, $limit, 0);
}
public function unreadCount(int $userId): int
{
if ($userId <= 0) {
return 0;
}
return $this->notificationRepository->countUnreadByUser($userId);
}
public function markAsRead(int $userId, int $notificationId): bool
{
if ($userId <= 0 || $notificationId <= 0) {
return false;
}
return $this->notificationRepository->markRead($notificationId, $userId);
}
public function markAllAsRead(int $userId): int
{
if ($userId <= 0) {
return 0;
}
return $this->notificationRepository->markAllReadByUser($userId);
}
public function dismiss(int $userId, int $notificationId): bool
{
if ($userId <= 0 || $notificationId <= 0) {
return false;
}
return $this->notificationRepository->delete($notificationId, $userId);
}
public function createForUser(
int $recipientUserId,
?int $tenantId,
string $type,
string $title,
?string $body,
?string $link,
array $data
): int|false {
if ($recipientUserId <= 0 || $type === '' || $title === '') {
return false;
}
return $this->notificationRepository->create([
'recipient_user_id' => $recipientUserId,
'tenant_id' => $tenantId,
'type' => $type,
'title' => $title,
'body' => $body,
'link' => $link,
'data' => $data ?: null,
]);
}
/**
* Fan-out: create notification for all active users in a tenant, optionally excluding actor.
*
* @return int Number of notifications created
*/
public function createForTenantUsers(
int $tenantId,
string $type,
string $title,
?string $body,
?string $link,
array $data,
?int $excludeUserId
): int {
if ($tenantId <= 0 || $type === '' || $title === '') {
return 0;
}
$userIds = $this->listUserIdsForTenant($tenantId);
$created = 0;
foreach ($userIds as $userId) {
if ($excludeUserId !== null && $userId === $excludeUserId) {
continue;
}
$result = $this->createForUser($userId, $tenantId, $type, $title, $body, $link, $data);
if ($result !== false) {
$created++;
}
}
return $created;
}
/**
* Fan-out: create notification only for tenant users whose ID is in $allowedUserIds.
*
* @param array<int, mixed> $allowedUserIds Flipped array (user ID as key) for fast lookup
* @return int Number of notifications created
*/
public function createForTenantAdminUsers(
int $tenantId,
string $type,
string $title,
?string $body,
?string $link,
array $data,
?int $excludeUserId,
array $allowedUserIds
): int {
if ($tenantId <= 0 || $type === '' || $title === '' || $allowedUserIds === []) {
return 0;
}
$userIds = $this->listUserIdsForTenant($tenantId);
$created = 0;
foreach ($userIds as $userId) {
if (!isset($allowedUserIds[$userId])) {
continue;
}
if ($excludeUserId !== null && $userId === $excludeUserId) {
continue;
}
$result = $this->createForUser($userId, $tenantId, $type, $title, $body, $link, $data);
if ($result !== false) {
$created++;
}
}
return $created;
}
public function purgeOldRead(int $retentionDays = 90): int
{
return $this->notificationRepository->purgeReadOlderThanDays($retentionDays);
}
public function deleteAllForUser(int $userId): int
{
if ($userId <= 0) {
return 0;
}
return $this->notificationRepository->deleteAllByUser($userId);
}
/**
* Look up active user IDs belonging to a tenant.
*
* @return list<int>
*/
private function listUserIdsForTenant(int $tenantId): array
{
if ($tenantId <= 0) {
return [];
}
$rows = \MintyPHP\DB::select(
'select ut.user_id from user_tenants ut join users u on u.id = ut.user_id and u.active = 1 where ut.tenant_id = ?',
(string) $tenantId
);
if (!is_array($rows)) {
return [];
}
$ids = [];
foreach ($rows as $row) {
$data = $row['user_tenants'] ?? $row;
if (is_array($data) && isset($data['user_id'])) {
$ids[] = (int) $data['user_id'];
}
}
return array_values(array_unique($ids));
}
}