Skip to content

Instantly share code, notes, and snippets.

@kmuenkel
Last active November 21, 2024 22:07
Show Gist options
  • Save kmuenkel/c063511e572fc07deb713c67d1185c13 to your computer and use it in GitHub Desktop.
Save kmuenkel/c063511e572fc07deb713c67d1185c13 to your computer and use it in GitHub Desktop.
Dot-delimited recursive array data extraction with wildcard and nested expression support
<?php
namespace Tests;
class NodePath
{
private const string ESCAPED = '\\\\';
private const string UNESCAPED = '(?<!\\\\)';
private static string $notFound;
private function __construct(private readonly array $items)
{
self::$notFound ??= uniqid();
}
public static function get(array $items, string $index, $default = null)
{
return (new static($items))->getPath($index, $default);
}
private function getPath(string $index, $default = null)
{
$indexes = array_map(
fn (string $index): string => preg_replace('~' . static::ESCAPED . '\.~', '.', $index),
preg_split('~' . static::UNESCAPED . '\.~', $index)
);
return $this->getNodes($indexes, $this->items, $default);
}
private function getOperators(): array
{
return [
'~=' => fn ($value, string $expression) => fnmatch("*$expression", $value),
'=~' => fn ($value, string $expression) => fnmatch("$expression*", $value),
'<=' => fn ($value, string $expression) => $value <= $expression,
'>=' => fn ($value, string $expression) => $value >= $expression,
'=' => fn ($value, string|bool|null $expression) => $expression === $value,
'<' => fn ($value, string $expression) => $value < $expression,
'>' => fn ($value, string $expression) => $value > $expression,
'~' => fn ($value, string $expression) => preg_match($expression, $value),
];
}
private function getNodes(array $indexes, array $items, $default = null)
{
while (null !== ($index = array_shift($indexes))) {
if (!is_array($items)) {
return $default;
} elseif (preg_match('/^(' . static::UNESCAPED . '\[)(.+)(' . static::UNESCAPED . '])$/', $index, $checks)) {
$items = $this->findMatches($items, $checks[2]);
} elseif (preg_match('~' . static::UNESCAPED . '\*~', $index)) {
return $this->wildcardExpression($indexes, $index, $items, $default);
} elseif (!array_key_exists($this->unescape($index), $items)) {
return $default;
} else {
$items = $items[$this->unescape($index)];
}
}
return $items;
}
private function unescape(string $index): string
{
$index = preg_replace('~' . static::ESCAPED . '\*~', '*', $index);
$index = preg_replace('~' . static::ESCAPED . '\[~', '[', $index);
return preg_replace('~' . static::ESCAPED . ']~', ']', $index);
}
private function wildcardExpression(array $indexes, string $index, array $items, $default = null)
{
$items = array_filter($items, fn ($key): bool => fnmatch($index, (string) $key), ARRAY_FILTER_USE_KEY);
$items = array_map(fn ($subset) => is_array($subset) ? $this->getNodes($indexes, $subset, $default) : self::$notFound, $items);
return array_filter($items, fn ($item) => $item !== self::$notFound) ?: $default;
}
private function findMatches(array $items, string $check): array
{
$operatorPatterns = array_map(fn (string $operator) => preg_quote($operator, '/'), array_keys($this->getOperators()));
$byOperator = '/^(.+)(' . implode('|', $operatorPatterns) . ')(.+)$/';
[$field, $operator, $expression] = preg_split($byOperator, $check, 3, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE) + ['*', '~=', ''];
return array_filter([$items], function ($element) use ($field, $operator, $expression): bool {
$expression = !strcasecmp($expression, 'true') ?: (!strcasecmp($expression, 'false') ? false : (!strcasecmp($expression, 'null') ? null : $expression));
$expression = in_array($expression, [true, false, null]) ? $expression : preg_replace('~^(\'|")(.+)' . static::UNESCAPED . '(\'|")$~', '$2', $expression);
$value = (new static($element))->getPath($field);
$isMet = fn ($value) => ($this->getOperators()[$operator])($value, $expression);
return $value !== self::$notFound && (is_array($value) ? array_filter($value, $isMet) : $isMet($value));
});
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment