Update Composer packages
This commit is contained in:
parent
0320235f6c
commit
a8b68fb61b
378 changed files with 28466 additions and 28852 deletions
108
kirby/src/Query/Argument.php
Normal file
108
kirby/src/Query/Argument.php
Normal file
|
@ -0,0 +1,108 @@
|
|||
<?php
|
||||
|
||||
namespace Kirby\Query;
|
||||
|
||||
use Closure;
|
||||
use Kirby\Toolkit\Str;
|
||||
|
||||
/**
|
||||
* The Argument class represents a single
|
||||
* parameter passed to a method in a chained query
|
||||
*
|
||||
* @package Kirby Query
|
||||
* @author Nico Hoffmann <nico@getkirby.com>
|
||||
* @link https://getkirby.com
|
||||
* @copyright Bastian Allgeier
|
||||
* @license https://opensource.org/licenses/MIT
|
||||
*/
|
||||
class Argument
|
||||
{
|
||||
public function __construct(
|
||||
public mixed $value
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitizes argument string into actual
|
||||
* PHP type/object as new Argument instance
|
||||
*/
|
||||
public static function factory(string $argument): static
|
||||
{
|
||||
$argument = trim($argument);
|
||||
|
||||
// remove grouping parantheses
|
||||
if (
|
||||
Str::startsWith($argument, '(') &&
|
||||
Str::endsWith($argument, ')')
|
||||
) {
|
||||
$argument = trim(substr($argument, 1, -1));
|
||||
}
|
||||
|
||||
// string with single or double quotes
|
||||
if (
|
||||
(
|
||||
Str::startsWith($argument, '"') &&
|
||||
Str::endsWith($argument, '"')
|
||||
) || (
|
||||
Str::startsWith($argument, "'") &&
|
||||
Str::endsWith($argument, "'")
|
||||
)
|
||||
) {
|
||||
$string = substr($argument, 1, -1);
|
||||
$string = str_replace(['\"', "\'"], ['"', "'"], $string);
|
||||
return new static($string);
|
||||
}
|
||||
|
||||
// array: split and recursive sanitizing
|
||||
if (
|
||||
Str::startsWith($argument, '[') &&
|
||||
Str::endsWith($argument, ']')
|
||||
) {
|
||||
$array = substr($argument, 1, -1);
|
||||
$array = Arguments::factory($array);
|
||||
return new static($array);
|
||||
}
|
||||
|
||||
// numeric
|
||||
if (is_numeric($argument) === true) {
|
||||
return new static((float)$argument);
|
||||
}
|
||||
|
||||
// Closure
|
||||
if (Str::startsWith($argument, '() =>')) {
|
||||
$query = Str::after($argument, '() =>');
|
||||
$query = trim($query);
|
||||
return new static(fn () => $query);
|
||||
}
|
||||
|
||||
return new static(match ($argument) {
|
||||
'null' => null,
|
||||
'true' => true,
|
||||
'false' => false,
|
||||
|
||||
// resolve parameter for objects and methods itself
|
||||
default => new Query($argument)
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the argument value and
|
||||
* resolves nested objects to scaler types
|
||||
*/
|
||||
public function resolve(array|object $data = []): mixed
|
||||
{
|
||||
// don't resolve the Closure immediately, instead
|
||||
// resolve it to the sub-query and create a new Closure
|
||||
// that resolves the sub-query with the same data set once called
|
||||
if ($this->value instanceof Closure) {
|
||||
$query = ($this->value)();
|
||||
return fn () => static::factory($query)->resolve($data);
|
||||
}
|
||||
|
||||
if (is_object($this->value) === true) {
|
||||
return $this->value->resolve($data);
|
||||
}
|
||||
|
||||
return $this->value;
|
||||
}
|
||||
}
|
58
kirby/src/Query/Arguments.php
Normal file
58
kirby/src/Query/Arguments.php
Normal file
|
@ -0,0 +1,58 @@
|
|||
<?php
|
||||
|
||||
namespace Kirby\Query;
|
||||
|
||||
use Kirby\Toolkit\A;
|
||||
use Kirby\Toolkit\Collection;
|
||||
|
||||
/**
|
||||
* The Argument class represents a single
|
||||
* parameter passed to a method in a chained query
|
||||
*
|
||||
* @package Kirby Query
|
||||
* @author Nico Hoffmann <nico@getkirby.com>
|
||||
* @link https://getkirby.com
|
||||
* @copyright Bastian Allgeier
|
||||
* @license https://opensource.org/licenses/MIT
|
||||
*/
|
||||
class Arguments extends Collection
|
||||
{
|
||||
// skip all matches inside of parantheses
|
||||
public const NO_PNTH = '\([^)]+\)(*SKIP)(*FAIL)';
|
||||
// skip all matches inside of square brackets
|
||||
public const NO_SQBR = '\[[^]]+\](*SKIP)(*FAIL)';
|
||||
// skip all matches inside of double quotes
|
||||
public const NO_DLQU = '\"(?:[^"\\\\]|\\\\.)*\"(*SKIP)(*FAIL)';
|
||||
// skip all matches inside of single quotes
|
||||
public const NO_SLQU = '\'(?:[^\'\\\\]|\\\\.)*\'(*SKIP)(*FAIL)';
|
||||
// skip all matches inside of any of the above skip groups
|
||||
public const OUTSIDE = self::NO_PNTH . '|' . self::NO_SQBR . '|' .
|
||||
self::NO_DLQU . '|' . self::NO_SLQU;
|
||||
|
||||
/**
|
||||
* Splits list of arguments into individual
|
||||
* Argument instances while respecting skip groups
|
||||
*/
|
||||
public static function factory(string $arguments): static
|
||||
{
|
||||
$arguments = A::map(
|
||||
// split by comma, but not inside skip groups
|
||||
preg_split('!,|' . self::OUTSIDE . '!', $arguments),
|
||||
fn ($argument) => Argument::factory($argument)
|
||||
);
|
||||
|
||||
return new static($arguments);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve each argument, so that they can
|
||||
* passed together to the actual method call
|
||||
*/
|
||||
public function resolve(array|object $data = []): array
|
||||
{
|
||||
return A::map(
|
||||
$this->data,
|
||||
fn ($argument) => $argument->resolve($data)
|
||||
);
|
||||
}
|
||||
}
|
116
kirby/src/Query/Expression.php
Normal file
116
kirby/src/Query/Expression.php
Normal file
|
@ -0,0 +1,116 @@
|
|||
<?php
|
||||
|
||||
namespace Kirby\Query;
|
||||
|
||||
use Kirby\Exception\LogicException;
|
||||
use Kirby\Toolkit\A;
|
||||
|
||||
/**
|
||||
* The Expression class adds support for simple shorthand
|
||||
* comparisons (`a ? b : c`, `a ?: c` and `a ?? b`)
|
||||
*
|
||||
* @package Kirby Query
|
||||
* @author Nico Hoffmann <nico@getkirby.com>
|
||||
* @link https://getkirby.com
|
||||
* @copyright Bastian Allgeier
|
||||
* @license https://opensource.org/licenses/MIT
|
||||
*/
|
||||
class Expression
|
||||
{
|
||||
public function __construct(
|
||||
public array $parts
|
||||
) {
|
||||
}
|
||||
|
||||
public static function factory(string $expression, Query $parent = null): static|Segments
|
||||
{
|
||||
// split into different expression parts and operators
|
||||
$parts = static::parse($expression);
|
||||
|
||||
// shortcut: if expression has only one part, directly
|
||||
// continue with the segments chain
|
||||
if (count($parts) === 1) {
|
||||
return Segments::factory(query: $parts[0], parent: $parent);
|
||||
}
|
||||
|
||||
// turn all non-operator parts into an Argument
|
||||
// which takes care of converting string, arrays booleans etc.
|
||||
// into actual types and treats all other parts as their own queries
|
||||
$parts = A::map(
|
||||
$parts,
|
||||
fn ($part) =>
|
||||
in_array($part, ['?', ':', '?:', '??'])
|
||||
? $part
|
||||
: Argument::factory($part)
|
||||
);
|
||||
|
||||
return new static(parts: $parts);
|
||||
}
|
||||
|
||||
/**
|
||||
* Splits a comparison string into an array
|
||||
* of expressions and operators
|
||||
* @internal
|
||||
*/
|
||||
public static function parse(string $string): array
|
||||
{
|
||||
// split by multiples of `?` and `:`, but not inside skip groups
|
||||
// (parantheses, quotes etc.)
|
||||
return preg_split(
|
||||
'/\s+([\?\:]+)\s+|' . Arguments::OUTSIDE . '/',
|
||||
trim($string),
|
||||
flags: PREG_SPLIT_DELIM_CAPTURE|PREG_SPLIT_NO_EMPTY
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves the expression by evaluating
|
||||
* the supported comparisons and consecutively
|
||||
* resolving the resulting query/argument
|
||||
*/
|
||||
public function resolve(array|object $data = []): mixed
|
||||
{
|
||||
$base = null;
|
||||
|
||||
foreach ($this->parts as $index => $part) {
|
||||
// `a ?? b`
|
||||
// if the base/previous (e.g. `a`) isn't null,
|
||||
// stop the expression chain and return `a`
|
||||
if ($part === '??') {
|
||||
if ($base !== null) {
|
||||
return $base;
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
// `a ?: b`
|
||||
// if `a` isn't false, return `a`, otherwise `b`
|
||||
if ($part === '?:') {
|
||||
if ($base != false) {
|
||||
return $base;
|
||||
}
|
||||
|
||||
return $this->parts[$index + 1]->resolve($data);
|
||||
}
|
||||
|
||||
// `a ? b : c`
|
||||
// if `a` isn't false, return `b`, otherwise `c`
|
||||
if ($part === '?') {
|
||||
if (($this->parts[$index + 2] ?? null) !== ':') {
|
||||
throw new LogicException('Query: Incomplete ternary operator (missing matching `? :`)');
|
||||
}
|
||||
|
||||
if ($base != false) {
|
||||
return $this->parts[$index + 1]->resolve($data);
|
||||
}
|
||||
|
||||
return $this->parts[$index + 3]->resolve($data);
|
||||
}
|
||||
|
||||
$base = $part->resolve($data);
|
||||
}
|
||||
|
||||
return $base;
|
||||
}
|
||||
}
|
138
kirby/src/Query/Query.php
Normal file
138
kirby/src/Query/Query.php
Normal file
|
@ -0,0 +1,138 @@
|
|||
<?php
|
||||
|
||||
namespace Kirby\Query;
|
||||
|
||||
use Closure;
|
||||
use Kirby\Cms\App;
|
||||
use Kirby\Cms\Collection;
|
||||
use Kirby\Cms\File;
|
||||
use Kirby\Cms\Page;
|
||||
use Kirby\Cms\Site;
|
||||
use Kirby\Cms\User;
|
||||
use Kirby\Toolkit\I18n;
|
||||
|
||||
/**
|
||||
* The Query class can be used to query arrays and objects,
|
||||
* including their methods with a very simple string-based syntax.
|
||||
*
|
||||
* Namespace structure - what handles what:
|
||||
* - Query Main interface, direct entries
|
||||
* - Expression Simple comparisons (`a ? b :c`)
|
||||
* - Segments Chain of method calls (`site.find('notes').url`)
|
||||
* - Segment Single method call (`find('notes')`)
|
||||
* - Arguments Method call parameters (`'template', '!=', 'note'`)
|
||||
* - Argument Single parameter, resolving into actual types
|
||||
*
|
||||
* @package Kirby Query
|
||||
* @author Bastian Allgeier <bastian@getkirby.com>,
|
||||
* Nico Hoffmann <nico@getkirby.com>
|
||||
* @link https://getkirby.com
|
||||
* @copyright Bastian Allgeier
|
||||
* @license https://opensource.org/licenses/MIT
|
||||
*/
|
||||
class Query
|
||||
{
|
||||
/**
|
||||
* Default data entries
|
||||
*/
|
||||
public static array $entries = [];
|
||||
|
||||
/**
|
||||
* Creates a new Query object
|
||||
*/
|
||||
public function __construct(
|
||||
public string|null $query = null
|
||||
) {
|
||||
if ($query !== null) {
|
||||
$this->query = trim($query);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new Query object
|
||||
*/
|
||||
public static function factory(string $query): static
|
||||
{
|
||||
return new static(query: $query);
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to help classes that extend Query
|
||||
* to intercept a segment's result.
|
||||
*/
|
||||
public function intercept(mixed $result): mixed
|
||||
{
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the query result if anything
|
||||
* can be found, otherwise returns null
|
||||
*
|
||||
* @throws \Kirby\Exception\BadMethodCallException If an invalid method is accessed by the query
|
||||
*/
|
||||
public function resolve(array|object $data = []): mixed
|
||||
{
|
||||
if (empty($this->query) === true) {
|
||||
return $data;
|
||||
}
|
||||
|
||||
// merge data with default entries
|
||||
if (is_array($data) === true) {
|
||||
$data = array_merge(static::$entries, $data);
|
||||
}
|
||||
|
||||
// direct data array access via key
|
||||
if (
|
||||
is_array($data) === true &&
|
||||
array_key_exists($this->query, $data) === true
|
||||
) {
|
||||
$value = $data[$this->query];
|
||||
|
||||
if ($value instanceof Closure) {
|
||||
$value = $value();
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
// loop through all segments to resolve query
|
||||
return Expression::factory($this->query, $this)->resolve($data);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Default entries/functions
|
||||
*/
|
||||
Query::$entries['kirby'] = function (): App {
|
||||
return App::instance();
|
||||
};
|
||||
|
||||
Query::$entries['collection'] = function (string $name): Collection|null {
|
||||
return App::instance()->collection($name);
|
||||
};
|
||||
|
||||
Query::$entries['file'] = function (string $id): File|null {
|
||||
return App::instance()->file($id);
|
||||
};
|
||||
|
||||
Query::$entries['page'] = function (string $id): Page|null {
|
||||
return App::instance()->site()->find($id);
|
||||
};
|
||||
|
||||
Query::$entries['site'] = function (): Site {
|
||||
return App::instance()->site();
|
||||
};
|
||||
|
||||
|
||||
Query::$entries['t'] = function (
|
||||
string $key,
|
||||
string|array $fallback = null,
|
||||
string $locale = null
|
||||
): string|null {
|
||||
return I18n::translate($key, $fallback, $locale);
|
||||
};
|
||||
|
||||
Query::$entries['user'] = function (string $id = null): User|null {
|
||||
return App::instance()->user($id);
|
||||
};
|
145
kirby/src/Query/Segment.php
Normal file
145
kirby/src/Query/Segment.php
Normal file
|
@ -0,0 +1,145 @@
|
|||
<?php
|
||||
|
||||
namespace Kirby\Query;
|
||||
|
||||
use Closure;
|
||||
use Kirby\Exception\BadMethodCallException;
|
||||
use Kirby\Exception\InvalidArgumentException;
|
||||
use Kirby\Toolkit\Str;
|
||||
|
||||
/**
|
||||
* The Segment class represents a single
|
||||
* part of a chained query
|
||||
*
|
||||
* @package Kirby Query
|
||||
* @author Nico Hoffmann <nico@getkirby.com>
|
||||
* @link https://getkirby.com
|
||||
* @copyright Bastian Allgeier
|
||||
* @license https://opensource.org/licenses/MIT
|
||||
*/
|
||||
class Segment
|
||||
{
|
||||
public function __construct(
|
||||
public string $method,
|
||||
public int $position,
|
||||
public Arguments|null $arguments = null,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Throws an exception for an access to an invalid method
|
||||
* @internal
|
||||
*
|
||||
* @param mixed $data Variable on which the access was tried
|
||||
* @param string $name Name of the method/property that was accessed
|
||||
* @param string $label Type of the name (`method`, `property` or `method/property`)
|
||||
*
|
||||
* @throws \Kirby\Exception\BadMethodCallException
|
||||
*/
|
||||
public static function error(mixed $data, string $name, string $label): void
|
||||
{
|
||||
$type = strtolower(gettype($data));
|
||||
|
||||
if ($type === 'double') {
|
||||
$type = 'float';
|
||||
}
|
||||
|
||||
$nonExisting = in_array($type, ['array', 'object']) ? 'non-existing ' : '';
|
||||
|
||||
$error = 'Access to ' . $nonExisting . $label . ' "' . $name . '" on ' . $type;
|
||||
|
||||
throw new BadMethodCallException($error);
|
||||
}
|
||||
|
||||
public static function factory(
|
||||
string $segment,
|
||||
int $position = 0
|
||||
): static {
|
||||
if (Str::endsWith($segment, ')') === false) {
|
||||
return new static(method: $segment, position: $position);
|
||||
}
|
||||
|
||||
// the args are everything inside the *outer* parentheses
|
||||
$args = Str::substr($segment, Str::position($segment, '(') + 1, -1);
|
||||
|
||||
return new static(
|
||||
method: Str::before($segment, '('),
|
||||
position: $position,
|
||||
arguments: Arguments::factory($args)
|
||||
);
|
||||
}
|
||||
|
||||
public function resolve(mixed $base = null, array|object $data = []): mixed
|
||||
{
|
||||
// resolve arguments to array
|
||||
$args = $this->arguments?->resolve($data) ?? [];
|
||||
|
||||
// 1st segment, start from $data array
|
||||
if ($this->position === 0) {
|
||||
if (is_array($data) == true) {
|
||||
return $this->resolveArray($data, $args);
|
||||
}
|
||||
|
||||
return $this->resolveObject($data, $args);
|
||||
}
|
||||
|
||||
if (is_array($base) === true) {
|
||||
return $this->resolveArray($base, $args);
|
||||
}
|
||||
|
||||
if (is_object($base) === true) {
|
||||
return $this->resolveObject($base, $args);
|
||||
}
|
||||
|
||||
// trying to access further segments on a scalar/null value
|
||||
static::error($base, $this->method, 'method/property');
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves segment by calling the corresponding array key
|
||||
*/
|
||||
protected function resolveArray(array $array, array $args): mixed
|
||||
{
|
||||
if (array_key_exists($this->method, $array) === false) {
|
||||
static::error($array, $this->method, 'property');
|
||||
}
|
||||
|
||||
$value = $array[$this->method];
|
||||
|
||||
if ($value instanceof Closure) {
|
||||
return $value(...$args);
|
||||
}
|
||||
|
||||
if ($args !== []) {
|
||||
throw new InvalidArgumentException('Cannot access array element "' . $this->method . '" with arguments');
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves segment by calling the method/accessing the property
|
||||
* on the base object
|
||||
*/
|
||||
protected function resolveObject(object $object, array $args): mixed
|
||||
{
|
||||
if (
|
||||
method_exists($object, $this->method) === true ||
|
||||
method_exists($object, '__call') === true
|
||||
) {
|
||||
return $object->{$this->method}(...$args);
|
||||
}
|
||||
|
||||
if (
|
||||
$args === [] && (
|
||||
property_exists($object, $this->method) === true ||
|
||||
method_exists($object, '__get') === true
|
||||
)
|
||||
) {
|
||||
return $object->{$this->method};
|
||||
}
|
||||
|
||||
$label = ($args === []) ? 'method/property' : 'method';
|
||||
static::error($object, $this->method, $label);
|
||||
}
|
||||
}
|
99
kirby/src/Query/Segments.php
Normal file
99
kirby/src/Query/Segments.php
Normal file
|
@ -0,0 +1,99 @@
|
|||
<?php
|
||||
|
||||
namespace Kirby\Query;
|
||||
|
||||
use Kirby\Toolkit\A;
|
||||
use Kirby\Toolkit\Collection;
|
||||
|
||||
/**
|
||||
* The Segments class helps splitting a
|
||||
* query string into processable segments
|
||||
*
|
||||
* @package Kirby Query
|
||||
* @author Nico Hoffmann <nico@getkirby.com>
|
||||
* @link https://getkirby.com
|
||||
* @copyright Bastian Allgeier
|
||||
* @license https://opensource.org/licenses/MIT
|
||||
*/
|
||||
class Segments extends Collection
|
||||
{
|
||||
public function __construct(
|
||||
array $data = [],
|
||||
protected Query|null $parent = null,
|
||||
) {
|
||||
parent::__construct($data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Split query string into segments by dot
|
||||
* but not inside (nested) parens
|
||||
*/
|
||||
public static function factory(string $query, Query $parent = null): static
|
||||
{
|
||||
$segments = static::parse($query);
|
||||
$position = 0;
|
||||
|
||||
$segments = A::map(
|
||||
$segments,
|
||||
function ($segment) use (&$position) {
|
||||
// leave connectors as they are
|
||||
if (in_array($segment, ['.', '?.']) === true) {
|
||||
return $segment;
|
||||
}
|
||||
|
||||
// turn all other parts into Segment objects
|
||||
// and pass their position in the chain (ignoring connectors)
|
||||
$position++;
|
||||
return Segment::factory($segment, $position - 1);
|
||||
}
|
||||
);
|
||||
|
||||
return new static($segments, $parent);
|
||||
}
|
||||
|
||||
/**
|
||||
* Splits the string of a segment chaing into an
|
||||
* array of segments as well as conenctors (`.` or `?.`)
|
||||
* @internal
|
||||
*/
|
||||
public static function parse(string $string): array
|
||||
{
|
||||
return preg_split(
|
||||
'/(\??\.)|(\(([^()]+|(?2))*+\))(*SKIP)(*FAIL)/',
|
||||
trim($string),
|
||||
flags: PREG_SPLIT_DELIM_CAPTURE|PREG_SPLIT_NO_EMPTY
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves the segments chain by looping through
|
||||
* each segment call to be applied to the value of
|
||||
* all previous segment calls, returning gracefully at
|
||||
* `?.` when current value is `null`
|
||||
*/
|
||||
public function resolve(array|object $data = [])
|
||||
{
|
||||
$value = null;
|
||||
|
||||
foreach ($this->data as $segment) {
|
||||
// optional chaining: stop if current value is null
|
||||
if ($segment === '?.' && $value === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// for regular connectors, just skip
|
||||
if ($segment === '.') {
|
||||
continue;
|
||||
}
|
||||
|
||||
// offer possibility to intercept on objects
|
||||
if ($value !== null) {
|
||||
$value = $this->parent?->intercept($value) ?? $value;
|
||||
}
|
||||
|
||||
$value = $segment->resolve($value, $data);
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue