1
0
Files
breadcrumb-the-shire/core/Repository/Stats/AdminStatsRepository.php
fs 3f0db68b27 refactor: fix all 105 PHPStan 2 strict type findings across core and modules
Resolve all non-false-positive PHPStan 2 findings, reducing the baseline
from 486 to 382 entries (only unused-public false positives remain).

Categories fixed:
- Remove 39 redundant type checks (is_string, is_array, is_object, method_exists)
- Remove 12 no-op array_values() on already-sequential lists
- Simplify 13 null-coalesce on guaranteed offsets
- Remove 8 always-true instanceof checks
- Replace 12 redundant test assertions (assertTrue(true) → addToAssertionCount)
- Simplify 6 unnecessary nullsafe operators (?-> → ->)
- Fix 6 always-true !== '' comparisons
- Fix remaining: unset.offset, return.unusedType, staticMethod narrowing

53 files changed across core/, modules/, pages/, and tests/.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-14 12:54:20 +02:00

1088 lines
55 KiB
PHP

<?php
namespace MintyPHP\Repository\Stats;
use MintyPHP\DB;
use MintyPHP\Domain\Taxonomy\MailLogStatus;
use MintyPHP\Domain\Taxonomy\ScheduledJobRunStatus;
use MintyPHP\Domain\Taxonomy\ScheduledJobTriggerType;
use MintyPHP\Domain\Taxonomy\TenantStatus;
class AdminStatsRepository implements AdminStatsRepositoryInterface
{
/**
* @return array<string, mixed>
*/
public function buildViewData(): array
{
$tenantActiveStatus = TenantStatus::Active->value;
$tenantInactiveStatus = TenantStatus::Inactive->value;
$importStatusRunning = 'running';
$importStatusSuccess = 'success';
$importStatusPartial = 'partial';
$importStatusFailed = 'failed';
$systemAuditOutcomeFailed = 'failed';
$systemAuditOutcomeDenied = 'denied';
$lifecycleStatusFailed = 'failed';
$lifecycleStatusSkipped = 'skipped';
$lifecycleActionRestore = 'restore';
$schedulerTriggerScheduler = ScheduledJobTriggerType::Scheduler->value;
$schedulerRunStatusFailed = ScheduledJobRunStatus::Failed->value;
$mailStatusFailed = MailLogStatus::Failed->value;
$mailStatusSent = MailLogStatus::Sent->value;
$activeUserCount = (int) (DB::selectValue('select count(*) from users where active = 1') ?? 0);
$inactiveUserCount = (int) (DB::selectValue('select count(*) from users where active = 0') ?? 0);
$activeTenantCount = (int) (DB::selectValue('select count(*) from tenants where status = ?', $tenantActiveStatus) ?? 0);
$inactiveTenantCount = (int) (DB::selectValue('select count(*) from tenants where status = ?', $tenantInactiveStatus) ?? 0);
$activeDepartmentCount = (int) (DB::selectValue('select count(*) from departments where active = 1') ?? 0);
$inactiveDepartmentCount = (int) (DB::selectValue('select count(*) from departments where active = 0') ?? 0);
$activeRoleCount = (int) (DB::selectValue('select count(*) from roles where active = 1') ?? 0);
$inactiveRoleCount = (int) (DB::selectValue('select count(*) from roles where active = 0') ?? 0);
$rolesWithoutPermissionsCount = (int) (DB::selectValue(
'select count(*) from roles r left join role_permissions rp on rp.role_id = r.id where r.active = 1 and rp.role_id is null'
) ?? 0);
$permissionsWithoutRolesCount = (int) (DB::selectValue(
'select count(*) from permissions p left join role_permissions rp on rp.permission_id = p.id where p.active = 1 and rp.permission_id is null'
) ?? 0);
$usersWithoutRolesCount = (int) (DB::selectValue(
'select count(*) from users u left join user_roles ur on ur.user_id = u.id where u.active = 1 and ur.user_id is null'
) ?? 0);
$usersWithoutTenantsCount = (int) (DB::selectValue(
'select count(*) from users u left join user_tenants ut on ut.user_id = u.id where u.active = 1 and ut.user_id is null'
) ?? 0);
$flattenStatsRow = static function ($row): array {
if (!is_array($row)) {
return [];
}
if (isset($row['']) && is_array($row[''])) {
return $row[''];
}
$flat = [];
foreach ($row as $key => $value) {
if (is_array($value)) {
$flat = array_merge($flat, $value);
} elseif (is_string($key)) {
$flat[$key] = $value;
}
}
return $flat;
};
$ssoStatsAvailable = (int) (DB::selectValue(
"select count(*) from information_schema.tables where table_schema = database() and table_name = 'tenant_auth_microsoft'"
) ?? 0) === 1;
$ssoEnabledTenantCount = 0;
$ssoEnforcedTenantCount = 0;
$ssoEnabledTenantPercent = 0.0;
$usersMicrosoftOnlyCount = 0;
$usersMicrosoftSyncGapsCount = 0;
$ssoTenantRows = [];
$usersMicrosoftOnlyRows = [];
$usersMicrosoftSyncGapRows = [];
if ($ssoStatsAvailable) {
$ssoEnabledTenantCount = (int) (DB::selectValue(
"select count(*) from tenants t " .
"left join tenant_auth_microsoft tam on tam.tenant_id = t.id " .
"where t.status = ? and coalesce(tam.enabled, 0) = 1",
$tenantActiveStatus
) ?? 0);
$ssoEnforcedTenantCount = (int) (DB::selectValue(
"select count(*) from tenants t " .
"left join tenant_auth_microsoft tam on tam.tenant_id = t.id " .
"where t.status = ? and coalesce(tam.enabled, 0) = 1 and coalesce(tam.enforce_microsoft_login, 0) = 1",
$tenantActiveStatus
) ?? 0);
if ($activeTenantCount > 0) {
$ssoEnabledTenantPercent = round(((float) $ssoEnabledTenantCount * 100) / (float) $activeTenantCount, 1);
}
$usersMicrosoftOnlyCount = (int) (DB::selectValue(
"select count(*) from users u " .
"where u.active = 1 " .
"and exists ( " .
" select 1 from user_tenants ut " .
" join tenants t on t.id = ut.tenant_id and t.status = ? " .
" where ut.user_id = u.id " .
") " .
"and not exists ( " .
" select 1 from user_tenants ut " .
" join tenants t on t.id = ut.tenant_id and t.status = ? " .
" left join tenant_auth_microsoft tam on tam.tenant_id = t.id " .
" where ut.user_id = u.id " .
" and (coalesce(tam.enabled, 0) = 0 or coalesce(tam.enforce_microsoft_login, 0) = 0) " .
")",
$tenantActiveStatus,
$tenantActiveStatus
) ?? 0);
$usersMicrosoftSyncGapsCount = (int) (DB::selectValue(
"select count(*) from ( " .
" select u.id, " .
" trim(coalesce(u.first_name, '')) as first_name_value, " .
" trim(coalesce(u.last_name, '')) as last_name_value, " .
" trim(coalesce(u.phone, '')) as phone_value, " .
" trim(coalesce(u.mobile, '')) as mobile_value, " .
" max(find_in_set('first_name', coalesce(nullif(tam.sync_profile_fields, ''), 'first_name,last_name')) > 0) as need_first_name, " .
" max(find_in_set('last_name', coalesce(nullif(tam.sync_profile_fields, ''), 'first_name,last_name')) > 0) as need_last_name, " .
" max(find_in_set('phone', coalesce(nullif(tam.sync_profile_fields, ''), 'first_name,last_name')) > 0) as need_phone, " .
" max(find_in_set('mobile', coalesce(nullif(tam.sync_profile_fields, ''), 'first_name,last_name')) > 0) as need_mobile " .
" from users u " .
" join user_tenants ut on ut.user_id = u.id " .
" join tenants t on t.id = ut.tenant_id and t.status = ? " .
" join tenant_auth_microsoft tam on tam.tenant_id = t.id " .
" where u.active = 1 and u.last_login_provider = 'microsoft' and tam.enabled = 1 and tam.sync_profile_on_login = 1 " .
" group by u.id, u.first_name, u.last_name, u.phone, u.mobile " .
") x " .
"where (x.need_first_name = 1 and x.first_name_value = '') " .
" or (x.need_last_name = 1 and x.last_name_value = '') " .
" or (x.need_phone = 1 and x.phone_value = '') " .
" or (x.need_mobile = 1 and x.mobile_value = '')",
$tenantActiveStatus
) ?? 0);
$ssoTenantRowsRaw = DB::select(
"select " .
" t.uuid as tenant_uuid, " .
" t.description as tenant_description, " .
" coalesce(tam.enabled, 0) as sso_enabled, " .
" coalesce(tam.enforce_microsoft_login, 0) as enforce_microsoft_login, " .
" coalesce(tam.sync_profile_on_login, 0) as sync_profile_on_login, " .
" coalesce(nullif(tam.sync_profile_fields, ''), '-') as sync_profile_fields " .
"from tenants t " .
"left join tenant_auth_microsoft tam on tam.tenant_id = t.id " .
"where t.status = ? " .
"order by sso_enabled desc, enforce_microsoft_login desc, t.description asc " .
"limit 10",
$tenantActiveStatus
);
$ssoTenantRows = is_array($ssoTenantRowsRaw) ? array_values(array_filter(array_map(
static function ($row) use ($flattenStatsRow): ?array {
$flat = $flattenStatsRow($row);
$tenantUuid = trim((string) ($flat['tenant_uuid'] ?? ($flat['uuid'] ?? '')));
$tenantDescription = trim((string) ($flat['tenant_description'] ?? ($flat['description'] ?? '')));
if ($tenantUuid === '' || $tenantDescription === '') {
return null;
}
return [
'tenant_uuid' => $tenantUuid,
'tenant_description' => $tenantDescription,
'sso_enabled' => (int) ($flat['sso_enabled'] ?? ($flat['enabled'] ?? 0)),
'enforce_microsoft_login' => (int) ($flat['enforce_microsoft_login'] ?? 0),
'sync_profile_on_login' => (int) ($flat['sync_profile_on_login'] ?? 0),
'sync_profile_fields' => trim((string) ($flat['sync_profile_fields'] ?? '-')),
];
},
$ssoTenantRowsRaw
))) : [];
$usersMicrosoftOnlyRowsRaw = DB::select(
"select " .
" u.uuid as user_uuid, " .
" u.display_name as display_name, " .
" u.email as email, " .
" group_concat(distinct t.description order by t.description separator ', ') as tenant_labels " .
"from users u " .
"join user_tenants ut on ut.user_id = u.id " .
"join tenants t on t.id = ut.tenant_id and t.status = ? " .
"left join tenant_auth_microsoft tam on tam.tenant_id = t.id " .
"where u.active = 1 " .
"group by u.id, u.uuid, u.display_name, u.email " .
"having sum(case when coalesce(tam.enabled, 0) = 0 or coalesce(tam.enforce_microsoft_login, 0) = 0 then 1 else 0 end) = 0 " .
"order by u.display_name asc " .
"limit 10",
$tenantActiveStatus
);
$usersMicrosoftOnlyRows = is_array($usersMicrosoftOnlyRowsRaw) ? array_values(array_filter(array_map(
static function ($row) use ($flattenStatsRow): ?array {
$flat = $flattenStatsRow($row);
$userUuid = trim((string) ($flat['user_uuid'] ?? ($flat['uuid'] ?? '')));
$email = trim((string) ($flat['user_email'] ?? ($flat['email'] ?? '')));
if ($userUuid === '' || $email === '') {
return null;
}
return [
'user_uuid' => $userUuid,
'display_name' => trim((string) ($flat['user_display_name'] ?? ($flat['display_name'] ?? ''))),
'email' => $email,
'tenant_labels' => trim((string) ($flat['tenant_labels'] ?? ($flat['description'] ?? ''))),
];
},
$usersMicrosoftOnlyRowsRaw
))) : [];
$usersMicrosoftSyncGapRowsRaw = DB::select(
"select " .
" x.user_uuid, x.user_display_name, x.user_email, " .
" trim(concat_ws(', ', " .
" if(x.need_first_name = 1, 'first_name', null), " .
" if(x.need_last_name = 1, 'last_name', null), " .
" if(x.need_phone = 1, 'phone', null), " .
" if(x.need_mobile = 1, 'mobile', null) " .
" )) as configured_fields, " .
" trim(concat_ws(', ', " .
" if(x.need_first_name = 1 and x.first_name_value = '', 'first_name', null), " .
" if(x.need_last_name = 1 and x.last_name_value = '', 'last_name', null), " .
" if(x.need_phone = 1 and x.phone_value = '', 'phone', null), " .
" if(x.need_mobile = 1 and x.mobile_value = '', 'mobile', null) " .
" )) as missing_fields " .
"from ( " .
" select " .
" u.id, u.uuid as user_uuid, u.display_name as user_display_name, u.email as user_email, " .
" trim(coalesce(u.first_name, '')) as first_name_value, " .
" trim(coalesce(u.last_name, '')) as last_name_value, " .
" trim(coalesce(u.phone, '')) as phone_value, " .
" trim(coalesce(u.mobile, '')) as mobile_value, " .
" max(find_in_set('first_name', coalesce(nullif(tam.sync_profile_fields, ''), 'first_name,last_name')) > 0) as need_first_name, " .
" max(find_in_set('last_name', coalesce(nullif(tam.sync_profile_fields, ''), 'first_name,last_name')) > 0) as need_last_name, " .
" max(find_in_set('phone', coalesce(nullif(tam.sync_profile_fields, ''), 'first_name,last_name')) > 0) as need_phone, " .
" max(find_in_set('mobile', coalesce(nullif(tam.sync_profile_fields, ''), 'first_name,last_name')) > 0) as need_mobile " .
" from users u " .
" join user_tenants ut on ut.user_id = u.id " .
" join tenants t on t.id = ut.tenant_id and t.status = ? " .
" join tenant_auth_microsoft tam on tam.tenant_id = t.id " .
" where u.active = 1 and u.last_login_provider = 'microsoft' and tam.enabled = 1 and tam.sync_profile_on_login = 1 " .
" group by u.id, u.uuid, u.display_name, u.email, u.first_name, u.last_name, u.phone, u.mobile " .
") x " .
"where (x.need_first_name = 1 and x.first_name_value = '') " .
" or (x.need_last_name = 1 and x.last_name_value = '') " .
" or (x.need_phone = 1 and x.phone_value = '') " .
" or (x.need_mobile = 1 and x.mobile_value = '') " .
"order by x.user_display_name asc " .
"limit 10",
$tenantActiveStatus
);
$usersMicrosoftSyncGapRows = is_array($usersMicrosoftSyncGapRowsRaw) ? array_values(array_filter(array_map(
static function ($row) use ($flattenStatsRow): ?array {
$flat = $flattenStatsRow($row);
$userUuid = trim((string) ($flat['user_uuid'] ?? ($flat['uuid'] ?? '')));
$email = trim((string) ($flat['user_email'] ?? ($flat['email'] ?? '')));
if ($userUuid === '' || $email === '') {
return null;
}
return [
'user_uuid' => $userUuid,
'display_name' => trim((string) ($flat['user_display_name'] ?? ($flat['display_name'] ?? ''))),
'email' => $email,
'configured_fields' => trim((string) ($flat['configured_fields'] ?? '')),
'missing_fields' => trim((string) ($flat['missing_fields'] ?? '')),
];
},
$usersMicrosoftSyncGapRowsRaw
))) : [];
}
$customFieldStatsAvailable = (int) (DB::selectValue(
"select count(*) from information_schema.tables " .
"where table_schema = database() " .
"and table_name in ('tenant_custom_field_definitions', 'tenant_custom_field_options', 'user_custom_field_values')"
) ?? 0) === 3;
$customFieldsSelectionWithoutOptionsCount = 0;
$customFieldTenantsWithSelectionWithoutOptionsCount = 0;
$customFieldsInactiveWithValuesCount = 0;
$customFieldsUnusedActiveCount = 0;
$customFieldsSelectionWithoutOptionsHref = 'admin/tenants';
$customFieldTenantsWithSelectionWithoutOptionsHref = 'admin/tenants';
$customFieldsInactiveWithValuesHref = 'admin/tenants';
$customFieldsUnusedActiveHref = 'admin/tenants';
$customFieldIssueTenantRows = [];
if ($customFieldStatsAvailable) {
$loadCustomFieldIssueTenants = static function (string $query): array {
$rows = DB::select($query);
if (!is_array($rows)) {
return [];
}
$items = [];
foreach ($rows as $row) {
$tenant = $row['tenants'] ?? [];
$meta = $row[''] ?? [];
$tenantUuid = trim((string) ($tenant['uuid'] ?? ''));
$tenantDescription = trim((string) ($tenant['description'] ?? ''));
$issueCount = (int) ($meta['issue_count'] ?? 0);
if ($tenantUuid === '' || $tenantDescription === '' || $issueCount <= 0) {
continue;
}
$items[] = [
'tenant_uuid' => $tenantUuid,
'tenant_description' => $tenantDescription,
'issue_count' => $issueCount,
];
}
return $items;
};
$customFieldsSelectionWithoutOptionsCount = (int) (DB::selectValue(
"select count(*) from ( " .
"select d.id " .
"from tenant_custom_field_definitions d " .
"left join tenant_custom_field_options o on o.definition_id = d.id and o.active = 1 " .
"where d.active = 1 and d.type in ('select', 'multiselect') " .
"group by d.id " .
"having count(o.id) = 0 " .
') missing_options'
) ?? 0);
$customFieldTenantsWithSelectionWithoutOptionsCount = (int) (DB::selectValue(
"select count(distinct missing_options.tenant_id) from ( " .
"select d.tenant_id " .
"from tenant_custom_field_definitions d " .
"left join tenant_custom_field_options o on o.definition_id = d.id and o.active = 1 " .
"where d.active = 1 and d.type in ('select', 'multiselect') " .
"group by d.id, d.tenant_id " .
"having count(o.id) = 0 " .
') missing_options'
) ?? 0);
$customFieldsInactiveWithValuesCount = (int) (DB::selectValue(
'select count(distinct d.id) from tenant_custom_field_definitions d ' .
'inner join user_custom_field_values v on v.definition_id = d.id ' .
'where d.active = 0'
) ?? 0);
$customFieldsUnusedActiveCount = (int) (DB::selectValue(
"select count(*) from ( " .
"select d.id " .
"from tenant_custom_field_definitions d " .
"left join user_custom_field_values v on v.definition_id = d.id " .
"where d.active = 1 " .
"group by d.id " .
"having count(v.id) = 0 " .
') unused_active'
) ?? 0);
$selectionWithoutOptionsTenantUuid = (string) (DB::selectValue(
"select t.uuid from tenants t " .
"inner join ( " .
"select d.tenant_id " .
"from tenant_custom_field_definitions d " .
"left join tenant_custom_field_options o on o.definition_id = d.id and o.active = 1 " .
"where d.active = 1 and d.type in ('select', 'multiselect') " .
"group by d.id, d.tenant_id " .
"having count(o.id) = 0 " .
"order by d.tenant_id asc " .
"limit 1" .
') issue on issue.tenant_id = t.id ' .
'limit 1'
) ?? '');
$inactiveWithValuesTenantUuid = (string) (DB::selectValue(
"select t.uuid from tenants t " .
"inner join ( " .
"select d.tenant_id " .
"from tenant_custom_field_definitions d " .
"inner join user_custom_field_values v on v.definition_id = d.id " .
"where d.active = 0 " .
"group by d.tenant_id " .
"order by d.tenant_id asc " .
"limit 1" .
') issue on issue.tenant_id = t.id ' .
'limit 1'
) ?? '');
$unusedActiveTenantUuid = (string) (DB::selectValue(
"select t.uuid from tenants t " .
"inner join ( " .
"select d.tenant_id " .
"from tenant_custom_field_definitions d " .
"left join user_custom_field_values v on v.definition_id = d.id " .
"where d.active = 1 " .
"group by d.id, d.tenant_id " .
"having count(v.id) = 0 " .
"order by d.tenant_id asc " .
"limit 1" .
') issue on issue.tenant_id = t.id ' .
'limit 1'
) ?? '');
if ($selectionWithoutOptionsTenantUuid !== '') {
$customFieldsSelectionWithoutOptionsHref = 'admin/tenants/edit/' . $selectionWithoutOptionsTenantUuid . '?tab=custom_fields';
$customFieldTenantsWithSelectionWithoutOptionsHref = $customFieldsSelectionWithoutOptionsHref;
}
if ($inactiveWithValuesTenantUuid !== '') {
$customFieldsInactiveWithValuesHref = 'admin/tenants/edit/' . $inactiveWithValuesTenantUuid . '?tab=custom_fields';
}
if ($unusedActiveTenantUuid !== '') {
$customFieldsUnusedActiveHref = 'admin/tenants/edit/' . $unusedActiveTenantUuid . '?tab=custom_fields';
}
$selectionIssueTenants = $loadCustomFieldIssueTenants(
"select t.uuid, t.description, count(*) as issue_count " .
"from tenants t " .
"inner join ( " .
"select d.id, d.tenant_id " .
"from tenant_custom_field_definitions d " .
"left join tenant_custom_field_options o on o.definition_id = d.id and o.active = 1 " .
"where d.active = 1 and d.type in ('select', 'multiselect') " .
"group by d.id, d.tenant_id " .
"having count(o.id) = 0 " .
") issue on issue.tenant_id = t.id " .
"group by t.id, t.uuid, t.description " .
"order by issue_count desc, t.description asc " .
"limit 5"
);
foreach ($selectionIssueTenants as $row) {
$customFieldIssueTenantRows[] = [
'issue' => t('Selection fields without options'),
'tenant_uuid' => $row['tenant_uuid'],
'tenant_description' => $row['tenant_description'],
'issue_count' => $row['issue_count'],
];
}
$inactiveIssueTenants = $loadCustomFieldIssueTenants(
"select t.uuid, t.description, count(distinct d.id) as issue_count " .
"from tenants t " .
"inner join tenant_custom_field_definitions d on d.tenant_id = t.id and d.active = 0 " .
"inner join user_custom_field_values v on v.definition_id = d.id " .
"group by t.id, t.uuid, t.description " .
"order by issue_count desc, t.description asc " .
"limit 5"
);
foreach ($inactiveIssueTenants as $row) {
$customFieldIssueTenantRows[] = [
'issue' => t('Inactive custom fields with values'),
'tenant_uuid' => $row['tenant_uuid'],
'tenant_description' => $row['tenant_description'],
'issue_count' => $row['issue_count'],
];
}
$unusedIssueTenants = $loadCustomFieldIssueTenants(
"select t.uuid, t.description, count(*) as issue_count " .
"from tenants t " .
"inner join ( " .
"select d.id, d.tenant_id " .
"from tenant_custom_field_definitions d " .
"left join user_custom_field_values v on v.definition_id = d.id " .
"where d.active = 1 " .
"group by d.id, d.tenant_id " .
"having count(v.id) = 0 " .
") issue on issue.tenant_id = t.id " .
"group by t.id, t.uuid, t.description " .
"order by issue_count desc, t.description asc " .
"limit 5"
);
foreach ($unusedIssueTenants as $row) {
$customFieldIssueTenantRows[] = [
'issue' => t('Unused active custom fields'),
'tenant_uuid' => $row['tenant_uuid'],
'tenant_description' => $row['tenant_description'],
'issue_count' => $row['issue_count'],
];
}
}
$apiStatsAvailable = (int) (DB::selectValue(
"select count(*) from information_schema.tables " .
"where table_schema = database() and table_name in ('api_audit_log', 'user_api_tokens')"
) ?? 0) === 2;
$apiRequests24hCount = 0;
$apiRequestsPrev24hCount = 0;
$apiRequestTrendPercent = 0.0;
$apiErrors4xx24hCount = 0;
$apiErrors5xx24hCount = 0;
$apiP95DurationMs = 0;
$apiTokensExpiring7dCount = 0;
$apiTopErrorRows = [];
$apiSlowEndpointRows = [];
if ($apiStatsAvailable) {
$apiRequests24hCount = (int) (DB::selectValue(
'select count(*) from api_audit_log where created_at >= (NOW() - INTERVAL 24 HOUR)'
) ?? 0);
$apiRequestsPrev24hCount = (int) (DB::selectValue(
'select count(*) from api_audit_log where created_at >= (NOW() - INTERVAL 48 HOUR) and created_at < (NOW() - INTERVAL 24 HOUR)'
) ?? 0);
if ($apiRequestsPrev24hCount > 0) {
$apiRequestTrendPercent = round((($apiRequests24hCount - $apiRequestsPrev24hCount) * 100) / $apiRequestsPrev24hCount, 1);
}
$apiErrors4xx24hCount = (int) (DB::selectValue(
'select count(*) from api_audit_log where created_at >= (NOW() - INTERVAL 24 HOUR) and status_code between 400 and 499'
) ?? 0);
$apiErrors5xx24hCount = (int) (DB::selectValue(
'select count(*) from api_audit_log where created_at >= (NOW() - INTERVAL 24 HOUR) and status_code between 500 and 599'
) ?? 0);
$durationCount = (int) (DB::selectValue(
'select count(*) from api_audit_log where created_at >= (NOW() - INTERVAL 24 HOUR) and status_code between 200 and 299 and duration_ms is not null'
) ?? 0);
if ($durationCount > 0) {
$offset = (int) floor(($durationCount - 1) * 0.95);
$apiP95DurationMs = (int) (DB::selectValue(
'select duration_ms from api_audit_log ' .
'where created_at >= (NOW() - INTERVAL 24 HOUR) and status_code between 200 and 299 and duration_ms is not null ' .
'order by duration_ms asc limit ' . $offset . ', 1'
) ?? 0);
}
$apiTokensExpiring7dCount = (int) (DB::selectValue(
'select count(*) from user_api_tokens ' .
'where revoked_at is null and expires_at is not null and expires_at > NOW() and expires_at <= (NOW() + INTERVAL 7 DAY)'
) ?? 0);
$apiTopErrorRowsRaw = DB::select(
"select " .
" coalesce(nullif(error_code, ''), concat('http_', status_code)) as error_label, " .
" count(*) as hit_count " .
"from api_audit_log " .
"where created_at >= (NOW() - INTERVAL 24 HOUR) and status_code >= 400 " .
"group by error_label " .
"order by hit_count desc, error_label asc " .
"limit 5"
);
$apiTopErrorRows = is_array($apiTopErrorRowsRaw) ? array_values(array_filter(array_map(
static function ($row) use ($flattenStatsRow): ?array {
$flat = $flattenStatsRow($row);
$label = trim((string) ($flat['error_label'] ?? ''));
$hits = (int) ($flat['hit_count'] ?? 0);
if ($label === '' || $hits <= 0) {
return null;
}
return [
'error_label' => $label,
'hit_count' => $hits,
];
},
$apiTopErrorRowsRaw
))) : [];
$apiSlowEndpointRowsRaw = DB::select(
"select " .
" path, " .
" count(*) as hit_count, " .
" round(avg(duration_ms), 1) as avg_duration_ms, " .
" max(duration_ms) as max_duration_ms " .
"from api_audit_log " .
"where created_at >= (NOW() - INTERVAL 24 HOUR) and status_code between 200 and 299 and duration_ms is not null " .
"group by path " .
"having count(*) >= 3 " .
"order by avg(duration_ms) desc, max(duration_ms) desc " .
"limit 5"
);
$apiSlowEndpointRows = is_array($apiSlowEndpointRowsRaw) ? array_values(array_filter(array_map(
static function ($row) use ($flattenStatsRow): ?array {
$flat = $flattenStatsRow($row);
$path = trim((string) ($flat['path'] ?? ''));
$hits = (int) ($flat['hit_count'] ?? 0);
if ($path === '' || $hits <= 0) {
return null;
}
return [
'path' => $path,
'hit_count' => $hits,
'avg_duration_ms' => (float) ($flat['avg_duration_ms'] ?? 0),
'max_duration_ms' => (int) ($flat['max_duration_ms'] ?? 0),
];
},
$apiSlowEndpointRowsRaw
))) : [];
}
$importStatsAvailable = (int) (DB::selectValue(
"select count(*) from information_schema.tables " .
"where table_schema = database() and table_name = 'import_audit_runs'"
) ?? 0) === 1;
$importRuns7dCount = 0;
$importRunsPrev7dCount = 0;
$importRunTrendPercent = 0.0;
$importRowsCreated7dCount = 0;
$importRowsFailed7dCount = 0;
$importPartialOrFailedRuns7dCount = 0;
$importSuccessRate7dPercent = 0.0;
$importAverageDurationMs7d = 0;
$importRecentRunRows = [];
$importProfileRows = [];
$importTopErrorCodeRows = [];
if ($importStatsAvailable) {
$importRuns7dCount = (int) (DB::selectValue(
"select count(*) from import_audit_runs where started_at >= (NOW() - INTERVAL 7 DAY)"
) ?? 0);
$importRunsPrev7dCount = (int) (DB::selectValue(
"select count(*) from import_audit_runs where started_at >= (NOW() - INTERVAL 14 DAY) and started_at < (NOW() - INTERVAL 7 DAY)"
) ?? 0);
if ($importRunsPrev7dCount > 0) {
$importRunTrendPercent = round((($importRuns7dCount - $importRunsPrev7dCount) * 100) / $importRunsPrev7dCount, 1);
}
$importRowsCreated7dCount = (int) (DB::selectValue(
"select coalesce(sum(created_count), 0) from import_audit_runs where started_at >= (NOW() - INTERVAL 7 DAY)"
) ?? 0);
$importRowsFailed7dCount = (int) (DB::selectValue(
"select coalesce(sum(failed_count), 0) from import_audit_runs where started_at >= (NOW() - INTERVAL 7 DAY)"
) ?? 0);
$importPartialOrFailedRuns7dCount = (int) (DB::selectValue(
"select count(*) from import_audit_runs where started_at >= (NOW() - INTERVAL 7 DAY) and status in (???)",
[$importStatusPartial, $importStatusFailed]
) ?? 0);
$importAverageDurationMs7d = (int) (DB::selectValue(
"select round(avg(duration_ms), 0) from import_audit_runs " .
"where started_at >= (NOW() - INTERVAL 7 DAY) and duration_ms is not null"
) ?? 0);
$importSuccessRuns7dCount = (int) (DB::selectValue(
"select count(*) from import_audit_runs where started_at >= (NOW() - INTERVAL 7 DAY) and status = ?",
$importStatusSuccess
) ?? 0);
if ($importRuns7dCount > 0) {
$importSuccessRate7dPercent = round(($importSuccessRuns7dCount * 100) / $importRuns7dCount, 1);
}
$importRecentRunRowsRaw = DB::select(
"select " .
" import_audit_runs.id, " .
" import_audit_runs.started_at, " .
" import_audit_runs.profile_key, " .
" import_audit_runs.status, " .
" import_audit_runs.rows_total, " .
" import_audit_runs.created_count, " .
" import_audit_runs.failed_count, " .
" import_audit_runs.duration_ms, " .
" users.uuid as user_uuid, " .
" users.display_name as user_display_name, " .
" users.email as user_email " .
"from import_audit_runs " .
"left join users on users.id = import_audit_runs.user_id " .
"order by import_audit_runs.started_at desc " .
"limit 10"
);
$importRecentRunRows = is_array($importRecentRunRowsRaw) ? array_values(array_filter(array_map(
static function ($row) use ($flattenStatsRow): ?array {
$flat = $flattenStatsRow($row);
$id = (int) ($flat['id'] ?? 0);
$startedAt = trim((string) ($flat['started_at'] ?? ''));
$profileKey = trim((string) ($flat['profile_key'] ?? ''));
$status = trim((string) ($flat['status'] ?? ''));
if ($id <= 0 || $startedAt === '' || $profileKey === '' || $status === '') {
return null;
}
return [
'id' => $id,
'started_at' => $startedAt,
'profile_key' => $profileKey,
'status' => $status,
'rows_total' => (int) ($flat['rows_total'] ?? 0),
'created_count' => (int) ($flat['created_count'] ?? 0),
'failed_count' => (int) ($flat['failed_count'] ?? 0),
'duration_ms' => (int) ($flat['duration_ms'] ?? 0),
'user_uuid' => trim((string) ($flat['user_uuid'] ?? '')),
'user_display_name' => trim((string) ($flat['user_display_name'] ?? '')),
'user_email' => trim((string) ($flat['user_email'] ?? '')),
];
},
$importRecentRunRowsRaw
))) : [];
$importProfileRowsRaw = DB::select(
"select " .
" profile_key, " .
" count(*) as run_count, " .
" sum(case when status = ? then 1 else 0 end) as success_count, " .
" coalesce(sum(created_count), 0) as created_count_sum, " .
" coalesce(sum(failed_count), 0) as failed_count_sum " .
"from import_audit_runs " .
"where started_at >= (NOW() - INTERVAL 7 DAY) " .
"group by profile_key " .
"order by run_count desc, profile_key asc " .
"limit 10",
$importStatusSuccess
);
$importProfileRows = is_array($importProfileRowsRaw) ? array_values(array_filter(array_map(
static function ($row) use ($flattenStatsRow): ?array {
$flat = $flattenStatsRow($row);
$profileKey = trim((string) ($flat['profile_key'] ?? ''));
$runCount = (int) ($flat['run_count'] ?? 0);
if ($profileKey === '' || $runCount <= 0) {
return null;
}
$successCount = (int) ($flat['success_count'] ?? 0);
$successRate = round(($successCount * 100) / $runCount, 1);
return [
'profile_key' => $profileKey,
'run_count' => $runCount,
'created_count_sum' => (int) ($flat['created_count_sum'] ?? 0),
'failed_count_sum' => (int) ($flat['failed_count_sum'] ?? 0),
'success_rate' => $successRate,
];
},
$importProfileRowsRaw
))) : [];
$importTopErrorRowsRaw = DB::select(
"select error_codes_json from import_audit_runs " .
"where started_at >= (NOW() - INTERVAL 7 DAY) " .
"and error_codes_json is not null and error_codes_json <> '' " .
"order by started_at desc " .
"limit 1000"
);
$errorCodeCounts = [];
if (is_array($importTopErrorRowsRaw)) {
foreach ($importTopErrorRowsRaw as $row) {
$flat = $flattenStatsRow($row);
$json = (string) ($flat['error_codes_json'] ?? '');
if ($json === '') {
continue;
}
$decoded = json_decode($json, true);
if (!is_array($decoded)) {
continue;
}
foreach ($decoded as $code => $count) {
$codeKey = trim((string) $code);
$countValue = (int) $count;
if ($codeKey === '' || $countValue <= 0) {
continue;
}
$errorCodeCounts[$codeKey] = (int) ($errorCodeCounts[$codeKey] ?? 0) + $countValue;
}
}
}
if (!empty($errorCodeCounts)) {
arsort($errorCodeCounts);
foreach (array_slice($errorCodeCounts, 0, 5, true) as $code => $count) {
$importTopErrorCodeRows[] = [
'error_code' => $code,
'count' => (int) $count,
];
}
}
}
$auditNow = new \DateTimeImmutable('now');
$systemAuditCreatedFrom = $auditNow->modify('-1 day')->format('Y-m-d');
$systemAuditCreatedTo = $auditNow->format('Y-m-d');
$lifecycleCreatedFrom = $auditNow->modify('-7 days')->format('Y-m-d');
$lifecycleCreatedTo = $auditNow->format('Y-m-d');
$systemAuditStatsAvailable = (int) (DB::selectValue(
"select count(*) from information_schema.tables " .
"where table_schema = database() and table_name = 'system_audit_log'"
) ?? 0) === 1;
$userLifecycleAuditStatsAvailable = (int) (DB::selectValue(
"select count(*) from information_schema.tables " .
"where table_schema = database() and table_name = 'user_lifecycle_audit_log'"
) ?? 0) === 1;
$auditStatsAvailable = $systemAuditStatsAvailable || $userLifecycleAuditStatsAvailable;
$systemAuditEvents24hCount = 0;
$systemAuditFailed24hCount = 0;
$systemAuditDenied24hCount = 0;
$systemAuditEventsPrev24hCount = 0;
$systemAuditTrendPercent = 0.0;
$systemAuditTopEventTypeRows = [];
$systemAuditRecentRiskRows = [];
$systemAuditAll24hHref = 'admin/system-audit';
$systemAuditFailed24hHref = 'admin/system-audit';
$systemAuditDenied24hHref = 'admin/system-audit';
if ($systemAuditStatsAvailable) {
$systemAuditEvents24hCount = (int) (DB::selectValue(
"select count(*) from system_audit_log where created_at >= (NOW() - INTERVAL 24 HOUR)"
) ?? 0);
$systemAuditFailed24hCount = (int) (DB::selectValue(
"select count(*) from system_audit_log " .
"where created_at >= (NOW() - INTERVAL 24 HOUR) and outcome = ?",
$systemAuditOutcomeFailed
) ?? 0);
$systemAuditDenied24hCount = (int) (DB::selectValue(
"select count(*) from system_audit_log " .
"where created_at >= (NOW() - INTERVAL 24 HOUR) and outcome = ?",
$systemAuditOutcomeDenied
) ?? 0);
$systemAuditEventsPrev24hCount = (int) (DB::selectValue(
"select count(*) from system_audit_log " .
"where created_at >= (NOW() - INTERVAL 48 HOUR) and created_at < (NOW() - INTERVAL 24 HOUR)"
) ?? 0);
if ($systemAuditEventsPrev24hCount > 0) {
$systemAuditTrendPercent = round(
(($systemAuditEvents24hCount - $systemAuditEventsPrev24hCount) * 100) / $systemAuditEventsPrev24hCount,
1
);
}
$systemAuditAll24hHref = 'admin/system-audit?' . http_build_query([
'created_from' => $systemAuditCreatedFrom,
'created_to' => $systemAuditCreatedTo,
]);
$systemAuditFailed24hHref = 'admin/system-audit?' . http_build_query([
'outcome' => $systemAuditOutcomeFailed,
'created_from' => $systemAuditCreatedFrom,
'created_to' => $systemAuditCreatedTo,
]);
$systemAuditDenied24hHref = 'admin/system-audit?' . http_build_query([
'outcome' => $systemAuditOutcomeDenied,
'created_from' => $systemAuditCreatedFrom,
'created_to' => $systemAuditCreatedTo,
]);
$systemAuditTopEventTypeRowsRaw = DB::select(
"select " .
" event_type, " .
" count(*) as hit_count, " .
" sum(case when outcome in (???) then 1 else 0 end) as failed_or_denied_count " .
"from system_audit_log " .
"where created_at >= (NOW() - INTERVAL 24 HOUR) " .
"group by event_type " .
"order by failed_or_denied_count desc, hit_count desc, event_type asc " .
"limit 5",
[$systemAuditOutcomeFailed, $systemAuditOutcomeDenied]
);
$systemAuditTopEventTypeRows = is_array($systemAuditTopEventTypeRowsRaw) ? array_values(array_filter(array_map(
static function ($row) use ($flattenStatsRow): ?array {
$flat = $flattenStatsRow($row);
$eventType = trim((string) ($flat['event_type'] ?? ''));
$hitCount = (int) ($flat['hit_count'] ?? 0);
if ($eventType === '' || $hitCount <= 0) {
return null;
}
return [
'event_type' => $eventType,
'hit_count' => $hitCount,
'failed_or_denied_count' => (int) ($flat['failed_or_denied_count'] ?? 0),
];
},
$systemAuditTopEventTypeRowsRaw
))) : [];
$systemAuditRecentRiskRowsRaw = DB::select(
"select " .
" id, event_type, outcome, channel, created_at " .
"from system_audit_log " .
"where created_at >= (NOW() - INTERVAL 24 HOUR) and outcome in (???) " .
"order by created_at desc " .
"limit 10",
[$systemAuditOutcomeFailed, $systemAuditOutcomeDenied]
);
$systemAuditRecentRiskRows = is_array($systemAuditRecentRiskRowsRaw) ? array_values(array_filter(array_map(
static function ($row) use ($flattenStatsRow): ?array {
$flat = $flattenStatsRow($row);
$id = (int) ($flat['id'] ?? 0);
$eventType = trim((string) ($flat['event_type'] ?? ''));
$outcome = trim((string) ($flat['outcome'] ?? ''));
if ($id <= 0 || $eventType === '' || $outcome === '') {
return null;
}
return [
'id' => $id,
'event_type' => $eventType,
'outcome' => $outcome,
'channel' => trim((string) ($flat['channel'] ?? '')),
'created_at' => trim((string) ($flat['created_at'] ?? '')),
];
},
$systemAuditRecentRiskRowsRaw
))) : [];
}
$lifecycleRuns7dCount = 0;
$lifecycleRunsPrev7dCount = 0;
$lifecycleRunTrendPercent = 0.0;
$lifecycleFailed7dCount = 0;
$lifecycleSkipped7dCount = 0;
$lifecycleRestore7dCount = 0;
$lifecycleTopReasonRows = [];
$lifecycleRecentRiskRows = [];
$lifecycleAll7dHref = 'admin/user-lifecycle-audit';
$lifecycleFailed7dHref = 'admin/user-lifecycle-audit';
$lifecycleRestore7dHref = 'admin/user-lifecycle-audit';
if ($userLifecycleAuditStatsAvailable) {
$lifecycleRuns7dCount = (int) (DB::selectValue(
"select count(*) from user_lifecycle_audit_log where created_at >= (NOW() - INTERVAL 7 DAY)"
) ?? 0);
$lifecycleRunsPrev7dCount = (int) (DB::selectValue(
"select count(*) from user_lifecycle_audit_log " .
"where created_at >= (NOW() - INTERVAL 14 DAY) and created_at < (NOW() - INTERVAL 7 DAY)"
) ?? 0);
if ($lifecycleRunsPrev7dCount > 0) {
$lifecycleRunTrendPercent = round(
(($lifecycleRuns7dCount - $lifecycleRunsPrev7dCount) * 100) / $lifecycleRunsPrev7dCount,
1
);
}
$lifecycleFailed7dCount = (int) (DB::selectValue(
"select count(*) from user_lifecycle_audit_log " .
"where created_at >= (NOW() - INTERVAL 7 DAY) and status = ?",
$lifecycleStatusFailed
) ?? 0);
$lifecycleSkipped7dCount = (int) (DB::selectValue(
"select count(*) from user_lifecycle_audit_log " .
"where created_at >= (NOW() - INTERVAL 7 DAY) and status = ?",
$lifecycleStatusSkipped
) ?? 0);
$lifecycleRestore7dCount = (int) (DB::selectValue(
"select count(*) from user_lifecycle_audit_log " .
"where created_at >= (NOW() - INTERVAL 7 DAY) and action = ?",
$lifecycleActionRestore
) ?? 0);
$lifecycleAll7dHref = 'admin/user-lifecycle-audit?' . http_build_query([
'created_from' => $lifecycleCreatedFrom,
'created_to' => $lifecycleCreatedTo,
]);
$lifecycleFailed7dHref = 'admin/user-lifecycle-audit?' . http_build_query([
'statuses' => $lifecycleStatusFailed . ',' . $lifecycleStatusSkipped,
'created_from' => $lifecycleCreatedFrom,
'created_to' => $lifecycleCreatedTo,
]);
$lifecycleRestore7dHref = 'admin/user-lifecycle-audit?' . http_build_query([
'actions' => $lifecycleActionRestore,
'created_from' => $lifecycleCreatedFrom,
'created_to' => $lifecycleCreatedTo,
]);
$lifecycleTopReasonRowsRaw = DB::select(
"select " .
" coalesce(nullif(reason_code, ''), '-') as reason_code, " .
" count(*) as hit_count " .
"from user_lifecycle_audit_log " .
"where created_at >= (NOW() - INTERVAL 7 DAY) " .
"group by reason_code " .
"order by hit_count desc, reason_code asc " .
"limit 5"
);
$lifecycleTopReasonRows = is_array($lifecycleTopReasonRowsRaw) ? array_values(array_filter(array_map(
static function ($row) use ($flattenStatsRow): ?array {
$flat = $flattenStatsRow($row);
$reasonCode = trim((string) ($flat['reason_code'] ?? '-'));
$hitCount = (int) ($flat['hit_count'] ?? 0);
if ($hitCount <= 0) {
return null;
}
if ($reasonCode === '') {
$reasonCode = '-';
}
return [
'reason_code' => $reasonCode,
'hit_count' => $hitCount,
];
},
$lifecycleTopReasonRowsRaw
))) : [];
$lifecycleRecentRiskRowsRaw = DB::select(
"select " .
" id, action, status, trigger_type, target_user_uuid, target_user_email, created_at " .
"from user_lifecycle_audit_log " .
"where created_at >= (NOW() - INTERVAL 7 DAY) " .
" and (status in (???) or action = ?) " .
"order by created_at desc " .
"limit 10",
[$lifecycleStatusFailed, $lifecycleStatusSkipped],
$lifecycleActionRestore
);
$lifecycleRecentRiskRows = is_array($lifecycleRecentRiskRowsRaw) ? array_values(array_filter(array_map(
static function ($row) use ($flattenStatsRow): ?array {
$flat = $flattenStatsRow($row);
$id = (int) ($flat['id'] ?? 0);
$action = trim((string) ($flat['action'] ?? ''));
$status = trim((string) ($flat['status'] ?? ''));
if ($id <= 0 || $action === '' || $status === '') {
return null;
}
return [
'id' => $id,
'action' => $action,
'status' => $status,
'trigger_type' => trim((string) ($flat['trigger_type'] ?? '')),
'target_user_uuid' => trim((string) ($flat['target_user_uuid'] ?? '')),
'target_user_email' => trim((string) ($flat['target_user_email'] ?? '')),
'created_at' => trim((string) ($flat['created_at'] ?? '')),
];
},
$lifecycleRecentRiskRowsRaw
))) : [];
}
$scheduledStatsAvailable = (int) (DB::selectValue(
"select count(*) from information_schema.tables " .
"where table_schema = database() and table_name in ('scheduled_jobs', 'scheduled_job_runs', 'scheduler_runtime_status')"
) ?? 0) === 3;
$scheduledEnabledJobsCount = 0;
$scheduledOverdueJobsCount = 0;
$scheduledRuns24hCount = 0;
$scheduledFailedRuns24hCount = 0;
$schedulerRunnerLastHeartbeatAt = '';
$schedulerRunnerLastResult = '';
$schedulerRunnerLastErrorCode = '';
$schedulerRunnerIsActive = false;
if ($scheduledStatsAvailable) {
$scheduledEnabledJobsCount = (int) (DB::selectValue(
"select count(*) from scheduled_jobs where enabled = 1"
) ?? 0);
$scheduledOverdueJobsCount = (int) (DB::selectValue(
"select count(*) from scheduled_jobs " .
"where enabled = 1 and next_run_at is not null and next_run_at <= NOW()"
) ?? 0);
$scheduledRuns24hCount = (int) (DB::selectValue(
"select count(*) from scheduled_job_runs " .
"where trigger_type = ? and started_at >= (NOW() - INTERVAL 24 HOUR)",
$schedulerTriggerScheduler
) ?? 0);
$scheduledFailedRuns24hCount = (int) (DB::selectValue(
"select count(*) from scheduled_job_runs " .
"where trigger_type = ? and status = ? and started_at >= (NOW() - INTERVAL 24 HOUR)",
$schedulerTriggerScheduler,
$schedulerRunStatusFailed
) ?? 0);
$schedulerRunnerIsActive = (int) (DB::selectValue(
"select case when last_heartbeat_at >= (NOW() - INTERVAL 3 MINUTE) then 1 else 0 end " .
"from scheduler_runtime_status where id = 1"
) ?? 0) === 1;
$schedulerRuntimeRowRaw = DB::selectOne(
"select last_heartbeat_at, last_result, last_error_code from scheduler_runtime_status where id = 1 limit 1"
);
$schedulerRuntimeRow = $flattenStatsRow($schedulerRuntimeRowRaw);
$schedulerRunnerLastHeartbeatAt = trim((string) ($schedulerRuntimeRow['last_heartbeat_at'] ?? ''));
$schedulerRunnerLastResult = trim((string) ($schedulerRuntimeRow['last_result'] ?? ''));
$schedulerRunnerLastErrorCode = trim((string) ($schedulerRuntimeRow['last_error_code'] ?? ''));
}
$usersUnverifiedCount = (int) (DB::selectValue(
'select count(*) from users where active = 1 and email_verified_at is null'
) ?? 0);
$usersNeverLoggedCount = (int) (DB::selectValue(
'select count(*) from users where active = 1 and last_login_at is null'
) ?? 0);
$recentLogins = DB::select(
'select id, uuid, first_name, last_name, email, last_login_at from users where active = 1 and last_login_at is not null order by last_login_at desc limit 10'
);
$recentLogins = is_array($recentLogins) ? array_map(
static fn ($row) => $row['users'] ?? $row,
$recentLogins
) : [];
$neverLoggedUsers = DB::select(
'select id, uuid, first_name, last_name, email, created from users where active = 1 and last_login_at is null order by created desc limit 10'
);
$neverLoggedUsers = is_array($neverLoggedUsers) ? array_map(
static fn ($row) => $row['users'] ?? $row,
$neverLoggedUsers
) : [];
$mailLogFailedCount = (int) (DB::selectValue('select count(*) from mail_log where status = ?', $mailStatusFailed) ?? 0);
$mailLogSentCount = (int) (DB::selectValue('select count(*) from mail_log where status = ?', $mailStatusSent) ?? 0);
$mailLogLastSentAt = (string) (DB::selectValue('select max(sent_at) from mail_log where status = ?', $mailStatusSent) ?? '');
$mailLogRecentFailed = DB::select(
'select id, to_email, subject, error_message, created_at from mail_log where status = ? order by created_at desc limit 5',
$mailStatusFailed
);
$mailLogRecentFailed = is_array($mailLogRecentFailed) ? array_map(
static fn ($row) => $row['mail_log'] ?? $row,
$mailLogRecentFailed
) : [];
$vars = get_defined_vars();
/** @phpstan-ignore unset.offset (get_defined_vars() includes $this in object context; must remove before returning) */
unset($vars['this']);
return $vars;
}
}