forked from fa/breadcrumb-the-shire
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>
828 lines
22 KiB
PHP
828 lines
22 KiB
PHP
<?php
|
|
|
|
/**
|
|
* Enforce GET-only access for grid/data endpoints with a JSON 405 response.
|
|
*/
|
|
function gridRequireGetRequest(): void
|
|
{
|
|
if (requestInput()->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<string,mixed>
|
|
*/
|
|
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<string,string> $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<string,mixed>|array<int,mixed> $schema
|
|
* @return array<string,mixed>|array<int,mixed>
|
|
*/
|
|
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<string,mixed>
|
|
*/
|
|
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<int,array<string,mixed>>
|
|
*/
|
|
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<int,array<string,mixed>> $toolbarSchema
|
|
* @param string[] $searchKeys
|
|
* @return array{search: array<int,array<string,mixed>>, drawer: array<int,array<string,mixed>>}
|
|
*/
|
|
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) {
|
|
$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<int,array<string,mixed>> $toolbarSchema
|
|
* @return array<string,array<string,mixed>>
|
|
*/
|
|
function gridSchemaByKey(array $toolbarSchema): array
|
|
{
|
|
$schemaByKey = [];
|
|
foreach ($toolbarSchema as $field) {
|
|
$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<int,array<string,mixed>> $toolbarSchema
|
|
* @param array<string,mixed> $filterState
|
|
* @return array<string,mixed>
|
|
*/
|
|
function gridBuildToolbarFilterState(array $toolbarSchema, array $filterState): array
|
|
{
|
|
$state = [];
|
|
foreach ($toolbarSchema as $field) {
|
|
|
|
$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') {
|
|
$state[$key] = array_values(array_unique(gridNormalizeLabelList($value, ',')));
|
|
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<string,mixed> $filterSchema
|
|
* @param array<string,mixed> $options
|
|
* @return array{
|
|
* filterState: array<string,mixed>,
|
|
* toolbarFilterSchema: array<int,array<string,mixed>>,
|
|
* toolbarFilterState: array<string,mixed>,
|
|
* searchToolbarFilterSchema: array<int,array<string,mixed>>,
|
|
* drawerToolbarFilterSchema: array<int,array<string,mixed>>,
|
|
* schemaByKey: array<string,array<string,mixed>>,
|
|
* toolbarOptionSets: array<string,mixed>,
|
|
* clientFilterSchema: array<int,array<string,mixed>>,
|
|
* searchConfig: ?array<string,mixed>
|
|
* }
|
|
*/
|
|
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<int,array<string,mixed>> $items
|
|
* @return array<string,string>
|
|
*/
|
|
function gridOptionMapFromItems(array $items): array
|
|
{
|
|
$map = [];
|
|
foreach ($items as $item) {
|
|
$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<string,mixed> $field
|
|
* @return array<string,string>
|
|
*/
|
|
function gridOptionMapFromAllowed(array $field): array
|
|
{
|
|
$map = [];
|
|
foreach ((array) ($field['allowed'] ?? []) as $item) {
|
|
$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<int,array<string,mixed>>
|
|
*/
|
|
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<string,mixed>|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<string, mixed>
|
|
*/
|
|
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] = gridQueryEnum($query, $source, $allowed, $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;
|
|
}
|
|
|
|
/**
|
|
* Extract raw items from a comma-separated or array query parameter.
|
|
*
|
|
* @return list<mixed>
|
|
*/
|
|
function gridQueryCsvInput(array $query, string $key): array
|
|
{
|
|
$value = $query[$key] ?? '';
|
|
if (is_string($value)) {
|
|
return $value === '' ? [] : explode(',', $value);
|
|
}
|
|
if (is_array($value)) {
|
|
$items = [];
|
|
array_walk_recursive($value, static function (mixed $item) use (&$items): void {
|
|
$items[] = $item;
|
|
});
|
|
return $items;
|
|
}
|
|
|
|
return [];
|
|
}
|
|
|
|
/**
|
|
* Normalize comma-separated positive IDs from query params.
|
|
*
|
|
* @return int[]
|
|
*/
|
|
function gridQueryCsvIds(array $query, string $key, int $max = 200): array
|
|
{
|
|
if ($max <= 0) {
|
|
return [];
|
|
}
|
|
|
|
$items = gridQueryCsvInput($query, $key);
|
|
|
|
$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 [];
|
|
}
|
|
|
|
$items = gridQueryCsvInput($query, $key);
|
|
|
|
$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 [];
|
|
}
|
|
|
|
$items = gridQueryCsvInput($query, $key);
|
|
|
|
$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;
|
|
}
|