forked from fa/breadcrumb-the-shire
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>
136 lines
3.7 KiB
PHP
136 lines
3.7 KiB
PHP
<?php
|
|
|
|
namespace MintyPHP\Module\Notifications\Repository;
|
|
|
|
use MintyPHP\DB;
|
|
use MintyPHP\Repository\Support\RepositoryArrayHelper;
|
|
|
|
class NotificationRepository implements NotificationRepositoryInterface
|
|
{
|
|
private function unwrapList(mixed $rows): array
|
|
{
|
|
return RepositoryArrayHelper::unwrapList($rows, 'user_notifications');
|
|
}
|
|
|
|
public function create(array $data): int|false
|
|
{
|
|
$result = DB::insert(
|
|
'insert into user_notifications (recipient_user_id, tenant_id, type, title, body, link, data, created) values (?, ?, ?, ?, ?, ?, ?, NOW())',
|
|
(string) ($data['recipient_user_id'] ?? 0),
|
|
$data['tenant_id'] !== null ? (string) $data['tenant_id'] : null,
|
|
(string) ($data['type'] ?? ''),
|
|
(string) ($data['title'] ?? ''),
|
|
$data['body'] ?? null,
|
|
$data['link'] ?? null,
|
|
isset($data['data']) ? json_encode($data['data'], JSON_THROW_ON_ERROR) : null
|
|
);
|
|
|
|
return is_int($result) && $result > 0 ? $result : false;
|
|
}
|
|
|
|
public function listByUser(int $userId, int $limit, int $offset): array
|
|
{
|
|
if ($userId <= 0) {
|
|
return [];
|
|
}
|
|
|
|
$limit = max(1, min($limit, 100));
|
|
$offset = max(0, $offset);
|
|
|
|
$rows = DB::select(
|
|
'select id, type, title, body, link, is_read, created from user_notifications where recipient_user_id = ? order by created desc limit ? offset ?',
|
|
(string) $userId,
|
|
(string) $limit,
|
|
(string) $offset
|
|
);
|
|
|
|
return $this->unwrapList($rows);
|
|
}
|
|
|
|
public function countUnreadByUser(int $userId): int
|
|
{
|
|
if ($userId <= 0) {
|
|
return 0;
|
|
}
|
|
|
|
$count = DB::selectValue(
|
|
'select count(*) from user_notifications where recipient_user_id = ? and is_read = 0',
|
|
(string) $userId
|
|
);
|
|
|
|
return (int) $count;
|
|
}
|
|
|
|
public function markRead(int $id, int $userId): bool
|
|
{
|
|
if ($id <= 0 || $userId <= 0) {
|
|
return false;
|
|
}
|
|
|
|
$affected = DB::update(
|
|
'update user_notifications set is_read = 1, read_at = NOW() where id = ? and recipient_user_id = ? and is_read = 0',
|
|
(string) $id,
|
|
(string) $userId
|
|
);
|
|
|
|
return is_int($affected) && $affected > 0;
|
|
}
|
|
|
|
public function markAllReadByUser(int $userId): int
|
|
{
|
|
if ($userId <= 0) {
|
|
return 0;
|
|
}
|
|
|
|
$affected = DB::update(
|
|
'update user_notifications set is_read = 1, read_at = NOW() where recipient_user_id = ? and is_read = 0',
|
|
(string) $userId
|
|
);
|
|
|
|
return is_int($affected) ? $affected : 0;
|
|
}
|
|
|
|
public function delete(int $id, int $userId): bool
|
|
{
|
|
if ($id <= 0 || $userId <= 0) {
|
|
return false;
|
|
}
|
|
|
|
$affected = DB::delete(
|
|
'delete from user_notifications where id = ? and recipient_user_id = ?',
|
|
(string) $id,
|
|
(string) $userId
|
|
);
|
|
|
|
return is_int($affected) && $affected > 0;
|
|
}
|
|
|
|
public function deleteAllByUser(int $userId): int
|
|
{
|
|
if ($userId <= 0) {
|
|
return 0;
|
|
}
|
|
|
|
$affected = DB::delete(
|
|
'delete from user_notifications where recipient_user_id = ?',
|
|
(string) $userId
|
|
);
|
|
|
|
return is_int($affected) ? $affected : 0;
|
|
}
|
|
|
|
public function purgeReadOlderThanDays(int $days): int
|
|
{
|
|
if ($days <= 0) {
|
|
return 0;
|
|
}
|
|
|
|
$affected = DB::delete(
|
|
'delete from user_notifications where is_read = 1 and read_at < DATE_SUB(NOW(), INTERVAL ? DAY)',
|
|
(string) $days
|
|
);
|
|
|
|
return is_int($affected) ? $affected : 0;
|
|
}
|
|
}
|