Upgrade to 3.8.3
This commit is contained in:
@@ -15,7 +15,7 @@ use Kirby\Toolkit\Str;
|
||||
* @copyright Bastian Allgeier
|
||||
* @license https://opensource.org/licenses/MIT
|
||||
*/
|
||||
final class Argument
|
||||
class Argument
|
||||
{
|
||||
public function __construct(
|
||||
public mixed $value
|
||||
@@ -23,13 +23,21 @@ final class Argument
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitizes argument string into
|
||||
* 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 (
|
||||
(
|
||||
|
@@ -15,25 +15,39 @@ use Kirby\Toolkit\Collection;
|
||||
* @copyright Bastian Allgeier
|
||||
* @license https://opensource.org/licenses/MIT
|
||||
*/
|
||||
final class Arguments extends Collection
|
||||
class Arguments extends Collection
|
||||
{
|
||||
public const NO_PNTH = '\([^(]+\)(*SKIP)(*FAIL)';
|
||||
// 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::NO_PNTH . '|' . self::NO_SQBR . '|' .
|
||||
self::NO_DLQU . '|' . self::NO_SLQU . '!', $arguments),
|
||||
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(
|
||||
|
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;
|
||||
}
|
||||
}
|
@@ -12,9 +12,16 @@ 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.
|
||||
* 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>,
|
||||
@@ -90,7 +97,7 @@ class Query
|
||||
}
|
||||
|
||||
// loop through all segments to resolve query
|
||||
return Segments::factory($this->query, $this)->resolve($data);
|
||||
return Expression::factory($this->query, $this)->resolve($data);
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -28,6 +28,7 @@ class Segment
|
||||
|
||||
/**
|
||||
* 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
|
||||
@@ -45,7 +46,7 @@ class Segment
|
||||
|
||||
$nonExisting = in_array($type, ['array', 'object']) ? 'non-existing ' : '';
|
||||
|
||||
$error = 'Access to ' . $nonExisting . $label . ' ' . $name . ' on ' . $type;
|
||||
$error = 'Access to ' . $nonExisting . $label . ' "' . $name . '" on ' . $type;
|
||||
|
||||
throw new BadMethodCallException($error);
|
||||
}
|
||||
@@ -110,7 +111,7 @@ class Segment
|
||||
}
|
||||
|
||||
if ($args !== []) {
|
||||
throw new InvalidArgumentException('Cannot access array element ' . $this->method . ' with arguments');
|
||||
throw new InvalidArgumentException('Cannot access array element "' . $this->method . '" with arguments');
|
||||
}
|
||||
|
||||
return $value;
|
||||
|
@@ -15,7 +15,7 @@ use Kirby\Toolkit\Collection;
|
||||
* @copyright Bastian Allgeier
|
||||
* @license https://opensource.org/licenses/MIT
|
||||
*/
|
||||
final class Segments extends Collection
|
||||
class Segments extends Collection
|
||||
{
|
||||
public function __construct(
|
||||
array $data = [],
|
||||
@@ -30,26 +30,62 @@ final class Segments extends Collection
|
||||
*/
|
||||
public static function factory(string $query, Query $parent = null): static
|
||||
{
|
||||
$segments = preg_split(
|
||||
'!\.|(\(([^()]+|(?1))*+\))(*SKIP)(*FAIL)!',
|
||||
trim($query),
|
||||
-1,
|
||||
PREG_SPLIT_NO_EMPTY
|
||||
);
|
||||
$segments = static::parse($query);
|
||||
$position = 0;
|
||||
|
||||
$segments = A::map(
|
||||
array_keys($segments),
|
||||
fn ($index) => Segment::factory($segments[$index], $index)
|
||||
$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;
|
||||
|
Reference in New Issue
Block a user