Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 25 additions & 13 deletions boot.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,18 +10,30 @@
App::set(ImanagerBootstrap::create(__DIR__));

/*
* Phase 14a status: only the iManager 2.0 container is wired up.
* Phase 14b-1 boot path:
* - vendor/autoload.php (Composer + iManager 2.0)
* - iManager container set on App::container()
* - $config loaded from data/settings/scriptor-config.php (legacy file
* format kept verbatim — themes still read it from $site->config)
*
* The legacy 1.x bootstrap below is intentionally disabled — the embedded
* Scriptor/imanager/ library shares the Imanager\ namespace with the new
* vendor/bigins/imanager package, so loading both at once collides.
*
* Sub-phases re-enable functionality piece by piece:
* - 14b: Site / Page / Pages frontend on the new container
* - 14c: Editor modules (auth, pages, users, settings, install, profile)
* - 14f: delete Scriptor/imanager/ entirely
*
* Until 14b lands, the public site and editor are non-functional on this
* branch. Use /test-bootstrap.php to verify the container, or check out
* `master` for the working 1.x build.
* The legacy 1.x bootstrap (Scriptor/imanager/, editor/core/) is intentionally
* NOT loaded — it shares the Imanager\ namespace with the new vendor package
* and would collide. Sub-phases reintroduce functionality piece by piece:
* - 14b-2: BasicTheme back on the new container
* - 14c: editor modules (auth, pages, users, settings, install, profile)
* - 14f: delete Scriptor/imanager/ entirely
*/

// Legacy config files guard their first line with `defined('IS_IM')`.
\defined('IS_IM') || \define('IS_IM', true);
// Legacy themes use IM_DATAPATH for theme-config files (BasicTheme in 14b-2).
\defined('IM_DATAPATH') || \define('IM_DATAPATH', __DIR__ . '/data');

/** @var array<string, mixed> $config */
require __DIR__ . '/data/settings/scriptor-config.php';
if (file_exists(__DIR__ . '/data/settings/custom.scriptor-config.php')) {
$config = array_replace_recursive(
$config,
include __DIR__ . '/data/settings/custom.scriptor-config.php',
);
}
102 changes: 102 additions & 0 deletions boot/Frontend/Page.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
<?php

declare(strict_types=1);

namespace Scriptor\Boot\Frontend;

use Imanager\Domain\Item;

/**
* Read-only page DTO exposed to themes as `$site->page`.
*
* Adapts an `Imanager\Domain\Item` (Pages category) to the property surface
* the legacy Scriptor themes know: `name`, `slug`, `template`, `parent`,
* `pagetype`, `content`, `menu_title`, `images`, plus structural columns.
*
* Field values that aren't promoted to a top-level column live in the item's
* `data` bag — `__get()` reaches in lazily so theme code that asks for an
* uncommon field still works without us declaring every possible property.
*/
final readonly class Page
{
public string $name;
public string $slug;
public string $template;
public string $pagetype;
public string $menu_title;
public string $content;
public int $parent;
/** @var list<array<string, mixed>> */
public array $images;

public function __construct(
public Item $item,
) {
$data = $item->data;
$this->name = $item->name ?? '';
$this->slug = self::str($data->get('slug'));
$this->template = self::str($data->get('template'));
$this->pagetype = self::str($data->get('pagetype'));
$this->menu_title = self::str($data->get('menu_title'));
$this->content = self::str($data->get('content'));
$this->parent = (int) ($data->get('parent') ?? 0);

$rawImages = $data->get('images');
$this->images = \is_array($rawImages) ? array_values($rawImages) : [];
}

public function id(): ?int
{
return $this->item->id;
}

public function active(): bool
{
return $this->item->active;
}

public function created(): int
{
return $this->item->created;
}

public function updated(): int
{
return $this->item->updated;
}

/**
* Lazy access to any field not promoted to a typed property, e.g.
* theme-specific custom fields. Returns `null` for unknown keys instead
* of raising — themes that probe optional fields stay quiet.
*/
public function __get(string $name): mixed
{
return match ($name) {
'id' => $this->item->id,
'active' => $this->item->active,
'created' => $this->item->created,
'updated' => $this->item->updated,
default => $this->item->data->get($name),
};
}

public function __isset(string $name): bool
{
return match ($name) {
'id', 'active', 'created', 'updated' => true,
default => $this->item->data->has($name),
};
}

private static function str(mixed $value): string
{
if (\is_string($value)) {
return $value;
}
if (\is_int($value) || \is_float($value)) {
return (string) $value;
}
return '';
}
}
118 changes: 118 additions & 0 deletions boot/Frontend/PageRepository.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
<?php

declare(strict_types=1);

namespace Scriptor\Boot\Frontend;

use Imanager\Domain\Item;
use Imanager\Query\Operator;
use Imanager\Query\Query;
use Imanager\Storage\CategoryRepository;
use Imanager\Storage\ItemRepository;

/**
* Read access to the Pages category as a thin wrapper over the iManager 2.0
* `ItemRepository` + `Query` AST. Returns Frontend\Page DTOs so themes can
* keep using `$page->slug`, `$page->template`, etc.
*
* Phase 14b-1 only needs lookup + parent/children traversal. Sorting,
* pagination and richer filters land with BasicTheme in 14b-2.
*/
final readonly class PageRepository
{
public int $categoryId;

public function __construct(
private CategoryRepository $categories,
private ItemRepository $items,
string $categorySlug = 'pages',
) {
$category = $this->categories->findBySlug($categorySlug);
if ($category === null || $category->id === null) {
throw new \RuntimeException(\sprintf(
'Category with slug "%s" not found in the iManager database',
$categorySlug,
));
}
$this->categoryId = $category->id;
}

public function find(int $id): ?Page
{
$item = $this->items->find($id);
return $item !== null && $item->categoryId === $this->categoryId
? new Page($item)
: null;
}

public function findBySlug(string $slug): ?Page
{
$query = (new Query($this->categoryId))
->where('slug', Operator::Eq, $slug)
->limit(1);
foreach ($this->items->query($query) as $item) {
return new Page($item);
}
return null;
}

/**
* Finds the home page — by convention the page with id = 1 in Scriptor.
* Falls back to the lowest-position page if id 1 is missing.
*/
public function findHome(): ?Page
{
$home = $this->find(1);
if ($home !== null) {
return $home;
}
$items = $this->items->findByCategory($this->categoryId, 0, 1);
return $items === [] ? null : new Page($items[0]);
}

/**
* @return list<Page>
*/
public function findAll(): array
{
return self::wrap($this->items->findByCategory($this->categoryId));
}

/**
* @return list<Page>
*/
public function findByParent(int $parentId): array
{
$query = (new Query($this->categoryId))
->where('parent', Operator::Eq, $parentId)
->orderBy('position');
return self::wrap($this->items->query($query));
}

/**
* @return list<Page>
*/
public function findActiveByParent(int $parentId): array
{
$pages = [];
foreach ($this->findByParent($parentId) as $page) {
if ($page->active()) {
$pages[] = $page;
}
}
return $pages;
}

/**
* @param list<Item> $items
* @return list<Page>
*/
private static function wrap(array $items): array
{
$out = [];
foreach ($items as $item) {
$out[] = new Page($item);
}
return $out;
}
}
66 changes: 66 additions & 0 deletions boot/Frontend/Sanitizer.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
<?php

declare(strict_types=1);

namespace Scriptor\Boot\Frontend;

use Imanager\Validation\Sanitizer as ImanagerSanitizer;

/**
* Backwards-compatible facade over `Imanager\Validation\Sanitizer`.
*
* Themes call methods on `$site->sanitizer` that the new iManager Sanitizer
* doesn't ship — `templateName()`, `pageName()`, `url()` (legacy variants),
* `text()`. We forward the modern equivalents and add the legacy-only
* helpers without growing the iManager surface.
*/
final readonly class Sanitizer
{
public function __construct(private ImanagerSanitizer $delegate) {}

public function templateName(string $value): string
{
$clean = preg_replace('/[^a-zA-Z0-9_\-\/]/', '', $value);
return $clean ?? '';
}

public function pageName(string $value): string
{
return $this->delegate->slug($value);
}

public function text(string $value, int $maxLength = 255): string
{
return $this->delegate->text($value, $maxLength);
}

public function multiline(string $value, int $maxLength = 65535): string
{
return $this->delegate->multiline($value, $maxLength);
}

public function slug(string $value, int $maxLength = 128): string
{
return $this->delegate->slug($value, $maxLength);
}

public function email(string $value): ?string
{
return $this->delegate->email($value);
}

public function url(string $value): string
{
return $this->delegate->url($value) ?? '';
}

public function entities(string $value): string
{
return $this->delegate->entities($value);
}

public function markdown(string $value): string
{
return $this->delegate->markdown($value);
}
}
Loading