isMethod('GET')) { return; } http_response_code(405); \MintyPHP\Router::json(['error' => 'method_not_allowed']); } /** * Load a local filter schema file and parse request query values. * * @return array */ function gridParseFiltersFromSchemaFile(string $schemaFile, ?array $query = null): array { $schemaFile = trim($schemaFile); if ($schemaFile === '' || !is_file($schemaFile)) { return []; } $schema = require $schemaFile; if (!is_array($schema)) { return []; } return gridParseFilters($query ?? requestInput()->queryAll(), gridSchemaQuery($schema)); } /** * Emit a standardized grid JSON payload. */ function gridJsonDataResult(array $rows, int $total): void { \MintyPHP\Router::json([ 'data' => array_values($rows), 'total' => max(0, $total), ]); } /** * Resolve a badge variant from a status/outcome map. * * @param array $variantMap */ function gridResolveBadgeVariant( string $value, array $variantMap, string $default = 'neutral', bool $normalizeLowercase = true ): string { $key = trim($value); if ($normalizeLowercase) { $key = strtolower($key); } $variant = (string) ($variantMap[$key] ?? $default); return trim($variant) !== '' ? $variant : $default; } /** * Build a csv_strings sanitizer for string-backed enums. * * @param class-string<\BackedEnum> $enumClass */ function gridEnumSanitizer(string $enumClass, bool $lowercase = true): callable { return static function (string $value) use ($enumClass, $lowercase): string { if (!enum_exists($enumClass) || !is_subclass_of($enumClass, \BackedEnum::class)) { return ''; } $value = trim($value); if ($value === '') { return ''; } if ($lowercase) { $value = strtolower($value); } foreach ($enumClass::cases() as $case) { $candidate = (string) $case->value; $normalizedCandidate = $lowercase ? strtolower($candidate) : $candidate; if ($normalizedCandidate === $value) { return $candidate; } } return ''; }; } /** * Identity helper for filter schema declarations. * * @param array|array $schema * @return array|array */ function gridFilterSchema(array $schema = []): array { return $schema; } /** * Render a value as safe inline JavaScript JSON. */ function gridJsonForJs(mixed $value): void { $json = json_encode( $value, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT ); if (!is_string($json)) { $json = 'null'; } echo $json; } /** * Extract query parsing rules from a list filter schema. * * Supports two shapes: * - ['query' => [...], 'toolbar' => [...]] * - [['key' => '...', 'query' => [...]], ...] * * @return array */ function gridSchemaQuery(array $schema): array { if (isset($schema['query']) && is_array($schema['query'])) { return $schema['query']; } $rules = []; foreach ($schema as $entry) { if (!is_array($entry)) { continue; } $key = trim((string) ($entry['key'] ?? '')); if ($key === '') { continue; } if (is_array($entry['query'] ?? null)) { $rules[$key] = $entry['query']; } } return $rules; } /** * Extract toolbar field schema from a list filter schema. * * @return array> */ function gridSchemaToolbar(array $schema): array { if (isset($schema['toolbar']) && is_array($schema['toolbar'])) { return array_values(array_filter($schema['toolbar'], 'is_array')); } $toolbar = []; foreach ($schema as $entry) { if (!is_array($entry) || (($entry['render'] ?? true) === false)) { continue; } if (!isset($entry['type']) || !isset($entry['key'])) { continue; } $toolbar[] = $entry; } return $toolbar; } /** * Split toolbar schema into search and drawer sections. * * @param array> $toolbarSchema * @param string[] $searchKeys * @return array{search: array>, drawer: array>} */ function gridSplitToolbarSchema(array $toolbarSchema, array $searchKeys = []): array { $searchKeyMap = []; foreach ($searchKeys as $searchKey) { $key = trim((string) $searchKey); if ($key !== '') { $searchKeyMap[$key] = true; } } $search = []; $drawer = []; foreach ($toolbarSchema as $field) { if (!is_array($field)) { continue; } $key = trim((string) ($field['key'] ?? '')); $isSearch = (bool) ($field['search'] ?? false) || ($key !== '' && isset($searchKeyMap[$key])); if ($isSearch) { $search[] = $field; continue; } $drawer[] = $field; } return ['search' => $search, 'drawer' => $drawer]; } /** * Index toolbar schema entries by filter key. * * @param array> $toolbarSchema * @return array> */ function gridSchemaByKey(array $toolbarSchema): array { $schemaByKey = []; foreach ($toolbarSchema as $field) { if (!is_array($field)) { continue; } $key = trim((string) ($field['key'] ?? '')); if ($key === '') { continue; } $schemaByKey[$key] = $field; } return $schemaByKey; } /** * Build toolbar state values from parsed filter state and toolbar schema defaults. * * @param array> $toolbarSchema * @param array $filterState * @return array */ function gridBuildToolbarFilterState(array $toolbarSchema, array $filterState): array { $state = []; foreach ($toolbarSchema as $field) { if (!is_array($field)) { continue; } $key = trim((string) ($field['key'] ?? '')); if ($key === '') { continue; } $type = strtolower(trim((string) ($field['type'] ?? ''))); $default = array_key_exists('default', $field) ? $field['default'] : ''; $value = array_key_exists($key, $filterState) ? $filterState[$key] : $default; if ($type === 'multi_csv') { if (is_array($value)) { $items = array_values(array_filter( array_map(static fn (mixed $item): string => trim((string) $item), $value), static fn (string $item): bool => $item !== '' )); $state[$key] = array_values(array_unique($items)); continue; } $text = trim((string) $value); if ($text === '') { $state[$key] = []; continue; } $items = array_values(array_filter( array_map(static fn (string $item): string => trim($item), explode(',', $text)), static fn (string $item): bool => $item !== '' )); $state[$key] = array_values(array_unique($items)); continue; } if (is_array($value)) { $value = (string) (reset($value) ?: ''); } $state[$key] = trim((string) $value); } return $state; } /** * Build normalized list-filter context used by index actions/templates. * * @param array $filterSchema * @param array $options * @return array{ * filterState: array, * toolbarFilterSchema: array>, * toolbarFilterState: array, * searchToolbarFilterSchema: array>, * drawerToolbarFilterSchema: array>, * schemaByKey: array>, * toolbarOptionSets: array, * clientFilterSchema: array>, * searchConfig: ?array * } */ function gridBuildListFilterContext(array $filterSchema, array $options = []): array { $query = is_array($options['query'] ?? null) ? $options['query'] : requestInput()->queryAll(); $querySchema = is_array($options['query_schema'] ?? null) ? $options['query_schema'] : gridSchemaQuery($filterSchema); $filterState = is_array($options['filter_state'] ?? null) ? $options['filter_state'] : gridParseFilters($query, $querySchema); $toolbarFilterSchema = gridSchemaToolbar($filterSchema); $toolbarFilterState = gridBuildToolbarFilterState($toolbarFilterSchema, $filterState); $toolbarStateOverrides = is_array($options['toolbar_state_overrides'] ?? null) ? $options['toolbar_state_overrides'] : []; if ($toolbarStateOverrides) { $toolbarFilterState = array_replace($toolbarFilterState, $toolbarStateOverrides); } $searchKeys = is_array($options['search_keys'] ?? null) ? $options['search_keys'] : ['search']; $toolbarSchemaSections = gridSplitToolbarSchema($toolbarFilterSchema, $searchKeys); $toolbarOptionSets = is_array($options['toolbar_option_sets'] ?? null) ? $options['toolbar_option_sets'] : []; return [ 'filterState' => $filterState, 'toolbarFilterSchema' => $toolbarFilterSchema, 'toolbarFilterState' => $toolbarFilterState, 'searchToolbarFilterSchema' => $toolbarSchemaSections['search'], 'drawerToolbarFilterSchema' => $toolbarSchemaSections['drawer'], 'schemaByKey' => gridSchemaByKey($toolbarFilterSchema), 'toolbarOptionSets' => $toolbarOptionSets, 'clientFilterSchema' => gridSchemaClientFilters($filterSchema), 'searchConfig' => gridSchemaClientSearch($filterSchema), ]; } /** * Build id => label maps from option set items. * * @param array> $items * @return array */ function gridOptionMapFromItems(array $items): array { $map = []; foreach ($items as $item) { if (!is_array($item)) { continue; } $id = trim((string) ($item['id'] ?? '')); if ($id === '') { continue; } $map[$id] = (string) ($item['description'] ?? $id); } return $map; } /** * Build id => label maps from schema allowed declarations. * * @param array $field * @return array */ function gridOptionMapFromAllowed(array $field): array { $map = []; foreach ((array) ($field['allowed'] ?? []) as $item) { if (!is_array($item)) { continue; } $id = trim((string) ($item['id'] ?? '')); if ($id === '') { continue; } $label = (string) ($item['description'] ?? $id); $translate = !array_key_exists('translate', $item) || (bool) $item['translate']; $map[$id] = $translate ? t($label) : $label; } return $map; } /** * Build client filter schema consumed by JS helper gridFiltersFromSchema(). * * @return array> */ function gridSchemaClientFilters(array $schema): array { if (isset($schema['client_filters']) && is_array($schema['client_filters'])) { return array_values(array_filter($schema['client_filters'], 'is_array')); } $filters = []; foreach (gridSchemaToolbar($schema) as $field) { $type = strtolower(trim((string) ($field['type'] ?? ''))); if (!in_array($type, ['text', 'date', 'number', 'select', 'multi_csv', 'hidden'], true)) { continue; } if ((bool) ($field['search'] ?? false)) { continue; } if (($field['bind'] ?? true) === false) { continue; } $key = trim((string) ($field['key'] ?? '')); if ($key === '') { continue; } $inputId = trim((string) ($field['input_id'] ?? '')); if ($inputId === '') { $inputId = $key . '-filter'; } $filter = [ 'type' => $type, 'input' => '#' . $inputId, 'param' => (string) ($field['param'] ?? $key), ]; if (array_key_exists('default', $field)) { $filter['default'] = $field['default']; } if (isset($field['event'])) { $filter['event'] = (string) $field['event']; } if (isset($field['normalize'])) { $filter['normalize'] = (string) $field['normalize']; } $filters[] = $filter; } return $filters; } /** * Build client search config consumed by createServerGrid(). * * @return array|null */ function gridSchemaClientSearch(array $schema): ?array { foreach (gridSchemaToolbar($schema) as $field) { if (!(bool) ($field['search'] ?? false)) { continue; } $key = trim((string) ($field['key'] ?? '')); if ($key === '') { continue; } $inputId = trim((string) ($field['input_id'] ?? '')); if ($inputId === '') { $inputId = $key . '-filter'; } return [ 'input' => '#' . $inputId, 'param' => (string) ($field['param'] ?? $key), 'debounce' => (int) ($field['debounce'] ?? 250), ]; } return null; } /** * Parse filter/query values by a compact schema. * * Supported types: * - int * - number * - bool * - string * - enum * - uuid * - date (Y-m-d) * - csv_ids * - csv_uuid * - csv_strings * - order * - dir * * @return array */ function gridParseFilters(array $query, array $schema): array { $parsed = []; foreach ($schema as $key => $rule) { if (!is_array($rule)) { $rule = ['type' => (string) $rule]; } $type = strtolower(trim((string) ($rule['type'] ?? 'string'))); $source = (string) ($rule['source'] ?? $key); if ($source === '') { $source = (string) $key; } if ($type === 'int') { $default = (int) ($rule['default'] ?? 0); $min = isset($rule['min']) ? (int) $rule['min'] : null; $max = isset($rule['max']) ? (int) $rule['max'] : null; $value = (int) ($query[$source] ?? $default); if ($min !== null && $value < $min) { $value = $default; } if ($max !== null && $value > $max) { $value = $max; } $parsed[$key] = $value; continue; } if ($type === 'number') { $default = (float) ($rule['default'] ?? 0); $min = isset($rule['min']) ? (float) $rule['min'] : null; $max = isset($rule['max']) ? (float) $rule['max'] : null; $parsed[$key] = gridQueryNumber($query, $source, $default, $min, $max); continue; } if ($type === 'bool') { $default = (bool) ($rule['default'] ?? false); $parsed[$key] = gridQueryBool($query, $source, $default); continue; } if ($type === 'string') { $parsed[$key] = gridQueryString($query, $source, (string) ($rule['default'] ?? '')); continue; } if ($type === 'enum') { $parsed[$key] = gridQueryEnum( $query, $source, (array) ($rule['allowed'] ?? []), (string) ($rule['default'] ?? ''), (bool) ($rule['lowercase'] ?? false) ); continue; } if ($type === 'date') { $parsed[$key] = gridQueryDate($query, $source, (string) ($rule['default'] ?? '')); continue; } if ($type === 'uuid') { $parsed[$key] = gridQueryUuid($query, $source, (string) ($rule['default'] ?? '')); continue; } if ($type === 'csv_ids') { $ids = gridQueryCsvIds($query, $source, (int) ($rule['max'] ?? 200)); $parsed[$key] = (($rule['return'] ?? 'csv') === 'array') ? $ids : implode(',', array_map('strval', $ids)); continue; } if ($type === 'csv_uuid') { $items = gridQueryCsvUuids($query, $source, (int) ($rule['max'] ?? 200)); $parsed[$key] = (($rule['return'] ?? 'csv') === 'array') ? $items : implode(',', $items); continue; } if ($type === 'csv_strings') { $items = gridQueryCsvStrings( $query, $source, (int) ($rule['max'] ?? 200), $rule['sanitizer'] ?? null ); $parsed[$key] = (($rule['return'] ?? 'csv') === 'array') ? $items : implode(',', $items); continue; } if ($type === 'order') { $allowed = array_values(array_filter(array_map( static fn (mixed $item): string => trim((string) $item), (array) ($rule['allowed'] ?? []) ), static fn (string $item): bool => $item !== '')); $default = (string) ($rule['default'] ?? ($allowed[0] ?? '')); $parsed[$key] = in_array(trim((string) ($query[$source] ?? $default)), $allowed, true) ? trim((string) ($query[$source] ?? $default)) : $default; continue; } if ($type === 'dir') { $parsed[$key] = gridQueryEnum($query, $source, ['asc', 'desc'], (string) ($rule['default'] ?? 'asc'), true); continue; } $parsed[$key] = gridQueryString($query, $source, (string) ($rule['default'] ?? '')); } return $parsed; } /** * Read a trimmed string from query params. */ function gridQueryString(array $query, string $key, string $default = ''): string { return trim((string) ($query[$key] ?? $default)); } /** * Read and normalize a numeric query value. */ function gridQueryNumber(array $query, string $key, float $default = 0, ?float $min = null, ?float $max = null): float { $value = trim((string) ($query[$key] ?? '')); if ($value === '' || !is_numeric($value)) { $number = $default; } else { $number = (float) $value; } if ($min !== null && $number < $min) { $number = $min; } if ($max !== null && $number > $max) { $number = $max; } return $number; } /** * Read and normalize a bool query value. */ function gridQueryBool(array $query, string $key, bool $default = false): bool { if (!array_key_exists($key, $query)) { return $default; } return settingToBool(trim((string) $query[$key]), $default); } /** * Read and validate a date (Y-m-d) from query params. */ function gridQueryDate(array $query, string $key, string $default = ''): string { $value = trim((string) ($query[$key] ?? $default)); if ($value === '') { return ''; } return preg_match('/^\d{4}-\d{2}-\d{2}$/', $value) === 1 ? $value : ''; } /** * Read and validate a UUID from query params. */ function gridQueryUuid(array $query, string $key, string $default = ''): string { $value = strtolower(trim((string) ($query[$key] ?? $default))); if ($value === '') { return ''; } return preg_match('/^[a-f0-9]{8}-[a-f0-9]{4}-[1-8][a-f0-9]{3}-[89ab][a-f0-9]{3}-[a-f0-9]{12}$/', $value) === 1 ? $value : ''; } /** * Read a single enum value from query params. * * @param string[] $allowed */ function gridQueryEnum(array $query, string $key, array $allowed, string $default = '', bool $lowercase = false): string { $value = trim((string) ($query[$key] ?? $default)); if ($value === '') { return $default; } if ($lowercase) { $value = strtolower($value); $allowed = array_map(static fn (string $item): string => strtolower($item), $allowed); } return in_array($value, $allowed, true) ? $value : $default; } /** * Normalize comma-separated positive IDs from query params. * * @return int[] */ function gridQueryCsvIds(array $query, string $key, int $max = 200): array { if ($max <= 0) { return []; } $value = $query[$key] ?? ''; $items = []; if (is_string($value)) { $items = $value === '' ? [] : explode(',', $value); } elseif (is_array($value)) { array_walk_recursive($value, static function (mixed $item) use (&$items): void { $items[] = $item; }); } $ids = []; $seen = []; foreach ($items as $item) { $id = (int) trim((string) $item); if ($id <= 0 || isset($seen[$id])) { continue; } $seen[$id] = true; $ids[] = $id; if (count($ids) >= $max) { break; } } return $ids; } /** * Normalize comma-separated UUIDs from query params. * * @return string[] */ function gridQueryCsvUuids(array $query, string $key, int $max = 200): array { if ($max <= 0) { return []; } $value = $query[$key] ?? ''; $items = []; if (is_string($value)) { $items = $value === '' ? [] : explode(',', $value); } elseif (is_array($value)) { array_walk_recursive($value, static function (mixed $item) use (&$items): void { $items[] = $item; }); } $uuids = []; $seen = []; foreach ($items as $item) { $uuid = gridQueryUuid(['v' => $item], 'v'); if ($uuid === '' || isset($seen[$uuid])) { continue; } $seen[$uuid] = true; $uuids[] = $uuid; if (count($uuids) >= $max) { break; } } return $uuids; } /** * Normalize comma-separated strings from query params. * * @return string[] */ function gridQueryCsvStrings(array $query, string $key, int $max = 200, ?callable $sanitizer = null): array { if ($max <= 0) { return []; } $value = $query[$key] ?? ''; $items = []; if (is_string($value)) { $items = $value === '' ? [] : explode(',', $value); } elseif (is_array($value)) { array_walk_recursive($value, static function (mixed $item) use (&$items): void { $items[] = $item; }); } $result = []; $seen = []; foreach ($items as $item) { $text = trim((string) $item); if ($text === '') { continue; } if ($sanitizer !== null) { $text = trim((string) $sanitizer($text)); if ($text === '') { continue; } } if (isset($seen[$text])) { continue; } $seen[$text] = true; $result[] = $text; if (count($result) >= $max) { break; } } return $result; } /** * Normalize a mixed value (string "a||b" or list) to a clean label list. * * @return string[] */ function gridNormalizeLabelList(mixed $value, string $separator = '||'): array { if (is_string($value)) { $items = $value === '' ? [] : explode($separator, $value); } elseif (is_array($value)) { $items = $value; } else { return []; } $items = array_map(static fn (mixed $item): string => trim((string) $item), $items); $items = array_values(array_filter($items, static fn (string $item): bool => $item !== '')); return $items; }