Initial commit

This commit is contained in:
isUnknown 2024-07-10 16:10:33 +02:00
commit 08a8a71c55
631 changed files with 139902 additions and 0 deletions

View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2022 Bastian Allgeier
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

987
public/site/plugins/kql/README.md Executable file
View file

@ -0,0 +1,987 @@
# Kirby QL
Kirby's Query Language API combines the flexibility of Kirby's data structures, the power of GraphQL and the simplicity of REST.
The Kirby QL API takes POST requests with standard JSON objects and returns highly customized results that fit your application.
## Playground
You can play in our [KQL sandbox](https://kql.getkirby.com). The sandbox is based on the Kirby starterkit.
> Source code of the playground is [available on GitHub](https://github.com/getkirby/kql.getkirby.com).
## Example
Given a POST request to: `/api/query`
```json
{
"query": "page('photography').children",
"select": {
"url": true,
"title": true,
"text": "page.text.markdown",
"images": {
"query": "page.images",
"select": {
"url": true
}
}
},
"pagination": {
"limit": 10
}
}
```
<details open>
<summary>🆗 Response</summary>
```json
{
"code": 200,
"result": {
"data": [
{
"url": "https://example.com/photography/trees",
"title": "Trees",
"text": "Lorem <strong>ipsum</strong> …",
"images": [
{
"url": "https://example.com/media/pages/photography/trees/1353177920-1579007734/cheesy-autumn.jpg"
},
{
"url": "https://example.com/media/pages/photography/trees/1940579124-1579007734/last-tree-standing.jpg"
},
{
"url": "https://example.com/media/pages/photography/trees/3506294441-1579007734/monster-trees-in-the-fog.jpg"
}
]
},
{
"url": "https://example.com/photography/sky",
"title": "Sky",
"text": "<h1>Dolor sit amet</h1> …",
"images": [
{
"url": "https://example.com/media/pages/photography/sky/183363500-1579007734/blood-moon.jpg"
},
{
"url": "https://example.com/media/pages/photography/sky/3904851178-1579007734/coconut-milkyway.jpg"
}
]
}
],
"pagination": {
"page": 1,
"pages": 1,
"offset": 0,
"limit": 10,
"total": 2
}
},
"status": "ok"
}
```
</details>
## Installation
### Manual
[Download](https://github.com/getkirby/kql/releases) and copy this repository to `/site/plugins/kql` of your Kirby installation.
### Composer
```bash
composer require getkirby/kql
```
## Documentation
### API Endpoint
KQL adds a new `query` API endpoint to your Kirby API (i.e. `yoursite.com/api/query`). This endpoint [requires authentication](https://getkirby.com/docs/guide/api/authentication).
You can switch off authentication in your config at your own risk:
```php
return [
'kql' => [
'auth' => false
]
];
```
### Sending POST Requests
You can use any HTTP request library in your language of choice to make regular POST requests to your `/api/query` endpoint. In this example, we are using [the `fetch` API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API) and JavaScript to retrieve data from our Kirby installation.
```js
const api = "https://yoursite.com/api/query";
const username = "apiuser";
const password = "strong-secret-api-password";
const headers = {
Authorization: "Basic " + Buffer.from(`${username}:${password}`).toString("base64"),
"Content-Type": "application/json",
Accept: "application/json",
};
const response = await fetch(api, {
method: "post",
body: JSON.stringify({
query: "page('notes').children",
select: {
title: true,
text: "page.text.kirbytext",
slug: true,
date: "page.date.toDate('d.m.Y')",
},
}),
headers,
});
console.log(await response.json());
```
### `query`
With the query, you can fetch data from anywhere in your Kirby site. You can query fields, pages, files, users, languages, roles and more.
#### Queries Without Selects
When you don't pass the select option, Kirby will try to come up with the most useful result set for you. This is great for simple queries.
##### Fetching the Site Title
```js
const response = await fetch(api, {
method: "post",
body: JSON.stringify({
query: "site.title",
}),
headers,
});
console.log(await response.json());
```
<details>
<summary>🆗 Response</summary>
```js
{
code: 200,
result: "Kirby Starterkit",
status: "ok"
}
```
</details>
##### Fetching a List of Page IDs
```js
const response = await fetch(api, {
method: "post",
body: JSON.stringify({
query: "site.children",
}),
headers,
});
console.log(await response.json());
```
<details>
<summary>🆗 Response</summary>
```js
{
code: 200,
result: [
"photography",
"notes",
"about",
"error",
"home"
],
status: "ok"
}
```
</details>
#### Running Field Methods
Queries can even execute field methods.
```js
const response = await fetch(api, {
method: "post",
body: JSON.stringify({
query: "site.title.upper",
}),
headers,
});
console.log(await response.json());
```
<details>
<summary>🆗 Response</summary>
```js
{
code: 200,
result: "KIRBY STARTERKIT",
status: "ok"
}
```
</details>
### `select`
KQL becomes really powerful by its flexible way to control the result set with the select option.
#### Select Single Properties and Fields
To include a property or field in your results, list them as an array. Check out our [reference for available properties](https://getkirby.com/docs/reference) for pages, users, files, etc.
```js
const response = await fetch(api, {
method: "post",
body: JSON.stringify({
query: "site.children",
select: ["title", "url"],
}),
headers,
});
console.log(await response.json());
```
<details>
<summary>🆗 Response</summary>
```js
{
code: 200,
result: {
data: [
{
title: "Photography",
url: "/photography"
},
{
title: "Notes",
url: "/notes"
},
{
title: "About us",
url: "/about"
},
{
title: "Error",
url: "/error"
},
{
title: "Home",
url: "/"
}
],
pagination: {
page: 1,
pages: 1,
offset: 0,
limit: 100,
total: 5
}
},
status: "ok"
}
```
</details>
You can also use the object notation and pass true for each key/property you want to include.
```js
const response = await fetch(api, {
method: "post",
body: JSON.stringify({
query: "site.children",
select: {
title: true,
url: true,
},
}),
headers,
});
console.log(await response.json());
```
<details>
<summary>🆗 Response</summary>
```js
{
code: 200,
result: {
data: [
{
title: "Photography",
url: "/photography"
},
{
title: "Notes",
url: "/notes"
},
{
title: "About us",
url: "/about"
},
{
title: "Error",
url: "/error"
},
{
title: "Home",
url: "/"
}
],
pagination: { ... }
},
status: "ok"
}
```
</details>
#### Using Queries for Properties and Fields
Instead of passing true, you can also pass a string query to specify what you want to return for each key in your select object.
```js
const response = await fetch(api, {
method: "post",
body: JSON.stringify({
query: "site.children",
select: {
title: "page.title",
},
}),
headers,
});
console.log(await response.json());
```
<details>
<summary>🆗 Response</summary>
```js
{
code: 200,
result: {
data: [
{
title: "Photography",
},
{
title: "Notes",
},
...
],
pagination: { ... }
},
status: "ok"
}
```
</details>
#### Executing Field Methods
```js
const response = await fetch(api, {
method: "post",
body: JSON.stringify({
query: "site.children",
select: {
title: "page.title.upper",
},
}),
headers,
});
console.log(await response.json());
```
<details>
<summary>🆗 Response</summary>
```js
{
code: 200,
result: {
data: [
{
title: "PHOTOGRAPHY",
},
{
title: "NOTES",
},
...
],
pagination: { ... }
},
status: "ok"
}
```
</details>
#### Creating Aliases
String queries are a perfect way to create aliases or return variations of the same field or property multiple times.
```js
const response = await fetch(api, {
method: "post",
body: JSON.stringify({
query: "page('notes').children",
select: {
title: "page.title",
upperCaseTitle: "page.title.upper",
lowerCaseTitle: "page.title.lower",
guid: "page.id",
date: "page.date.toDate('d.m.Y')",
timestamp: "page.date.toTimestamp",
},
}),
headers,
});
console.log(await response.json());
```
<details>
<summary>🆗 Response</summary>
```js
{
code: 200,
result: {
data: [
{
title: "Explore the universe",
upperCaseTitle: "EXPLORE THE UNIVERSE",
lowerCaseTitle: "explore the universe",
guid: "notes/explore-the-universe",
date: "21.04.2018",
timestamp: 1524316200
},
{ ... },
{ ... },
...
],
pagination: { ... }
},
status: "ok"
}
```
</details>
#### Subqueries
With such string queries you can of course also include nested data
```js
const response = await fetch(api, {
method: "post",
body: JSON.stringify({
query: "page('photography').children",
select: {
title: "page.title",
images: "page.images",
},
}),
headers,
});
console.log(await response.json());
```
<details>
<summary>🆗 Response</summary>
```js
{
code: 200,
result: {
data: [
{
title: "Trees",
images: [
"photography/trees/cheesy-autumn.jpg",
"photography/trees/last-tree-standing.jpg",
"photography/trees/monster-trees-in-the-fog.jpg",
"photography/trees/sharewood-forest.jpg",
"photography/trees/stay-in-the-car.jpg"
]
},
{ ... },
{ ... },
...
],
pagination: { ... }
},
status: "ok"
}
```
</details>
#### Subqueries With Selects
You can also pass an object with a `query` and a `select` option
```js
const response = await fetch(api, {
method: "post",
body: JSON.stringify({
query: "page('photography').children",
select: {
title: "page.title",
images: {
query: "page.images",
select: {
filename: true,
},
},
},
}),
headers,
});
console.log(await response.json());
```
<details>
<summary>🆗 Response</summary>
```js
{
code: 200,
result: {
data: [
{
title: "Trees",
images: {
{
filename: "cheesy-autumn.jpg"
},
{
filename: "last-tree-standing.jpg"
},
{
filename: "monster-trees-in-the-fog.jpg"
},
{
filename: "sharewood-forest.jpg"
},
{
filename: "stay-in-the-car.jpg"
}
}
},
{ ... },
{ ... },
...
],
pagination: { ... }
},
status: "ok"
}
```
</details>
### Pagination
Whenever you query a collection (pages, files, users, roles, languages) you can limit the resultset and also paginate through entries. You've probably already seen the pagination object in the results above. It is included in all results for collections, even if you didn't specify any pagination settings.
#### `limit`
You can specify a custom limit with the limit option. The default limit for collections is 100 entries.
```js
const response = await fetch(api, {
method: "post",
body: JSON.stringify({
query: "page('notes').children",
pagination: {
limit: 5,
},
select: {
title: "page.title",
},
}),
headers,
});
console.log(await response.json());
```
<details>
<summary>🆗 Response</summary>
```js
{
code: 200,
result: {
data: [
{
title: "Across the ocean"
},
{
title: "A night in the forest"
},
{
title: "In the jungle of Sumatra"
},
{
title: "Through the desert"
},
{
title: "Himalaya and back"
}
],
pagination: {
page: 1,
pages: 2,
offset: 0,
limit: 5,
total: 7
}
},
status: "ok"
}
```
</details>
#### `page`
You can jump to any page in the resultset with the `page` option.
```js
const response = await fetch(api, {
method: "post",
body: JSON.stringify({
query: "page('notes').children",
pagination: {
page: 2,
limit: 5,
},
select: {
title: "page.title",
},
}),
headers,
});
console.log(await response.json());
```
<details>
<summary>🆗 Response</summary>
```js
{
code: 200,
result: {
data: [
{
title: "Chasing waterfalls"
},
{
title: "Exploring the universe"
}
],
pagination: {
page: 2,
pages: 2,
offset: 5,
limit: 5,
total: 7
}
},
status: "ok"
}
```
</details>
### Pagination in Subqueries
Pagination settings also work for subqueries.
```js
const response = await fetch(api, {
method: "post",
body: JSON.stringify({
query: "page('photography').children",
select: {
title: "page.title",
images: {
query: "page.images",
pagination: {
page: 2,
limit: 5,
},
select: {
filename: true,
},
},
},
}),
headers,
});
console.log(await response.json());
```
### Multiple Queries in a Single Call
With the power of selects and subqueries you can basically query the entire site in a single request
```js
const response = await fetch(api, {
method: "post",
body: JSON.stringify({
query: "site",
select: {
title: "site.title",
url: "site.url",
notes: {
query: "page('notes').children.listed",
select: {
title: true,
url: true,
date: "page.date.toDate('d.m.Y')",
text: "page.text.kirbytext",
},
},
photography: {
query: "page('photography').children.listed",
select: {
title: true,
images: {
query: "page.images",
select: {
url: true,
alt: true,
caption: "file.caption.kirbytext",
},
},
},
},
about: {
text: "page.text.kirbytext",
},
},
}),
headers,
});
console.log(await response.json());
```
### Allowing Methods
KQL is very strict with allowed methods by default. Custom page methods, file methods or model methods are not allowed to make sure you don't miss an important security issue by accident. You can allow additional methods though.
#### Allow List
The most straight forward way is to define allowed methods in your config.
```php
return [
'kql' => [
'methods' => [
'allowed' => [
'MyCustomPage::cover'
]
]
]
];
```
#### DocBlock Comment
You can also add a comment to your methods' doc blocks to allow them:
```php
class MyCustomPage extends Page
{
/**
* @kql-allowed
*/
public function cover()
{
return $this->images()->findBy('name', 'cover') ?? $this->image();
}
}
```
This works for model methods as well as for custom page methods, file methods or other methods defined in plugins.
```php
Kirby::plugin('your-name/your-plugin', [
'pageMethods' => [
/**
* @kql-allowed
*/
'cover' => function () {
return $this->images()->findBy('name', 'cover') ?? $this->image();
}
]
]);
```
### Blocking Methods
You can block individual class methods that would normally be accessible by listing them in your config:
```php
return [
'kql' => [
'methods' => [
'blocked' => [
'Kirby\Cms\Page::url'
]
]
]
];
```
### Blocking Classes
Sometimes you might want to reduce access to various parts of the system. This can be done by blocking individual methods (see above) or by blocking entire classes.
```php
return [
'kql' => [
'classes' => [
'blocked' => [
'Kirby\Cms\User'
]
]
]
];
```
Now, access to any user is blocked.
### Custom Classes and Interceptors
If you want to add support for a custom class or a class in Kirby's source that is not supported yet, you can list your own interceptors in your config
```php
return [
'kql' => [
'interceptors' => [
'Kirby\Cms\System' => 'SystemInterceptor'
]
]
];
```
You can put the class for such a custom interceptor in a plugin for example.
```php
class SystemInterceptor extends Kirby\Kql\Interceptors\Interceptor
{
public const CLASS_ALIAS = 'system';
protected $toArray = [
'isInstallable',
];
public function allowedMethods(): array
{
return [
'isInstallable',
];
}
}
```
Interceptor classes are pretty straight forward. With the `CLASS_ALIAS` you can give objects with that class a short name for KQL queries. The `$toArray` property lists all methods that should be rendered if you don't run a subquery. I.e. in this case `kirby.system` would render an array with the `isInstallable` value.
The `allowedMethods` method must return an array of all methods that can be access for this object. In addition to that you can also create your own custom methods in an interceptor that will then become available in KQL.
```php
class SystemInterceptor extends Kirby\Kql\Interceptors\Interceptor
{
...
public function isReady()
{
return 'yes it is!';
}
}
```
This custom method can now be used with `kirby.system.isReady` in KQL and will return `yes it is!`
### Unintercepted Classes
If you want to fully allow access to an entire class without putting an interceptor in between, you can add the class to the allow list in your config:
```php
return [
'kql' => [
'classes' => [
'allowed' => [
'Kirby\Cms\System'
]
]
]
];
```
This will introduce full access to all public class methods. This can be very risky though and you should avoid this if possible.
### No Mutations
KQL only offers access to data in your site. It does not support any mutations. All destructive methods are blocked and cannot be accessed in queries.
## Plugins
- [KQL + 11ty](https://github.com/getkirby/eleventykit)
- [KQL + Nuxt](https://nuxt-kql.jhnn.dev)
## What's Kirby?
- **[getkirby.com](https://getkirby.com)** Get to know the CMS.
- **[Try it](https://getkirby.com/try)** Take a test ride with our online demo. Or download one of our kits to get started.
- **[Documentation](https://getkirby.com/docs/guide)** Read the official guide, reference and cookbook recipes.
- **[Issues](https://github.com/getkirby/kirby/issues)** Report bugs and other problems.
- **[Feedback](https://feedback.getkirby.com)** You have an idea for Kirby? Share it.
- **[Forum](https://forum.getkirby.com)** Whenever you get stuck, don't hesitate to reach out for questions and support.
- **[Discord](https://chat.getkirby.com)** Hang out and meet the community.
- **[Mastodon](https://mastodon.social/@getkirby)** Spread the word.
- **[Instagram](https://www.instagram.com/getkirby/)** Share your creations: #madewithkirby.
---
## License
[MIT](./LICENSE) License © 2020-2023 [Bastian Allgeier](https://getkirby.com)

View file

@ -0,0 +1,73 @@
{
"name": "getkirby/kql",
"description": "Kirby Query Language",
"license": "MIT",
"type": "kirby-plugin",
"version": "2.1.0",
"keywords": [
"kirby",
"cms",
"api",
"json",
"query",
"headless"
],
"authors": [
{
"name": "Bastian Allgeier",
"email": "bastian@getkirby.com"
},
{
"name": "Nico Hoffmann",
"email": "nico@getkirby.com"
}
],
"homepage": "https://getkirby.com",
"support": {
"email": "support@getkirby.com",
"issues": "https://github.com/getkirby/kql/issues",
"forum": "https://forum.getkirby.com",
"source": "https://github.com/getkirby/kql"
},
"require": {
"php": ">=8.0.0 <8.3.0",
"getkirby/cms": ">=3.8.2",
"getkirby/composer-installer": "^1.2.1"
},
"autoload": {
"psr-4": {
"Kirby\\": [
"tests/"
]
}
},
"config": {
"allow-plugins": {
"getkirby/composer-installer": true
},
"optimize-autoloader": true
},
"extra": {
"installer-name": "kql",
"kirby-cms-path": false
},
"scripts": {
"analyze": [
"@analyze:composer",
"@analyze:psalm",
"@analyze:phpcpd",
"@analyze:phpmd"
],
"analyze:composer": "composer validate --strict --no-check-version --no-check-all",
"analyze:phpcpd": "phpcpd --fuzzy --exclude tests --exclude vendor .",
"analyze:phpmd": "phpmd . ansi phpmd.xml.dist --exclude 'dependencies/*,tests/*,vendor/*'",
"analyze:psalm": "psalm",
"ci": [
"@fix",
"@analyze",
"@test"
],
"fix": "php-cs-fixer fix",
"test": "phpunit --stderr --coverage-html=tests/coverage"
}
}

View file

@ -0,0 +1,8 @@
<?php
class_alias(\Kirby\Kql\Kql::class, 'Kql');
class_alias(\Kirby\Kql\Interceptor::class, 'Kirby\Kql\Interceptors\Interceptor');
// Provide backwards compatibility for Kirby 3 core classes
class_alias(\Kirby\Kql\Interceptors\Content\Content::class, 'Kirby\Kql\Interceptors\Cms\Content');
class_alias(\Kirby\Kql\Interceptors\Content\Field::class, 'Kirby\Kql\Interceptors\Cms\Field');

View file

@ -0,0 +1,25 @@
<?php
use Kirby\Kql\Kql;
return [
'routes' => function ($kirby) {
return [
[
'pattern' => 'query',
'method' => 'POST|GET',
'auth' => $kirby->option('kql.auth') === false ? false : true,
'action' => function () use ($kirby) {
$input = $kirby->request()->get();
$result = Kql::run($input);
return [
'code' => 200,
'result' => $result,
'status' => 'ok',
];
}
]
];
}
];

View file

@ -0,0 +1,21 @@
<?php
namespace Kirby\Kql;
use Kirby\Toolkit\Str;
function autoload(string $namespace, string $dir)
{
spl_autoload_register(function ($class) use ($namespace, $dir) {
if (str_contains($class, '.') === true || str_starts_with($class, $namespace) === false) {
return;
}
$path = Str::after($class, $namespace);
$path = $dir . '/' . str_replace('\\', '/', $path) . '.php';
if (is_file($path) === true) {
include $path;
}
});
}

View file

@ -0,0 +1,11 @@
<?php
use Kirby\Cms\Helpers;
use Kirby\Kql\Kql;
if (Helpers::hasOverride('kql') === false) {
function kql($input, $model = null)
{
return Kql::run($input, $model);
}
}

View file

@ -0,0 +1,16 @@
<?php
namespace Kirby\Kql;
use Kirby\Cms\App;
require_once __DIR__ . '/extensions/autoload.php';
autoload('Kirby\\', __DIR__ . '/src/');
require_once __DIR__ . '/extensions/aliases.php';
require_once __DIR__ . '/extensions/helpers.php';
App::plugin('getkirby/kql', [
'api' => require_once 'extensions/api.php'
]);

View file

@ -0,0 +1,152 @@
<?php
namespace Kirby\Kql;
use Kirby\Toolkit\A;
use ReflectionClass;
use ReflectionMethod;
/**
* Providing help information about
* queried objects, methods, arrays...
*
* @package Kirby KQL
* @author Bastian Allgeier <bastian@getkirby.com>
* @link https://getkirby.com
* @copyright Bastian Allgeier
* @license https://getkirby.com/license
*/
class Help
{
/**
* Provides information about passed value
* depending on its type
*/
public static function for($value): array
{
if (is_array($value) === true) {
return static::forArray($value);
}
if (is_object($value) === true) {
return static::forObject($value);
}
return [
'type' => gettype($value),
'value' => $value
];
}
/**
* @internal
*/
public static function forArray(array $array): array
{
return [
'type' => 'array',
'keys' => array_keys($array),
];
}
/**
* Gathers information for method about
* name, parameters, return type etc.
* @internal
*/
public static function forMethod(object $object, string $method): array
{
$reflection = new ReflectionMethod($object, $method);
$returns = $reflection->getReturnType()?->getName();
$params = [];
foreach ($reflection->getParameters() as $param) {
$name = $param->getName();
$required = $param->isOptional() === false;
$type = $param->hasType() ? $param->getType()->getName() : null;
$default = null;
if ($param->isDefaultValueAvailable()) {
$default = $param->getDefaultValue();
}
$call = '';
if ($type !== null) {
$call = $type . ' ';
}
$call .= '$' . $name;
if ($required === false && $default !== null) {
$call .= ' = ' . var_export($default, true);
}
$p['call'] = $call;
$params[$name] = compact('name', 'type', 'required', 'default', 'call');
}
$call = '.' . $method;
if (empty($params) === false) {
$call .= '(' . implode(', ', array_column($params, 'call')) . ')';
}
return [
'call' => $call,
'name' => $method,
'params' => $params,
'returns' => $returns
];
}
/**
* Gathers informations for each unique method
* @internal
*/
public static function forMethods(object $object, array $methods): array
{
$methods = array_unique($methods);
$reflection = [];
sort($methods);
foreach ($methods as $methodName) {
if (method_exists($object, $methodName) === false) {
continue;
}
$reflection[$methodName] = static::forMethod($object, $methodName);
}
return $reflection;
}
/**
* Retrieves info for objects either from Interceptor (to
* only list allowed methods) or via reflection
* @internal
*/
public static function forObject(object $object): array
{
// get interceptor object to only return info on allowed methods
$interceptor = Interceptor::replace($object);
if ($interceptor instanceof Interceptor) {
return $interceptor->__debugInfo();
}
// for original classes, use reflection
$class = new ReflectionClass($object);
$methods = A::map(
$class->getMethods(),
fn ($method) => static::forMethod($object, $method->getName())
);
return [
'type' => $class->getName(),
'methods' => $methods
];
}
}

View file

@ -0,0 +1,295 @@
<?php
namespace Kirby\Kql;
use Closure;
use Kirby\Cms\App;
use Kirby\Exception\InvalidArgumentException;
use Kirby\Exception\PermissionException;
use Kirby\Toolkit\Str;
use ReflectionFunction;
use ReflectionMethod;
use Throwable;
/**
* Base class for proxying core classes to
* intercept method calls that are not allowed
* on the related core class
*
* @package Kirby KQL
* @author Bastian Allgeier <bastian@getkirby.com>
* @link https://getkirby.com
* @copyright Bastian Allgeier
* @license https://getkirby.com/license
*/
abstract class Interceptor
{
public const CLASS_ALIAS = null;
protected $toArray = [];
public function __construct(protected $object)
{
}
/**
* Magic caller that prevents access
* to restricted methods
*/
public function __call(string $method, array $args = [])
{
if ($this->isAllowedMethod($method) === true) {
return $this->object->$method(...$args);
}
$this->forbiddenMethod($method);
}
/**
* Return information about corresponding object
* incl. information about allowed methods
*/
public function __debugInfo(): array
{
$help = Help::forMethods($this->object, $this->allowedMethods());
return [
'type' => $this::CLASS_ALIAS,
'methods' => $help,
'value' => $this->toArray()
];
}
/**
* Returns list of allowed classes. Specific list
* to be implemented in specific interceptor child classes.
* @codeCoverageIgnore
*/
public function allowedMethods(): array
{
return [];
}
/**
* Returns class name for Interceptor that responds
* to passed name string of a Kirby core class
* @internal
*/
public static function class(string $class): string
{
return str_replace('Kirby\\', 'Kirby\\Kql\\Interceptors\\', $class);
}
/**
* Throws exception for accessing a restricted method
* @throws \Kirby\Exception\PermissionException
*/
protected function forbiddenMethod(string $method)
{
$name = get_class($this->object) . '::' . $method . '()';
throw new PermissionException('The method "' . $name . '" is not allowed in the API context');
}
/**
* Checks if method is allowed to call
*/
public function isAllowedMethod($method)
{
$kirby = App::instance();
$name = strtolower(get_class($this->object) . '::' . $method);
// get list of blocked methods from config
$blocked = $kirby->option('kql.methods.blocked', []);
$blocked = array_map('strtolower', $blocked);
// check in the block list from the config
if (in_array($name, $blocked) === true) {
return false;
}
// check in class allow list
if (in_array($method, $this->allowedMethods()) === true) {
return true;
}
// get list of explicitly allowed methods from config
$allowed = $kirby->option('kql.methods.allowed', []);
$allowed = array_map('strtolower', $allowed);
// check in the allow list from the config
if (in_array($name, $allowed) === true) {
return true;
}
// support for model methods with docblock comment
if ($this->isAllowedCallable($method) === true) {
return true;
}
// support for custom methods with docblock comment
if ($this->isAllowedCustomMethod($method) === true) {
return true;
}
return false;
}
/**
* Checks if closure or object method is allowed
*/
protected function isAllowedCallable($method): bool
{
try {
$ref = match (true) {
$method instanceof Closure
=> new ReflectionFunction($method),
is_string($method) === true
=> new ReflectionMethod($this->object, $method),
default
=> throw new InvalidArgumentException('Invalid method')
};
if ($comment = $ref->getDocComment()) {
if (Str::contains($comment, '@kql-allowed') === true) {
return true;
}
}
} catch (Throwable) {
return false;
}
return false;
}
protected function isAllowedCustomMethod(string $method): bool
{
// has no custom methods
if (property_exists($this->object, 'methods') === false) {
return false;
}
// does not have that method
if (!$call = $this->method($method)) {
return false;
}
// check for a docblock comment
if ($this->isAllowedCallable($call) === true) {
return true;
}
return false;
}
/**
* Returns a registered method by name, either from
* the current class or from a parent class ordered by
* inheritance order (top to bottom)
*/
protected function method(string $method)
{
if (isset($this->object::$methods[$method]) === true) {
return $this->object::$methods[$method];
}
foreach (class_parents($this->object) as $parent) {
if (isset($parent::$methods[$method]) === true) {
return $parent::$methods[$method];
}
}
return null;
}
/**
* Tries to replace a Kirby core object with the
* corresponding interceptor.
* @throws \Kirby\Exception\InvalidArgumentException for non-objects
* @throws \Kirby\Exception\PermissionException when accessing blocked class
*/
public static function replace($object)
{
if (is_object($object) === false) {
throw new InvalidArgumentException('Unsupported value: ' . gettype($object));
}
$kirby = App::instance();
$class = get_class($object);
$name = strtolower($class);
// 1. Is $object class explicitly blocked?
// get list of blocked classes from config
$blocked = $kirby->option('kql.classes.blocked', []);
$blocked = array_map('strtolower', $blocked);
// check in the block list from the config
if (in_array($name, $blocked) === true) {
throw new PermissionException('Access to the class "' . $class . '" is blocked');
}
// 2. Is $object already an interceptor?
// directly return interceptor objects
if ($object instanceof Interceptor) {
return $object;
}
// 3. Does an interceptor class for $object exist?
// check for an interceptor class
$interceptors = $kirby->option('kql.interceptors', []);
$interceptors = array_change_key_case($interceptors, CASE_LOWER);
// load an interceptor from config if it exists and otherwise fall back to a built-in interceptor
$interceptor = $interceptors[$name] ?? static::class($class);
// check for a valid interceptor class
if ($class !== $interceptor && class_exists($interceptor) === true) {
return new $interceptor($object);
}
// 4. Also check for parent classes of $object
// go through parents of the current object to use their interceptors as fallback
foreach (class_parents($object) as $parent) {
$interceptor = static::class($parent);
if (class_exists($interceptor) === true) {
return new $interceptor($object);
}
}
// 5. $object has no interceptor but is explicitly allowed?
// check for a class in the allow list
$allowed = $kirby->option('kql.classes.allowed', []);
$allowed = array_map('strtolower', $allowed);
// return the plain object if it is allowed
if (in_array($name, $allowed) === true) {
return $object;
}
// 6. None of the above? Block class.
throw new PermissionException('Access to the class "' . $class . '" is not supported');
}
public function toArray(): array|null
{
$toArray = [];
// filter methods which cannot be called
foreach ($this->toArray as $method) {
if ($this->isAllowedMethod($method) === true) {
$toArray[] = $method;
}
}
return Kql::select($this, $toArray);
}
/**
* Mirrors by default ::toArray but can be
* implemented differently by specifc interceptor.
* KQL will prefer ::toResponse over ::toArray
*/
public function toResponse()
{
return $this->toArray();
}
}

View file

@ -0,0 +1,39 @@
<?php
namespace Kirby\Kql\Interceptors\Cms;
use Kirby\Kql\Interceptor;
class App extends Interceptor
{
public const CLASS_ALIAS = 'kirby';
protected $toArray = [
'site',
'url'
];
public function allowedMethods(): array
{
return [
'collection',
'defaultLanguage',
'detectedLanguage',
'draft',
'file',
'language',
'languageCode',
'languages',
'multilang',
'page',
'roles',
'site',
'translation',
'translations',
'url',
'user',
'users',
'version'
];
}
}

View file

@ -0,0 +1,34 @@
<?php
namespace Kirby\Kql\Interceptors\Cms;
class Block extends Model
{
public const CLASS_ALIAS = 'block';
protected $toArray = [
'content',
'id',
'isEmpty',
'isHidden',
'type'
];
public function allowedMethods(): array
{
return array_merge(
$this->allowedMethodsForSiblings(),
[
'content',
'id',
'isEmpty',
'isHidden',
'isNotEmpty',
'toField',
'toHtml',
'parent',
'type'
]
);
}
}

View file

@ -0,0 +1,24 @@
<?php
namespace Kirby\Kql\Interceptors\Cms;
class Blocks extends Collection
{
public const CLASS_ALIAS = 'blocks';
public function allowedMethods(): array
{
return array_merge(
parent::allowedMethods(),
[
'excerpt',
'toHtml'
]
);
}
public function toArray(): array
{
return $this->object->toArray();
}
}

View file

@ -0,0 +1,72 @@
<?php
namespace Kirby\Kql\Interceptors\Cms;
use Kirby\Kql\Interceptor;
class Blueprint extends Interceptor
{
public const CLASS_ALIAS = 'blueprint';
protected $toArray = [
'description',
'fields',
'isDefault',
'name',
'sections',
'options',
'tabs',
'title',
];
public function allowedMethods(): array
{
return [
'description',
'field',
'fields',
'isDefault',
'name',
'options',
'section',
'sections',
'tab',
'tabs',
'title',
];
}
public function fields(): array
{
return $this->object->fields();
}
public function sections(): array
{
return array_keys($this->object->sections());
}
public function tab(string $name): ?array
{
if ($tab = $this->object->tab($name)) {
foreach ($tab['columns'] as $columnIndex => $column) {
$tab['columns'][$columnIndex]['sections'] = array_keys($column['sections']);
}
return $tab;
}
return null;
}
public function tabs(): array
{
$tabs = [];
foreach ($this->object->tabs() as $tab) {
$tabs[$tab['name']] = $this->tab($tab['name']);
}
return $tabs;
}
}

View file

@ -0,0 +1,49 @@
<?php
namespace Kirby\Kql\Interceptors\Cms;
use Kirby\Kql\Interceptor;
class Collection extends Interceptor
{
public const CLASS_ALIAS = 'collection';
public function allowedMethods(): array
{
return [
'chunk',
'count',
'filterBy',
'find',
'findBy',
'findByKey',
'first',
'flip',
'groupBy',
'has',
'isEmpty',
'isEven',
'isNotEmpty',
'isOdd',
'keys',
'last',
'limit',
'next',
'not',
'nth',
'offset',
'pagination',
'pluck',
'prev',
'shuffle',
'slice',
'sortBy',
'without',
];
}
public function toArray(): array
{
return $this->object->keys();
}
}

View file

@ -0,0 +1,71 @@
<?php
namespace Kirby\Kql\Interceptors\Cms;
class File extends Model
{
public const CLASS_ALIAS = 'file';
protected $toArray = [
'extension',
'filename',
'height',
'id',
'mime',
'niceSize',
'template',
'type',
'url',
'width'
];
public function allowedMethods(): array
{
return array_merge(
$this->allowedMethodsForModels(),
$this->allowedMethodsForParents(),
$this->allowedMethodsForSiblings(),
[
'blur',
'bw',
'crop',
'dataUri',
'dimensions',
'exif',
'extension',
'filename',
'files',
'grayscale',
'greyscale',
'height',
'html',
'isPortrait',
'isLandscape',
'isSquare',
'mime',
'name',
'niceSize',
'orientation',
'ratio',
'resize',
'size',
'srcset',
'template',
'templateSiblings',
'thumb',
'type',
'width'
]
);
}
public function dimensions(): array
{
return $this->object->dimensions()->toArray();
}
public function exif(): array
{
return $this->object->exif()->toArray();
}
}

View file

@ -0,0 +1,8 @@
<?php
namespace Kirby\Kql\Interceptors\Cms;
class FileVersion extends File
{
public const CLASS_ALIAS = 'file';
}

View file

@ -0,0 +1,18 @@
<?php
namespace Kirby\Kql\Interceptors\Cms;
class Files extends Collection
{
public const CLASS_ALIAS = 'files';
public function allowedMethods(): array
{
return array_merge(
parent::allowedMethods(),
[
'template'
]
);
}
}

View file

@ -0,0 +1,30 @@
<?php
namespace Kirby\Kql\Interceptors\Cms;
class Layout extends Model
{
public const CLASS_ALIAS = 'layout';
protected $toArray = [
'attrs',
'columns',
'id',
'isEmpty',
];
public function allowedMethods(): array
{
return array_merge(
$this->allowedMethodsForSiblings(),
[
'attrs',
'columns',
'id',
'isEmpty',
'isNotEmpty',
'parent'
]
);
}
}

View file

@ -0,0 +1,30 @@
<?php
namespace Kirby\Kql\Interceptors\Cms;
class LayoutColumn extends Model
{
public const CLASS_ALIAS = 'layoutColumn';
protected $toArray = [
'blocks',
'id',
'isEmpty',
'width',
];
public function allowedMethods(): array
{
return array_merge(
$this->allowedMethodsForSiblings(),
[
'blocks',
'id',
'isEmpty',
'isNotEmpty',
'span',
'width'
]
);
}
}

View file

@ -0,0 +1,13 @@
<?php
namespace Kirby\Kql\Interceptors\Cms;
class LayoutColumns extends Collection
{
public const CLASS_ALIAS = 'layoutColumns';
public function toArray(): array
{
return $this->object->toArray();
}
}

View file

@ -0,0 +1,13 @@
<?php
namespace Kirby\Kql\Interceptors\Cms;
class Layouts extends Collection
{
public const CLASS_ALIAS = 'layouts';
public function toArray(): array
{
return $this->object->toArray();
}
}

View file

@ -0,0 +1,113 @@
<?php
namespace Kirby\Kql\Interceptors\Cms;
use Kirby\Kql\Interceptor;
class Model extends Interceptor
{
public const CLASS_ALIAS = 'model';
public function __call($method, array $args = [])
{
if ($this->isAllowedMethod($method) === true) {
return $this->object->$method(...$args);
}
if (method_exists($this->object, $method) === false) {
return $this->object->content()->get($method);
}
$this->forbiddenMethod($method);
}
protected function allowedMethodsForChildren()
{
return [
'children',
'childrenAndDrafts',
'draft',
'drafts',
'find',
'findPageOrDraft',
'grandChildren',
'hasChildren',
'hasDrafts',
'hasListedChildren',
'hasUnlistedChildren',
'index',
'search',
];
}
protected function allowedMethodsForFiles()
{
return [
'audio',
'code',
'documents',
'file',
'files',
'hasAudio',
'hasCode',
'hasDocuments',
'hasFiles',
'hasImages',
'hasVideos',
'image',
'images',
'videos'
];
}
protected function allowedMethodsForModels()
{
return [
'apiUrl',
'blueprint',
'content',
'dragText',
'exists',
'id',
'mediaUrl',
'modified',
'permissions',
'panel',
'permalink',
'previewUrl',
'url',
];
}
protected function allowedMethodsForSiblings()
{
return [
'indexOf',
'next',
'nextAll',
'prev',
'prevAll',
'siblings',
'hasNext',
'hasPrev',
'isFirst',
'isLast',
'isNth'
];
}
protected function allowedMethodsForParents()
{
return [
'parent',
'parentId',
'parentModel',
'site',
];
}
public function uuid(): string
{
return $this->object->uuid()->toString();
}
}

View file

@ -0,0 +1,68 @@
<?php
namespace Kirby\Kql\Interceptors\Cms;
class Page extends Model
{
public const CLASS_ALIAS = 'page';
protected $toArray = [
'children',
'content',
'drafts',
'files',
'id',
'intendedTemplate',
'isHomePage',
'isErrorPage',
'num',
'template',
'title',
'slug',
'status',
'uid',
'url'
];
public function allowedMethods(): array
{
return array_merge(
$this->allowedMethodsForChildren(),
$this->allowedMethodsForFiles(),
$this->allowedMethodsForModels(),
$this->allowedMethodsForParents(),
$this->allowedMethodsForSiblings(),
[
'blueprints',
'depth',
'hasTemplate',
'intendedTemplate',
'isDraft',
'isErrorPage',
'isHomePage',
'isHomeOrErrorPage',
'isListed',
'isReadable',
'isSortable',
'isUnlisted',
'num',
'slug',
'status',
'template',
'title',
'uid',
'uri',
]
);
}
public function intendedTemplate(): string
{
return $this->object->intendedTemplate()->name();
}
public function template(): string
{
return $this->object->template()->name();
}
}

View file

@ -0,0 +1,34 @@
<?php
namespace Kirby\Kql\Interceptors\Cms;
class Pages extends Collection
{
public const CLASS_ALIAS = 'pages';
public function allowedMethods(): array
{
return array_merge(
parent::allowedMethods(),
[
'audio',
'children',
'code',
'documents',
'drafts',
'files',
'findByUri',
'images',
'index',
'listed',
'notTemplate',
'nums',
'published',
'search',
'template',
'unlisted',
'videos',
]
);
}
}

View file

@ -0,0 +1,33 @@
<?php
namespace Kirby\Kql\Interceptors\Cms;
use Kirby\Kql\Interceptor;
class Role extends Interceptor
{
public const CLASS_ALIAS = 'role';
protected $toArray = [
'description',
'id',
'name',
'title',
];
public function allowedMethods(): array
{
return [
'description',
'id',
'name',
'permissions',
'title'
];
}
public function permissions(): array
{
return $this->object->permissions()->toArray();
}
}

View file

@ -0,0 +1,36 @@
<?php
namespace Kirby\Kql\Interceptors\Cms;
class Site extends Model
{
public const CLASS_ALIAS = 'site';
protected $toArray = [
'children',
'drafts',
'files',
'title',
'url',
];
public function allowedMethods(): array
{
return array_merge(
$this->allowedMethodsForChildren(),
$this->allowedMethodsForFiles(),
$this->allowedMethodsForModels(),
[
'blueprints',
'breadcrumb',
'errorPage',
'errorPageId',
'homePage',
'homePageId',
'page',
'pages',
'title',
]
);
}
}

View file

@ -0,0 +1,13 @@
<?php
namespace Kirby\Kql\Interceptors\Cms;
class Structure extends Collection
{
public const CLASS_ALIAS = 'structure';
public function toArray(): array
{
return $this->object->toArray();
}
}

View file

@ -0,0 +1,20 @@
<?php
namespace Kirby\Kql\Interceptors\Cms;
class StructureObject extends Model
{
public const CLASS_ALIAS = 'structureItem';
public function allowedMethods(): array
{
return array_merge(
$this->allowedMethodsForSiblings(),
[
'content',
'id',
'parent',
]
);
}
}

View file

@ -0,0 +1,35 @@
<?php
namespace Kirby\Kql\Interceptors\Cms;
use Kirby\Kql\Interceptor;
class Translation extends Interceptor
{
public const CLASS_ALIAS = 'translation';
protected $toArray = [
'code',
'data',
'direction',
'id',
'name',
'locale',
'author'
];
public function allowedMethods(): array
{
return [
'code',
'data',
'dataWithFallback',
'direction',
'get',
'id',
'name',
'locale',
'author'
];
}
}

View file

@ -0,0 +1,35 @@
<?php
namespace Kirby\Kql\Interceptors\Cms;
class User extends Model
{
public const CLASS_ALIAS = 'user';
protected $toArray = [
'id',
'name',
'role',
'username'
];
public function allowedMethods(): array
{
return array_merge(
$this->allowedMethodsForFiles(),
$this->allowedMethodsForModels(),
$this->allowedMethodsForSiblings(),
[
'avatar',
'email',
'id',
'isAdmin',
'language',
'modified',
'name',
'role',
'username',
]
);
}
}

View file

@ -0,0 +1,18 @@
<?php
namespace Kirby\Kql\Interceptors\Cms;
class Users extends Collection
{
public const CLASS_ALIAS = 'users';
public function allowedMethods(): array
{
return array_merge(
parent::allowedMethods(),
[
'role'
]
);
}
}

View file

@ -0,0 +1,40 @@
<?php
namespace Kirby\Kql\Interceptors\Content;
use Kirby\Kql\Interceptor;
class Content extends Interceptor
{
public const CLASS_ALIAS = 'content';
public function __call($method, array $args = [])
{
if ($this->isAllowedMethod($method) === true) {
return $this->object->$method(...$args);
}
if (method_exists($this->object, $method) === false) {
return $this->object->get($method);
}
$this->forbiddenMethod($method);
}
public function allowedMethods(): array
{
return [
'data',
'fields',
'has',
'get',
'keys',
'not',
];
}
public function toArray(): array
{
return $this->object->toArray();
}
}

View file

@ -0,0 +1,52 @@
<?php
namespace Kirby\Kql\Interceptors\Content;
use Kirby\Kql\Interceptor;
class Field extends Interceptor
{
public const CLASS_ALIAS = 'field';
public function __call($method, array $args = [])
{
if ($this->isAllowedMethod($method) === true) {
return $this->object->$method(...$args);
}
// field methods
$methods = array_keys($this->object::$methods);
$method = strtolower($method);
if (in_array($method, $methods) === true) {
return $this->object->$method(...$args);
}
// aliases
$aliases = array_keys($this->object::$aliases);
$alias = strtolower($method);
if (in_array($alias, $aliases) === true) {
return $this->object->$method(...$args);
}
$this->forbiddenMethod($method);
}
public function allowedMethods(): array
{
return [
'exists',
'isEmpty',
'isNotEmpty',
'key',
'or',
'value'
];
}
public function toResponse()
{
return $this->object->toString();
}
}

View file

@ -0,0 +1,30 @@
<?php
namespace Kirby\Kql\Interceptors\Panel;
use Kirby\Kql\Interceptor;
class Model extends Interceptor
{
public const CLASS_ALIAS = 'panel';
public function allowedMethods(): array
{
return [
'dragText',
'image',
'path',
'url',
];
}
public function toArray(): array
{
return [
'dragText' => $this->dragText(),
'image' => $this->image(),
'path' => $this->path(),
'url' => $this->url(),
];
}
}

View file

@ -0,0 +1,24 @@
<?php
namespace Kirby\Kql\Interceptors\Toolkit;
use Kirby\Kql\Interceptor;
class Obj extends Interceptor
{
public const CLASS_ALIAS = 'obj';
public function allowedMethods(): array
{
return [
'get',
'toArray',
'toJson',
];
}
public function toArray(): array
{
return $this->object->toArray();
}
}

View file

@ -0,0 +1,226 @@
<?php
namespace Kirby\Kql;
use Exception;
use Kirby\Cms\App;
use Kirby\Cms\Collection;
use Kirby\Toolkit\Str;
/**
* ...
*
* @package Kirby KQL
* @author Bastian Allgeier <bastian@getkirby.com>
* @link https://getkirby.com
* @copyright Bastian Allgeier
* @license https://getkirby.com/license
*/
class Kql
{
public static function fetch($model, $key, $selection)
{
// simple key/value
if ($selection === true) {
return static::render($model->$key());
}
// selection without additional query
if (
is_array($selection) === true &&
empty($selection['query']) === true
) {
return static::select(
$model->$key(),
$selection['select'] ?? null,
$selection['options'] ?? []
);
}
// nested queries
return static::run($selection, $model);
}
/**
* Returns helpful information about the object
* type as well as, if available, values and methods
*/
public static function help($object): array
{
return Help::for($object);
}
public static function query(string $query, $model = null)
{
$model ??= App::instance()->site();
$data = [$model::CLASS_ALIAS => $model];
return Query::factory($query)->resolve($data);
}
public static function render($value)
{
if (is_object($value) === true) {
// replace actual object with intercepting proxy class
$object = Interceptor::replace($value);
if (method_exists($object, 'toResponse') === true) {
return $object->toResponse();
}
if (method_exists($object, 'toArray') === true) {
return $object->toArray();
}
throw new Exception('The object "' . get_class($object) . '" cannot be rendered. Try querying one of its methods instead.');
}
return $value;
}
public static function run($input, $model = null)
{
// string queries
if (is_string($input) === true) {
$result = static::query($input, $model);
return static::render($result);
}
// multiple queries
if (isset($input['queries']) === true) {
$result = [];
foreach ($input['queries'] as $name => $query) {
$result[$name] = static::run($query);
}
return $result;
}
$query = $input['query'] ?? 'site';
$select = $input['select'] ?? null;
$options = ['pagination' => $input['pagination'] ?? null];
// check for invalid queries
if (is_string($query) === false) {
throw new Exception('The query must be a string');
}
$result = static::query($query, $model);
return static::select($result, $select, $options);
}
public static function select(
$data,
array|string|null $select = null,
array $options = []
) {
if ($select === null) {
return static::render($data);
}
if ($select === '?') {
return static::help($data);
}
if ($data instanceof Collection) {
return static::selectFromCollection($data, $select, $options);
}
if (is_object($data) === true) {
return static::selectFromObject($data, $select);
}
if (is_array($data) === true) {
return static::selectFromArray($data, $select);
}
}
/**
* @internal
*/
public static function selectFromArray(array $array, array $select): array
{
$result = [];
foreach ($select as $key => $selection) {
if ($selection === false) {
continue;
}
if (is_int($key) === true) {
$key = $selection;
$selection = true;
}
$result[$key] = $array[$key] ?? null;
}
return $result;
}
/**
* @internal
*/
public static function selectFromCollection(
Collection $collection,
array|string $select,
array $options = []
): array {
if ($options['pagination'] ?? false) {
$collection = $collection->paginate($options['pagination']);
}
$data = [];
foreach ($collection as $model) {
$data[] = static::selectFromObject($model, $select);
}
if ($pagination = $collection->pagination()) {
return [
'data' => $data,
'pagination' => [
'page' => $pagination->page(),
'pages' => $pagination->pages(),
'offset' => $pagination->offset(),
'limit' => $pagination->limit(),
'total' => $pagination->total(),
],
];
}
return $data;
}
/**
* @internal
*/
public static function selectFromObject(
object $object,
array|string $select
): array {
// replace actual object with intercepting proxy class
$object = Interceptor::replace($object);
$result = [];
if (is_string($select) === true) {
$select = Str::split($select);
}
foreach ($select as $key => $selection) {
if ($selection === false) {
continue;
}
if (is_int($key) === true) {
$key = $selection;
$selection = true;
}
$result[$key] = static::fetch($object, $key, $selection);
}
return $result;
}
}

View file

@ -0,0 +1,29 @@
<?php
namespace Kirby\Kql;
use Kirby\Query\Query as BaseQuery;
/**
* Extends the core Query class with the KQL-specific
* functionalities to intercept the segments chain calls
*
* @package Kirby KQL
* @author Nico Hoffmann <nico@getkirby.com>
* @link https://getkirby.com
* @copyright Bastian Allgeier
* @license https://getkirby.com/license
*/
class Query extends BaseQuery
{
/**
* Intercepts the chain of segments called
* on each other by replacing objects with
* their corresponding Interceptor which
* handles blocking calls to restricted methods
*/
public function intercept(mixed $result): mixed
{
return is_object($result) ? Interceptor::replace($result): $result;
}
}