diff --git a/plugins/twig/TwigExtension.php b/plugins/twig/TwigExtension.php index b313f6d1..57576999 100644 --- a/plugins/twig/TwigExtension.php +++ b/plugins/twig/TwigExtension.php @@ -18,7 +18,6 @@ use herbie\PageTreeTextRenderer; use herbie\Pagination; use herbie\QueryBuilder; -use herbie\Selector; use herbie\Site; use herbie\Translator; use herbie\UrlManager; diff --git a/system/PageList.php b/system/PageList.php index 16cef103..f373ca33 100644 --- a/system/PageList.php +++ b/system/PageList.php @@ -76,6 +76,11 @@ public function find(string $value, string $key): ?Page return null; } + public function query(): QueryBuilder + { + return (new QueryBuilder())->from($this); + } + /** * Run a filter over each of the items. * diff --git a/system/QueryBuilder.php b/system/QueryBuilder.php index 49392bfe..736aafd6 100644 --- a/system/QueryBuilder.php +++ b/system/QueryBuilder.php @@ -16,6 +16,7 @@ final class QueryBuilder implements IteratorAggregate { private const WHERE_CLAUSE_OPERATORS = ['AND', 'OR']; + // ordered by key string length private const OPERATORS = [ "!=" => 'matchNotEqual', ">=" => 'matchGreaterThanEqual', @@ -24,10 +25,10 @@ final class QueryBuilder implements IteratorAggregate "^=" => 'matchStarts', "~=" => 'matchContainsWords', "$=" => 'matchEnds', - "&" => 'matchBitwiseAnd', + "=" => 'matchEqual', ">" => 'matchGreaterThan', "<" => 'matchLessThan', - "=" => 'matchEqual', + "&" => 'matchBitwiseAnd', ]; private array $where; private int $limit; @@ -91,12 +92,45 @@ private function parseCondition(string $condition): array $position = stripos($condition, $syntax); if ($position !== false) { $syntaxLength = strlen($syntax); - return [$name, substr($condition, 0, $position), substr($condition, $position + $syntaxLength)]; + $value1 = substr($condition, 0, $position); + $value2 = substr($condition, $position + $syntaxLength); + if (str_contains($value1, '|')) { + $values1 = str_explode_filtered($value1, '|'); + $conditions = ['OR']; + foreach ($values1 as $value1) { + $conditions[] = [$name, $value1, $value2]; + } + return $conditions; + } + if (str_contains($value2, '|')) { + $values2 = str_explode_filtered($value2, '|'); + $conditions = ['OR']; + foreach ($values2 as $value2) { + $conditions[] = [$name, $value1, $value2]; + } + return $conditions; + } + return [$name, $value1, $value2]; } } throw new \InvalidArgumentException('Unsupported operator'); } + private function convertType(mixed $value1, mixed $value2): mixed + { + if (is_bool($value1) && is_string($value2)) { + $lowered = strtolower($value2); + if ($lowered === 'true') { + return true; + } + if ($lowered === 'false') { + return false; + } + return $value2; + } + return $value2; + } + private function parseConditionsInOperatorFormat(array $conditions): array { if (!isset($conditions[0]) || !in_array(strtoupper($conditions[0]), self::WHERE_CLAUSE_OPERATORS)) { @@ -119,7 +153,8 @@ private function parseConditionsInHashFormat(array $conditions): array $items = []; foreach ($conditions as $key => $value) { if (is_scalar($value)) { - $items[] = ['match' . ucfirst(gettype($value)), $key, $value]; + $type = \herbie\gettype($value); + $items[] = ['match' . ucfirst($type), $key, $value]; } } return array_merge(['AND'], $items); @@ -149,13 +184,11 @@ public function all(): iterable return $this->processed; } - /** - * @return null|mixed - */ - public function one() + public function one(): array|object|null { + $this->limit = 1; $this->processData(); - $item = reset($this->data); + $item = reset($this->processed); if ($item === false) { return null; } @@ -202,13 +235,24 @@ private function processItem(ArrayAccess|array $item, array $conditions): bool if (isset($condition[0]) && in_array(strtoupper($condition[0]), self::WHERE_CLAUSE_OPERATORS)) { $status[] = $this->processItem($item, $condition); } else { - [$operator, $field, $value] = $condition; + [$operator, $field, $value2] = $condition; if (!isset($item[$field])) { $status[] = false; } else { /** @var callable $callable */ $callable = [$this, $operator]; - $status[] = call_user_func_array($callable, [$item[$field], $value]); + $value1 = $item[$field]; + if (is_array($value1)) { + $arrStatus = []; + foreach ($value1 as $arrValue1) { + $value2 = $this->convertType($arrValue1, $value2); + $arrStatus[] = call_user_func_array($callable, [$arrValue1, $value2]); + } + $status[] = in_array(true, $arrStatus); + } else { + $value2 = $this->convertType($value1, $value2); + $status[] = call_user_func_array($callable, [$value1, $value2]); + } } } } @@ -260,7 +304,7 @@ private function sort(): bool protected function matchString(string $value1, string $value2): bool { - return $this->matchEqual($value1, $value2); + return $value1 === $value2; } protected function matchBoolean(bool $value1, bool $value2): bool @@ -278,39 +322,39 @@ protected function matchFloat(float $value1, float $value2): bool return $value1 === $value2; } - protected function matchEqual(string $value1, string $value2): bool + protected function matchEqual(string|float|int|bool $value1, string|float|int|bool $value2): bool { return $value1 === $value2; } - protected function matchNotEqual(string $value1, string $value2): bool + protected function matchNotEqual(string|float|int|bool $value1, string|float|int|bool $value2): bool { return $value1 !== $value2; } - protected function matchGreaterThan(string $value1, string $value2): bool + protected function matchGreaterThan(string|float|int|bool $value1, string|float|int|bool $value2): bool { return $value1 > $value2; } - protected function matchLessThan(string $value1, string $value2): bool + protected function matchLessThan(string|float|int|bool $value1, string|float|int|bool $value2): bool { return $value1 < $value2; } - protected function matchGreaterThanEqual(string $value1, string $value2): bool + protected function matchGreaterThanEqual(string|float|int|bool $value1, string|float|int|bool $value2): bool { return $value1 >= $value2; } - protected function matchLessThanEqual(string $value1, string $value2): bool + protected function matchLessThanEqual(string|float|int|bool $value1, string|float|int|bool $value2): bool { return $value1 <= $value2; } - protected function matchBitwiseAnd(string $value1, string $value2): bool + protected function matchBitwiseAnd(int $value1, int $value2): bool { - return ((int)$value1 & (int)$value2) > 0; + return ($value1 & $value2) > 0; } protected function matchContains(string $value1, string $value2): bool diff --git a/system/Selector.php b/system/Selector.php deleted file mode 100644 index 0359b738..00000000 --- a/system/Selector.php +++ /dev/null @@ -1,283 +0,0 @@ - 'matchNotEqual', - ">=" => 'matchGreaterThanEqual', - "<=" => 'matchLessThanEqual', - "*=" => 'matchContains', - "^=" => 'matchStarts', - "~=" => 'matchContainsWords', - "$=" => 'matchEnds', - "&" => 'matchBitwiseAnd', - ">" => 'matchGreaterThan', - "<" => 'matchLessThan', - "=" => 'matchEqual', - ]; - - protected array $selectors = []; - - /** - * Find and return all items matching the given selector string. - * - * = Equal to - * != Not equal to - * < Less than - * > Greater than - * <= Less than or equal to - * >= Greater than or equal to - * *= Contains the exact word or phrase - * ~= Contains all the words - * ^= Contains the exact word or phrase at the beginning of the field - * $= Contains the exact word or phrase at the end of the field - * & Bitwise and - * - * @param array|string $selector - * @return mixed - * @throws \Exception - */ - public function find($selector, array $data) - { - $selectors = $this->getSelector($selector); - $sort = $this->extractSort($selectors); - $limit = $this->extractLimit($selectors); - - unset($selector); - - if (!empty($sort)) { - $this->sort($sort, $data); - } - - if (empty($selectors)) { - return $data; - } - - $return = []; - $i = 1; - foreach ($data as $item) { - if (($limit > 0) && ($i > $limit)) { - break; - } - - $bool = true; - foreach ($selectors as $selector) { - [$field, $value, $function] = $selector; - if (!isset($item[$field])) { - $bool = false; - break; - } - /** @var callable $callable */ - $callable = [$this, $function]; - $bool &= call_user_func_array($callable, [$item[$field], $value]); - } - - if ($bool) { - $return[] = $item; - $i++; - } - } - - return $return; - } - - protected function extractSort(array &$selectors): string - { - $sort = ""; - foreach ($selectors as $index => $selector) { - if ($selector[0] === "sort") { - $sort = $selector[1]; - unset($selectors[$index]); - break; - } - } - return $sort; - } - - protected function extractLimit(array &$selectors): int - { - $limit = 0; - foreach ($selectors as $index => $selector) { - if ($selector[0] === "limit") { - $limit = abs((int)$selector[1]); - unset($selectors[$index]); - break; - } - } - return $limit; - } - - /** - * @param string $selector - * @param array &$data - * @return mixed - * @throws \Exception - */ - public function get($selector, &$data) - { - return $this->find($selector, $data)->first(); - } - - protected function matchEqual(string $value1, string $value2): bool - { - return $value1 === $value2; - } - - protected function matchNotEqual(string $value1, string $value2): bool - { - return $value1 !== $value2; - } - - protected function matchGreaterThan(string $value1, string $value2): bool - { - return $value1 > $value2; - } - - protected function matchLessThan(string $value1, string $value2): bool - { - return $value1 < $value2; - } - - protected function matchGreaterThanEqual(string $value1, string $value2): bool - { - return $value1 >= $value2; - } - - protected function matchLessThanEqual(string $value1, string $value2): bool - { - return $value1 <= $value2; - } - - protected function matchBitwiseAnd(string $value1, string $value2): bool - { - return ((int)$value1 & (int)$value2) > 0; - } - - protected function matchContains(string $value1, string $value2): bool - { - return stripos($value1, $value2) !== false; - } - - protected function matchContainsWords(string $value1, string $value2): bool - { - $words = preg_split('/[-\s]/', $value2, -1, PREG_SPLIT_NO_EMPTY); - if (!is_array($words) || count($words) === 0) { - return false; - } - foreach ($words as $word) { - if (!preg_match('/\b' . preg_quote($word) . '\b/i', $value1)) { - return false; - } - } - return true; - } - - protected function matchStarts(string $value1, string $value2): bool - { - return stripos(trim($value1), $value2) === 0; - } - - protected function matchEnds(string $value1, string $value2): bool - { - $value2 = trim($value2); - $value1 = substr($value1, -1 * strlen($value2)); - return strcasecmp($value1, $value2) === 0; - } - - /** - * @param string|array $selector - * @return array - */ - protected function getSelector($selector): array - { - if (is_array($selector)) { - $selectors = $selector; - } elseif (is_string($selector)) { - $selectors = [trim($selector)]; - } else { - throw new \InvalidArgumentException("Selector has to be a string or an array."); - } - unset($selector); - - $return = []; - foreach ($selectors as $selector) { - foreach ($this->operators as $op => $methodName) { - $pos = stripos($selector, $op); - if ($pos !== false) { - $return[] = [ - substr($selector, 0, $pos), - substr($selector, $pos + strlen($op)), - $methodName - ]; - break; - } - } - } - - return $return; - } - - /** - * @param callable|string $sort - */ - public function sort($sort, array &$items): bool - { - if (is_numeric($sort)) { - return false; - } - - if (is_callable($sort)) { - $bool = uasort($items, $sort); - return $bool; - } - - $field = "title"; - if (!empty($sort)) { - $field = trim($sort, "+"); - } - - $direction = "asc"; - if (substr($field, 0, 1) === "-") { - $field = substr($field, 1); - $direction = "desc"; - } - - return uasort($items, function ($value1, $value2) use ($field, $direction) { - if (!isset($value1[$field]) || !isset($value2[$field])) { - return 0; - } - if ($value1[$field] === $value2[$field]) { - return 0; - } - if ($direction === 'asc') { - return ($value1[$field] < $value2[$field]) ? -1 : 1; - } else { - return ($value2[$field] < $value1[$field]) ? -1 : 1; - } - }); - } - - /** - * @param array|string $selector1 - * @param array|string $selector2 - */ - public static function mergeSelectors($selector1, $selector2): array - { - $selectors = []; - if (is_array($selector1)) { - $selectors = $selector1; - } else { - $selectors[] = $selector1; - } - if (is_array($selector2)) { - $selectors = array_merge($selectors, $selector2); - } else { - $selectors[] = $selector2; - } - return array_filter($selectors); // filter empty - } -} diff --git a/system/SystemInfoPlugin.php b/system/SystemInfoPlugin.php index 0b14dfb6..0ec62f03 100644 --- a/system/SystemInfoPlugin.php +++ b/system/SystemInfoPlugin.php @@ -97,7 +97,7 @@ private function getConfig(): array foreach ($this->config->flatten() as $key => $value) { $configs[] = [ $key, - gettype($value), + \herbie\gettype($value), $this->filterValue($value) ]; } @@ -187,13 +187,13 @@ private function getTwigGlobalsFromContext(array $context): array foreach ($context as $string => $mixed) { if (is_scalar($mixed)) { $value = $mixed; - $type = gettype($mixed); + $type = \herbie\gettype($mixed); } elseif (is_object($mixed)) { $value = get_class($mixed); $type = 'class'; } else { $value = json_encode($mixed); - $type = gettype($mixed); + $type = \herbie\gettype($mixed); } $globals[] = [$string, $value, $type]; } diff --git a/system/Translator.php b/system/Translator.php index 55fa57ac..17dd8460 100644 --- a/system/Translator.php +++ b/system/Translator.php @@ -96,7 +96,7 @@ public function addPath(string $category, $path): void if (is_string($path)) { $path = [$path]; } elseif (!is_array($path)) { - $message = sprintf('Argument $path has to be an array or a string, %s given.', gettype($path)); + $message = sprintf('Argument $path has to be an array or a string, %s given.', \herbie\gettype($path)); throw new \InvalidArgumentException($message); } $this->paths[$category] = array_merge($this->paths[$category], $path); diff --git a/system/functions.php b/system/functions.php index 67fb8169..c118a559 100644 --- a/system/functions.php +++ b/system/functions.php @@ -475,3 +475,14 @@ function array_is_assoc(array $array): bool $keys = array_keys($array); return array_keys($keys) !== $keys; } + +function gettype(mixed $value): string +{ + $type = \gettype($value); + // for historical reasons "double" is returned in case of a float, and not simply "float" + // see https://www.php.net/manual/en/function.gettype + if ($type === 'double') { + $type = 'float'; + } + return $type; +}