designtopack/public/kirby/src/Query/Expression.php

120 lines
2.8 KiB
PHP
Raw Normal View History

2024-07-10 16:10:33 +02:00
<?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
) {
}
/**
* Parses an expression string into its 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;
}
}