Skip to content

Commit

Permalink
🚀 ready for liftoff
Browse files Browse the repository at this point in the history
  • Loading branch information
bnomei committed Feb 14, 2025
1 parent 5d4e834 commit 72712c4
Show file tree
Hide file tree
Showing 30 changed files with 9,991 additions and 200 deletions.
85 changes: 57 additions & 28 deletions classes/APIRecord.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,21 +9,21 @@
use Kirby\Uuid\Uuid;

/**
* @property ?string $id
* @property ?string $uuid
* @property string $id
* @property string $uuid
* @property string $title
* @property ?string $slug
* @property string $slug
* @property ?string $model
* @property ?string $template
* @property string $template
* @property ?int $num
* @property ?array $content
* @property array $content
*
* @method self id(string $id)
* @method self uuid(string $uuid)
* @method self title(string $title)
* @method self slug(string $slug)
* @method self model(string $model)
* @method self template(string $template)
* @method self model(?string $model)
* @method self template(?string $template)
* @method self num(?int $num)
* @method self content(array $content)
*/
Expand All @@ -33,35 +33,31 @@ class APIRecord extends Obj
public function __construct(
array $data = [],
array $map = [],
public ?string $parent = null,
) {
parent::__construct(
get_object_vars($this)
);
$this->title(A::get($data, 'title')); // title and slug
$this->num = null; // unlisted
$this->model = null; // automatic matching to template
$this->template = 'default';
$this->uuid = Uuid::generate();
// defaults
parent::__construct([
'num' => null,
]);
$this->content([]);
$this->template('default'); // => model
$this->title(''); // => slug & id
$this->uuid(Uuid::generate()); // => content.uuid

// load
foreach ($map as $property => $path) {
$this->$property($this->resolveMap($data, $property, $path));
$this->$property($this->resolveMap($data, $path));
}

// uuid might be created in content. keep it in sync in two steps.
if ($uuid = A::get($this->content, 'uuid')) {
$this->uuid = $uuid;
}
$this->content['uuid'] = $this->uuid;
}

private function resolveMap(array $data, string $property, string|array|\Closure $path): mixed
private function resolveMap(array $data, string|array|\Closure $path): mixed
{
if (is_string($path)) {
return A::get($data, $path); // dot-notion support
} elseif (is_array($path)) {
$out = [];
foreach ($path as $key => $value) {
$out[$key] = $this->resolveMap($data, $key, $value);
$out[$key] = $this->resolveMap($data, $value);
}

return $out;
Expand All @@ -72,29 +68,62 @@ private function resolveMap(array $data, string $property, string|array|\Closure
return null; // @phpstan-ignore-line
}

public function __get(string $property): mixed
{
if (in_array($property, ['uuid', 'title'])) {
return A::get($this->get('content'), $property);
}

return $this->get($property);
}

public function __call(string $property, array $arguments): self
{
$value = $arguments[0] ?? null;

// do not set on null to allow config with null value to keep current value
if (is_null($value)) {
return $this;
}

// move some into content instead
if (in_array($property, ['uuid', 'title'])) {
$this->content([$property => $value]);
} elseif ($property === 'content') {
$this->content = array_merge(
$this->content ?? [],
$value
);
} else {
$this->$property = $value;
}

// infer slug from title
if ($property === 'title' && ! $this->get('slug')) {
$this->slug = Str::slug(strval($value));
$this->slug(Str::slug(strval($value)));
}

// infer model from template
if ($property === 'template' && ! $this->get('model')) {
$this->model = $value;
$this->model($value);
}

$this->$property = $value;
// infer id from parent and slug
if ($property === 'slug' && ! $this->get('id')) {
$this->id($this->parent ?
$this->parent.'/'.$this->slug :
$this->slug
);
}

return $this;
}

public function toArray(): array
{
$result = parent::toArray();
ksort($result);

unset($result['parent']); // do not expose the parent

return $result;
}
Expand Down
170 changes: 170 additions & 0 deletions classes/APIRecords.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
<?php

namespace Bnomei;

use Kirby\Cms\Nest;
use Kirby\Cms\Page;
use Kirby\Content\Field;
use Kirby\Http\Remote;
use Kirby\Query\Query;
use Kirby\Toolkit\A;

class APIRecords
{
private ?array $records;

private ?string $endpointUrl;

private ?array $endpointParams;

private ?string $recordsDataQuery;

private ?array $recordsDataMap;

private ?int $recordsCacheExpire;

private ?string $recordTemplate;

private ?string $recordModel;

private string $cacheKey;

public function __construct(protected ?Page $page = null)
{
$this->records = null;

$this->endpointUrl = $this->config('url', resolveClosures: true);

Check failure on line 36 in classes/APIRecords.php

View workflow job for this annotation

GitHub Actions / phpstan

Property Bnomei\APIRecords::$endpointUrl (string|null) does not accept mixed.
$this->endpointParams = $this->config('params', [], true);

Check failure on line 37 in classes/APIRecords.php

View workflow job for this annotation

GitHub Actions / phpstan

Property Bnomei\APIRecords::$endpointParams (array|null) does not accept mixed.
$this->recordsDataQuery = $this->config('query', resolveClosures: true);

Check failure on line 38 in classes/APIRecords.php

View workflow job for this annotation

GitHub Actions / phpstan

Property Bnomei\APIRecords::$recordsDataQuery (string|null) does not accept mixed.
$this->recordsDataMap = $this->config('map'); // closure resolving here would break the mapping by closure

Check failure on line 39 in classes/APIRecords.php

View workflow job for this annotation

GitHub Actions / phpstan

Property Bnomei\APIRecords::$recordsDataMap (array|null) does not accept mixed.
$this->recordsCacheExpire = $this->config('expire', intval(option('bnomei.api-pages.expire')), true);

Check failure on line 40 in classes/APIRecords.php

View workflow job for this annotation

GitHub Actions / phpstan

Parameter #1 $value of function intval expects array|bool|float|int|resource|string|null, mixed given.

Check failure on line 40 in classes/APIRecords.php

View workflow job for this annotation

GitHub Actions / phpstan

Property Bnomei\APIRecords::$recordsCacheExpire (int|null) does not accept mixed.
$this->recordTemplate = $this->config('template');

Check failure on line 41 in classes/APIRecords.php

View workflow job for this annotation

GitHub Actions / phpstan

Property Bnomei\APIRecords::$recordTemplate (string|null) does not accept mixed.
$this->recordModel = $this->config('model');

Check failure on line 42 in classes/APIRecords.php

View workflow job for this annotation

GitHub Actions / phpstan

Property Bnomei\APIRecords::$recordModel (string|null) does not accept mixed.
$this->cacheKey = md5($this->endpointUrl.json_encode($this->endpointParams));

Check failure on line 43 in classes/APIRecords.php

View workflow job for this annotation

GitHub Actions / phpstan

Binary operation "." between mixed and non-empty-string|false results in an error.
}

public function page(): ?Page
{
return $this->page;
}

public function config(string $key, mixed $default = null, bool $resolveClosures = false): mixed
{
$result = null;

// try from model itself
if ($config = $this->page?->recordsConfig()) {

Check failure on line 56 in classes/APIRecords.php

View workflow job for this annotation

GitHub Actions / phpstan

Call to an undefined method Kirby\Cms\Page::recordsConfig().
if (is_array($config)) {
$result = A::get($config, $key);
} elseif ($config instanceof Field) {
$result = $config->value();
$json = is_string($result) ? json_decode($result, true) : false;
if (is_array($json)) {
$result = $json;
}
}
}

// try a config value for page template name
if (! $result && $template = $this->page?->intendedTemplate()->name()) {
$result = option("bnomei.api-pages.records.{$template}.{$key}", null);
}

// try blueprint
if (! $result && $fromBlueprint = A::get($this->page?->blueprint()->toArray() ?? [], 'records.'.$key)) {
$result = $fromBlueprint;
}

// resolve closures
if ($resolveClosures && is_array($result)) {
array_walk($result, function (&$value) {
if ($value instanceof \Closure) {
$value = $value($this);
}
});
}

return $result ?? $default;
}

/**
* @return array<int, APIRecord>
*/
public function toArray(): array
{
if ($this->records) {
return $this->records;
}

$map = $this->recordsDataMap;
$data = $this->fetch();

// handle the data like Kirby's OptionApi does to allow for the entry query with sorting etc.
$data = Nest::create($data);
$data = Query::factory($this->recordsDataQuery)->resolve($data)?->toArray() ?? [];

$records = array_map(function (array $data) use ($map) {
// create the record object which resolves data with the map
$record = new APIRecord(
data: $data,
map: $map ?? [],
parent: $this->page?->id(),
);

// mapping might not have set template and model yet, so apply from config.
// null values will not overwrite the values from the mapping step before.
$record->template($this->recordTemplate)
->model($this->recordModel);

// lastly, if there is no map at all then add ALL data as content
if (empty($map) || in_array(A::get($map, 'content'), [null, '*'])) {
$record->content($data);
}

return $record;
}, $data);

return $this->records = $records;
}

public function fetch(): array
{
// get cache if it exists
$cache = kirby()->cache('bnomei.api-pages')->get($this->cacheKey);
if ($cache) {
return $cache;
}

// fetch from remote
$params = $this->endpointParams;
$expire = $this->recordsCacheExpire;
$method = strtolower($params['method'] ?? 'GET');
$remote = Remote::$method($this->endpointUrl, $params);

if ($remote->code() >= 200 && $remote->code() <= 300) {
$json = $remote->json() ?? [];
if ($expire >= 0) {
kirby()->cache('bnomei.api-pages')->set($this->cacheKey, $json, $expire);
}

return $json;
} else {
$ex = option('bnomei.api-pages.exception');
if ($ex instanceof \Closure) {
$ex($remote);
}
}

return [];
}

public function remove(): bool
{
return kirby()->cache('bnomei.api-pages')->remove($this->cacheKey);
}

public static function flush(): bool
{
return kirby()->cache('bnomei.api-pages')->flush();
}
}
Loading

0 comments on commit 72712c4

Please sign in to comment.