Upgrade to 3.8.2
This commit is contained in:
100
kirby/src/Query/Argument.php
Normal file
100
kirby/src/Query/Argument.php
Normal file
@@ -0,0 +1,100 @@
|
||||
<?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
|
||||
*/
|
||||
final class Argument
|
||||
{
|
||||
public function __construct(
|
||||
public mixed $value
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitizes argument string into
|
||||
* PHP type/object as new Argument instance
|
||||
*/
|
||||
public static function factory(string $argument): static
|
||||
{
|
||||
$argument = trim($argument);
|
||||
|
||||
// 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;
|
||||
}
|
||||
}
|
44
kirby/src/Query/Arguments.php
Normal file
44
kirby/src/Query/Arguments.php
Normal file
@@ -0,0 +1,44 @@
|
||||
<?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
|
||||
*/
|
||||
final class Arguments extends Collection
|
||||
{
|
||||
public const NO_PNTH = '\([^(]+\)(*SKIP)(*FAIL)';
|
||||
public const NO_SQBR = '\[[^]]+\](*SKIP)(*FAIL)';
|
||||
public const NO_DLQU = '\"(?:[^"\\\\]|\\\\.)*\"(*SKIP)(*FAIL)';
|
||||
public const NO_SLQU = '\'(?:[^\'\\\\]|\\\\.)*\'(*SKIP)(*FAIL)';
|
||||
|
||||
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),
|
||||
fn ($argument) => Argument::factory($argument)
|
||||
);
|
||||
|
||||
return new static($arguments);
|
||||
}
|
||||
|
||||
public function resolve(array|object $data = []): array
|
||||
{
|
||||
return A::map(
|
||||
$this->data,
|
||||
fn ($argument) => $argument->resolve($data)
|
||||
);
|
||||
}
|
||||
}
|
131
kirby/src/Query/Query.php
Normal file
131
kirby/src/Query/Query.php
Normal file
@@ -0,0 +1,131 @@
|
||||
<?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.
|
||||
*
|
||||
* @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 Segments::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);
|
||||
};
|
144
kirby/src/Query/Segment.php
Normal file
144
kirby/src/Query/Segment.php
Normal file
@@ -0,0 +1,144 @@
|
||||
<?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
|
||||
*
|
||||
* @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);
|
||||
}
|
||||
}
|
63
kirby/src/Query/Segments.php
Normal file
63
kirby/src/Query/Segments.php
Normal file
@@ -0,0 +1,63 @@
|
||||
<?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
|
||||
*/
|
||||
final 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 = preg_split(
|
||||
'!\.|(\(([^()]+|(?1))*+\))(*SKIP)(*FAIL)!',
|
||||
trim($query),
|
||||
-1,
|
||||
PREG_SPLIT_NO_EMPTY
|
||||
);
|
||||
|
||||
$segments = A::map(
|
||||
array_keys($segments),
|
||||
fn ($index) => Segment::factory($segments[$index], $index)
|
||||
);
|
||||
|
||||
return new static($segments, $parent);
|
||||
}
|
||||
|
||||
public function resolve(array|object $data = [])
|
||||
{
|
||||
$value = null;
|
||||
|
||||
foreach ($this->data as $segment) {
|
||||
// offer possibility to intercept on objects
|
||||
if ($value !== null) {
|
||||
$value = $this->parent?->intercept($value) ?? $value;
|
||||
}
|
||||
|
||||
$value = $segment->resolve($value, $data);
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user