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:
@@ -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());
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user