mirror of
https://github.com/getformwork/formwork.git
synced 2025-01-17 13:38:22 +01:00
Rewrite Page
, Site
and related classes
This commit is contained in:
parent
e1f88af263
commit
e629e1bbf2
@ -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
|
||||
|
@ -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
|
||||
*/
|
||||
|
@ -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;
|
||||
|
@ -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
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
60
formwork/src/Languages/Language.php
Normal file
60
formwork/src/Languages/Language.php
Normal 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;
|
||||
}
|
||||
}
|
18
formwork/src/Languages/LanguageCollection.php
Normal file
18
formwork/src/Languages/LanguageCollection.php
Normal 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)])));
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
70
formwork/src/Pages/ContentFile.php
Normal file
70
formwork/src/Pages/ContentFile.php
Normal 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);
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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');
|
||||
}
|
||||
}
|
||||
|
@ -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, '/');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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();
|
||||
|
@ -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, '-');
|
||||
}
|
||||
}
|
||||
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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');
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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));
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
|
@ -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([]);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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:
|
||||
|
@ -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;
|
||||
?>
|
||||
|
@ -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;
|
||||
?>
|
||||
|
@ -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
25
site/schemes/default.yml
Normal 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}}'
|
7
site/templates/default.php
Normal file
7
site/templates/default.php
Normal file
@ -0,0 +1,7 @@
|
||||
<?= $this->layout('site') ?>
|
||||
<main>
|
||||
<div class="container">
|
||||
<h1><?= $page->title() ?></h1>
|
||||
<?= $page->content() ?>
|
||||
</div>
|
||||
</main>
|
Loading…
x
Reference in New Issue
Block a user