add composer-unused, comprehensive docs, and project restructure
- Add icanhazstring/composer-unused as dev dependency for dependency hygiene checks - Add German documentation (docs/) covering architecture, conventions, workflows, and developer checklists - Add API layer (ApiAuth, ApiBootstrap, ApiResponse), audit, scheduler, custom fields, and SSO services - Add Microsoft OIDC SSO, API token management, and user lifecycle features - Add swagger-ui vendor integration and OpenAPI spec - Add production Docker setup and bin/ scripts - Update composer dependencies, config, templates, and frontend assets throughout Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -2,257 +2,88 @@
|
||||
|
||||
namespace MintyPHP\Support;
|
||||
|
||||
use MintyPHP\Service\Access\PermissionService;
|
||||
use MintyPHP\Service\User\UserAvatarService;
|
||||
use MintyPHP\Support\Search\SearchItemMapperProvider;
|
||||
use MintyPHP\Support\Search\SearchQueryNormalizer;
|
||||
use MintyPHP\Support\Search\SearchSpecialResourceProvider;
|
||||
use MintyPHP\Support\Search\SearchSqlResourceProvider;
|
||||
use MintyPHP\Support\Search\SearchUiMetaProvider;
|
||||
|
||||
class SearchConfig
|
||||
{
|
||||
public static function tenantScopeFilters(): array
|
||||
{
|
||||
return [
|
||||
'users' => 'and exists (select 1 from user_tenants ut where ut.user_id = users.id and ut.tenant_id in (???))',
|
||||
'address-book' => 'and exists (select 1 from user_tenants ut where ut.user_id = users.id and ut.tenant_id in (???))',
|
||||
'tenants' => 'and id in (???)',
|
||||
'departments' => 'and exists (select 1 from tenant_departments td where td.department_id = departments.id and td.tenant_id in (???))',
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
private static function normalizeLikeQuery(string $query): string
|
||||
{
|
||||
$query = trim($query);
|
||||
if ($query === '') {
|
||||
return '';
|
||||
}
|
||||
$query = str_replace('\\', '\\\\', $query);
|
||||
$query = str_replace('_', '\\_', $query);
|
||||
$query = str_replace('*', '%', $query);
|
||||
if (!str_starts_with($query, '%')) {
|
||||
$query = '%' . $query;
|
||||
}
|
||||
if (!str_ends_with($query, '%')) {
|
||||
$query = $query . '%';
|
||||
}
|
||||
return $query;
|
||||
}
|
||||
|
||||
public static function normalizeScoreQuery(string $query): string
|
||||
{
|
||||
$query = trim($query);
|
||||
if ($query == '') {
|
||||
return '';
|
||||
}
|
||||
return str_replace(['*', '%', '_'], '', $query);
|
||||
return SearchSqlResourceProvider::tenantScopeFilters();
|
||||
}
|
||||
|
||||
public static function resources(string $query, string $locale): array
|
||||
{
|
||||
$like = self::normalizeLikeQuery($query);
|
||||
|
||||
return [
|
||||
[
|
||||
'key' => 'address-book',
|
||||
'label' => t('Address book'),
|
||||
'permission' => PermissionService::ADDRESS_BOOK_VIEW,
|
||||
'countSql' => "select count(*) from users where active = 1 and (first_name like ? escape '\\\\' or last_name like ? escape '\\\\' or email like ? escape '\\\\') {{tenantFilter}}",
|
||||
'countParams' => [$like, $like, $like],
|
||||
'previewSql' => "select uuid, first_name, last_name, email from users where active = 1 and (first_name like ? escape '\\\\' or last_name like ? escape '\\\\' or email like ? escape '\\\\') {{tenantFilter}} order by last_name, first_name limit ?",
|
||||
'previewParams' => [$like, $like, $like],
|
||||
'resultSql' => "select uuid, first_name, last_name, email from users where active = 1 and (first_name like ? escape '\\\\' or last_name like ? escape '\\\\' or email like ? escape '\\\\') {{tenantFilter}} order by last_name, first_name",
|
||||
'resultParams' => [$like, $like, $like],
|
||||
],
|
||||
[
|
||||
'key' => 'users',
|
||||
'label' => t('Users'),
|
||||
'permission' => PermissionService::USERS_VIEW,
|
||||
'countSql' => "select count(*) from users where (first_name like ? escape '\\\\' or last_name like ? escape '\\\\' or email like ? escape '\\\\') {{tenantFilter}}",
|
||||
'countParams' => [$like, $like, $like],
|
||||
'previewSql' => "select uuid, first_name, last_name, email from users where (first_name like ? escape '\\\\' or last_name like ? escape '\\\\' or email like ? escape '\\\\') {{tenantFilter}} order by last_name, first_name limit ?",
|
||||
'previewParams' => [$like, $like, $like],
|
||||
'resultSql' => "select uuid, first_name, last_name, email from users where (first_name like ? escape '\\\\' or last_name like ? escape '\\\\' or email like ? escape '\\\\') {{tenantFilter}} order by last_name, first_name",
|
||||
'resultParams' => [$like, $like, $like],
|
||||
],
|
||||
[
|
||||
'key' => 'tenants',
|
||||
'label' => t('Tenants'),
|
||||
'permission' => PermissionService::TENANTS_VIEW,
|
||||
'countSql' => "select count(*) from tenants where description like ? escape '\\\\' {{tenantFilter}}",
|
||||
'countParams' => [$like],
|
||||
'previewSql' => "select uuid, description from tenants where description like ? escape '\\\\' {{tenantFilter}} order by description limit ?",
|
||||
'previewParams' => [$like],
|
||||
'resultSql' => "select uuid, description from tenants where description like ? escape '\\\\' {{tenantFilter}} order by description",
|
||||
'resultParams' => [$like],
|
||||
],
|
||||
[
|
||||
'key' => 'departments',
|
||||
'label' => t('Departments'),
|
||||
'permission' => PermissionService::DEPARTMENTS_VIEW,
|
||||
'countSql' => "select count(*) from departments where description like ? escape '\\\\' {{tenantFilter}}",
|
||||
'countParams' => [$like],
|
||||
'previewSql' => "select uuid, description from departments where description like ? escape '\\\\' {{tenantFilter}} order by description limit ?",
|
||||
'previewParams' => [$like],
|
||||
'resultSql' => "select uuid, description from departments where description like ? escape '\\\\' {{tenantFilter}} order by description",
|
||||
'resultParams' => [$like],
|
||||
],
|
||||
[
|
||||
'key' => 'roles',
|
||||
'label' => t('Roles'),
|
||||
'permission' => PermissionService::ROLES_VIEW,
|
||||
'countSql' => "select count(*) from roles where active = 1 and description like ? escape '\\\\'",
|
||||
'countParams' => [$like],
|
||||
'previewSql' => "select uuid, description from roles where active = 1 and description like ? escape '\\\\' order by description limit ?",
|
||||
'previewParams' => [$like],
|
||||
'resultSql' => "select uuid, description from roles where active = 1 and description like ? escape '\\\\' order by description",
|
||||
'resultParams' => [$like],
|
||||
],
|
||||
[
|
||||
'key' => 'permissions',
|
||||
'label' => t('Permissions'),
|
||||
'permission' => PermissionService::PERMISSIONS_VIEW,
|
||||
'countSql' => "select count(*) from permissions where active = 1 and (`key` like ? escape '\\\\' or description like ? escape '\\\\')",
|
||||
'countParams' => [$like, $like],
|
||||
'previewSql' => "select id, description, `key` from permissions where active = 1 and (`key` like ? escape '\\\\' or description like ? escape '\\\\') order by `key` limit ?",
|
||||
'previewParams' => [$like, $like],
|
||||
'resultSql' => "select id, description, `key` from permissions where active = 1 and (`key` like ? escape '\\\\' or description like ? escape '\\\\') order by `key`",
|
||||
'resultParams' => [$like, $like],
|
||||
],
|
||||
[
|
||||
'key' => 'pages',
|
||||
'label' => t('Pages'),
|
||||
'permission' => '',
|
||||
'countSql' => "select count(*) from pages p join page_contents pc on pc.page_id = p.id and pc.locale = ? where p.slug like ? escape '\\\\' or pc.content like ? escape '\\\\'",
|
||||
'countParams' => [$locale, $like, $like],
|
||||
'previewSql' => "select p.slug from pages p join page_contents pc on pc.page_id = p.id and pc.locale = ? where p.slug like ? escape '\\\\' or pc.content like ? escape '\\\\' order by p.slug limit ?",
|
||||
'previewParams' => [$locale, $like, $like],
|
||||
'resultSql' => "select p.slug from pages p join page_contents pc on pc.page_id = p.id and pc.locale = ? where p.slug like ? escape '\\\\' or pc.content like ? escape '\\\\' order by p.slug",
|
||||
'resultParams' => [$locale, $like, $like],
|
||||
],
|
||||
[
|
||||
'key' => 'settings',
|
||||
'label' => t('Settings'),
|
||||
'permission' => PermissionService::SETTINGS_VIEW,
|
||||
'countSql' => "select count(*) from settings where `key` like ? escape '\\\\' or value like ? escape '\\\\'",
|
||||
'countParams' => [$like, $like],
|
||||
'previewSql' => null,
|
||||
'previewParams' => [],
|
||||
'resultSql' => "select `key`, value from settings where `key` like ? escape '\\\\' or value like ? escape '\\\\' order by `key`",
|
||||
'resultParams' => [$like, $like],
|
||||
],
|
||||
];
|
||||
return SearchSqlResourceProvider::resources($query, $locale);
|
||||
}
|
||||
|
||||
public static function iconForKey(string $key): string
|
||||
{
|
||||
return match ($key) {
|
||||
'address-book' => 'bi-people',
|
||||
'users' => 'bi-person-badge',
|
||||
'tenants' => 'bi-buildings',
|
||||
'departments' => 'bi-diagram-3',
|
||||
'roles' => 'bi-people',
|
||||
'permissions' => 'bi-shield-lock',
|
||||
'pages' => 'bi-file-text',
|
||||
'settings' => 'bi-gear',
|
||||
default => 'bi-search',
|
||||
};
|
||||
return SearchUiMetaProvider::iconForKey($key);
|
||||
}
|
||||
|
||||
public static function listUrl(string $key, string $query): string
|
||||
{
|
||||
$encoded = urlencode($query);
|
||||
|
||||
return match ($key) {
|
||||
'address-book' => lurl('address-book') . '?search=' . $encoded,
|
||||
'users' => lurl('admin/users') . '?search=' . $encoded,
|
||||
'tenants' => lurl('admin/tenants') . '?search=' . $encoded,
|
||||
'departments' => lurl('admin/departments') . '?search=' . $encoded,
|
||||
'roles' => lurl('admin/roles') . '?search=' . $encoded,
|
||||
'permissions' => lurl('admin/permissions') . '?search=' . $encoded,
|
||||
'pages' => lurl(''),
|
||||
'settings' => lurl('admin/settings'),
|
||||
default => lurl(''),
|
||||
};
|
||||
return SearchUiMetaProvider::listUrl($key, $query);
|
||||
}
|
||||
|
||||
public static function mapPreviewItem(string $key, array $data, string $query): ?array
|
||||
{
|
||||
if ($key === 'users') {
|
||||
$label = trim(($data['first_name'] ?? '') . ' ' . ($data['last_name'] ?? ''));
|
||||
$email = trim((string) ($data['email'] ?? ''));
|
||||
if ($email !== '') {
|
||||
$label = $label === '' ? $email : ($label . ' — ' . $email);
|
||||
}
|
||||
$url = lurl('admin/users/edit/' . ($data['uuid'] ?? '')) . '?search=' . urlencode($query);
|
||||
} elseif ($key === 'address-book') {
|
||||
$label = trim(($data['first_name'] ?? '') . ' ' . ($data['last_name'] ?? ''));
|
||||
$email = trim((string) ($data['email'] ?? ''));
|
||||
if ($email !== '') {
|
||||
$label = $label === '' ? $email : ($label . ' — ' . $email);
|
||||
}
|
||||
$url = lurl('address-book/view/' . ($data['uuid'] ?? '')) . '?search=' . urlencode($query);
|
||||
} elseif ($key === 'permissions') {
|
||||
$label = (string) ($data['description'] ?? '');
|
||||
$url = lurl('admin/permissions/edit/' . ($data['id'] ?? '')) . '?search=' . urlencode($query);
|
||||
} elseif ($key === 'pages') {
|
||||
$label = (string) ($data['slug'] ?? '');
|
||||
$url = lurl('page/' . $label) . '?search=' . urlencode($query);
|
||||
} else {
|
||||
$label = (string) ($data['description'] ?? '');
|
||||
$url = lurl('admin/' . $key . '/edit/' . ($data['uuid'] ?? '')) . '?search=' . urlencode($query);
|
||||
}
|
||||
|
||||
if ($label === '' || $url === '') {
|
||||
return null;
|
||||
}
|
||||
return ['label' => $label, 'url' => $url];
|
||||
return SearchItemMapperProvider::mapPreviewItem($key, $data, $query);
|
||||
}
|
||||
|
||||
public static function mapResultItem(string $key, string $label, array $data): ?array
|
||||
{
|
||||
$title = '';
|
||||
$description = '';
|
||||
$url = '';
|
||||
$image = '';
|
||||
if ($key === 'users') {
|
||||
$title = trim(($data['first_name'] ?? '') . ' ' . ($data['last_name'] ?? ''));
|
||||
$description = (string) ($data['email'] ?? '');
|
||||
$url = lurl('admin/users/edit/' . ($data['uuid'] ?? ''));
|
||||
} elseif ($key === 'address-book') {
|
||||
$title = trim(($data['first_name'] ?? '') . ' ' . ($data['last_name'] ?? ''));
|
||||
$description = (string) ($data['email'] ?? '');
|
||||
$uuid = (string) ($data['uuid'] ?? '');
|
||||
$url = lurl('address-book/view/' . $uuid);
|
||||
if ($uuid !== '' && UserAvatarService::hasAvatar($uuid)) {
|
||||
$image = lurl('admin/users/avatar-file') . '?uuid=' . urlencode($uuid) . '&size=64';
|
||||
}
|
||||
} elseif ($key === 'permissions') {
|
||||
$title = (string) ($data['key'] ?? '');
|
||||
$description = (string) ($data['description'] ?? '');
|
||||
$url = lurl('admin/permissions/edit/' . ($data['id'] ?? ''));
|
||||
} elseif ($key === 'pages') {
|
||||
$title = (string) ($data['slug'] ?? '');
|
||||
$description = t('Page');
|
||||
$url = lurl('page/' . $title);
|
||||
} elseif ($key === 'settings') {
|
||||
$title = (string) ($data['key'] ?? '');
|
||||
$description = (string) ($data['value'] ?? '');
|
||||
$url = lurl('admin/settings');
|
||||
} else {
|
||||
$title = (string) ($data['description'] ?? '');
|
||||
$description = $label;
|
||||
$url = lurl('admin/' . $key . '/edit/' . ($data['uuid'] ?? ''));
|
||||
}
|
||||
return SearchItemMapperProvider::mapResultItem($key, $label, $data);
|
||||
}
|
||||
|
||||
if ($title === '' || $url === '') {
|
||||
return null;
|
||||
public static function hotkeyPreviewResource(string $query): ?array
|
||||
{
|
||||
return SearchSpecialResourceProvider::hotkeyPreviewResource($query);
|
||||
}
|
||||
|
||||
public static function hotkeyResultItems(string $query): array
|
||||
{
|
||||
return SearchSpecialResourceProvider::hotkeyResultItems($query);
|
||||
}
|
||||
|
||||
public static function docsPreviewResource(string $query): ?array
|
||||
{
|
||||
return SearchSpecialResourceProvider::docsPreviewResource($query);
|
||||
}
|
||||
|
||||
public static function docsResultItems(string $query): array
|
||||
{
|
||||
return SearchSpecialResourceProvider::docsResultItems($query);
|
||||
}
|
||||
|
||||
public static function normalizeScoreQuery(string $query): string
|
||||
{
|
||||
return SearchQueryNormalizer::normalizeScoreQuery($query);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{missingIcons: string[], missingListUrls: string[]}
|
||||
*/
|
||||
public static function providerCoverageGaps(): array
|
||||
{
|
||||
$missingIcons = [];
|
||||
$missingListUrls = [];
|
||||
foreach (SearchSqlResourceProvider::resourceKeys() as $key) {
|
||||
if (!SearchUiMetaProvider::hasIconForKey($key)) {
|
||||
$missingIcons[] = $key;
|
||||
}
|
||||
if (!SearchUiMetaProvider::hasListUrlForKey($key)) {
|
||||
$missingListUrls[] = $key;
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'type' => $label,
|
||||
'title' => $title,
|
||||
'description' => $description,
|
||||
'url' => $url,
|
||||
'image' => $image,
|
||||
'icon' => self::iconForKey($key),
|
||||
'missingIcons' => $missingIcons,
|
||||
'missingListUrls' => $missingListUrls,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user