* @implements Iterator
*/
class JSONPath implements ArrayAccess, Iterator, JsonSerializable, Countable
{
public const int ALLOW_MAGIC = 1;
/** @var array> */
protected static array $tokenCache = [];
protected mixed $data = [];
protected int $options = 0;
final public function __construct(mixed $data = [], int $options = 0)
{
$this->data = $data;
$this->options = $options;
}
/**
* Evaluate an expression
*
* @throws JSONPathException
*
* @return static
*/
public function find(string $expression): self
{
$tokens = $this->parseTokens($expression);
$collectionData = [$this->data];
foreach ($tokens as $token) {
$filter = $token->buildFilter($this->options);
$filter->setRootData($this->data);
$filteredDataList = [];
foreach ($collectionData as $value) {
if (AccessHelper::isCollectionType($value)) {
$filteredDataList[] = $filter->filter($value);
}
}
if (!empty($filteredDataList)) {
$collectionData = \array_merge(...$filteredDataList);
} else {
$collectionData = [];
}
}
return new static($collectionData, $this->options);
}
public function first(): mixed
{
$keys = AccessHelper::collectionKeys($this->data);
if (empty($keys)) {
return null;
}
$value = $this->data[$keys[0]] ?? null;
return AccessHelper::isCollectionType($value) ? new static($value, $this->options) : $value;
}
/**
* Evaluate an expression and return the last result
*/
public function last(): mixed
{
$keys = AccessHelper::collectionKeys($this->data);
if (empty($keys)) {
return null;
}
$value = $this->data[\end($keys)] ?? null;
return AccessHelper::isCollectionType($value) ? new static($value, $this->options) : $value;
}
/**
* Evaluate an expression and return the first key
*/
public function firstKey(): string|int|null
{
$keys = AccessHelper::collectionKeys($this->data);
if (empty($keys)) {
return null;
}
return $keys[0];
}
/**
* Evaluate an expression and return the last key
*/
public function lastKey(): string|int|null
{
$keys = AccessHelper::collectionKeys($this->data);
if (empty($keys)) {
return null;
}
return \end($keys);
}
/**
* @return list
* @throws JSONPathException
*/
public function parseTokens(string $expression): array
{
$cacheKey = \crc32($expression);
if (isset(static::$tokenCache[$cacheKey])) {
return static::$tokenCache[$cacheKey];
}
$lexer = new JSONPathLexer($expression);
$tokens = $lexer->parseExpression();
static::$tokenCache[$cacheKey] = $tokens;
return $tokens;
}
public function getData(): mixed
{
return $this->data;
}
/**
* @noinspection MagicMethodsValidityInspection
*/
public function __get(string|int $key): mixed
{
return $this->offsetExists($key) ? $this->offsetGet($key) : null;
}
#[Override]
public function offsetExists(mixed $offset): bool
{
return AccessHelper::keyExists($this->data, $offset);
}
#[Override]
public function offsetGet(mixed $offset): mixed
{
$value = AccessHelper::getValue($this->data, $offset);
return AccessHelper::isCollectionType($value)
? new static($value, $this->options)
: $value;
}
#[Override]
public function offsetSet(mixed $offset, mixed $value): void
{
if ($offset === null) {
$this->data[] = $value;
} else {
AccessHelper::setValue($this->data, $offset, $value);
}
}
#[Override]
public function offsetUnset(mixed $offset): void
{
AccessHelper::unsetValue($this->data, $offset);
}
#[Override]
public function jsonSerialize(): mixed
{
return $this->getData();
}
#[Override]
public function current(): mixed
{
$value = \current($this->data);
return AccessHelper::isCollectionType($value) ? new static($value, $this->options) : $value;
}
#[Override]
public function next(): void
{
\next($this->data);
}
#[Override]
public function key(): string|int|null
{
return \key($this->data);
}
#[Override]
public function valid(): bool
{
return \key($this->data) !== null;
}
#[Override]
public function rewind(): void
{
\reset($this->data);
}
#[Override]
public function count(): int
{
return \count($this->data);
}
}