Rewrite Page, Site and related classes

This commit is contained in:
Giuseppe Criscione 2022-12-04 20:46:46 +01:00
parent e1f88af263
commit e629e1bbf2
31 changed files with 928 additions and 443 deletions

View File

@ -18,13 +18,13 @@ class PageController extends AbstractController
$route = $params->get('page', $formwork->config()->get('pages.index'));
if ($resolvedAlias = $formwork->site()->resolveAlias($route)) {
if ($resolvedAlias = $formwork->site()->resolveRouteAlias($route)) {
$route = $resolvedAlias;
}
if ($page = $formwork->site()->findPage($route)) {
if ($page->canonical() !== null) {
$canonical = $page->canonical();
if ($page->canonicalRoute() !== null) {
$canonical = $page->canonicalRoute();
if ($params->get('page', '/') !== $canonical) {
$route = $formwork->router()->rewrite(['page' => $canonical]);
@ -41,7 +41,7 @@ class PageController extends AbstractController
|| (!$page->isPublished() && !$formwork->site()->modifiedSince($page->unpublishDate()->toTimestamp()))) {
// Clear cache if the site was not modified since the page has been published or unpublished
$formwork->cache()->clear();
FileSystem::touch($formwork->config()->get('content.path'));
FileSystem::touch($formwork->site()->path());
}
}
@ -80,7 +80,7 @@ class PageController extends AbstractController
$cache = $formwork->cache();
$cacheKey = $page->route();
$cacheKey = $page->uri(includeLanguage: true);
if ($config->get('cache.enabled') && $cache->has($cacheKey)) {
// Validate cached response

View File

@ -2,12 +2,9 @@
namespace Formwork\Files;
use Formwork\Formwork;
use Formwork\Utils\FileSystem;
use Formwork\Utils\HTTPRequest;
use Formwork\Utils\MimeType;
use Formwork\Utils\Str;
use Formwork\Utils\Uri;
class File
{
@ -26,11 +23,6 @@ class File
*/
protected string $extension;
/**
* File uri
*/
protected string $uri;
/**
* File MIME type
*/
@ -46,6 +38,11 @@ class File
*/
protected string $size;
/**
* File last modified time
*/
protected int $lastModifiedTime;
/**
* File hash
*/
@ -60,7 +57,6 @@ class File
$this->name = basename($path);
$this->extension = FileSystem::extension($path);
$this->mimeType = FileSystem::mimeType($path);
$this->uri = Uri::resolveRelative($this->name, HTTPRequest::root() . ltrim(Formwork::instance()->request(), '/'));
$this->size = FileSystem::formatSize(FileSystem::fileSize($path));
}
@ -93,14 +89,6 @@ class File
return $this->extension;
}
/**
* Get file URI
*/
public function uri(): string
{
return $this->uri;
}
/**
* Get file MIME type
*/
@ -155,6 +143,17 @@ class File
return $this->size;
}
/**
* Get file last modified time
*/
public function lastModifiedTime(): int
{
if (isset($this->lastModifiedTime)) {
return $this->lastModifiedTime;
}
return FileSystem::lastModifiedTime($this->path);
}
/**
* Get file hash
*/

View File

@ -5,7 +5,7 @@ namespace Formwork\Files;
use Formwork\Data\AbstractCollection;
use Formwork\Utils\FileSystem;
class Files extends AbstractCollection
class FileCollection extends AbstractCollection
{
protected bool $associative = true;

View File

@ -255,8 +255,10 @@ final class Formwork
protected function loadSite(): void
{
$config = YAML::parseFile(CONFIG_PATH . 'site.yml');
$this->site = new Site($config);
$this->site->setLanguages($this->languages);
$this->site = Site::fromPath(
$this->config()->get('content.path'),
['languages' => $this->languages] + $config
);
}
/**

View File

@ -0,0 +1,60 @@
<?php
namespace Formwork\Languages;
class Language
{
/**
* Language code
*/
protected string $code;
/**
* Language name (in English)
*/
protected ?string $name = null;
/**
* Language native name
*/
protected ?string $nativeName = null;
public function __construct(string $code)
{
$this->code = $code;
if (LanguageCodes::hasCode($code)) {
$this->name = LanguageCodes::codeToName($code);
$this->nativeName = LanguageCodes::codeToNativeName($code);
}
}
public function __toString(): string
{
return $this->code;
}
/**
* Get language code
*/
public function code(): string
{
return $this->code;
}
/**
* Get language name
*/
public function name(): ?string
{
return $this->name;
}
/**
* Get language native name
*/
public function nativeName(): ?string
{
return $this->nativeName;
}
}

View File

@ -0,0 +1,18 @@
<?php
namespace Formwork\Languages;
use Formwork\Data\AbstractCollection;
use Formwork\Utils\Arr;
class LanguageCollection extends AbstractCollection
{
protected ?string $dataType = Language::class;
protected bool $associative = true;
public function __construct(array $data)
{
parent::__construct(Arr::fromEntries(Arr::map($data, fn ($code) => [$code, new Language($code)])));
}
}

View File

@ -10,41 +10,48 @@ class Languages
/**
* Array containing available languages
*/
protected array $available = [];
protected LanguageCollection $available;
/**
* Default language code
*/
protected ?string $default = null;
protected ?Language $default = null;
/**
* Current language code
*/
protected ?string $current = null;
protected ?Language $current = null;
/**
* Requested language code
*/
protected ?string $requested = null;
protected ?Language $requested = null;
/**
* Preferred language code
*/
protected ?string $preferred = null;
protected ?Language $preferred = null;
/**
* Create a new Languages instance
*/
public function __construct()
public function __construct(array $options = [])
{
$this->available = (array) Formwork::instance()->config()->get('languages.available');
$this->current = $this->default = Formwork::instance()->config()->get('languages.default', $this->available[0] ?? null);
$this->available = new LanguageCollection($options['available'] ?? []);
$this->default = $this->resolveLanguage($options['default'] ?? null);
$this->current = $this->resolveLanguage($options['current'] ?? $this->default);
$this->requested = $this->resolveLanguage($options['requested'] ?? null);
$this->preferred = $this->resolveLanguage($options['preferred'] ?? null);
}
/**
* Get available languages
*/
public function available(): array
public function available(): LanguageCollection
{
return $this->available;
}
@ -52,7 +59,7 @@ class Languages
/**
* Get default language code
*/
public function default(): ?string
public function default(): ?Language
{
return $this->default;
}
@ -60,7 +67,7 @@ class Languages
/**
* Get current language code
*/
public function current(): ?string
public function current(): ?Language
{
return $this->current;
}
@ -68,7 +75,7 @@ class Languages
/**
* Get requested language code
*/
public function requested(): ?string
public function requested(): ?Language
{
return $this->requested;
}
@ -76,7 +83,7 @@ class Languages
/**
* Get preferred language code
*/
public function preferred(): ?string
public function preferred(): ?Language
{
return $this->preferred;
}
@ -94,21 +101,52 @@ class Languages
*/
public static function fromRequest(string $request): self
{
$languages = new static();
$config = Formwork::instance()->config();
if (preg_match('~^/(' . implode('|', $languages->available) . ')/~i', $request, $matches)) {
$languages->requested = $languages->current = $matches[1];
/**
* @var array<string>
*/
$available = (array) $config->get('languages.available');
if (preg_match('~^/(' . implode('|', $available) . ')/~i', $request, $matches)) {
$requested = $current = $matches[1];
}
if (Formwork::instance()->config()->get('languages.http_preferred')) {
/**
* @var bool
*/
if ($config->get('languages.http_preferred')) {
foreach (array_keys(HTTPNegotiation::language()) as $code) {
if (in_array($code, $languages->available, true)) {
$languages->preferred = $code;
if (in_array($code, $available, true)) {
$preferred = $available[$code];
break;
}
}
}
return $languages;
return new static([
'available' => $available,
'default' => $config->get('languages.default', $available[0] ?? null),
'current' => $current ?? null,
'requested' => $requested ?? null,
'preferred' => $preferred ?? null
]);
}
/**
* Get the proper `Language` instance
*/
protected function resolveLanguage(Language|string|null $language): ?Language
{
switch (true) {
case $language instanceof Language:
return $language;
case is_string($language):
return $this->available->get($language, null);
default:
return null;
}
}
}

View File

@ -0,0 +1,70 @@
<?php
namespace Formwork\Pages;
use Formwork\Files\File;
use Formwork\Parsers\YAML;
use Formwork\Utils\FileSystem;
use UnexpectedValueException;
class ContentFile extends File
{
/**
* Data from the YAML frontmatter
*/
protected array $frontmatter;
/**
* Content below the frontmatter
*/
protected string $content;
public function __construct(string $path)
{
parent::__construct($path);
$this->load();
}
/**
* Return whether the content file is empty
*/
public function isEmpty()
{
return $this->frontmatter === [];
}
/**
* Get the data from the YAML frontmatter
*/
public function frontmatter(): array
{
return $this->frontmatter;
}
/**
* Get the content
*/
public function content(): string
{
return $this->content;
}
/**
* Load data from the content file
*/
protected function load()
{
$contents = FileSystem::read($this->path);
if (!preg_match('/(?:\s|^)-{3}\s*(.+?)\s*-{3}\s*(.*?)\s*$/s', $contents, $matches)) {
throw new UnexpectedValueException('Invalid page format');
}
[, $frontmatter, $content] = $matches;
$this->frontmatter = YAML::parse($frontmatter);
$this->content = str_replace("\r\n", "\n", $content);
}
}

View File

@ -4,8 +4,10 @@ namespace Formwork\Pages;
use Formwork\Data\Contracts\Arrayable;
use Formwork\Fields\FieldCollection;
use Formwork\Files\Files;
use Formwork\Files\FileCollection;
use Formwork\Formwork;
use Formwork\Languages\Language;
use Formwork\Languages\Languages;
use Formwork\Metadata\MetadataCollection;
use Formwork\Pages\Templates\Template;
use Formwork\Pages\Traits\PageData;
@ -13,14 +15,14 @@ use Formwork\Pages\Traits\PageStatus;
use Formwork\Pages\Traits\PageTraversal;
use Formwork\Pages\Traits\PageUid;
use Formwork\Pages\Traits\PageUri;
use Formwork\Parsers\YAML;
use Formwork\Schemes\Scheme;
use Formwork\Utils\Arr;
use Formwork\Utils\Date;
use Formwork\Utils\FileSystem;
use Formwork\Utils\Path;
use Formwork\Utils\Str;
use Formwork\Utils\Uri;
use InvalidArgumentException;
use ReflectionClass;
use RuntimeException;
class Page implements Arrayable
@ -37,114 +39,79 @@ class Page implements Arrayable
public const NUM_REGEX = '/^(\d+)-/';
/**
* Page 'published' status
* Page `published` status
*/
public const PAGE_STATUS_PUBLISHED = 'published';
/**
* Page 'not published' status
* Page `not published` status
*/
public const PAGE_STATUS_NOT_PUBLISHED = 'not-published';
/**
* Page 'not routable' status
* Page `not routable` status
*/
public const PAGE_STATUS_NOT_ROUTABLE = 'not-routable';
/**
* Page path relative to content path
* Page path
*/
protected string $relativePath;
protected ?string $path = null;
/**
* Page unique identifier
* Page path relative to the content path
*/
protected string $uid;
protected ?string $relativePath = null;
/**
* Page content file
*/
protected ?ContentFile $contentFile = null;
/**
* Page route
*/
protected string $route;
protected ?string $route = null;
/**
* Page data
* Page canonical route
*/
protected array $data = [];
/**
* Page uri
*/
protected string $uri;
/**
* Page absolute uri
*/
protected string $absoluteUri;
/**
* Page last modified time
*/
protected int $lastModifiedTime;
/**
* Page modified date
*/
protected string $timestamp;
/**
* Page name (the name of the containing directory)
*/
protected string $name;
protected ?string $canonicalRoute = null;
/**
* Page slug
*/
protected string $slug;
protected ?string $slug = null;
/**
* Page language
* Page num used to order pages
*/
protected ?string $language;
protected ?int $num = null;
/**
* Available page languages
*/
protected array $availableLanguages;
protected Languages $languages;
/**
* Page filename
* Current page language
*/
protected string $filename;
/**
* Page template
*/
protected Template $template;
protected ?Language $language = null;
/**
* Page scheme
*/
protected Scheme $scheme;
/**
* Page files
*/
protected Files $files;
/**
* Page frontmatter
*/
protected array $frontmatter;
/**
* Page fields
*/
protected FieldCollection $fields;
/**
* Page canonical URI
* Page template
*/
protected ?string $canonical;
protected Template $template;
/**
* Page metadata
@ -152,39 +119,37 @@ class Page implements Arrayable
protected MetadataCollection $metadata;
/**
* Page response status
* Page files
*/
protected FileCollection $files;
/**
* Page HTTP response status
*/
protected int $responseStatus;
/**
* Page num (used to order pages)
* Page loading state
*/
protected ?int $num;
protected bool $loaded = false;
/**
* Create a new Page instance
*/
public function __construct(string $path)
public function __construct(array $data = [])
{
$this->path = FileSystem::normalizePath($path . DS);
$this->relativePath = Str::prepend(Path::makeRelative($this->path, Formwork::instance()->site()->path(), DS), DS);
$this->route = Uri::normalize(preg_replace('~[/\\\\](\d+-)~', '/', $this->relativePath));
$this->name = basename($this->relativePath);
$this->slug = basename($this->route);
$this->language = null;
$this->availableLanguages = [];
$this->setMultiple($data);
$this->loadFiles();
if (!$this->isEmpty()) {
$this->loadContents();
if ($this->hasContentFile() && !$this->contentFile->isEmpty()) {
$this->data = array_merge(
$this->data,
$this->contentFile->frontmatter(),
['content' => $this->contentFile->content()]
);
}
$this->fields->validate($this->data);
$this->loaded = true;
}
public function __toString(): string
@ -192,6 +157,14 @@ class Page implements Arrayable
return $this->title() ?? $this->slug();
}
/**
* Create page from the given path
*/
public static function fromPath(string $path, array $data = []): static
{
return new static(['path' => $path] + $data);
}
/**
* Return page default data
*/
@ -204,21 +177,28 @@ class Page implements Arrayable
'searchable' => true,
'cacheable' => true,
'orderable' => true,
'canonical' => null,
'canonicalRoute' => null,
'headers' => [],
'responseStatus' => 200,
'metadata' => []
'metadata' => [],
'content' => ''
];
// Merge with scheme default field values
$defaults = array_merge($defaults, Arr::reject($this->fields()->pluck('default'), fn ($value) => $value === null));
// If the page hasn't a num, by default it won't be listed
// If the page doesn't have a route, by default it won't be routable nor cacheable
if ($this->route() === null) {
$defaults['routable'] = false;
$defaults['cacheable'] = false;
}
// If the page doesn't have a num, by default it won't be listed
if ($this->num() === null) {
$defaults['listed'] = false;
}
// If the page hasn't a num or numbering is 'date', by default it won't be orderable
// If the page doesn't have a num or numbering is `date`, by default it won't be orderable
if ($this->num() === null || $this->scheme->get('num') === 'date') {
$defaults['orderable'] = false;
}
@ -226,20 +206,113 @@ class Page implements Arrayable
return $defaults;
}
/**
* Get page path
*/
public function path(): ?string
{
return $this->path;
}
/**
* Get page relative path
*/
public function relativePath(): ?string
{
return $this->relativePath;
}
/**
* Get page filename
*/
public function contentFile(): ?ContentFile
{
return $this->contentFile;
}
/**
* Get page route
*/
public function route(): ?string
{
return $this->route;
}
/**
* Get the canonical page URI, or `null` if not available
*/
public function canonical(): ?string
public function canonicalRoute(): ?string
{
if (isset($this->canonical)) {
return $this->canonical;
if (isset($this->canonicalRoute)) {
return $this->canonicalRoute;
}
return $this->canonical = !empty($this->data['canonical'])
? Path::normalize($this->data['canonical'])
return $this->canonicalRoute = !empty($this->data['canonicalRoute'])
? Path::normalize($this->data['canonicalRoute'])
: null;
}
/**
* Get page slug
*/
public function slug(): ?string
{
return $this->slug;
}
/**
* Get page num
*/
public function num(): ?int
{
if (isset($this->num)) {
return $this->num;
}
preg_match(self::NUM_REGEX, basename($this->relativePath()), $matches);
return $this->num = isset($matches[1]) ? (int) $matches[1] : null;
}
/**
* Get page languages
*/
public function languages(): Languages
{
return $this->languages;
}
/**
* Get page language
*/
public function language(): ?Language
{
return $this->language;
}
/**
* Get page scheme
*/
public function scheme(): Scheme
{
return $this->scheme;
}
/**
* Get page fields
*/
public function fields(): FieldCollection
{
return $this->fields;
}
/**
* Get page template
*/
public function template(): Template
{
return $this->template;
}
/**
* Get page metadata
*/
@ -255,7 +328,15 @@ class Page implements Arrayable
}
/**
* Get the page response status
* Get page files
*/
public function files(): FileCollection
{
return $this->files;
}
/**
* Get page HTTP response status
*/
public function responseStatus(): int
{
@ -267,78 +348,40 @@ class Page implements Arrayable
$this->responseStatus = (int) $this->data['responseStatus'];
// Get a default 404 Not Found status for the error page
if ($this->isErrorPage() && $this->responseStatus() === 200 && !isset($this->frontmatter['responseStatus'])) {
if ($this->isErrorPage() && $this->responseStatus() === 200
&& !isset($this->contentFile, $this->contentFile->frontmatter()['responseStatus'])) {
$this->responseStatus = 404;
}
return $this->responseStatus;
}
/**
* Page last modified time
*/
public function lastModifiedTime(): int
{
if (isset($this->lastModifiedTime)) {
return $this->lastModifiedTime;
}
return $this->lastModifiedTime = FileSystem::lastModifiedTime($this->path . $this->filename);
}
/**
* Timestamp representing the publication date (modification time as fallback)
*/
public function timestamp(): int
{
if (isset($this->timestamp)) {
return $this->timestamp;
}
return $this->timestamp = isset($this->data['publishDate'])
? Date::toTimestamp($this->data['publishDate'])
: $this->lastModifiedTime();
}
/**
* Get page num
*/
public function num(): ?int
{
if (isset($this->num)) {
return $this->num;
}
preg_match(self::NUM_REGEX, $this->name, $matches);
return $this->num = isset($matches[1]) ? (int) $matches[1] : null;
}
/**
* Set page language
*/
public function setLanguage(string $language): void
public function setLanguage(Language|string $language): void
{
if (!$this->hasLanguage($language)) {
throw new RuntimeException(sprintf('Invalid page language "%s"', $language));
if (is_string($language)) {
$language = new Language($language);
}
$path = $this->path;
$this->resetProperties();
$this->language = $language;
$this->__construct($path);
}
/**
* Get page files
*/
public function files(): Files
{
return $this->files;
if (!$this->hasLoaded()) {
$this->language = $language;
return;
}
if ($this->languages()->current()->code() !== ($code = $language->code())) {
if (!$this->languages()->available()->has($code)) {
throw new InvalidArgumentException(sprintf('Invalid page language "%s"', $code));
}
$this->reload(['language' => $language]);
}
}
/**
* Return a Files collection containing only images
*/
public function images(): Files
public function images(): FileCollection
{
return $this->files()->filterByType('image');
}
@ -352,13 +395,24 @@ class Page implements Arrayable
}
/**
* Return whether page is empty
* Return whether the page has a content file
*/
public function hasContentFile(): bool
{
return $this->contentFile !== null;
}
/**
* Return whether the page content data is empty
*/
public function isEmpty(): bool
{
return !isset($this->filename);
return $this->contentFile?->frontmatter() !== [];
}
/**
* Return whether the page is published
*/
public function isPublished(): bool
{
return $this->status() === self::PAGE_STATUS_PUBLISHED;
@ -405,11 +459,11 @@ class Page implements Arrayable
}
/**
* Return whether the page has the specified language
* Return whether the page has loaded
*/
public function hasLanguage(string $language): bool
public function hasLoaded(): bool
{
return in_array($language, $this->availableLanguages, true);
return $this->loaded;
}
/**
@ -417,11 +471,28 @@ class Page implements Arrayable
*
* @internal
*/
public function reload(): void
public function reload(array $data = []): void
{
if (!$this->hasLoaded()) {
throw new RuntimeException('Unable to reload, the page has not been loaded yet');
}
$path = $this->path;
$this->resetProperties();
$this->__construct($path);
$this->__construct($data + ['path' => $path]);
}
/**
* Set page path
*/
protected function setPath(string $path): void
{
$this->path = FileSystem::normalizePath($path . DS);
$this->relativePath = Str::prepend(Path::makeRelative($this->path, Formwork::instance()->site()->path(), DS), DS);
$this->route ??= Uri::normalize(preg_replace('~[/\\\\](\d+-)~', '/', $this->relativePath));
$this->slug ??= basename($this->route);
}
/**
@ -429,33 +500,52 @@ class Page implements Arrayable
*/
protected function loadFiles(): void
{
/**
* @var array<string, string>
*/
$contentFiles = [];
/**
* @var array<string>
*/
$files = [];
foreach (FileSystem::listFiles($this->path) as $file) {
$name = FileSystem::name($file);
/**
* @var array<string>
*/
$languages = [];
$extension = '.' . FileSystem::extension($file);
$config = Formwork::instance()->config();
if ($extension === Formwork::instance()->config()->get('content.extension')) {
$language = null;
$site = Formwork::instance()->site();
if (preg_match('/([a-z0-9]+)\.([a-z]+)/', $name, $matches)) {
// Parse double extension
[$match, $name, $language] = $matches;
}
if (isset($this->path) && FileSystem::isDirectory($this->path, assertExists: false)) {
foreach (FileSystem::listFiles($this->path) as $file) {
$name = FileSystem::name($file);
if (Formwork::instance()->site()->templates()->has($name)) {
$contentFiles[$language] = [
'filename' => $file,
'template' => $name
];
if ($language !== null && !in_array($language, $this->availableLanguages, true)) {
$this->availableLanguages[] = $language;
$extension = '.' . FileSystem::extension($file);
if ($extension === $config->get('content.extension')) {
$language = null;
if (preg_match('/([a-z0-9]+)\.([a-z]+)/', $name, $matches)) {
// Parse double extension
[, $name, $language] = $matches;
}
if ($site->templates()->has($name)) {
$contentFiles[$language] = [
'path' => FileSystem::joinPaths($this->path, $file),
'filename' => $file,
'template' => $name
];
if ($language !== null && !in_array($language, $languages, true)) {
$languages[] = $language;
}
}
} elseif (in_array($extension, $config->get('files.allowed_extensions'), true)) {
$files[] = $file;
}
} elseif (in_array($extension, Formwork::instance()->config()->get('files.allowed_extensions'), true)) {
$files[] = $file;
}
}
@ -463,45 +553,47 @@ class Page implements Arrayable
// Get correct content file based on current language
ksort($contentFiles);
$currentLanguage = $this->language ?? Formwork::instance()->site()->languages()->current();
// Language may already be set
$currentLanguage = $this->language ?? $site->languages()->current();
$key = isset($contentFiles[$currentLanguage]) ? $currentLanguage : array_keys($contentFiles)[0];
/**
* @var string
*/
$key = isset($currentLanguage, $contentFiles[$currentLanguage->code()])
? $currentLanguage->code()
: array_keys($contentFiles)[0];
// Set actual language
$this->language = $key ?: null;
$this->language ??= $key ? new Language($key) : null;
$this->filename = $contentFiles[$key]['filename'];
$this->contentFile ??= new ContentFile($contentFiles[$key]['path']);
$this->template = new Template($contentFiles[$key]['template'], $this);
$this->template ??= new Template($contentFiles[$key]['template'], $this);
$this->scheme = Formwork::instance()->schemes()->get('pages', $this->template);
$this->scheme ??= Formwork::instance()->schemes()->get('pages', $this->template);
} else {
$this->template ??= new Template('default', $this);
$this->fields = $this->scheme()->fields();
$this->scheme ??= Formwork::instance()->schemes()->get('pages', 'default');
}
$this->files = Files::fromPath($this->path, $files);
}
$this->fields ??= $this->scheme()->fields();
/**
* Parse page content
*/
protected function loadContents(): void
{
$contents = FileSystem::read($this->path . $this->filename);
$defaultLanguage = in_array((string) $site->languages()->default(), $languages, true)
? $site->languages()->default()
: null;
if (!preg_match('/(?:\s|^)-{3}\s*(.+?)\s*-{3}\s*(.*?)\s*$/s', $contents, $matches)) {
throw new RuntimeException('Invalid page format');
}
$this->languages ??= new Languages([
'available' => $languages,
'default' => $defaultLanguage,
'current' => $this->language ?? null,
'requested' => $site->languages()->requested(),
'preferred' => $site->languages()->preferred()
]);
[, $rawFrontmatter, $rawContent] = $matches;
$this->files ??= isset($this->path) ? FileCollection::fromPath($this->path, $files) : new FileCollection();
$this->frontmatter = YAML::parse($rawFrontmatter);
$rawContent = str_replace("\r\n", "\n", $rawContent);
$this->data = array_merge($this->defaults(), $this->frontmatter, ['content' => $rawContent]);
$this->fields->validate($this->data);
$this->data = array_merge($this->defaults(), $this->data);
}
/**
@ -509,8 +601,14 @@ class Page implements Arrayable
*/
protected function resetProperties(): void
{
foreach (array_keys(get_class_vars(static::class)) as $property) {
unset($this->$property);
$reflectionClass = new ReflectionClass($this);
foreach ($reflectionClass->getProperties() as $property) {
unset($this->{$property->getName()});
if ($property->hasDefaultValue()) {
$this->{$property->getName()} = $property->getDefaultValue();
}
}
}
}

View File

@ -12,6 +12,8 @@ class PageCollection extends AbstractCollection implements Paginable
{
protected ?string $dataType = Page::class . '|' . Site::class;
protected bool $associative = true;
/**
* Pagination related to the collection
*/
@ -111,6 +113,9 @@ class PageCollection extends AbstractCollection implements Paginable
*/
public static function fromPath(string $path, bool $recursive = false): self
{
/**
* @var array<string, Page>
*/
$pages = [];
foreach (FileSystem::listDirectories($path) as $dir) {
@ -119,8 +124,8 @@ class PageCollection extends AbstractCollection implements Paginable
if ($dir[0] !== '_' && FileSystem::isDirectory($pagePath)) {
$page = Formwork::instance()->site()->retrievePage($pagePath);
if (!$page->isEmpty()) {
$pages[] = $page;
if ($page->hasContentFile()) {
$pages[$page->route()] = $page;
}
if ($recursive) {
@ -129,8 +134,8 @@ class PageCollection extends AbstractCollection implements Paginable
}
}
$pages = new static($pages);
$pageCollection = new static($pages);
return $pages->sortBy('relativePath');
return $pageCollection->sortBy('relativePath');
}
}

View File

@ -13,6 +13,7 @@ use Formwork\Pages\Traits\PageTraversal;
use Formwork\Pages\Traits\PageUid;
use Formwork\Pages\Traits\PageUri;
use Formwork\Schemes\Scheme;
use Formwork\Utils\Arr;
use Formwork\Utils\FileSystem;
class Site implements Arrayable
@ -22,30 +23,35 @@ class Site implements Arrayable
use PageUid;
use PageUri;
/**
* Site path
*/
protected ?string $path = null;
/**
* Site relative path
*/
protected string $relativePath;
protected ?string $relativePath = null;
/**
* Site content file
*/
protected ?ContentFile $contentFile = null;
/**
* Site route
*/
protected string $route;
protected ?string $route = null;
/**
* Site last modified time
* Site canonical route
*/
protected int $lastModifiedTime;
protected ?string $canonicalRoute = null;
/**
* Site storage (loaded pages)
* Site slug
*/
protected array $storage = [];
/**
* Site current page
*/
protected ?Page $currentPage = null;
protected ?string $slug = null;
/**
* Site languages
@ -67,36 +73,36 @@ class Site implements Arrayable
*/
protected TemplateCollection $templates;
/**
* Site aliases
*/
protected array $aliases;
/**
* Site metadata
*/
protected MetadataCollection $metadata;
/**
* Site storage (loaded pages)
*/
protected array $storage = [];
/**
* Site current page
*/
protected ?Page $currentPage = null;
/**
* Site aliases
*/
protected array $routeAliases;
/**
* Create a new Site instance
*/
public function __construct(array $data)
public function __construct(array $data = [])
{
$this->path = FileSystem::normalizePath(Formwork::instance()->config()->get('content.path'));
$this->setMultiple($data);
$this->relativePath = DS;
$this->load();
$this->route = '/';
$this->scheme = Formwork::instance()->schemes()->get('config', 'site');
$this->data = array_replace_recursive($this->defaults(), $data);
$this->fields = $this->scheme->fields()->validate($this->data);
$this->templates = TemplateCollection::fromPath(Formwork::instance()->config()->get('templates.path'));
$this->loadAliases();
$this->fields->validate($this->data);
}
public function __toString()
@ -104,29 +110,150 @@ class Site implements Arrayable
return $this->title();
}
public static function fromPath(string $path, array $data = []): static
{
return new static(['path' => $path] + $data);
}
/**
* Return site default data
*/
public function defaults(): array
{
// Formwork::instance()->schemes()->get('config', 'site')->fields();
return [
'title' => 'Formwork',
'aliases' => [],
'metadata' => [],
'canonical' => null
$defaults = [
'title' => 'Formwork',
'author' => '',
'description' => '',
'metadata' => [],
'canonicalRoute' => null,
'routeAliases' => []
];
$defaults = array_merge($defaults, Arr::reject($this->fields()->pluck('default'), fn ($value) => $value === null));
return $defaults;
}
/**
* Get the site last modified time
* Get site path
*/
public function lastModifiedTime(): int
public function path(): ?string
{
if (isset($this->lastModifiedTime)) {
return $this->lastModifiedTime;
return $this->path;
}
/**
* Get site relative path
*/
public function relativePath(): string
{
return $this->relativePath;
}
/**
* Get site filename
*/
public function contentFile(): ?ContentFile
{
return $this->contentFile;
}
/**
* Get site route
*/
public function route(): string
{
return $this->route;
}
/**
* Get site canonical route
*/
public function canonicalRoute(): ?string
{
return $this->canonicalRoute;
}
/**
* Get site slug
*/
public function slug(): string
{
return $this->slug;
}
/**
* Get site languages
*/
public function languages(): Languages
{
return $this->languages;
}
/**
* Get site scheme
*/
public function scheme(): Scheme
{
return $this->scheme;
}
/**
* Get site fields
*/
public function fields(): FieldCollection
{
return $this->fields;
}
/**
* Get site templates
*/
public function templates(): TemplateCollection
{
return $this->templates;
}
/**
* Get the current page of the site
*/
public function currentPage(): ?Page
{
return $this->currentPage;
}
/**
* Get site route aliases
*/
public function routeAliases(): array
{
return $this->routeAliases;
}
/**
* Get site metadata
*/
public function metadata(): MetadataCollection
{
if (isset($this->metadata)) {
return $this->metadata;
}
return $this->lastModifiedTime = FileSystem::lastModifiedTime($this->path);
$defaults = [
'charset' => Formwork::instance()->config()->get('charset'),
'author' => $this->get('author'),
'description' => $this->get('description'),
'generator' => 'Formwork',
'routeAliases' => []
];
$data = array_filter(array_merge($defaults, $this->data['metadata']));
if (!Formwork::instance()->config()->get('metadata.set_generator')) {
unset($data['generator']);
}
return $this->metadata = new MetadataCollection($data);
}
/**
@ -161,7 +288,7 @@ class Site implements Arrayable
if (isset($this->storage[$path])) {
return $this->storage[$path];
}
return $this->storage[$path] = new Page($path);
return $this->storage[$path] = Page::fromPath($path);
}
/**
@ -192,7 +319,7 @@ class Site implements Arrayable
$page = $this->retrievePage($path);
return !$page->isEmpty() ? $page : null;
return $page->hasContentFile() ? $page : null;
}
/**
@ -203,45 +330,12 @@ class Site implements Arrayable
return $this->currentPage = $page;
}
/**
* Set site languages
*/
public function setLanguages(Languages $languages): void
{
$this->languages = $languages;
}
/**
* Return alias of a given route
*/
public function resolveAlias(string $route): ?string
public function resolveRouteAlias(string $route): ?string
{
return $this->aliases[$route] ?? null;
}
/**
* Get site metadata
*/
public function metadata(): MetadataCollection
{
if (isset($this->metadata)) {
return $this->metadata;
}
$defaults = [
'charset' => Formwork::instance()->config()->get('charset'),
'author' => $this->get('author'),
'description' => $this->get('description'),
'generator' => 'Formwork'
];
$data = array_filter(array_merge($defaults, $this->data['metadata']));
if (!Formwork::instance()->config()->get('metadata.set_generator')) {
unset($data['generator']);
}
return $this->metadata = new MetadataCollection($data);
return $this->routeAliases[$route] ?? null;
}
/**
@ -292,21 +386,46 @@ class Site implements Arrayable
return false;
}
/**
* Return whether the page has the specified language
*/
public function hasLanguage(string $language): bool
protected function load()
{
return in_array($language, $this->availableLanguages, true);
$this->scheme = Formwork::instance()->schemes()->get('config', 'site');
$this->fields = $this->scheme->fields();
$this->templates = TemplateCollection::fromPath(Formwork::instance()->config()->get('templates.path'));
$this->data = array_merge($this->defaults(), $this->data);
$this->loadRouteAliases();
}
/**
* Site storage
*/
protected function storage(): array
{
return $this->storage;
}
protected function setPath(string $path): void
{
$this->path = FileSystem::normalizePath($path . DS);
$this->relativePath = DS;
$this->route = '/';
$this->slug = '';
}
/**
* Load site aliases
*/
protected function loadAliases(): void
protected function loadRouteAliases(): void
{
foreach ($this->data['aliases'] as $from => $to) {
$this->aliases[trim($from, '/')] = trim($to, '/');
$this->routeAliases = [];
foreach ($this->data['routeAliases'] as $from => $to) {
$this->routeAliases[trim($from, '/')] = trim($to, '/');
}
}
}

View File

@ -39,10 +39,10 @@ trait PageData
// Call getter method if exists. We check property existence before
// to avoid using get to call methods arbitrarily
if (method_exists($this, $key)) {
return $this->$key();
return $this->{$key}();
}
return $this->$key;
return $this->{$key} ?? $default;
}
// Get values from fields
@ -67,10 +67,17 @@ trait PageData
public function set(string $key, $value): void
{
if (property_exists($this, $key)) {
$this->$key = $value;
} else {
Arr::set($this->data, $key, $value);
// If defined use a setter
if (method_exists($this, $setter = 'set' . ucfirst($key))) {
$this->{$setter}($value);
return;
}
$this->{$key} = $value;
return;
}
Arr::set($this->data, $key, $value);
}
/**
@ -88,4 +95,12 @@ trait PageData
return $data;
}
/**
* Get page data
*/
public function data(): array
{
return $this->data;
}
}

View File

@ -7,6 +7,11 @@ use Formwork\Utils\Date;
trait PageStatus
{
/**
* Page data
*/
protected array $data = [];
/**
* Page status
*/
@ -21,14 +26,19 @@ trait PageStatus
return $this->status;
}
$published = $this->data['published'];
/**
* @var bool
*/
$published = $this->get('published', true);
$now = time();
/** @var ?string */
if ($publishDate = $this->data['publishDate'] ?? null) {
$published = $published && Date::toTimestamp($publishDate) < $now;
}
/** @var ?string */
if ($unpublishDate = $this->data['unpublishDate'] ?? null) {
$published = $published && Date::toTimestamp($unpublishDate) > $now;
}

View File

@ -9,11 +9,6 @@ use Formwork\Pages\Site;
trait PageTraversal
{
/**
* Page path
*/
protected string $path;
/**
* Parent page
*/
@ -44,6 +39,16 @@ trait PageTraversal
*/
protected PageCollection $inclusiveSiblings;
/**
* Get page or site path
*/
abstract public function path(): ?string;
/**
* Get page or site route
*/
abstract public function route(): ?string;
/**
* Return whether the page is site
*/
@ -58,13 +63,13 @@ trait PageTraversal
return $this->parent;
}
if ($this->isSite()) {
if ($this->isSite() || $this->path() === null) {
return $this->parent = null;
}
$parentPath = dirname($this->path) . DS;
$parentPath = dirname($this->path()) . DS;
if ($parentPath === Formwork::instance()->config()->get('content.path')) {
if ($parentPath === Formwork::instance()->site()->path()) {
return $this->parent = Formwork::instance()->site();
}
@ -96,7 +101,11 @@ trait PageTraversal
return $this->children;
}
return $this->children = PageCollection::fromPath($this->path);
if ($this->path() === null) {
return $this->children = new PageCollection();
}
return $this->children = PageCollection::fromPath($this->path());
}
/**
@ -124,7 +133,11 @@ trait PageTraversal
return $this->descendants;
}
return $this->descendants = PageCollection::fromPath($this->path, recursive: true);
if ($this->path() === null) {
return $this->descendants = new PageCollection();
}
return $this->descendants = PageCollection::fromPath($this->path(), recursive: true);
}
/**
@ -157,7 +170,7 @@ trait PageTraversal
$page = $this;
while (($parent = $page->parent()) !== null) {
$ancestors[] = $parent;
$ancestors[$parent->route()] = $parent;
$page = $parent;
}
@ -205,8 +218,8 @@ trait PageTraversal
return $this->inclusiveSiblings;
}
if ($this->isSite()) {
return $this->inclusiveSiblings = new PageCollection([$this]);
if ($this->isSite() || $this->path() === null) {
return $this->inclusiveSiblings = new PageCollection([$this->route() => $this]);
}
return $this->inclusiveSiblings = $this->parent()->children();

View File

@ -11,6 +11,11 @@ trait PageUid
*/
protected string $uid;
/**
* Get page or site relative path
*/
abstract public function relativePath(): ?string;
/**
* Get the page unique identifier
*/
@ -19,6 +24,9 @@ trait PageUid
if (isset($this->uid)) {
return $this->uid;
}
return $this->uid = Str::chunk(substr(hash('sha256', $this->relativePath), 0, 32), 8, '-');
$id = $this->relativePath() ?: spl_object_hash($this);
return $this->uid = Str::chunk(substr(hash('sha256', (string) $id), 0, 32), 8, '-');
}
}

View File

@ -10,14 +10,14 @@ use Formwork\Utils\Uri;
trait PageUri
{
/**
* Page route
* Get page or site route
*/
protected string $route;
abstract public function route(): ?string;
/**
* Page absolute URI
* Get page or site canonical route
*/
protected string $absoluteUri;
abstract public function canonicalRoute(): ?string;
/**
* Return a URI relative to page
@ -26,7 +26,7 @@ trait PageUri
{
$base = HTTPRequest::root();
$route = $this->canonical() ?? $this->route;
$route = $this->canonicalRoute() ?? $this->route();
if ($includeLanguage) {
$language = is_string($includeLanguage) ? $includeLanguage : Formwork::instance()->site()->languages()->current();
@ -45,11 +45,8 @@ trait PageUri
/**
* Get page absolute URI
*/
public function absoluteUri(): string
public function absoluteUri(string $path = '', bool|string $includeLanguage = true): string
{
if (isset($this->absoluteUri)) {
return $this->absoluteUri;
}
return $this->absoluteUri = Uri::resolveRelative($this->uri());
return Uri::resolveRelative($this->uri($path, $includeLanguage));
}
}

View File

@ -24,10 +24,14 @@ class DashboardController extends AbstractController
$this->modal('deletePage');
$timestamps = $this->site()->descendants()
->everyItem()->contentFile()
->everyItem()->lastModifiedTime();
return new Response($this->view('dashboard.index', [
'title' => $this->translate('panel.dashboard.dashboard'),
'lastModifiedPages' => $this->view('pages.list', [
'pages' => $this->site()->descendants()->sortBy('lastModifiedTime', direction: SORT_DESC)->slice(0, 5),
'pages' => $this->site()->descendants()->sort(direction: SORT_DESC, sortBy: $timestamps->toArray())->limit(5),
'subpages' => false,
'class' => 'pages-list-top',
'parent' => null,

View File

@ -49,7 +49,7 @@ class OptionsController extends AbstractController
// Touch content folder to invalidate cache
if ($differ) {
FileSystem::touch(Formwork::instance()->config()->get('content.path'));
FileSystem::touch(Formwork::instance()->site()->path());
}
$this->panel()->notify($this->translate('panel.options.updated'), 'success');
@ -89,7 +89,7 @@ class OptionsController extends AbstractController
// Touch content folder to invalidate cache
if ($differ) {
FileSystem::touch(Formwork::instance()->config()->get('content.path'));
FileSystem::touch(Formwork::instance()->site()->path());
}
$this->panel()->notify($this->translate('panel.options.updated'), 'success');

View File

@ -5,9 +5,9 @@ namespace Formwork\Panel\Controllers;
use Formwork\Data\DataGetter;
use Formwork\Exceptions\TranslatedException;
use Formwork\Fields\FieldCollection;
use Formwork\Files\File;
use Formwork\Files\Image;
use Formwork\Formwork;
use Formwork\Languages\LanguageCodes;
use Formwork\Pages\Page;
use Formwork\Pages\Site;
use Formwork\Panel\Uploader;
@ -16,6 +16,7 @@ use Formwork\Response\JSONResponse;
use Formwork\Response\RedirectResponse;
use Formwork\Response\Response;
use Formwork\Router\RouteParams;
use Formwork\Utils\Date;
use Formwork\Utils\FileSystem;
use Formwork\Utils\HTTPRequest;
use Formwork\Utils\Str;
@ -111,7 +112,7 @@ class PagesController extends AbstractController
return $this->redirect('/pages/' . trim($page->route(), '/') . '/edit/language/' . $this->site()->languages()->default() . '/');
}
if ($page->hasLanguage($language)) {
if ($page->languages()->available()->has($language)) {
$page->setLanguage($language);
}
} elseif ($page->language() !== null) {
@ -150,6 +151,7 @@ class PagesController extends AbstractController
if (HTTPRequest::hasFiles()) {
try {
$this->processPageUploads($page);
$page->reload();
} catch (TranslatedException $e) {
$this->panel()->notify($this->translate('panel.uploader.error', $e->getTranslatedMessage()), 'error');
}
@ -176,13 +178,12 @@ class PagesController extends AbstractController
$this->modal('deleteFile');
return new Response($this->view('pages.editor', [
'title' => $this->translate('panel.pages.edit-page', $page->title()),
'page' => $page,
'fields' => $fields,
'templates' => $this->site()->templates()->keys(),
'parents' => $this->site()->descendants()->sortBy('relativePath'),
'currentLanguage' => $params->get('language', $page->language()),
'availableLanguages' => $this->availableSiteLanguages()
'title' => $this->translate('panel.pages.edit-page', $page->title()),
'page' => $page,
'fields' => $fields,
'templates' => $this->site()->templates()->keys(),
'parents' => $this->site()->descendants()->sortBy('relativePath'),
'currentLanguage' => $params->get('language', $page->language()?->code())
], true));
}
@ -208,6 +209,9 @@ class PagesController extends AbstractController
return JSONResponse::error($this->translate('panel.pages.page.cannot-move'));
}
/**
* @var array<string, Page>
*/
$pages = $parent->children()->toArray();
$from = max(0, $data->get('from'));
@ -218,8 +222,8 @@ class PagesController extends AbstractController
array_splice($pages, $to, 0, array_splice($pages, $from, 1));
foreach ($pages as $i => $page) {
$name = $page->name();
foreach (array_values($pages) as $i => $page) {
$name = basename($page->relativePath());
if ($name === null) {
continue;
}
@ -248,7 +252,7 @@ class PagesController extends AbstractController
if ($params->has('language')) {
$language = $params->get('language');
if ($page->hasLanguage($language)) {
if ($page->languages()->available()->has($language)) {
$page->setLanguage($language);
} else {
$this->panel()->notify($this->translate('panel.pages.page.cannot-delete.invalid-language', $language), 'error');
@ -262,8 +266,8 @@ class PagesController extends AbstractController
}
// Delete just the content file only if there are more than one language
if ($params->has('language') && count($page->availableLanguages()) > 1) {
FileSystem::delete($page->path() . $page->filename());
if ($params->has('language') && count($page->languages()->available()) > 1) {
FileSystem::delete($page->contentFile()->path());
} else {
FileSystem::delete($page->path(), true);
}
@ -376,16 +380,16 @@ class PagesController extends AbstractController
FileSystem::createFile($path . $filename);
$frontmatter = [
$contentData = [
'title' => $data->get('title'),
'published' => false
];
$fileContent = Str::wrap(YAML::encode($frontmatter), '---' . PHP_EOL);
$fileContent = Str::wrap(YAML::encode($contentData), '---' . PHP_EOL);
FileSystem::write($path . $filename, $fileContent);
return new Page($path);
return Page::fromPath($path);
}
/**
@ -399,7 +403,7 @@ class PagesController extends AbstractController
}
// Load current page frontmatter
$frontmatter = $page->frontmatter();
$frontmatter = $page->contentFile()->frontmatter();
// Preserve the title if not given
if (!empty($data->get('title'))) {
@ -432,7 +436,7 @@ class PagesController extends AbstractController
throw new TranslatedException('Invalid page language', 'panel.pages.page.cannot-edit.invalid-language');
}
$differ = $frontmatter !== $page->frontmatter() || $content !== $page->data()['content'] || $language !== $page->language();
$differ = $frontmatter !== $page->contentFile()->frontmatter() || $content !== $page->data()['content'] || $language !== $page->language();
if ($differ) {
$filename = $data->get('template');
@ -442,19 +446,24 @@ class PagesController extends AbstractController
$fileContent = Str::wrap(YAML::encode($frontmatter), '---' . PHP_EOL) . $content;
FileSystem::write($page->path() . $filename, $fileContent);
FileSystem::touch(Formwork::instance()->config()->get('content.path'));
FileSystem::touch(Formwork::instance()->site()->path());
// Update page with the new data
$page->reload();
// Set correct page language if it has changed
if ($language !== $page->language()) {
if ($language !== $page->language()?->code()) {
$page->setLanguage($language);
}
// Check if page number has to change
if ($page->scheme()->get('num') === 'date' && $page->num() !== ($num = (int) date(self::DATE_NUM_FORMAT, $page->timestamp()))) {
$name = preg_replace(Page::NUM_REGEX, $num . '-', $page->name());
$timestamp = isset($page->data()['publishDate'])
? Date::toTimestamp($page->data()['publishDate'])
: $page->contentFile()->lastModifiedTime();
if ($page->scheme()->get('num') === 'date' && $page->num() !== ($num = (int) date(self::DATE_NUM_FORMAT, $timestamp))) {
$name = preg_replace(Page::NUM_REGEX, $num . '-', basename($page->relativePath()));
try {
$page = $this->changePageName($page, $name);
} catch (RuntimeException $e) {
@ -504,16 +513,15 @@ class PagesController extends AbstractController
{
$uploader = new Uploader($page->path());
$uploader->upload();
$page->reload();
/**
* @var File
*/
foreach ($uploader->uploadedFiles() as $file) {
$file = $page->files()->get($file);
// Process JPEG and PNG images according to system options (e.g. quality)
if (Formwork::instance()->config()->get('images.process_uploads') && in_array($file->mimeType(), ['image/jpeg', 'image/png'], true)) {
$image = new Image($file->path());
$image->saveOptimized();
$page->reload();
}
}
}
@ -551,7 +559,7 @@ class PagesController extends AbstractController
$directory = dirname($page->path());
$destination = FileSystem::joinPaths($directory, $name, DS);
FileSystem::moveDirectory($page->path(), $destination);
return new Page($destination);
return Page::fromPath($destination);
}
/**
@ -559,9 +567,9 @@ class PagesController extends AbstractController
*/
protected function changePageParent(Page $page, Page|Site $parent): Page
{
$destination = FileSystem::joinPaths($parent->path(), $page->name(), DS);
$destination = FileSystem::joinPaths($parent->path(), basename($page->relativePath()), DS);
FileSystem::moveDirectory($page->path(), $destination);
return new Page($destination);
return Page::fromPath($destination);
}
/**
@ -570,9 +578,8 @@ class PagesController extends AbstractController
protected function changePageTemplate(Page $page, string $template): Page
{
$destination = $page->path() . $template . Formwork::instance()->config()->get('content.extension');
FileSystem::move($page->path() . $page->filename(), $destination);
$page->reload();
return $page;
FileSystem::move($page->contentFile()->path(), $destination);
return Page::fromPath($page->path());
}
/**
@ -595,16 +602,4 @@ class PagesController extends AbstractController
{
return (bool) preg_match(self::SLUG_REGEX, $slug);
}
/**
* Return an array containing the available site languages as keys with proper labels as values
*/
protected function availableSiteLanguages(): array
{
$languages = [];
foreach (Formwork::instance()->config()->get('languages.available') as $code) {
$languages[$code] = LanguageCodes::hasCode($code) ? LanguageCodes::codeToNativeName($code) . ' (' . $code . ')' : $code;
}
return $languages;
}
}

View File

@ -7,7 +7,7 @@ use Formwork\Formwork;
use Formwork\Languages\LanguageCodes;
use Formwork\Panel\Controllers\ErrorsController;
use Formwork\Panel\Users\User;
use Formwork\Panel\Users\Users;
use Formwork\Panel\Users\UserCollection;
use Formwork\Utils\FileSystem;
use Formwork\Utils\HTTPRequest;
use Formwork\Utils\Notification;
@ -21,7 +21,7 @@ final class Panel
/**
* All the registered users
*/
protected Users $users;
protected UserCollection $users;
/**
* Errors controller
@ -44,7 +44,7 @@ final class Panel
public function load(): void
{
$this->loadSchemes();
$this->users = Users::load();
$this->users = UserCollection::load();
$this->loadTranslations();
$this->loadErrorHandler();
}
@ -61,7 +61,7 @@ final class Panel
/**
* Return all registered users
*/
public function users(): Users
public function users(): UserCollection
{
return $this->users;
}

View File

@ -3,6 +3,7 @@
namespace Formwork\Panel;
use Formwork\Exceptions\TranslatedException;
use Formwork\Files\FileCollection;
use Formwork\Formwork;
use Formwork\Utils\FileSystem;
use Formwork\Utils\HTTPRequest;
@ -49,7 +50,7 @@ class Uploader
/**
* Array containing uploaded files
*/
protected array $uploadedFiles = [];
protected FileCollection $uploadedFiles;
/**
* Create a new Uploader instance
@ -85,19 +86,25 @@ class Uploader
if (!HTTPRequest::hasFiles()) {
return false;
}
$count = count(HTTPRequest::files());
$filenames = [];
foreach (HTTPRequest::files() as $file) {
if ($file['error'] === 0) {
if ($name === null || $count > 1) {
$name = $file['name'];
}
$this->move($file['tmp_name'], $this->destination, $name);
$filenames[] = $this->move($file['tmp_name'], $this->destination, $name);
} else {
throw new TranslatedException(self::ERROR_MESSAGES[$file['error']], self::ERROR_LANGUAGE_STRINGS[$file['error']]);
}
}
$this->uploadedFiles = FileCollection::fromPath($this->destination, $filenames);
return true;
}
@ -115,17 +122,15 @@ class Uploader
/**
* Return uploaded files
*/
public function uploadedFiles(): array
public function uploadedFiles(): FileCollection
{
return $this->uploadedFiles;
}
/**
* Move uploaded file to a destination
*
* @return bool Whether file was successfully moved or not
*/
protected function move(string $source, string $destination, string $filename): bool
protected function move(string $source, string $destination, string $filename): string
{
$mimeType = FileSystem::mimeType($source);
@ -165,12 +170,9 @@ class Uploader
}
if (@move_uploaded_file($source, $destinationPath)) {
$this->uploadedFiles[] = $filename;
return true;
return $filename;
}
throw new TranslatedException('Cannot move uploaded file to destination', 'panel.uploader.error.cannot-move-to-destination');
return false;
}
}

View File

@ -26,7 +26,7 @@ class Permissions
*/
public function __construct(string $name)
{
$this->permissions = array_merge($this->permissions, Users::getRolePermissions($name));
$this->permissions = array_merge($this->permissions, UserCollection::getRolePermissions($name));
}
/**

View File

@ -81,7 +81,7 @@ class User implements Arrayable
{
$this->data = array_merge($this->defaults, $data);
foreach (['username', 'fullname', 'hash', 'email', 'language', 'role'] as $var) {
$this->$var = $this->data[$var];
$this->{$var} = $this->data[$var];
}
$this->permissions = new Permissions($this->role);

View File

@ -7,7 +7,7 @@ use Formwork\Formwork;
use Formwork\Parsers\YAML;
use Formwork\Utils\FileSystem;
class Users extends AbstractCollection
class UserCollection extends AbstractCollection
{
protected bool $associative = true;

View File

@ -66,7 +66,7 @@ class Router
/**
* Route params
*/
protected ?RouteParams $params;
protected RouteParams $params;
public function __construct(?string $request = null)
{
@ -75,6 +75,8 @@ class Router
// Ensure requested route is wrapped in slashes
$this->request = Str::wrap($request ?? Uri::path(HTTPRequest::uri()), '/');
$this->params = new RouteParams([]);
}
/**

View File

@ -40,7 +40,7 @@ fields:
label: '{{panel.user.role}}'
disabled: true
import:
options: 'Formwork\Panel\Users\Users::availableRoles'
options: 'Formwork\Panel\Users\UserCollection::availableRoles'
color-scheme:

View File

@ -31,15 +31,15 @@
endif;
?>
<?php
if ($availableLanguages):
if (!$site->languages()->available()->isEmpty()):
?>
<div class="dropdown">
<button type="button" class="dropdown-button button-accent" data-dropdown="languages-dropdown"><?= $this->icon('translate') ?> <?= $this->translate('panel.pages.languages') ?><?php if ($currentLanguage): ?> <span class="page-language"><?= $currentLanguage ?></span><?php endif; ?></button>
<div class="dropdown-menu" id="languages-dropdown">
<?php
foreach ($availableLanguages as $languageCode => $languageLabel):
foreach ($site->languages()->available() as $language):
?>
<a href="<?= $panel->uri('/pages/' . trim($page->route(), '/') . '/edit/language/' . $languageCode . '/') ?>" class="dropdown-item"><?= $page->hasLanguage($languageCode) ? $this->translate('panel.pages.languages.edit-language', $languageLabel) : $this->translate('panel.pages.languages.add-language', $languageLabel); ?></a>
<a href="<?= $panel->uri('/pages/' . trim($page->route(), '/') . '/edit/language/' . $language . '/') ?>" class="dropdown-item"><?= $page->languages()->available()->has($language) ? $this->translate('panel.pages.languages.edit-language', $language->nativeName() . ' (' . $language->code() . ')') : $this->translate('panel.pages.languages.add-language', $language->nativeName() . ' (' . $language->code() . ')'); ?></a>
<?php
endforeach;
?>

View File

@ -14,7 +14,7 @@
<?php
foreach ($pages as $page):
$routable = $page->published() && $page->routable();
$date = $this->datetime($page->lastModifiedTime());
$date = $this->datetime($page->contentFile()->lastModifiedTime());
?>
<li class="<?php if ($subpages): ?>pages-level-<?= $page->level() ?><?php endif; ?>" <?php if (!$page->orderable()): ?>data-sortable="false"<?php endif; ?>>
<div class="pages-item">
@ -37,9 +37,9 @@
<?= $this->icon($page->get('icon', 'page')) ?>
<a href="<?= $panel->uri('/pages/' . trim($page->route(), '/') . '/edit/') ?>" title="<?= $this->escapeAttr($page->title()) ?>"><?= $this->escape($page->title()) ?></a>
<?php
foreach ($page->availableLanguages() as $code):
foreach ($page->languages()->available() as $language):
?>
<span class="page-language"><?= $code ?></span>
<span class="page-language"><?= $language->code() ?></span>
<?php
endforeach;
?>

View File

@ -12,7 +12,7 @@ layout:
label: '{{panel.options.site.advanced}}'
collapsible: true
collapsed: true
fields: [metadata, aliases]
fields: [metadata, routeAliases]
fields:
title:
@ -23,13 +23,11 @@ fields:
author:
type: text
label: '{{panel.options.site.info.author}}'
default: null
description:
type: textarea
label: '{{panel.options.site.info.description}}'
default: null
metadata:
type: array
@ -38,7 +36,7 @@ fields:
placeholder_key: '{{panel.options.site.advanced.metadata.name}}'
placeholder_value: '{{panel.options.site.advanced.metadata.content}}'
aliases:
routeAliases:
type: array
label: '{{panel.options.site.advanced.aliases}}'
associative: true

25
site/schemes/default.yml Normal file
View File

@ -0,0 +1,25 @@
title: Default
fields:
title:
type: text
class: input-large
required: true
content:
type: markdown
published:
type: checkbox
label: '{{panel.pages.status.published}}'
default: true
parent:
type: page.parents
access: panel
label: '{{panel.pages.parent}}'
template:
type: page.template
access: panel
label: '{{panel.pages.template}}'

View File

@ -0,0 +1,7 @@
<?= $this->layout('site') ?>
<main>
<div class="container">
<h1><?= $page->title() ?></h1>
<?= $page->content() ?>
</div>
</main>