Upgrade to 3.8.3

This commit is contained in:
Bastian Allgeier
2022-12-06 15:37:13 +01:00
parent f9e812cb0c
commit 8381ccb96c
69 changed files with 752 additions and 966 deletions

View File

@@ -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 (
(

View File

@@ -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(

View 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;
}
}

View File

@@ -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);
}
}

View File

@@ -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;

View File

@@ -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;