initial commit

This commit is contained in:
isUnknown 2026-01-13 10:21:41 +01:00
commit 5210d78d7d
969 changed files with 223828 additions and 0 deletions

View file

@ -0,0 +1,188 @@
<?php
namespace Kirby\Query\Visitors;
use Closure;
use Exception;
use Kirby\Query\AST\ClosureNode;
use Kirby\Query\Runners\Scope;
/**
* Processes a query AST
*
* @package Kirby Query
* @author Roman Steiner <roman@toastlab.ch>,
* Nico Hoffmann <nico@getkirby.com>
* @link https://getkirby.com
* @license https://opensource.org/licenses/MIT
* @since 5.1.0
* @unstable
*/
class DefaultVisitor extends Visitor
{
/**
* Processes list of arguments
*/
public function arguments(array $arguments): array
{
return $arguments;
}
/**
* Processes arithmetic operation
*/
public function arithmetic(
int|float $left,
string $operator,
int|float $right
): mixed {
return match ($operator) {
'+' => $left + $right,
'-' => $left - $right,
'*' => $left * $right,
'/' => $left / $right,
'%' => $left % $right,
default => throw new Exception("Unknown arithmetic operator: $operator")
};
}
/**
* Processes array
*/
public function arrayList(array $elements): array
{
return $elements;
}
/**
* Processes node into actual closure
*/
public function closure(ClosureNode $node): Closure
{
$self = $this;
return function (...$params) use ($self, $node) {
// [key1, key2] + [value1, value2] =>
// [key1 => value1, key2 => value2]
$arguments = array_combine(
$node->arguments,
$params
);
// Create new nested visitor with combined
// data context for resolving the closure body
$visitor = new static(
global: $self->global,
context: [...$self->context, ...$arguments],
interceptor: $self->interceptor
);
return $node->body->resolve($visitor);
};
}
/**
* Processes coalescence operator
*/
public function coalescence(mixed $left, mixed $right): mixed
{
return $left ?? $right;
}
/**
* Processes comparison operation
*/
public function comparison(
mixed $left,
string $operator,
mixed $right
): bool {
return match ($operator) {
'==' => $left == $right,
'===' => $left === $right,
'!=' => $left != $right,
'!==' => $left !== $right,
'<' => $left < $right,
'<=' => $left <= $right,
'>' => $left > $right,
'>=' => $left >= $right,
default => throw new Exception("Unknown comparison operator: $operator")
};
}
/**
* Processes global function
*/
public function function(string $name, array $arguments = []): mixed
{
$function = $this->global[$name] ?? null;
if ($function === null) {
throw new Exception("Invalid global function in query: $name");
}
return $function(...$arguments);
}
/**
* Processes literals
*/
public function literal(mixed $value): mixed
{
return $value;
}
/**
* Processes logical operation
*/
public function logical(
mixed $left,
string $operator,
mixed $right
): bool {
return match ($operator) {
'&&', 'AND' => $left && $right,
'||', 'OR' => $left || $right,
default => throw new Exception("Unknown logical operator: $operator")
};
}
/**
* Processes member access
*/
public function memberAccess(
mixed $object,
string|int $member,
array|null $arguments = null,
bool $nullSafe = false
): mixed {
if ($this->interceptor !== null) {
$object = ($this->interceptor)($object);
}
return Scope::access($object, $member, $nullSafe, ...$arguments ?? []);
}
/**
* Processes ternary operator
*/
public function ternary(
mixed $condition,
mixed $true,
mixed $false
): mixed {
if ($true === null) {
return $condition ?: $false;
}
return $condition ? $true : $false;
}
/**
* Get variable from context or global function
*/
public function variable(string $name): mixed
{
return Scope::get($name, $this->context, $this->global);
}
}

View file

@ -0,0 +1,46 @@
<?php
namespace Kirby\Query\Visitors;
use Closure;
/**
* @package Kirby Query
* @author Roman Steiner <roman@toastlab.ch>,
* Nico Hoffmann <nico@getkirby.com>
* @link https://getkirby.com
* @license https://opensource.org/licenses/MIT
* @since 5.1.0
* @unstable
*
* Every visitor class must implement the following methods.
* As PHP won't allow increasing the typing specificity, we
* aren't actually adding them here in the abstract class, so that
* the actual visitor classes can work with much more specific type hints.
*
* @method mixed arguments(array $arguments)
* @method mixed arithmetic(mixed $left, string $operator, mixed $right)
* @method mixed arrayList(array $elements)
* @method mixed closure($ClosureNode $node))
* @method mixed coalescence($left, $right)
* @method mixed comparison(mixed $left, string $operator, mixed $right)
* @method mixed function($name, $arguments)
* @method mixed literal($value)
* @method mixed logical(mixed $left, string $operator, mixed $right)
* @method mixed memberAccess($object, string|int $member, $arguments, bool $nullSafe = false)
* @method mixed ternary($condition, $true, $false)
* @method mixed variable(string $name)
*/
abstract class Visitor
{
/**
* @param array<string,Closure> $global valid global function closures
* @param array<string,mixed> $context data bindings for the query
*/
public function __construct(
public array $global = [],
public array $context = [],
protected Closure|null $interceptor = null
) {
}
}