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:
2026-02-22 15:27:35 +01:00
parent 3eb9cc0ac4
commit 25370a1a55
389 changed files with 40506 additions and 8071 deletions

View File

@@ -1,5 +1,8 @@
<?php
/**
* Grid.js localization map used across list pages.
*/
function gridLang(): array
{
return [
@@ -42,6 +45,7 @@ function multiSelectFilter(
array $items,
array $selected
): void {
// The hidden input holds a comma-separated value that server-side code already expects.
?>
<label class="app-field">
<span><?php e(t($label)); ?></span>
@@ -96,14 +100,18 @@ function multiSelectForm(
string|array $labelKeys = 'description',
?string $formId = null
): void {
// Allow a fallback chain for label keys, e.g. ['description', 'label', 'name'].
$labelKeyList = is_array($labelKeys) ? $labelKeys : [$labelKeys];
// Vendor MultiSelect appends [] to the field name internally for hidden inputs.
// Strip a trailing [] here to avoid accidental [][] payloads.
$normalizedName = preg_replace('/\[\]$/', '', $name) ?: $name;
?>
<label>
<span><?php e(t($label)); ?></span>
</label>
<select
id="<?php e($id); ?>"
name="<?php e($name); ?>"
name="<?php e($normalizedName); ?>"
<?php if ($formId !== null): ?>form="<?php e($formId); ?>"<?php endif; ?>
multiple
data-multi-select="true"
@@ -136,9 +144,17 @@ function multiSelectForm(
<?php
}
/**
* Resolve nav active state for one or many paths.
*
* @param string|array $path A single path or a list of candidate paths.
* @param bool $prefix Prefix matching (path starts with candidate).
* @return array{class:string,aria:string,isActive:bool}
*/
function navActive($path, bool $prefix = false): array
{
$current = \MintyPHP\Http\Request::path();
// Normalize to array to keep the matching loop simple.
$paths = is_array($path) ? $path : [$path];
$isActive = false;
foreach ($paths as $p) {
@@ -160,6 +176,12 @@ function navActive($path, bool $prefix = false): array
];
}
/**
* Resolve active state for public page links (static pages and page/* slugs).
*
* @param array $extraSlugs Additional public slugs that should be treated as active.
* @return array{class:string,aria:string,isActive:bool}
*/
function navActivePublicPages(array $extraSlugs = []): array
{
$current = \MintyPHP\Http\Request::path();
@@ -167,6 +189,7 @@ function navActivePublicPages(array $extraSlugs = []): array
return ['class' => '', 'aria' => '', 'isActive' => false];
}
// "page/<slug>" routes and known standalone slugs should highlight the same nav entry.
$publicSlugs = array_merge(['imprint', 'privacy'], $extraSlugs);
$isActive = strpos($current, 'page/') === 0 || in_array($current, $publicSlugs, true);
@@ -176,3 +199,156 @@ function navActivePublicPages(array $extraSlugs = []): array
'isActive' => $isActive,
];
}
/**
* Load and cache the asset registry config from config/assets.php.
*/
function appAssetConfig(): array
{
// Static local cache avoids repeated filesystem access during one request.
static $config = null;
if ($config !== null) {
return $config;
}
$file = dirname(__DIR__, 3) . '/config/assets.php';
if (!is_file($file)) {
$config = [];
return $config;
}
$loaded = include $file;
$config = is_array($loaded) ? $loaded : [];
return $config;
}
/**
* Resolve all CSS paths for one or more asset groups.
*
* @param string[] $groupNames
*
* @return string[] List of unique web-relative asset paths.
*/
function appStylePathsForGroups(array $groupNames): array
{
$config = appAssetConfig();
$styles = $config['styles'] ?? [];
if (!is_array($styles)) {
return [];
}
$groups = $styles['groups'] ?? [];
if (!is_array($groups)) {
return [];
}
$resolved = [];
// Used as a fast set to keep insertion order while removing duplicates.
$seen = [];
foreach ($groupNames as $groupName) {
$groupKey = trim((string) $groupName);
if ($groupKey === '' || !isset($groups[$groupKey]) || !is_array($groups[$groupKey])) {
continue;
}
foreach ($groups[$groupKey] as $path) {
$assetPath = ltrim(trim((string) $path), '/');
if ($assetPath === '' || isset($seen[$assetPath])) {
continue;
}
$seen[$assetPath] = true;
$resolved[] = $assetPath;
}
}
return $resolved;
}
/**
* Resolve all CSS paths for a template based on grouped config entries.
*
* @return string[] List of unique web-relative asset paths.
*/
function appTemplateStylePaths(string $templateName): array
{
$config = appAssetConfig();
$styles = $config['styles'] ?? [];
if (!is_array($styles)) {
return [];
}
$templates = $styles['templates'] ?? [];
if (!is_array($templates)) {
return [];
}
$groupNames = $templates[$templateName] ?? [];
if (!is_array($groupNames)) {
return [];
}
return appStylePathsForGroups($groupNames);
}
/**
* Read a raw Buffer value without echoing it to output.
*/
function appBufferValue(string $key): string
{
ob_start();
$hasValue = \MintyPHP\Buffer::get($key);
$raw = (string) ob_get_clean();
return $hasValue ? trim($raw) : '';
}
/**
* Resolve page-specific CSS files declared via Buffer::set('style_groups', ...).
*
* Accepted formats:
* - JSON array (e.g. ["address-book"])
* - comma-separated list (e.g. "address-book,foo")
*
* @return string[] List of unique web-relative asset paths.
*/
function appPageStylePaths(): array
{
$raw = appBufferValue('style_groups');
if ($raw === '') {
return [];
}
$groupNames = json_decode($raw, true);
if (!is_array($groupNames)) {
$groupNames = array_filter(array_map('trim', explode(',', $raw)));
}
return appStylePathsForGroups($groupNames);
}
/**
* Render stylesheet link tags.
*
* @param string[] $stylePaths
*/
function renderStylePaths(array $stylePaths): void
{
foreach ($stylePaths as $stylePath) {
echo '<link href="';
e(assetVersion($stylePath));
echo "\" rel=\"stylesheet\">\n";
}
}
/**
* Render stylesheet link tags for the given template.
*/
function renderTemplateStyles(string $templateName): void
{
renderStylePaths(appTemplateStylePaths($templateName));
}
/**
* Render page-specific stylesheet links from Buffer::set('style_groups', ...).
*/
function renderPageStyles(): void
{
renderStylePaths(appPageStylePaths());
}